/** * Unit tests for src/controllers/AgentController.ts * Services are mocked; handlers are invoked with mock req/res/next. */ import { Request, Response, NextFunction } from 'express'; import { AgentController } from '../../../src/controllers/AgentController'; import { AgentService } from '../../../src/services/AgentService'; import { IAgent, ITokenPayload } from '../../../src/types/index'; import { ValidationError, AuthorizationError, AgentNotFoundError } from '../../../src/utils/errors'; jest.mock('../../../src/services/AgentService'); const MockAgentService = AgentService as jest.MockedClass; // ─── helpers ───────────────────────────────────────────────────────────────── const MOCK_USER: ITokenPayload = { sub: 'agent-id-001', client_id: 'agent-id-001', scope: 'agents:read agents:write', jti: 'jti-001', iat: 1000, exp: 9999999999, }; const MOCK_AGENT: IAgent = { agentId: 'agent-id-001', email: 'agent@sentryagent.ai', agentType: 'screener', version: '1.0.0', capabilities: ['resume:read'], owner: 'team-a', deploymentEnv: 'production', status: 'active', createdAt: new Date('2026-03-28T09:00:00Z'), updatedAt: new Date('2026-03-28T09:00:00Z'), }; function buildMocks(): { req: Partial; res: Partial; next: NextFunction; } { const res: Partial = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), send: jest.fn().mockReturnThis(), }; return { req: { user: MOCK_USER, body: {}, params: {}, query: {}, headers: {}, ip: '127.0.0.1', }, res, next: jest.fn() as NextFunction, }; } // ─── suite ─────────────────────────────────────────────────────────────────── describe('AgentController', () => { let agentService: jest.Mocked; let controller: AgentController; beforeEach(() => { jest.clearAllMocks(); agentService = new MockAgentService({} as never, {} as never, {} as never) as jest.Mocked; controller = new AgentController(agentService); }); // ── registerAgent ──────────────────────────────────────────────────────────── describe('registerAgent()', () => { it('should return 201 with the created agent on success', async () => { const { req, res, next } = buildMocks(); req.body = { email: 'agent@sentryagent.ai', agentType: 'screener', version: '1.0.0', capabilities: ['resume:read'], owner: 'team-a', deploymentEnv: 'production', }; agentService.registerAgent.mockResolvedValue(MOCK_AGENT); await controller.registerAgent(req as Request, res as Response, next); expect(agentService.registerAgent).toHaveBeenCalledTimes(1); expect(res.status).toHaveBeenCalledWith(201); expect(res.json).toHaveBeenCalledWith(MOCK_AGENT); expect(next).not.toHaveBeenCalled(); }); it('should call next(ValidationError) when body is invalid', async () => { const { req, res, next } = buildMocks(); req.body = { agentType: 'screener' }; // missing required fields await controller.registerAgent(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(expect.any(ValidationError)); expect(agentService.registerAgent).not.toHaveBeenCalled(); }); it('should call next(AuthorizationError) when req.user is missing', async () => { const { req, res, next } = buildMocks(); req.user = undefined; await controller.registerAgent(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(expect.any(AuthorizationError)); }); it('should forward service errors to next', async () => { const { req, res, next } = buildMocks(); req.body = { email: 'agent@sentryagent.ai', agentType: 'screener', version: '1.0.0', capabilities: ['resume:read'], owner: 'team-a', deploymentEnv: 'production', }; const serviceError = new Error('DB error'); agentService.registerAgent.mockRejectedValue(serviceError); await controller.registerAgent(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(serviceError); }); }); // ── listAgents ─────────────────────────────────────────────────────────────── describe('listAgents()', () => { it('should return 200 with paginated agents', async () => { const { req, res, next } = buildMocks(); req.query = { page: '1', limit: '20' }; const paginatedResponse = { data: [MOCK_AGENT], total: 1, page: 1, limit: 20 }; agentService.listAgents.mockResolvedValue(paginatedResponse); await controller.listAgents(req as Request, res as Response, next); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith(paginatedResponse); }); it('should call next(AuthorizationError) when req.user is missing', async () => { const { req, res, next } = buildMocks(); req.user = undefined; await controller.listAgents(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(expect.any(AuthorizationError)); }); it('should call next(ValidationError) when query params are invalid', async () => { const { req, res, next } = buildMocks(); req.query = { page: 'not-a-number' }; await controller.listAgents(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(expect.any(ValidationError)); }); it('should forward service errors to next', async () => { const { req, res, next } = buildMocks(); req.query = {}; const serviceError = new Error('Service error'); agentService.listAgents.mockRejectedValue(serviceError); await controller.listAgents(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(serviceError); }); }); // ── getAgentById ───────────────────────────────────────────────────────────── describe('getAgentById()', () => { it('should return 200 with the agent', async () => { const { req, res, next } = buildMocks(); req.params = { agentId: MOCK_AGENT.agentId }; agentService.getAgentById.mockResolvedValue(MOCK_AGENT); await controller.getAgentById(req as Request, res as Response, next); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith(MOCK_AGENT); }); it('should call next(AuthorizationError) when req.user is missing', async () => { const { req, res, next } = buildMocks(); req.user = undefined; req.params = { agentId: 'any' }; await controller.getAgentById(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(expect.any(AuthorizationError)); }); it('should forward AgentNotFoundError to next', async () => { const { req, res, next } = buildMocks(); req.params = { agentId: 'nonexistent' }; const notFound = new AgentNotFoundError('nonexistent'); agentService.getAgentById.mockRejectedValue(notFound); await controller.getAgentById(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(notFound); }); }); // ── updateAgent ────────────────────────────────────────────────────────────── describe('updateAgent()', () => { it('should return 200 with the updated agent', async () => { const { req, res, next } = buildMocks(); req.params = { agentId: MOCK_AGENT.agentId }; req.body = { version: '2.0.0' }; const updated = { ...MOCK_AGENT, version: '2.0.0' }; agentService.updateAgent.mockResolvedValue(updated); await controller.updateAgent(req as Request, res as Response, next); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith(updated); }); it('should call next(AuthorizationError) when req.user is missing', async () => { const { req, res, next } = buildMocks(); req.user = undefined; req.params = { agentId: 'any' }; req.body = { version: '2.0.0' }; await controller.updateAgent(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(expect.any(AuthorizationError)); }); it('should call next(ValidationError) when body is invalid', async () => { const { req, res, next } = buildMocks(); req.params = { agentId: MOCK_AGENT.agentId }; req.body = {}; // empty body — updateAgentSchema requires at least 1 field await controller.updateAgent(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(expect.any(ValidationError)); }); it('should forward service errors to next', async () => { const { req, res, next } = buildMocks(); req.params = { agentId: MOCK_AGENT.agentId }; req.body = { version: '2.0.0' }; const serviceError = new AgentNotFoundError(MOCK_AGENT.agentId); agentService.updateAgent.mockRejectedValue(serviceError); await controller.updateAgent(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(serviceError); }); }); // ── decommissionAgent ──────────────────────────────────────────────────────── describe('decommissionAgent()', () => { it('should return 204 on success', async () => { const { req, res, next } = buildMocks(); req.params = { agentId: MOCK_AGENT.agentId }; agentService.decommissionAgent.mockResolvedValue(); await controller.decommissionAgent(req as Request, res as Response, next); expect(res.status).toHaveBeenCalledWith(204); expect(res.send).toHaveBeenCalled(); expect(next).not.toHaveBeenCalled(); }); it('should call next(AuthorizationError) when req.user is missing', async () => { const { req, res, next } = buildMocks(); req.user = undefined; req.params = { agentId: 'any' }; await controller.decommissionAgent(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(expect.any(AuthorizationError)); }); it('should forward service errors to next', async () => { const { req, res, next } = buildMocks(); req.params = { agentId: MOCK_AGENT.agentId }; const serviceError = new AgentNotFoundError(MOCK_AGENT.agentId); agentService.decommissionAgent.mockRejectedValue(serviceError); await controller.decommissionAgent(req as Request, res as Response, next); expect(next).toHaveBeenCalledWith(serviceError); }); }); });