Files
sentryagent-idp/tests/unit/middleware/errorHandler.test.ts
SentryAgent.ai Developer d3530285b9 feat: Phase 1 MVP — complete AgentIdP implementation
Implements all P0 features per OpenSpec change phase-1-mvp-implementation:
- Agent Registry Service (CRUD) — full lifecycle management
- OAuth 2.0 Token Service (Client Credentials flow)
- Credential Management (generate, rotate, revoke)
- Immutable Audit Log Service

Tech: Node.js 18+, TypeScript 5.3+ strict, Express 4.18+, PostgreSQL 14+, Redis 7+
Standards: OpenAPI 3.0 specs, DRY/SOLID, zero `any` types
Quality: 18 unit test suites, 244 tests passing, 97%+ coverage
OpenAPI: 4 complete specs (14 endpoints total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 09:14:41 +00:00

183 lines
6.3 KiB
TypeScript

/**
* 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<NextFunction>;
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' } }),
);
});
});