/** * Unit tests for FederationController (FED-M2-08). * * Coverage: * - listGrants: delegates to GrantsService with query params * - createGrant: delegates to GrantsService, validates body * - generateToken: returns enrollmentUrl containing the token * - listPeers: returns DB rows */ import 'reflect-metadata'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { NotFoundException } from '@nestjs/common'; import type { Db } from '@mosaicstack/db'; import { FederationController } from '../federation.controller.js'; import type { GrantsService } from '../grants.service.js'; import type { EnrollmentService } from '../enrollment.service.js'; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const GRANT_ID = 'g1111111-1111-1111-1111-111111111111'; const PEER_ID = 'p2222222-2222-2222-2222-222222222222'; const USER_ID = 'u3333333-3333-3333-3333-333333333333'; const MOCK_GRANT = { id: GRANT_ID, peerId: PEER_ID, subjectUserId: USER_ID, scope: { resources: ['tasks'], operations: ['list'] }, status: 'pending' as const, expiresAt: null, createdAt: new Date('2026-01-01T00:00:00Z'), revokedAt: null, revokedReason: null, }; const MOCK_PEER = { id: PEER_ID, commonName: 'test-peer', displayName: 'Test Peer', certPem: '', certSerial: 'pending', certNotAfter: new Date(0), clientKeyPem: null, state: 'pending' as const, endpointUrl: null, createdAt: new Date('2026-01-01T00:00:00Z'), updatedAt: new Date('2026-01-01T00:00:00Z'), }; // --------------------------------------------------------------------------- // DB mock builder // --------------------------------------------------------------------------- function makeDbMock(rows: unknown[] = []) { const orderBy = vi.fn().mockResolvedValue(rows); const where = vi.fn().mockReturnValue({ orderBy }); const from = vi.fn().mockReturnValue({ where, orderBy }); const select = vi.fn().mockReturnValue({ from }); return { select, from, where, orderBy, insert: vi.fn(), update: vi.fn(), delete: vi.fn(), } as unknown as Db; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('FederationController', () => { let db: Db; let grantsService: GrantsService; let enrollmentService: EnrollmentService; let controller: FederationController; beforeEach(() => { db = makeDbMock([MOCK_PEER]); grantsService = { createGrant: vi.fn().mockResolvedValue(MOCK_GRANT), getGrant: vi.fn().mockResolvedValue(MOCK_GRANT), listGrants: vi.fn().mockResolvedValue([MOCK_GRANT]), revokeGrant: vi.fn().mockResolvedValue({ ...MOCK_GRANT, status: 'revoked' }), activateGrant: vi.fn(), expireGrant: vi.fn(), } as unknown as GrantsService; enrollmentService = { createToken: vi.fn().mockResolvedValue({ token: 'abc123def456abc123def456abc123def456abc123def456abc123def456ab12', expiresAt: '2026-01-01T00:15:00.000Z', }), redeem: vi.fn(), } as unknown as EnrollmentService; controller = new FederationController(db, grantsService, enrollmentService); }); // ─── Grant management ────────────────────────────────────────────────── describe('listGrants', () => { it('delegates to GrantsService with provided query params', async () => { const query = { peerId: PEER_ID, status: 'pending' as const }; const result = await controller.listGrants(query); expect(grantsService.listGrants).toHaveBeenCalledWith(query); expect(result).toEqual([MOCK_GRANT]); }); it('delegates to GrantsService with empty filters', async () => { const result = await controller.listGrants({}); expect(grantsService.listGrants).toHaveBeenCalledWith({}); expect(result).toEqual([MOCK_GRANT]); }); }); describe('createGrant', () => { it('delegates to GrantsService and returns created grant', async () => { const body = { peerId: PEER_ID, subjectUserId: USER_ID, scope: { resources: ['tasks'], operations: ['list'] }, }; const result = await controller.createGrant(body); expect(grantsService.createGrant).toHaveBeenCalledWith(body); expect(result).toEqual(MOCK_GRANT); }); }); describe('getGrant', () => { it('delegates to GrantsService with provided ID', async () => { const result = await controller.getGrant(GRANT_ID); expect(grantsService.getGrant).toHaveBeenCalledWith(GRANT_ID); expect(result).toEqual(MOCK_GRANT); }); }); describe('revokeGrant', () => { it('delegates to GrantsService with id and reason', async () => { const result = await controller.revokeGrant(GRANT_ID, { reason: 'test reason' }); expect(grantsService.revokeGrant).toHaveBeenCalledWith(GRANT_ID, 'test reason'); expect(result).toMatchObject({ status: 'revoked' }); }); it('delegates without reason when omitted', async () => { await controller.revokeGrant(GRANT_ID, {}); expect(grantsService.revokeGrant).toHaveBeenCalledWith(GRANT_ID, undefined); }); }); describe('generateToken', () => { it('returns enrollmentUrl containing the token', async () => { const token = 'abc123def456abc123def456abc123def456abc123def456abc123def456ab12'; vi.mocked(enrollmentService.createToken).mockResolvedValueOnce({ token, expiresAt: '2026-01-01T00:15:00.000Z', }); const result = await controller.generateToken(GRANT_ID, { ttlSeconds: 900 }); expect(result.token).toBe(token); expect(result.enrollmentUrl).toContain(token); expect(result.enrollmentUrl).toContain('/api/federation/enrollment/'); }); it('creates token via EnrollmentService with correct grantId and peerId', async () => { await controller.generateToken(GRANT_ID, { ttlSeconds: 300 }); expect(enrollmentService.createToken).toHaveBeenCalledWith({ grantId: GRANT_ID, peerId: PEER_ID, ttlSeconds: 300, }); }); it('throws NotFoundException when grant does not exist', async () => { vi.mocked(grantsService.getGrant).mockRejectedValueOnce( new NotFoundException(`Grant ${GRANT_ID} not found`), ); await expect(controller.generateToken(GRANT_ID, { ttlSeconds: 900 })).rejects.toThrow( NotFoundException, ); }); }); // ─── Peer management ─────────────────────────────────────────────────── describe('listPeers', () => { it('returns DB rows ordered by commonName', async () => { const result = await controller.listPeers(); expect(db.select).toHaveBeenCalled(); // The DB mock resolves with [MOCK_PEER] expect(result).toEqual([MOCK_PEER]); }); }); });