2025-12-25 16:09:51 +00:00
|
|
|
/**
|
|
|
|
|
* Tests for Error Messages Utility
|
|
|
|
|
* FE-TEST-004: Test error message utility functions
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import {
|
|
|
|
|
ERROR_MESSAGES,
|
|
|
|
|
CONTEXT_ERROR_MESSAGES,
|
|
|
|
|
getErrorMessageByStatus,
|
|
|
|
|
getContextErrorMessage,
|
|
|
|
|
formatUserFriendlyError,
|
|
|
|
|
isRetryableError,
|
|
|
|
|
getRetryDelay,
|
|
|
|
|
} from './errorMessages';
|
2026-01-15 16:03:35 +00:00
|
|
|
import type { ApiError } from '@/schemas/apiSchemas';
|
2025-12-25 16:09:51 +00:00
|
|
|
|
|
|
|
|
describe('errorMessages utilities', () => {
|
|
|
|
|
describe('ERROR_MESSAGES', () => {
|
|
|
|
|
it('should have messages for common status codes', () => {
|
|
|
|
|
expect(ERROR_MESSAGES[400]).toBeTruthy();
|
|
|
|
|
expect(ERROR_MESSAGES[401]).toBeTruthy();
|
|
|
|
|
expect(ERROR_MESSAGES[404]).toBeTruthy();
|
|
|
|
|
expect(ERROR_MESSAGES[500]).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should have network error messages', () => {
|
|
|
|
|
expect(ERROR_MESSAGES.NETWORK).toBeTruthy();
|
|
|
|
|
expect(ERROR_MESSAGES.TIMEOUT).toBeTruthy();
|
|
|
|
|
expect(ERROR_MESSAGES.UNKNOWN).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getErrorMessageByStatus', () => {
|
|
|
|
|
it('should return message for known status', () => {
|
|
|
|
|
expect(getErrorMessageByStatus(404)).toBe(ERROR_MESSAGES[404]);
|
|
|
|
|
expect(getErrorMessageByStatus(500)).toBe(ERROR_MESSAGES[500]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return default message for unknown status', () => {
|
|
|
|
|
expect(getErrorMessageByStatus(999)).toBe(ERROR_MESSAGES.UNKNOWN);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use custom default message', () => {
|
|
|
|
|
const custom = 'Custom error';
|
|
|
|
|
expect(getErrorMessageByStatus(999, custom)).toBe(custom);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getContextErrorMessage', () => {
|
|
|
|
|
it('should return context-specific message', () => {
|
|
|
|
|
expect(getContextErrorMessage('auth', 'login')).toBe(
|
|
|
|
|
CONTEXT_ERROR_MESSAGES.auth.login,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return default for unknown context/action', () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
expect(getContextErrorMessage('auth', 'unknown')).toBe(
|
|
|
|
|
ERROR_MESSAGES.UNKNOWN,
|
|
|
|
|
);
|
2025-12-25 16:09:51 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use custom default message', () => {
|
|
|
|
|
const custom = 'Custom error';
|
|
|
|
|
expect(getContextErrorMessage('auth', 'unknown', custom)).toBe(custom);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('formatUserFriendlyError', () => {
|
|
|
|
|
it('should format ApiError', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 404,
|
|
|
|
|
message: 'Not found',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
const result = formatUserFriendlyError(error);
|
|
|
|
|
expect(result).toBe(ERROR_MESSAGES[404]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should format Error instance', () => {
|
|
|
|
|
const error = new Error('Test error');
|
|
|
|
|
const result = formatUserFriendlyError(error);
|
|
|
|
|
expect(result).toBe('Test error');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should format with context', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 404,
|
|
|
|
|
message: 'not found',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
const result = formatUserFriendlyError(error, 'playlist');
|
|
|
|
|
expect(result).toBe(CONTEXT_ERROR_MESSAGES.playlist.notFound);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should include details when requested', () => {
|
|
|
|
|
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 = formatUserFriendlyError(error, undefined, true);
|
|
|
|
|
// Details are included in parentheses
|
|
|
|
|
expect(result).toContain('Invalid email');
|
|
|
|
|
expect(result).toContain('Too short');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle network errors', () => {
|
|
|
|
|
const error = { code: 'ERR_NETWORK' };
|
|
|
|
|
const result = formatUserFriendlyError(error);
|
|
|
|
|
expect(result).toBe(ERROR_MESSAGES.NETWORK);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle timeout errors', () => {
|
|
|
|
|
const error = { code: 'ECONNABORTED' };
|
|
|
|
|
const result = formatUserFriendlyError(error);
|
|
|
|
|
expect(result).toBe(ERROR_MESSAGES.TIMEOUT);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return unknown for unhandled errors', () => {
|
|
|
|
|
const result = formatUserFriendlyError(null);
|
|
|
|
|
expect(result).toBe(ERROR_MESSAGES.UNKNOWN);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('isRetryableError', () => {
|
|
|
|
|
it('should return true for retryable status codes', () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
const error429: ApiError = {
|
|
|
|
|
code: 429,
|
|
|
|
|
message: 'Rate limit',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
const error500: ApiError = {
|
|
|
|
|
code: 500,
|
|
|
|
|
message: 'Server error',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
const error503: ApiError = {
|
|
|
|
|
code: 503,
|
|
|
|
|
message: 'Service unavailable',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
2025-12-25 16:09:51 +00:00
|
|
|
|
|
|
|
|
expect(isRetryableError(error429)).toBe(true);
|
|
|
|
|
expect(isRetryableError(error500)).toBe(true);
|
|
|
|
|
expect(isRetryableError(error503)).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return true for network errors', () => {
|
|
|
|
|
expect(isRetryableError({ code: 'ERR_NETWORK' })).toBe(true);
|
|
|
|
|
expect(isRetryableError({ code: 'ECONNABORTED' })).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return false for non-retryable errors', () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
const error404: ApiError = {
|
|
|
|
|
code: 404,
|
|
|
|
|
message: 'Not found',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
const error401: ApiError = {
|
|
|
|
|
code: 401,
|
|
|
|
|
message: 'Unauthorized',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
2025-12-25 16:09:51 +00:00
|
|
|
|
|
|
|
|
expect(isRetryableError(error404)).toBe(false);
|
|
|
|
|
expect(isRetryableError(error401)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getRetryDelay', () => {
|
|
|
|
|
it('should use retry_after from rate limit error', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 429,
|
|
|
|
|
message: 'Rate limit',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
retry_after: 5,
|
|
|
|
|
};
|
|
|
|
|
expect(getRetryDelay(error, 0)).toBe(5000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use exponential backoff', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 500,
|
|
|
|
|
message: 'Server error',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
expect(getRetryDelay(error, 0)).toBe(1000);
|
|
|
|
|
expect(getRetryDelay(error, 1)).toBe(2000);
|
|
|
|
|
expect(getRetryDelay(error, 2)).toBe(4000);
|
|
|
|
|
expect(getRetryDelay(error, 3)).toBe(8000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should cap at 30 seconds', () => {
|
|
|
|
|
const error: ApiError = {
|
|
|
|
|
code: 500,
|
|
|
|
|
message: 'Server error',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
expect(getRetryDelay(error, 10)).toBe(30000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|