/** * 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'; import { parseApiError, formatErrorMessage, getValidationErrors, } from './apiErrorHandler'; import type { ApiError } from '@/schemas/apiSchemas'; // Mock timeoutHandler vi.mock('./timeoutHandler', () => ({ isTimeoutError: vi.fn((error: unknown) => { if (error && typeof error === 'object' && 'code' in error) { return ( (error as any).code === 'ECONNABORTED' || (error as any).code === 'ETIMEDOUT' ); } 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(); }); }); });