213 lines
7.0 KiB
TypeScript
213 lines
7.0 KiB
TypeScript
/**
|
|
* 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]);
|
|
});
|
|
});
|
|
});
|