Skip to main content
Glama
secure-error-handler.test.tsβ€’12.2 kB
/** * Unit tests for secure error handler utilities */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SecureApiError, withSecureErrorHandling, createSecureErrorResponse, createSecureToolErrorResult, BatchErrorHandler, retryWithSecureErrors, SecureCircuitBreaker, } from '@/utils/secure-error-handler.js'; import type { ErrorContext } from '@/utils/secure-error-handler.js'; // Mock the logger vi.mock('@/utils/logger.js', () => ({ error: vi.fn(), OperationType: { API_CALL: 'api_call', }, clearLogContext: vi.fn(), })); // Mock the error utilities vi.mock('@/utils/error-utilities.js', () => ({ getErrorMessage: vi.fn((error: unknown, fallback?: string) => { if (error instanceof Error) return error.message; if (typeof error === 'string') return error; return fallback || 'Unknown error'; }), ensureError: vi.fn((error: unknown) => { if (error instanceof Error) return error; return new Error(typeof error === 'string' ? error : 'Unknown error'); }), getErrorStatus: vi.fn(() => undefined), })); // Mock the error sanitizer vi.mock('@/utils/error-sanitizer.js', () => ({ sanitizeErrorMessage: vi.fn((message: string) => `Sanitized: ${message}`), createSanitizedError: vi.fn((error: unknown) => ({ message: 'Sanitized error message', type: 'sanitized_error', statusCode: 500, })), })); describe('SecureApiError', () => { const mockContext: ErrorContext = { operation: 'test-operation', module: 'test-module', resourceType: 'test-resource', recordId: '123', }; it('should create a SecureApiError with correct properties', () => { const error = new SecureApiError( 'Test error message', 400, 'validation_error', mockContext ); expect(error.name).toBe('SecureApiError'); expect(error.message).toBe('Sanitized: Test error message'); expect(error.statusCode).toBe(400); expect(error.errorType).toBe('validation_error'); expect(error.context).toEqual(mockContext); expect(error.safeMetadata).toMatchObject({ operation: 'test-operation', resourceType: 'test-resource', timestamp: expect.any(String), }); }); it('should serialize to safe JSON format', () => { const error = new SecureApiError( 'Test error message', 404, 'not_found', mockContext ); const json = error.toJSON(); expect(json).toEqual({ error: { message: 'Sanitized: Test error message', type: 'not_found', statusCode: 404, metadata: { operation: 'test-operation', resourceType: 'test-resource', timestamp: expect.any(String), }, }, }); }); }); describe('withSecureErrorHandling', () => { const mockContext: ErrorContext = { operation: 'test-operation', module: 'test-module', }; it('should wrap successful function execution', async () => { const testFn = vi.fn().mockResolvedValue('success'); const wrappedFn = withSecureErrorHandling(testFn, mockContext); const result = await wrappedFn('arg1', 'arg2'); expect(result).toBe('success'); expect(testFn).toHaveBeenCalledWith('arg1', 'arg2'); }); it('should wrap and sanitize thrown errors', async () => { const testFn = vi.fn().mockRejectedValue(new Error('Original error')); const wrappedFn = withSecureErrorHandling(testFn, mockContext); await expect(wrappedFn()).rejects.toThrow(SecureApiError); }); it('should handle non-Error thrown values', async () => { const testFn = vi.fn().mockRejectedValue('String error'); const wrappedFn = withSecureErrorHandling(testFn, mockContext); await expect(wrappedFn()).rejects.toThrow(SecureApiError); }); }); describe('createSecureErrorResponse', () => { it('should create response from SecureApiError', () => { const secureError = new SecureApiError( 'Test error', 400, 'validation_error', { operation: 'test', module: 'test' } ); const response = createSecureErrorResponse(secureError); expect(response).toEqual({ success: false, error: { message: 'Sanitized: Test error', type: 'validation_error', statusCode: 400, suggestion: 'Review the tool arguments for missing or invalid fields.', }, }); }); it('should create response from unknown error', () => { const response = createSecureErrorResponse(new Error('Test error')); expect(response).toEqual({ success: false, error: { message: 'Sanitized error message', type: 'sanitized_error', statusCode: 500, suggestion: 'Retry the request. Contact support with the reference ID if it recurs.', }, }); }); }); describe('createSecureToolErrorResult', () => { it('should include correlation metadata and guidance', () => { const result = createSecureToolErrorResult(new Error('Tool failed'), { module: 'test-module', operation: 'test-operation', correlationId: 'corr-123', requestId: 'req-456', }); expect(result.isError).toBe(true); expect(result.error).toMatchObject({ type: 'sanitized_error', message: 'Sanitized error message', code: 500, correlationId: 'corr-123', requestId: 'req-456', suggestion: 'Retry the request. Contact support with the reference ID if it recurs.', }); expect(result.content[0]).toMatchObject({ type: 'text', text: expect.stringContaining('Reference ID: corr-123'), }); expect(result.content[0].text).toContain('Next steps:'); }); }); describe('BatchErrorHandler', () => { const mockContext: ErrorContext = { operation: 'batch-operation', module: 'test-module', }; let batchHandler: BatchErrorHandler; beforeEach(() => { batchHandler = new BatchErrorHandler(mockContext); }); it('should start with no errors', () => { expect(batchHandler.hasErrors()).toBe(false); expect(batchHandler.getSummary()).toEqual({ totalErrors: 0, errorsByType: {}, }); }); it('should add and track errors', () => { batchHandler.addError(0, new Error('First error')); batchHandler.addError(1, 'Second error'); expect(batchHandler.hasErrors()).toBe(true); const summary = batchHandler.getSummary(); expect(summary.totalErrors).toBe(2); expect(summary.errorsByType.batch_error).toBe(2); }); it('should provide error details', () => { batchHandler.addError(0, new Error('Test error')); const details = batchHandler.getErrorDetails(); expect(details).toHaveLength(1); expect(details[0]).toEqual({ index: 0, error: expect.stringContaining('Test error'), type: 'batch_error', }); }); }); describe('retryWithSecureErrors', () => { const mockContext: ErrorContext = { operation: 'retry-operation', module: 'test-module', }; beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('should succeed on first attempt', async () => { const testFn = vi.fn().mockResolvedValue('success'); const result = await retryWithSecureErrors(testFn, mockContext); expect(result).toBe('success'); expect(testFn).toHaveBeenCalledTimes(1); }); it('should retry on retryable errors', async () => { const testFn = vi .fn() .mockRejectedValueOnce(new Error('Server error')) .mockResolvedValue('success'); // Mock getErrorStatus to return 500 for retry logic const { getErrorStatus } = await import( '../../src/utils/error-utilities.js' ); vi.mocked(getErrorStatus).mockReturnValue(500); const promise = retryWithSecureErrors(testFn, mockContext, { maxRetries: 1, }); // Fast-forward time for retry delay await vi.advanceTimersByTimeAsync(1000); const result = await promise; expect(result).toBe('success'); expect(testFn).toHaveBeenCalledTimes(2); }); it('should throw SecureApiError after max retries', async () => { const testFn = vi.fn().mockRejectedValue(new Error('Persistent error')); // Mock getErrorStatus to return 500 for retry logic const { getErrorStatus } = await import( '../../src/utils/error-utilities.js' ); vi.mocked(getErrorStatus).mockReturnValue(500); // Use 0 retries to avoid timing issues await expect( retryWithSecureErrors(testFn, mockContext, { maxRetries: 0 }) ).rejects.toThrow(SecureApiError); expect(testFn).toHaveBeenCalledTimes(1); }); }); describe('SecureCircuitBreaker', () => { const mockContext: ErrorContext = { operation: 'circuit-breaker-operation', module: 'test-module', }; let circuitBreaker: SecureCircuitBreaker; beforeEach(() => { circuitBreaker = new SecureCircuitBreaker(mockContext, { failureThreshold: 2, resetTimeout: 5000, }); }); it('should start in closed state', () => { const status = circuitBreaker.getStatus(); expect(status.state).toBe('closed'); expect(status.failures).toBe(0); }); it('should execute function successfully in closed state', async () => { const testFn = vi.fn().mockResolvedValue('success'); const result = await circuitBreaker.execute(testFn); expect(result).toBe('success'); expect(testFn).toHaveBeenCalledTimes(1); }); it('should track failures and open circuit', async () => { const testFn = vi.fn().mockRejectedValue(new Error('Test error')); // First failure await expect(circuitBreaker.execute(testFn)).rejects.toThrow( SecureApiError ); expect(circuitBreaker.getStatus().failures).toBe(1); expect(circuitBreaker.getStatus().state).toBe('closed'); // Second failure should open the circuit await expect(circuitBreaker.execute(testFn)).rejects.toThrow( SecureApiError ); expect(circuitBreaker.getStatus().failures).toBe(2); expect(circuitBreaker.getStatus().state).toBe('open'); }); it('should reject immediately when circuit is open', async () => { const testFn = vi.fn().mockRejectedValue(new Error('Test error')); // Force circuit open by reaching failure threshold await expect(circuitBreaker.execute(testFn)).rejects.toThrow(); await expect(circuitBreaker.execute(testFn)).rejects.toThrow(); // Next call should be rejected immediately without calling the function const initialCallCount = testFn.mock.calls.length; await expect(circuitBreaker.execute(testFn)).rejects.toThrow( SecureApiError ); expect(testFn.mock.calls.length).toBe(initialCallCount); }); it('should reset manually', () => { // Force some failures circuitBreaker['failures'] = 5; circuitBreaker['state'] = 'open'; circuitBreaker.reset(); const status = circuitBreaker.getStatus(); expect(status.state).toBe('closed'); expect(status.failures).toBe(0); }); }); describe('Type Guards and Status Resolution', () => { it('should resolve status from error with statusCode', () => { // This tests the internal type guards indirectly through public APIs const errorWithStatusCode = { statusCode: 404, message: 'Not found' }; const response = createSecureErrorResponse(errorWithStatusCode); // The sanitized error will have the default statusCode from the mock expect(response.error.statusCode).toBe(500); }); it('should resolve status from error with status', () => { const errorWithStatus = { status: 401, message: 'Unauthorized' }; const response = createSecureErrorResponse(errorWithStatus); // The sanitized error will have the default statusCode from the mock expect(response.error.statusCode).toBe(500); }); it('should resolve status from Axios-like error', () => { const axiosLikeError = { message: 'Request failed', response: { status: 429, statusText: 'Too Many Requests', data: { error: 'Rate limited' }, }, }; const response = createSecureErrorResponse(axiosLikeError); // The sanitized error will have the default statusCode from the mock expect(response.error.statusCode).toBe(500); }); });

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/kesslerio/attio-mcp-server'

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