Skip to main content
Glama
error-handler.test.ts15.9 kB
/** * Tests for Error Handler Middleware */ import { Request, Response } from 'express'; import { globalErrorHandler, asyncErrorHandler, notFoundHandler, setupProcessErrorHandlers, MCPErrorResponse } from '../error-handler'; import { ErrorCategory, StructuredError } from '../../server'; // Mock Express Request and Response objects function createMockRequest(overrides: Partial<Request> = {}): Request { return { path: '/test', method: 'GET', url: '/test', headers: {}, params: {}, query: {}, body: {}, ...overrides } as Request; } function createMockResponse(): Response { const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), setHeader: jest.fn().mockReturnThis(), statusCode: 200, headers: {} } as unknown as Response; return res; } describe('Error Handler Middleware', () => { let mockNext: jest.Mock; let consoleErrorSpy: jest.SpyInstance; let originalEnv: string | undefined; beforeEach(() => { mockNext = jest.fn(); consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); originalEnv = process.env.NODE_ENV; }); afterEach(() => { jest.clearAllMocks(); consoleErrorSpy.mockRestore(); process.env.NODE_ENV = originalEnv; }); describe('globalErrorHandler', () => { it('should handle standard errors with default values', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Test error'); globalErrorHandler(error, req, res, mockNext); expect(res.status).toHaveBeenCalledWith(500); expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'application/json'); expect(res.json).toHaveBeenCalledWith({ error: { code: 'INTERNAL_ERROR', message: 'Test error', category: ErrorCategory.INTERNAL, requestId: 'unknown', timestamp: expect.any(String) } }); expect(mockNext).not.toHaveBeenCalled(); }); it('should handle structured errors', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest({ requestId: 'test-123' } as any); const res = createMockResponse(); const structuredError = new Error('Validation failed') as StructuredError; structuredError.category = ErrorCategory.VALIDATION; structuredError.code = 'INVALID_INPUT'; structuredError.statusCode = 400; structuredError.isOperational = true; globalErrorHandler(structuredError, req, res, mockNext); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith({ error: { code: 'INVALID_INPUT', message: 'Validation failed', category: ErrorCategory.VALIDATION, requestId: 'test-123', timestamp: expect.any(String) } }); }); it('should include error details in non-production environment', () => { process.env.NODE_ENV = 'development'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Test error'); error.stack = 'Error stack trace'; globalErrorHandler(error, req, res, mockNext); expect(res.json).toHaveBeenCalledWith({ error: { code: 'INTERNAL_ERROR', message: 'Test error', category: ErrorCategory.INTERNAL, requestId: 'unknown', timestamp: expect.any(String), details: { stack: 'Error stack trace', name: 'Error' } } }); }); it('should not include error details in production environment', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Test error'); globalErrorHandler(error, req, res, mockNext); const callArgs = (res.json as jest.Mock).mock.calls[0][0] as MCPErrorResponse; expect(callArgs.error.details).toBeUndefined(); }); it('should categorize network errors correctly', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('ECONNREFUSED connection failed'); globalErrorHandler(error, req, res, mockNext); expect(res.status).toHaveBeenCalledWith(503); expect(res.json).toHaveBeenCalledWith({ error: { code: 'NETWORK_ERROR', message: 'ECONNREFUSED connection failed', category: ErrorCategory.NETWORK, requestId: 'unknown', timestamp: expect.any(String) } }); }); it('should categorize validation errors correctly', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Validation failed: invalid email'); globalErrorHandler(error, req, res, mockNext); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith({ error: { code: 'VALIDATION_ERROR', message: 'Validation failed: invalid email', category: ErrorCategory.VALIDATION, requestId: 'unknown', timestamp: expect.any(String) } }); }); it('should categorize authentication errors correctly', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Unauthorized access'); globalErrorHandler(error, req, res, mockNext); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: { code: 'AUTHENTICATION_ERROR', message: 'Unauthorized access', category: ErrorCategory.AUTHENTICATION, requestId: 'unknown', timestamp: expect.any(String) } }); }); it('should categorize authorization errors correctly', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Forbidden: insufficient permissions'); globalErrorHandler(error, req, res, mockNext); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: { code: 'AUTHORIZATION_ERROR', message: 'Forbidden: insufficient permissions', category: ErrorCategory.AUTHORIZATION, requestId: 'unknown', timestamp: expect.any(String) } }); }); it('should categorize rate limit errors correctly', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Rate limit exceeded'); globalErrorHandler(error, req, res, mockNext); expect(res.status).toHaveBeenCalledWith(429); expect(res.json).toHaveBeenCalledWith({ error: { code: 'RATE_LIMIT_ERROR', message: 'Rate limit exceeded', category: ErrorCategory.RATE_LIMIT, requestId: 'unknown', timestamp: expect.any(String) } }); }); it('should categorize timeout errors correctly (as network errors due to categorization logic)', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Connection timeout occurred'); globalErrorHandler(error, req, res, mockNext); // Note: Due to categorization order, timeout errors are caught by network error check first expect(res.status).toHaveBeenCalledWith(503); expect(res.json).toHaveBeenCalledWith({ error: { code: 'NETWORK_ERROR', message: 'Connection timeout occurred', category: ErrorCategory.NETWORK, requestId: 'unknown', timestamp: expect.any(String) } }); }); it('should categorize pure timeout errors correctly', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error('Operation timeout exceeded'); globalErrorHandler(error, req, res, mockNext); expect(res.status).toHaveBeenCalledWith(503); expect(res.json).toHaveBeenCalledWith({ error: { code: 'NETWORK_ERROR', message: 'Operation timeout exceeded', category: ErrorCategory.NETWORK, requestId: 'unknown', timestamp: expect.any(String) } }); }); it('should handle errors without messages', () => { process.env.NODE_ENV = 'production'; const req = createMockRequest(); const res = createMockResponse(); const error = new Error(); error.message = ''; globalErrorHandler(error, req, res, mockNext); expect(res.json).toHaveBeenCalledWith({ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred', category: ErrorCategory.INTERNAL, requestId: 'unknown', timestamp: expect.any(String) } }); }); }); describe('asyncErrorHandler', () => { it('should catch and pass async errors to next middleware', async () => { const asyncFunction = jest.fn().mockRejectedValue(new Error('Async error')); const wrappedFunction = asyncErrorHandler(asyncFunction); const req = createMockRequest(); const res = createMockResponse(); await wrappedFunction(req, res, mockNext); expect(asyncFunction).toHaveBeenCalledWith(req, res, mockNext); expect(mockNext).toHaveBeenCalledWith(new Error('Async error')); }); it('should handle successful async functions', async () => { const asyncFunction = jest.fn().mockResolvedValue('success'); const wrappedFunction = asyncErrorHandler(asyncFunction); const req = createMockRequest(); const res = createMockResponse(); await wrappedFunction(req, res, mockNext); expect(asyncFunction).toHaveBeenCalledWith(req, res, mockNext); expect(mockNext).not.toHaveBeenCalled(); }); it('should handle non-async functions that return values', async () => { const syncFunction = jest.fn().mockReturnValue('sync result'); const wrappedFunction = asyncErrorHandler(syncFunction); const req = createMockRequest(); const res = createMockResponse(); await wrappedFunction(req, res, mockNext); expect(syncFunction).toHaveBeenCalledWith(req, res, mockNext); expect(mockNext).not.toHaveBeenCalled(); }); }); describe('notFoundHandler', () => { it('should create and pass a 404 structured error', () => { const req = createMockRequest({ path: '/non-existent', requestId: 'req-123' } as any); const res = createMockResponse(); notFoundHandler(req, res, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ message: 'Route not found: /non-existent', category: ErrorCategory.VALIDATION, code: 'ROUTE_NOT_FOUND', statusCode: 404, isOperational: true, requestId: 'req-123', context: { path: '/non-existent' } }) ); }); it('should handle requests without requestId', () => { const req = createMockRequest({ path: '/missing' }); const res = createMockResponse(); notFoundHandler(req, res, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ message: 'Route not found: /missing', category: ErrorCategory.VALIDATION, code: 'ROUTE_NOT_FOUND', statusCode: 404, isOperational: true, context: { path: '/missing' } }) ); }); }); describe('setupProcessErrorHandlers', () => { let originalProcessOn: typeof process.on; let processOnSpy: jest.SpyInstance; beforeEach(() => { originalProcessOn = process.on; processOnSpy = jest.spyOn(process, 'on').mockImplementation(); }); afterEach(() => { process.on = originalProcessOn; processOnSpy.mockRestore(); }); it('should setup unhandledRejection and uncaughtException handlers', () => { setupProcessErrorHandlers(); expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function)); }); it('should setup handlers with custom logger', () => { const mockLogger = jest.fn(); setupProcessErrorHandlers(mockLogger); expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function)); }); it('should handle unhandled rejection with logger', () => { const mockLogger = jest.fn(); setupProcessErrorHandlers(mockLogger); // Get the registered handler const rejectionHandler = processOnSpy.mock.calls.find(call => call[0] === 'unhandledRejection')?.[1]; const testError = new Error('Test rejection'); rejectionHandler(testError, Promise.resolve()); expect(mockLogger).toHaveBeenCalledWith( 'error', 'Unhandled Promise Rejection: Error: Test rejection', testError ); }); it('should handle unhandled rejection without logger', () => { setupProcessErrorHandlers(); // Get the registered handler const rejectionHandler = processOnSpy.mock.calls.find(call => call[0] === 'unhandledRejection')?.[1]; const testReason = 'Test rejection reason'; rejectionHandler(testReason, Promise.resolve()); expect(consoleErrorSpy).toHaveBeenCalledWith( 'Unhandled Promise Rejection: Test rejection reason', 'Test rejection reason' ); }); it('should handle uncaught exception with logger', () => { const mockLogger = jest.fn(); const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('Process exit called'); }); setupProcessErrorHandlers(mockLogger); // Get the registered handler const exceptionHandler = processOnSpy.mock.calls.find(call => call[0] === 'uncaughtException')?.[1]; const testError = new Error('Test exception'); expect(() => { exceptionHandler(testError); }).toThrow('Process exit called'); expect(mockLogger).toHaveBeenCalledWith( 'fatal', 'Uncaught Exception: Test exception', testError ); expect(processExitSpy).toHaveBeenCalledWith(1); processExitSpy.mockRestore(); }); it('should handle uncaught exception without logger', () => { const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('Process exit called'); }); setupProcessErrorHandlers(); // Get the registered handler const exceptionHandler = processOnSpy.mock.calls.find(call => call[0] === 'uncaughtException')?.[1]; const testError = new Error('Test exception'); expect(() => { exceptionHandler(testError); }).toThrow('Process exit called'); expect(consoleErrorSpy).toHaveBeenCalledWith( 'Uncaught Exception: Test exception', testError ); expect(processExitSpy).toHaveBeenCalledWith(1); processExitSpy.mockRestore(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server