/** * Unit tests for src/middleware/errorHandler.ts */ import { Request, Response, NextFunction } from 'express'; import { errorHandler } from '../../../src/middleware/errorHandler'; import { ValidationError, AgentNotFoundError, AgentAlreadyExistsError, AgentAlreadyDecommissionedError, CredentialNotFoundError, CredentialAlreadyRevokedError, CredentialError, AuthenticationError, AuthorizationError, RateLimitError, FreeTierLimitError, InsufficientScopeError, AuditEventNotFoundError, RetentionWindowError, } from '../../../src/utils/errors'; function makeRes(): { status: jest.Mock; json: jest.Mock } { const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), }; return res; } const req = {} as Request; const next = jest.fn() as jest.MockedFunction; describe('errorHandler', () => { beforeEach(() => jest.clearAllMocks()); it('should return 400 for ValidationError', () => { const res = makeRes(); errorHandler(new ValidationError('bad input'), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'VALIDATION_ERROR' })); }); it('should return 404 for AgentNotFoundError', () => { const res = makeRes(); errorHandler(new AgentNotFoundError(), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'AGENT_NOT_FOUND' })); }); it('should return 409 for AgentAlreadyExistsError', () => { const res = makeRes(); errorHandler(new AgentAlreadyExistsError('test@test.com'), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(409); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'AGENT_ALREADY_EXISTS' })); }); it('should return 409 for AgentAlreadyDecommissionedError', () => { const res = makeRes(); errorHandler(new AgentAlreadyDecommissionedError('id'), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(409); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'AGENT_ALREADY_DECOMMISSIONED' }), ); }); it('should return 404 for CredentialNotFoundError', () => { const res = makeRes(); errorHandler(new CredentialNotFoundError(), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'CREDENTIAL_NOT_FOUND' }), ); }); it('should return 409 for CredentialAlreadyRevokedError', () => { const res = makeRes(); errorHandler( new CredentialAlreadyRevokedError('cred-id', new Date().toISOString()), req, res as unknown as Response, next, ); expect(res.status).toHaveBeenCalledWith(409); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'CREDENTIAL_ALREADY_REVOKED' }), ); }); it('should return 400 for CredentialError', () => { const res = makeRes(); errorHandler(new CredentialError('error', 'AGENT_NOT_ACTIVE'), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(400); }); it('should return 401 for AuthenticationError', () => { const res = makeRes(); errorHandler(new AuthenticationError(), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'UNAUTHORIZED' })); }); it('should return 403 for AuthorizationError', () => { const res = makeRes(); errorHandler(new AuthorizationError(), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'FORBIDDEN' })); }); it('should return 429 for RateLimitError', () => { const res = makeRes(); errorHandler(new RateLimitError(), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(429); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'RATE_LIMIT_EXCEEDED' }), ); }); it('should return 403 for FreeTierLimitError', () => { const res = makeRes(); errorHandler(new FreeTierLimitError('Limit reached'), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'FREE_TIER_LIMIT_EXCEEDED' }), ); }); it('should return 403 for InsufficientScopeError', () => { const res = makeRes(); errorHandler(new InsufficientScopeError('audit:read'), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'INSUFFICIENT_SCOPE' }), ); }); it('should return 404 for AuditEventNotFoundError', () => { const res = makeRes(); errorHandler(new AuditEventNotFoundError(), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'AUDIT_EVENT_NOT_FOUND' }), ); }); it('should return 400 for RetentionWindowError', () => { const res = makeRes(); errorHandler( new RetentionWindowError(90, '2025-12-28T00:00:00.000Z'), req, res as unknown as Response, next, ); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'RETENTION_WINDOW_EXCEEDED' }), ); }); it('should return 500 for unknown errors', () => { const res = makeRes(); errorHandler(new Error('unexpected'), req, res as unknown as Response, next); expect(res.status).toHaveBeenCalledWith(500); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'INTERNAL_SERVER_ERROR' }), ); }); it('should include details in the response when present', () => { const res = makeRes(); errorHandler( new ValidationError('bad', { field: 'email' }), req, res as unknown as Response, next, ); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ details: { field: 'email' } }), ); }); });