feat(federation): admin controller + CLI federation commands (FED-M2-08) (#498)
This commit was merged in pull request #498.
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user