jest.mock('../error-logger', () => ({ logError: jest.fn(), })); const { asyncHandler, errorMiddleware, notFoundHandler } = require('../error-handler'); const { AppError, ValidationError, AuthenticationError, NotFoundError, RateLimitError, DockerError, } = require('../errors'); describe('Error Handler', () => { let req, res, next; beforeEach(() => { req = { method: 'GET', path: '/api/test', ip: '127.0.0.1', user: { id: 'user1' }, body: {}, }; res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), }; next = jest.fn(); }); describe('asyncHandler', () => { it('calls the wrapped function', async () => { const fn = jest.fn().mockResolvedValue(); const wrapped = asyncHandler(fn); await wrapped(req, res, next); expect(fn).toHaveBeenCalledWith(req, res, next); }); it('calls next(err) on rejected promise', async () => { const error = new Error('async fail'); const fn = jest.fn().mockRejectedValue(error); const wrapped = asyncHandler(fn); await wrapped(req, res, next); expect(next).toHaveBeenCalledWith(error); }); }); describe('errorMiddleware', () => { it('returns 400 for ValidationError', () => { const err = new ValidationError('bad input', 'email'); errorMiddleware(err, req, res, next); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ success: false, error: 'bad input', code: 'DC-400', field: 'email', }) ); }); it('returns 401 for AuthenticationError with requiresTotp', () => { const err = new AuthenticationError('auth needed', true); errorMiddleware(err, req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ success: false, error: 'auth needed', requiresTotp: true, }) ); }); it('returns 404 for NotFoundError with resource', () => { const err = new NotFoundError('Service'); errorMiddleware(err, req, res, next); expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: 'Service not found', resource: 'Service', }) ); }); it('returns 429 for RateLimitError with retryAfter', () => { const err = new RateLimitError(30); errorMiddleware(err, req, res, next); expect(res.status).toHaveBeenCalledWith(429); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: 'Rate limit exceeded', retryAfter: 30, }) ); }); it('returns 500 with "Internal server error" for generic Error', () => { const err = new Error('db connection lost'); errorMiddleware(err, req, res, next); expect(res.status).toHaveBeenCalledWith(500); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ success: false, error: 'Internal server error', // NOT the real message }) ); }); it('includes error code in DC-XXX format', () => { const err = new AppError('test', 418, 'DC-TEAPOT'); errorMiddleware(err, req, res, next); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ code: 'DC-TEAPOT' }) ); }); it('includes details for DockerError', () => { const err = new DockerError('container fail', 'create', { id: '123' }); errorMiddleware(err, req, res, next); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ details: { id: '123' }, }) ); }); it('includes stack trace in development mode', () => { const origEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'development'; const err = new AppError('test'); errorMiddleware(err, req, res, next); const response = res.json.mock.calls[0][0]; expect(response.stack).toBeDefined(); process.env.NODE_ENV = origEnv; }); it('excludes stack trace in production mode', () => { const origEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; const err = new AppError('test'); errorMiddleware(err, req, res, next); const response = res.json.mock.calls[0][0]; expect(response.stack).toBeUndefined(); process.env.NODE_ENV = origEnv; }); it('logs non-operational errors as FATAL', () => { const origError = console.error; console.error = jest.fn(); const err = new Error('programming bug'); errorMiddleware(err, req, res, next); expect(console.error).toHaveBeenCalledWith( 'FATAL: Non-operational error detected', expect.any(Object) ); console.error = origError; }); }); describe('notFoundHandler', () => { it('passes NotFoundError to next()', () => { notFoundHandler(req, res, next); expect(next).toHaveBeenCalledWith(expect.any(NotFoundError)); const passedError = next.mock.calls[0][0]; expect(passedError.message).toContain('GET'); expect(passedError.message).toContain('/api/test'); }); }); });