/** * Unit tests for src/controllers/DIDController.ts * DIDService is fully mocked. Handlers are invoked with mock req/res/next. */ import { Request, Response, NextFunction } from 'express'; import { DIDController } from '../../../src/controllers/DIDController'; import { DIDService } from '../../../src/services/DIDService'; import { AgentRepository } from '../../../src/repositories/AgentRepository'; import { AgentNotFoundError } from '../../../src/utils/errors'; import { IDIDDocument, IDIDResolutionResult, IAgentCard } from '../../../src/types/did'; // ─── Mocks ──────────────────────────────────────────────────────────────────── jest.mock('../../../src/services/DIDService'); jest.mock('../../../src/repositories/AgentRepository'); const MockDIDService = DIDService as jest.MockedClass; const MockAgentRepository = AgentRepository as jest.MockedClass; // ─── Fixtures ───────────────────────────────────────────────────────────────── const AGENT_ID = 'agt_test_ctrl'; const TEST_DOMAIN = 'test.example.com'; const INSTANCE_DID = `did:web:${TEST_DOMAIN}`; const AGENT_DID = `did:web:${TEST_DOMAIN}:agents:${AGENT_ID}`; const MOCK_INSTANCE_DOC: IDIDDocument = { '@context': ['https://www.w3.org/ns/did/v1'], id: INSTANCE_DID, controller: INSTANCE_DID, verificationMethod: [ { id: `${INSTANCE_DID}#keys-1`, type: 'JsonWebKey2020', controller: INSTANCE_DID, publicKeyJwk: { kty: 'EC', crv: 'P-256', use: 'sig' }, }, ], authentication: [`${INSTANCE_DID}#keys-1`], }; const MOCK_AGENT_DOC: IDIDDocument = { '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/agntcy/v1'], id: AGENT_DID, controller: INSTANCE_DID, verificationMethod: [], authentication: [], }; const MOCK_RESOLUTION_RESULT: IDIDResolutionResult = { didDocument: MOCK_AGENT_DOC, didDocumentMetadata: { created: '2026-01-01T00:00:00.000Z', updated: '2026-01-02T00:00:00.000Z', deactivated: false, }, didResolutionMetadata: { contentType: 'application/did+ld+json', retrieved: new Date().toISOString(), }, }; const MOCK_AGENT_CARD: IAgentCard = { did: AGENT_DID, name: 'test@example.com', agentType: 'orchestrator', capabilities: ['task-planning'], owner: 'acme', version: '1.0.0', deploymentEnv: 'production', identityProvider: 'https://idp.sentryagent.ai', issuedAt: '2026-01-01T00:00:00.000Z', }; // ─── Request/Response builder ───────────────────────────────────────────────── function buildMocks(params: Record = {}): { req: Partial; res: Partial; next: NextFunction; } { const res: Partial = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), set: jest.fn().mockReturnThis(), }; return { req: { params, body: {}, query: {}, headers: {}, }, res, next: jest.fn() as NextFunction, }; } // ─── Suite ──────────────────────────────────────────────────────────────────── describe('DIDController', () => { let didService: jest.Mocked; let agentRepo: jest.Mocked; let controller: DIDController; beforeEach(() => { jest.clearAllMocks(); didService = new MockDIDService({} as never, null, {} as never) as jest.Mocked; agentRepo = new MockAgentRepository({} as never) as jest.Mocked; controller = new DIDController(didService, agentRepo); }); // ─── getInstanceDIDDocument ────────────────────────────────────────────── describe('getInstanceDIDDocument()', () => { it('should return 200 with the instance DID Document', async () => { didService.buildInstanceDIDDocument.mockResolvedValueOnce(MOCK_INSTANCE_DOC); const { req, res, next } = buildMocks(); await controller.getInstanceDIDDocument(req as Request, res as Response, next); expect(res.json).toHaveBeenCalledWith(MOCK_INSTANCE_DOC); expect(next).not.toHaveBeenCalled(); }); it('should call next with the error when DIDService throws', async () => { const err = new Error('DID build failed'); didService.buildInstanceDIDDocument.mockRejectedValueOnce(err); const { req, res, next } = buildMocks(); await controller.getInstanceDIDDocument(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(err); expect(res.json).not.toHaveBeenCalled(); }); it('should not include private key material in the response', async () => { didService.buildInstanceDIDDocument.mockResolvedValueOnce(MOCK_INSTANCE_DOC); const { req, res, next } = buildMocks(); await controller.getInstanceDIDDocument(req as Request, res as Response, next); const callArg = (res.json as jest.Mock).mock.calls[0][0] as IDIDDocument; const serialised = JSON.stringify(callArg); expect(serialised).not.toContain('privateKeyPem'); expect(serialised).not.toContain('PRIVATE KEY'); }); }); // ─── getAgentDIDDocument ───────────────────────────────────────────────── describe('getAgentDIDDocument()', () => { it('should return 200 with the agent DID Document for an active agent', async () => { didService.buildAgentDIDDocument.mockResolvedValueOnce({ document: MOCK_AGENT_DOC, deactivated: false, createdAt: new Date(), updatedAt: new Date(), }); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentDIDDocument(req as Request, res as Response, next); expect(res.json).toHaveBeenCalledWith(MOCK_AGENT_DOC); expect(res.status).not.toHaveBeenCalled(); expect(next).not.toHaveBeenCalled(); }); it('should return 410 with AGENT_DECOMMISSIONED code for a decommissioned agent', async () => { didService.buildAgentDIDDocument.mockResolvedValueOnce({ document: MOCK_AGENT_DOC, deactivated: true, createdAt: new Date(), updatedAt: new Date(), }); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentDIDDocument(req as Request, res as Response, next); expect(res.status).toHaveBeenCalledWith(410); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'AGENT_DECOMMISSIONED' }), ); expect(next).not.toHaveBeenCalled(); }); it('should call next with AgentNotFoundError when agent does not exist', async () => { const err = new AgentNotFoundError(AGENT_ID); didService.buildAgentDIDDocument.mockRejectedValueOnce(err); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentDIDDocument(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(err); expect(res.json).not.toHaveBeenCalled(); }); it('should call next with generic error when DIDService throws', async () => { const err = new Error('Unexpected DB error'); didService.buildAgentDIDDocument.mockRejectedValueOnce(err); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentDIDDocument(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(err); }); it('should not include private key material in any response', async () => { didService.buildAgentDIDDocument.mockResolvedValueOnce({ document: MOCK_AGENT_DOC, deactivated: false, createdAt: new Date(), updatedAt: new Date(), }); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentDIDDocument(req as Request, res as Response, next); const callArg = (res.json as jest.Mock).mock.calls[0][0] as IDIDDocument; const serialised = JSON.stringify(callArg); expect(serialised).not.toContain('privateKeyPem'); expect(serialised).not.toContain('PRIVATE KEY'); }); }); // ─── resolveAgentDID ───────────────────────────────────────────────────── describe('resolveAgentDID()', () => { it('should return 200 with the DID Resolution result', async () => { didService.buildResolutionResult.mockResolvedValueOnce(MOCK_RESOLUTION_RESULT); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.resolveAgentDID(req as Request, res as Response, next); expect(res.json).toHaveBeenCalledWith(MOCK_RESOLUTION_RESULT); expect(next).not.toHaveBeenCalled(); }); it('should set Content-Type to application/ld+json with DID resolution profile', async () => { didService.buildResolutionResult.mockResolvedValueOnce(MOCK_RESOLUTION_RESULT); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.resolveAgentDID(req as Request, res as Response, next); expect(res.set).toHaveBeenCalledWith( 'Content-Type', 'application/ld+json;profile="https://w3id.org/did-resolution"', ); }); it('should call next with AgentNotFoundError when agent does not exist', async () => { const err = new AgentNotFoundError(AGENT_ID); didService.buildResolutionResult.mockRejectedValueOnce(err); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.resolveAgentDID(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(err); }); it('should call next with error when DIDService throws unexpectedly', async () => { const err = new Error('Redis connection lost'); didService.buildResolutionResult.mockRejectedValueOnce(err); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.resolveAgentDID(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(err); expect(res.json).not.toHaveBeenCalled(); }); it('should not include private key material in the resolution response', async () => { didService.buildResolutionResult.mockResolvedValueOnce(MOCK_RESOLUTION_RESULT); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.resolveAgentDID(req as Request, res as Response, next); const callArg = (res.json as jest.Mock).mock.calls[0][0] as IDIDResolutionResult; const serialised = JSON.stringify(callArg); expect(serialised).not.toContain('privateKeyPem'); expect(serialised).not.toContain('PRIVATE KEY'); }); }); // ─── getAgentCard ───────────────────────────────────────────────────────── describe('getAgentCard()', () => { it('should return 200 with the AGNTCY agent card', async () => { didService.buildAgentCard.mockResolvedValueOnce(MOCK_AGENT_CARD); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentCard(req as Request, res as Response, next); expect(res.json).toHaveBeenCalledWith(MOCK_AGENT_CARD); expect(next).not.toHaveBeenCalled(); }); it('should call next with AgentNotFoundError when agent does not exist', async () => { const err = new AgentNotFoundError(AGENT_ID); didService.buildAgentCard.mockRejectedValueOnce(err); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentCard(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(err); expect(res.json).not.toHaveBeenCalled(); }); it('should call next with generic error when DIDService throws', async () => { const err = new Error('Service unavailable'); didService.buildAgentCard.mockRejectedValueOnce(err); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentCard(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(err); }); it('should not include private key material in the agent card response', async () => { didService.buildAgentCard.mockResolvedValueOnce(MOCK_AGENT_CARD); const { req, res, next } = buildMocks({ agentId: AGENT_ID }); await controller.getAgentCard(req as Request, res as Response, next); const callArg = (res.json as jest.Mock).mock.calls[0][0] as IAgentCard; const serialised = JSON.stringify(callArg); expect(serialised).not.toContain('privateKeyPem'); expect(serialised).not.toContain('PRIVATE KEY'); }); }); });