2025-12-25 16:09:51 +00:00
|
|
|
/**
|
|
|
|
|
* 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';
|
2026-01-15 16:03:35 +00:00
|
|
|
import type { ApiError } from '@/schemas/apiSchemas';
|
2025-12-25 16:09:51 +00:00
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|