veza/apps/web/src/utils/apiErrorHandler.test.ts

244 lines
6.8 KiB
TypeScript
Raw Normal View History

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