2025-12-25 16:09:51 +00:00
|
|
|
/**
|
|
|
|
|
* Tests for API Error Handler Utility
|
|
|
|
|
* FE-TEST-004: Test API error handler utility functions
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { AxiosError } from 'axios';
|
2026-01-13 18:47:57 +00:00
|
|
|
import {
|
|
|
|
|
parseApiError,
|
|
|
|
|
formatErrorMessage,
|
|
|
|
|
getValidationErrors,
|
|
|
|
|
} from './apiErrorHandler';
|
2025-12-25 16:09:51 +00:00
|
|
|
import type { ApiError } from '@/types/api';
|
|
|
|
|
|
|
|
|
|
// Mock timeoutHandler
|
|
|
|
|
vi.mock('./timeoutHandler', () => ({
|
|
|
|
|
isTimeoutError: vi.fn((error: unknown) => {
|
|
|
|
|
if (error && typeof error === 'object' && 'code' in error) {
|
2026-01-13 18:47:57 +00:00
|
|
|
return (
|
|
|
|
|
(error as any).code === 'ECONNABORTED' ||
|
|
|
|
|
(error as any).code === 'ETIMEDOUT'
|
|
|
|
|
);
|
2025-12-25 16:09:51 +00:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}),
|
|
|
|
|
TIMEOUT_MESSAGES: {
|
|
|
|
|
timeout: 'Request timeout',
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
describe('apiErrorHandler utilities', () => {
|
|
|
|
|
describe('parseApiError', () => {
|
|
|
|
|
it('should return ApiError as-is', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 404,
|
|
|
|
|
message: 'Not found',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
const result = parseApiError(error);
|
|
|
|
|
expect(result).toEqual(error);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse AxiosError with standard format', () => {
|
|
|
|
|
const axiosError = {
|
|
|
|
|
isAxiosError: true,
|
|
|
|
|
response: {
|
|
|
|
|
status: 404,
|
|
|
|
|
data: {
|
|
|
|
|
success: false,
|
|
|
|
|
error: {
|
|
|
|
|
code: 404,
|
|
|
|
|
message: 'Not found',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as unknown as AxiosError;
|
|
|
|
|
|
|
|
|
|
const result = parseApiError(axiosError);
|
|
|
|
|
expect(result.code).toBe(404);
|
|
|
|
|
expect(result.message).toBe('Not found');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse AxiosError with Gin middleware format', () => {
|
|
|
|
|
const axiosError = {
|
|
|
|
|
isAxiosError: true,
|
|
|
|
|
response: {
|
|
|
|
|
status: 422,
|
|
|
|
|
data: {
|
|
|
|
|
error: {
|
|
|
|
|
code: 422,
|
|
|
|
|
message: 'Validation failed',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as unknown as AxiosError;
|
|
|
|
|
|
|
|
|
|
const result = parseApiError(axiosError);
|
|
|
|
|
expect(result.code).toBe(422);
|
|
|
|
|
expect(result.message).toBe('Validation failed');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse network error', () => {
|
|
|
|
|
const axiosError = {
|
|
|
|
|
isAxiosError: true,
|
|
|
|
|
request: {},
|
|
|
|
|
response: undefined,
|
|
|
|
|
} as unknown as AxiosError;
|
|
|
|
|
|
|
|
|
|
const result = parseApiError(axiosError);
|
|
|
|
|
expect(result.code).toBe(0);
|
|
|
|
|
expect(result.message).toContain('Network error');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse timeout error', () => {
|
|
|
|
|
const axiosError = {
|
|
|
|
|
isAxiosError: true,
|
|
|
|
|
request: {},
|
|
|
|
|
response: undefined,
|
|
|
|
|
code: 'ECONNABORTED',
|
|
|
|
|
} as unknown as AxiosError;
|
|
|
|
|
|
|
|
|
|
const result = parseApiError(axiosError);
|
|
|
|
|
expect(result.code).toBe(0);
|
|
|
|
|
expect(result.message).toBe('Request timeout');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse rate limit error with headers', () => {
|
|
|
|
|
const axiosError = {
|
|
|
|
|
isAxiosError: true,
|
|
|
|
|
response: {
|
|
|
|
|
status: 429,
|
|
|
|
|
headers: {
|
|
|
|
|
'x-ratelimit-limit': '100',
|
|
|
|
|
'x-ratelimit-remaining': '0',
|
|
|
|
|
'x-ratelimit-reset': String(Math.floor(Date.now() / 1000) + 60),
|
|
|
|
|
'retry-after': '60',
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
error: {
|
|
|
|
|
message: 'Rate limit exceeded',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as unknown as AxiosError;
|
|
|
|
|
|
|
|
|
|
const result = parseApiError(axiosError);
|
|
|
|
|
expect(result.code).toBe(429);
|
|
|
|
|
expect(result.rate_limit).toBeDefined();
|
|
|
|
|
expect(result.retry_after).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse service unavailable error', () => {
|
|
|
|
|
const axiosError = {
|
|
|
|
|
isAxiosError: true,
|
|
|
|
|
response: {
|
|
|
|
|
status: 503,
|
|
|
|
|
data: {
|
|
|
|
|
message: 'Service unavailable',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as unknown as AxiosError;
|
|
|
|
|
|
|
|
|
|
const result = parseApiError(axiosError);
|
|
|
|
|
expect(result.code).toBe(503);
|
|
|
|
|
expect(result.message).toContain('indisponible');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse standard Error', () => {
|
|
|
|
|
const error = new Error('Test error');
|
|
|
|
|
const result = parseApiError(error);
|
|
|
|
|
expect(result.code).toBe(0);
|
|
|
|
|
expect(result.message).toBe('Test error');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle unknown error', () => {
|
|
|
|
|
const result = parseApiError(null);
|
|
|
|
|
expect(result.code).toBe(0);
|
|
|
|
|
expect(result.message).toBe('An unexpected error occurred');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('formatErrorMessage', () => {
|
|
|
|
|
it('should format simple error', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 404,
|
|
|
|
|
message: 'Not found',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
const result = formatErrorMessage(error);
|
|
|
|
|
expect(result).toBe('Not found');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should include validation details', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 422,
|
|
|
|
|
message: 'Validation failed',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
details: [
|
|
|
|
|
{ field: 'email', message: 'Invalid email' },
|
|
|
|
|
{ field: 'password', message: 'Too short' },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
const result = formatErrorMessage(error);
|
|
|
|
|
expect(result).toContain('email');
|
|
|
|
|
expect(result).toContain('password');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should include request_id in dev mode', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 500,
|
|
|
|
|
message: 'Server error',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
request_id: 'req-123',
|
|
|
|
|
};
|
|
|
|
|
const result = formatErrorMessage(error, true);
|
|
|
|
|
expect(result).toContain('req-123');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getValidationErrors', () => {
|
|
|
|
|
it('should extract validation errors', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 422,
|
|
|
|
|
message: 'Validation failed',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
details: [
|
|
|
|
|
{ field: 'email', message: 'Invalid email' },
|
|
|
|
|
{ field: 'password', message: 'Too short' },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
const result = getValidationErrors(error);
|
|
|
|
|
expect(result.email).toBe('Invalid email');
|
|
|
|
|
expect(result.password).toBe('Too short');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return empty object for errors without details', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 404,
|
|
|
|
|
message: 'Not found',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
const result = getValidationErrors(error);
|
|
|
|
|
expect(result).toEqual({});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should filter out invalid details', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 422,
|
|
|
|
|
message: 'Validation failed',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
details: [
|
|
|
|
|
{ field: 'email', message: 'Invalid email' },
|
|
|
|
|
{ field: '', message: 'No field' },
|
|
|
|
|
{ message: 'No field' },
|
|
|
|
|
] as any,
|
|
|
|
|
};
|
|
|
|
|
const result = getValidationErrors(error);
|
|
|
|
|
expect(result.email).toBe('Invalid email');
|
|
|
|
|
expect(result['']).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|