Skip to main content
Glama
error-handlers.test.ts18.1 kB
/** * @vitest-environment node */ import { AxiosError } from 'axios'; import { ErrorCategory } from '../utils/errors/categories.js'; import { classifyGraphQLError, isError, isErrorWithMessage, isAxiosErrorWithCriteria, extractGraphQLErrorMessages, handleGraphQLSpecificError, handleNetworkError, handleHttpStatusError, handleApiError, } from '../utils/errors/handlers.js'; // Helper function to create a mock AxiosError const createMockAxiosError = (statusCode?: number, data?: unknown, code?: string): AxiosError => { const error = new Error('Axios error') as AxiosError; error.isAxiosError = true; error.code = code; if (statusCode) { error.response = { status: statusCode, statusText: `Status ${statusCode}`, headers: {}, config: { headers: {} as Record<string, unknown> }, data, }; } return error; }; describe('Error Handlers', () => { describe('classifyGraphQLError', () => { it('should classify authentication errors correctly', () => { const authErrors = [ new Error('Authentication failed'), new Error('Unauthorized access'), new Error('Access denied'), new Error('Not authorized to access this resource'), new Error('This action is forbidden'), new Error('Invalid token provided'), new Error('Missing API key'), ]; for (const error of authErrors) { expect(classifyGraphQLError(error)).toBe(ErrorCategory.AUTH); } }); it('should classify rate limit errors correctly', () => { const rateLimitErrors = [ new Error('Rate limit exceeded'), new Error('Too many requests'), new Error('Your account is throttled'), ]; for (const error of rateLimitErrors) { expect(classifyGraphQLError(error)).toBe(ErrorCategory.RATE_LIMIT); } }); it('should classify network errors correctly', () => { const networkErrors = [ new Error('Network error occurred'), new Error('Connection failed'), new Error('ECONNRESET error'), new Error('ECONNREFUSED error'), ]; for (const error of networkErrors) { expect(classifyGraphQLError(error)).toBe(ErrorCategory.NETWORK); } }); it('should classify timeout errors correctly', () => { const timeoutErrors = [ new Error('Request timeout'), new Error('Operation timed out'), new Error('ETIMEDOUT error'), ]; for (const error of timeoutErrors) { expect(classifyGraphQLError(error)).toBe(ErrorCategory.TIMEOUT); } }); it('should classify schema errors correctly', () => { const schemaErrors = [ new Error('Cannot query field "nonexistent"'), new Error('Unknown argument "invalid"'), new Error('Unknown type "Invalid"'), new Error('Field not defined on type "Object"'), ]; for (const error of schemaErrors) { expect(classifyGraphQLError(error)).toBe(ErrorCategory.SCHEMA); } }); it('should classify not found errors correctly', () => { const notFoundErrors = [ new Error('Resource not found'), new Error('Object not found'), new Error('NoneType error'), new Error('The requested project does not exist'), ]; for (const error of notFoundErrors) { expect(classifyGraphQLError(error)).toBe(ErrorCategory.NOT_FOUND); } }); it('should classify server errors correctly', () => { const serverErrors = [ new Error('Server error occurred'), new Error('Internal server error'), new Error('500 error'), ]; for (const error of serverErrors) { expect(classifyGraphQLError(error)).toBe(ErrorCategory.SERVER); } }); it('should classify unrecognized errors as OTHER', () => { const otherErrors = [ new Error('Some random error'), new Error('Unexpected behavior'), new Error('Unknown issue'), ]; for (const error of otherErrors) { expect(classifyGraphQLError(error)).toBe(ErrorCategory.OTHER); } }); }); describe('isError', () => { it('should return true for Error instances', () => { expect(isError(new Error('Test error'))).toBe(true); }); it('should return true for objects with message property', () => { expect(isError({ message: 'Test error' })).toBe(true); }); it('should return false for null or undefined', () => { expect(isError(null)).toBe(false); expect(isError(undefined)).toBe(false); }); it('should return false for primitive values', () => { expect(isError(123)).toBe(false); expect(isError('string')).toBe(false); expect(isError(true)).toBe(false); }); it('should return false for objects without message property', () => { expect(isError({})).toBe(false); expect(isError({ other: 'property' })).toBe(false); }); it('should return false if message is not a string', () => { expect(isError({ message: 123 })).toBe(false); expect(isError({ message: true })).toBe(false); expect(isError({ message: {} })).toBe(false); }); }); describe('isErrorWithMessage', () => { it('should return true if error message contains the substring', () => { const error = new Error('This is a test error message'); expect(isErrorWithMessage(error, 'test')).toBe(true); }); it('should return false if error message does not contain the substring', () => { const error = new Error('This is a test error message'); expect(isErrorWithMessage(error, 'missing')).toBe(false); }); it('should return false for non-error objects', () => { expect(isErrorWithMessage(null, 'test')).toBe(false); expect(isErrorWithMessage(undefined, 'test')).toBe(false); expect(isErrorWithMessage(123, 'test')).toBe(false); expect(isErrorWithMessage('string', 'test')).toBe(false); expect(isErrorWithMessage({}, 'test')).toBe(false); }); }); describe('isAxiosErrorWithCriteria', () => { it('should return true for basic axios errors', () => { const axiosError = createMockAxiosError(); expect(isAxiosErrorWithCriteria(axiosError)).toBe(true); }); it('should return true when status code matches', () => { const axiosError = createMockAxiosError(404); expect(isAxiosErrorWithCriteria(axiosError, 404)).toBe(true); }); it('should return false when status code does not match', () => { const axiosError = createMockAxiosError(404); expect(isAxiosErrorWithCriteria(axiosError, 500)).toBe(false); }); it('should return true when error code matches', () => { const axiosError = createMockAxiosError(undefined, undefined, 'ECONNREFUSED'); expect(isAxiosErrorWithCriteria(axiosError, undefined, 'ECONNREFUSED')).toBe(true); }); it('should return false when error code does not match', () => { const axiosError = createMockAxiosError(undefined, undefined, 'ECONNREFUSED'); expect(isAxiosErrorWithCriteria(axiosError, undefined, 'ETIMEDOUT')).toBe(false); }); it('should return true when both status and error code match', () => { const axiosError = createMockAxiosError(404, undefined, 'ENOTFOUND'); expect(isAxiosErrorWithCriteria(axiosError, 404, 'ENOTFOUND')).toBe(true); }); it('should return false when either status or error code does not match', () => { const axiosError = createMockAxiosError(404, undefined, 'ENOTFOUND'); expect(isAxiosErrorWithCriteria(axiosError, 500, 'ENOTFOUND')).toBe(false); expect(isAxiosErrorWithCriteria(axiosError, 404, 'ECONNREFUSED')).toBe(false); }); it('should return false for non-axios errors', () => { expect(isAxiosErrorWithCriteria(new Error('Not an axios error'))).toBe(false); expect(isAxiosErrorWithCriteria(null)).toBe(false); expect(isAxiosErrorWithCriteria(undefined)).toBe(false); expect(isAxiosErrorWithCriteria({})).toBe(false); }); }); describe('extractGraphQLErrorMessages', () => { it('should extract and join error messages from an array of errors', () => { const errors = [ { message: 'First error' }, { message: 'Second error' }, { message: 'Third error' }, ]; expect(extractGraphQLErrorMessages(errors)).toBe('First error, Second error, Third error'); }); it('should handle a single error', () => { const errors = [{ message: 'Only error' }]; expect(extractGraphQLErrorMessages(errors)).toBe('Only error'); }); it('should handle an empty array', () => { expect(extractGraphQLErrorMessages([])).toBe(''); }); }); describe('handleGraphQLSpecificError', () => { it('should create a ClassifiedError for GraphQL errors', () => { const graphqlErrors = [{ message: 'Authentication failed' }, { message: 'Invalid token' }]; const axiosError = createMockAxiosError(200, { errors: graphqlErrors }); const result = handleGraphQLSpecificError(axiosError); expect(result).not.toBeNull(); expect(result?.message).toContain('GraphQL Error'); expect(result?.message).toContain('Authentication failed'); expect(result?.message).toContain('Invalid token'); expect(result?.category).toBe(ErrorCategory.AUTH); expect(result?.originalError).toBe(axiosError); expect(result?.metadata).toEqual({ graphqlErrors }); }); it('should classify GraphQL errors correctly based on message content', () => { const testCases = [ { errors: [{ message: 'Authentication failed' }], expectedCategory: ErrorCategory.AUTH, }, { errors: [{ message: 'Rate limit exceeded' }], expectedCategory: ErrorCategory.RATE_LIMIT, }, { errors: [{ message: 'Network error' }], expectedCategory: ErrorCategory.NETWORK, }, { errors: [{ message: 'Cannot query field' }], expectedCategory: ErrorCategory.SCHEMA, }, { errors: [{ message: 'Resource not found' }], expectedCategory: ErrorCategory.NOT_FOUND, }, { errors: [{ message: 'Server error' }], expectedCategory: ErrorCategory.SERVER, }, { errors: [{ message: 'Unknown error' }], expectedCategory: ErrorCategory.OTHER, }, ]; for (const testCase of testCases) { const axiosError = createMockAxiosError(200, { errors: testCase.errors }); const result = handleGraphQLSpecificError(axiosError); expect(result).not.toBeNull(); expect(result?.category).toBe(testCase.expectedCategory); } }); it('should return null for non-GraphQL errors', () => { const nonGraphqlError = createMockAxiosError(404, { message: 'Not found' }); expect(handleGraphQLSpecificError(nonGraphqlError)).toBeNull(); }); it('should return null for non-axios errors', () => { expect(handleGraphQLSpecificError(new Error('Regular error'))).toBeNull(); expect(handleGraphQLSpecificError(null)).toBeNull(); expect(handleGraphQLSpecificError(undefined)).toBeNull(); }); }); describe('handleNetworkError', () => { it('should handle ECONNREFUSED errors', () => { const connectionError = createMockAxiosError(undefined, undefined, 'ECONNREFUSED'); const result = handleNetworkError(connectionError); expect(result).not.toBeNull(); expect(result?.message).toContain('Connection error'); expect(result?.category).toBe(ErrorCategory.NETWORK); expect(result?.originalError).toBe(connectionError); }); it('should handle ETIMEDOUT errors', () => { const timeoutError = createMockAxiosError(undefined, undefined, 'ETIMEDOUT'); const result = handleNetworkError(timeoutError); expect(result).not.toBeNull(); expect(result?.message).toContain('Timeout error'); expect(result?.category).toBe(ErrorCategory.TIMEOUT); expect(result?.originalError).toBe(timeoutError); }); it('should return null for other error types', () => { const otherError = createMockAxiosError(404); expect(handleNetworkError(otherError)).toBeNull(); const nonAxiosError = new Error('Not an axios error'); expect(handleNetworkError(nonAxiosError)).toBeNull(); }); }); describe('handleHttpStatusError', () => { it('should handle 401 errors', () => { const authError = createMockAxiosError(401); const result = handleHttpStatusError(authError); expect(result).not.toBeNull(); expect(result?.message).toContain('Authentication error'); expect(result?.category).toBe(ErrorCategory.AUTH); expect(result?.originalError).toBe(authError); }); it('should handle 429 errors', () => { const rateLimitError = createMockAxiosError(429); const result = handleHttpStatusError(rateLimitError); expect(result).not.toBeNull(); expect(result?.message).toContain('Rate limit exceeded'); expect(result?.category).toBe(ErrorCategory.RATE_LIMIT); expect(result?.originalError).toBe(rateLimitError); }); it('should handle 500+ errors', () => { const serverErrors = [ { status: 500, expectedMessage: 'Server error' }, { status: 502, expectedMessage: 'Bad Gateway' }, { status: 503, expectedMessage: 'Service Unavailable' }, { status: 504, expectedMessage: 'Gateway Timeout' }, { status: 520, expectedMessage: 'Server error' }, ]; for (const { status, expectedMessage } of serverErrors) { const error = createMockAxiosError(status); const result = handleHttpStatusError(error); expect(result).not.toBeNull(); expect(result?.message).toContain(expectedMessage); expect(result?.category).toBe(ErrorCategory.SERVER); expect(result?.originalError).toBe(error); } }); it('should handle 404 errors', () => { const notFoundError = createMockAxiosError(404); const result = handleHttpStatusError(notFoundError); expect(result).not.toBeNull(); expect(result?.message).toContain('Not found'); expect(result?.category).toBe(ErrorCategory.NOT_FOUND); expect(result?.originalError).toBe(notFoundError); }); it('should handle other 4xx errors', () => { const clientErrors = [400, 403, 405, 422].map((status) => createMockAxiosError(status)); for (const error of clientErrors) { const result = handleHttpStatusError(error); expect(result).not.toBeNull(); expect(result?.message).toContain('Client error'); expect(result?.category).toBe(ErrorCategory.CLIENT); expect(result?.originalError).toBe(error); } }); it('should return null for errors without status code', () => { const noStatusError = createMockAxiosError(); expect(handleHttpStatusError(noStatusError)).toBeNull(); }); it('should return null for non-axios errors', () => { expect(handleHttpStatusError(new Error('Regular error'))).toBeNull(); expect(handleHttpStatusError(null)).toBeNull(); expect(handleHttpStatusError(undefined)).toBeNull(); }); it('should return null for status codes outside the handled ranges', () => { // Status codes like 300, 301, 302 (redirects) don't fall into the 4xx or 5xx ranges const redirectError = createMockAxiosError(302); expect(handleHttpStatusError(redirectError)).toBeNull(); }); }); describe('handleApiError', () => { it('should return input if it is already a ClassifiedError', () => { const classifiedError = { name: 'Error', message: 'Already classified', category: ErrorCategory.CLIENT, }; expect(handleApiError(classifiedError)).toBe(classifiedError); }); it('should handle GraphQL errors', () => { const graphqlErrors = [{ message: 'Cannot query field' }]; const graphqlError = createMockAxiosError(200, { errors: graphqlErrors }); // Create a real GraphQL error that will be detected by handleGraphQLSpecificError const result = handleApiError(graphqlError); expect(result.category).toBe(ErrorCategory.SCHEMA); expect(result.message).toContain('GraphQL Error'); expect(result.message).toContain('Cannot query field'); }); it('should handle network errors', () => { const networkError = createMockAxiosError(undefined, undefined, 'ECONNREFUSED'); const result = handleApiError(networkError); expect(result.category).toBe(ErrorCategory.NETWORK); expect(result.message).toContain('Connection error'); }); it('should handle HTTP errors', () => { const httpError = createMockAxiosError(401); const result = handleApiError(httpError); expect(result.category).toBe(ErrorCategory.AUTH); expect(result.message).toContain('Authentication error'); }); it('should handle standard Error objects with classification', () => { const standardError = new Error('Authentication failed'); // We can't mock the imported functions directly in ES modules // So we'll just test the result of handleApiError const result = handleApiError(standardError); expect(result.category).toBe(ErrorCategory.AUTH); // Based on message classification expect(result.message).toContain('DeepSource API error'); expect(result.message).toContain('Authentication failed'); }); it('should handle unknown error types as a fallback', () => { const unknownError = { notAnError: true }; const result = handleApiError(unknownError); expect(result.category).toBe(ErrorCategory.OTHER); expect(result.message).toContain('Unknown error occurred'); expect(result.originalError).toBe(unknownError); }); }); });

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/sapientpants/deepsource-mcp-server'

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