/**
* @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);
});
});
});