veza/apps/web/src/utils/serviceErrorHandler.test.ts
senke 7df3a03e46 api-contracts: replace ApiError interface with Zod-inferred type
- Replace manual ApiError interface with Zod-inferred type from apiSchemas
- Update all imports (15+ files) to use ApiError from @/schemas/apiSchemas
- Remove ApiError interface from types/api.ts
- Update ApiResponse to import ApiError from schemas
- All TypeScript checks pass for ApiError-related code
2026-01-15 17:03:35 +01:00

210 lines
5.8 KiB
TypeScript

/**
* Tests for Service Error Handler Utility
* FE-TEST-004: Test service error handler utility functions
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AxiosError } from 'axios';
import {
handleServiceError,
withErrorHandling,
getServiceValidationErrors,
isErrorStatus,
isNetworkError,
getUserFriendlyMessage,
handleApiServiceError,
} from './serviceErrorHandler';
import type { ApiError } from '@/schemas/apiSchemas';
// Mock dependencies
vi.mock('./apiErrorHandler', () => ({
parseApiError: vi.fn((error: unknown): ApiError => {
if (error && typeof error === 'object' && 'code' in error) {
return error as ApiError;
}
return {
code: 0,
message: 'Unknown error',
timestamp: new Date().toISOString(),
};
}),
formatErrorMessage: vi.fn((error: ApiError) => error.message),
getValidationErrors: vi.fn((error: ApiError) => {
if (error.details) {
const errors: Record<string, string> = {};
error.details.forEach((detail: any) => {
if (detail.field && detail.message) {
errors[detail.field] = detail.message;
}
});
return errors;
}
return {};
}),
}));
vi.mock('./errorMessages', () => ({
formatUserFriendlyError: vi.fn((error: ApiError) => error.message),
isRetryableError: vi.fn(() => false),
}));
describe('serviceErrorHandler utilities', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('handleServiceError', () => {
it('should throw error by default', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
expect(() => handleServiceError(error)).toThrow();
});
it('should return message when throwError is false', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
const result = handleServiceError(error, { throwError: false });
expect(result).toBe('Not found');
});
it('should use custom message override', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
expect(() => {
handleServiceError(error, {
customMessages: { 404: 'Custom not found' },
});
}).toThrow('Custom not found');
});
it('should include context in error', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
try {
handleServiceError(error, { context: 'playlist' });
} catch (e: any) {
expect(e.apiError).toBeDefined();
}
});
});
describe('withErrorHandling', () => {
it('should return result on success', async () => {
const apiCall = vi.fn().mockResolvedValue('success');
const result = await withErrorHandling(apiCall);
expect(result).toBe('success');
});
it('should handle errors', async () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
const apiCall = vi.fn().mockRejectedValue(error);
await expect(withErrorHandling(apiCall)).rejects.toThrow();
});
});
describe('getServiceValidationErrors', () => {
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 = getServiceValidationErrors(error);
expect(result.email).toBe('Invalid email');
expect(result.password).toBe('Too short');
});
});
describe('isErrorStatus', () => {
it('should return true for matching status', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
expect(isErrorStatus(error, 404)).toBe(true);
});
it('should return false for non-matching status', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
expect(isErrorStatus(error, 500)).toBe(false);
});
});
describe('isNetworkError', () => {
it('should return true for AxiosError without response', () => {
const error = {
isAxiosError: true,
request: {},
response: undefined,
} as unknown as AxiosError;
expect(isNetworkError(error)).toBe(true);
});
it('should return false for AxiosError with response', () => {
const error = {
isAxiosError: true,
request: {},
response: { status: 404 },
} as unknown as AxiosError;
expect(isNetworkError(error)).toBe(false);
});
});
describe('getUserFriendlyMessage', () => {
it('should return user-friendly message', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
const result = getUserFriendlyMessage(error);
expect(result).toBe('Not found');
});
it('should use context', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
const result = getUserFriendlyMessage(error, 'playlist');
expect(result).toBe('Not found');
});
});
describe('handleApiServiceError', () => {
it('should always throw', () => {
const error: ApiError = {
code: 404,
message: 'Not found',
timestamp: new Date().toISOString(),
};
expect(() => handleApiServiceError(error)).toThrow();
});
});
});