fix issues
This commit is contained in:
165
apps/website/lib/api/base/ApiConnectionMonitor.test.ts
Normal file
165
apps/website/lib/api/base/ApiConnectionMonitor.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { ApiConnectionMonitor } from './ApiConnectionMonitor';
|
||||
|
||||
describe('ApiConnectionMonitor', () => {
|
||||
let monitor: ApiConnectionMonitor;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset singleton instance
|
||||
(ApiConnectionMonitor as any).instance = undefined;
|
||||
monitor = ApiConnectionMonitor.getInstance();
|
||||
});
|
||||
|
||||
describe('getInstance', () => {
|
||||
it('should return a singleton instance', () => {
|
||||
const instance1 = ApiConnectionMonitor.getInstance();
|
||||
const instance2 = ApiConnectionMonitor.getInstance();
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startMonitoring', () => {
|
||||
it('should start monitoring without errors', () => {
|
||||
expect(() => monitor.startMonitoring()).not.toThrow();
|
||||
});
|
||||
|
||||
it('should be idempotent', () => {
|
||||
monitor.startMonitoring();
|
||||
expect(() => monitor.startMonitoring()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordSuccess', () => {
|
||||
it('should record a successful request', () => {
|
||||
const responseTime = 100;
|
||||
monitor.recordSuccess(responseTime);
|
||||
|
||||
const health = monitor.getHealth();
|
||||
expect(health.totalRequests).toBe(1);
|
||||
expect(health.successfulRequests).toBe(1);
|
||||
expect(health.failedRequests).toBe(0);
|
||||
});
|
||||
|
||||
it('should update average response time', () => {
|
||||
monitor.recordSuccess(100);
|
||||
monitor.recordSuccess(200);
|
||||
|
||||
const health = monitor.getHealth();
|
||||
expect(health.averageResponseTime).toBe(150);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordFailure', () => {
|
||||
it('should record a failed request', () => {
|
||||
const error = new Error('Test error');
|
||||
monitor.recordFailure(error);
|
||||
|
||||
const health = monitor.getHealth();
|
||||
expect(health.totalRequests).toBe(1);
|
||||
expect(health.successfulRequests).toBe(0);
|
||||
expect(health.failedRequests).toBe(1);
|
||||
});
|
||||
|
||||
it('should track consecutive failures', () => {
|
||||
const error1 = new Error('Error 1');
|
||||
const error2 = new Error('Error 2');
|
||||
|
||||
monitor.recordFailure(error1);
|
||||
monitor.recordFailure(error2);
|
||||
|
||||
const health = monitor.getHealth();
|
||||
expect(health.consecutiveFailures).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('should return current status', () => {
|
||||
const status = monitor.getStatus();
|
||||
expect(typeof status).toBe('string');
|
||||
expect(['connected', 'disconnected', 'degraded', 'checking']).toContain(status);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHealth', () => {
|
||||
it('should return health metrics', () => {
|
||||
const health = monitor.getHealth();
|
||||
expect(health).toHaveProperty('status');
|
||||
expect(health).toHaveProperty('lastCheck');
|
||||
expect(health).toHaveProperty('lastSuccess');
|
||||
expect(health).toHaveProperty('lastFailure');
|
||||
expect(health).toHaveProperty('consecutiveFailures');
|
||||
expect(health).toHaveProperty('totalRequests');
|
||||
expect(health).toHaveProperty('successfulRequests');
|
||||
expect(health).toHaveProperty('failedRequests');
|
||||
expect(health).toHaveProperty('averageResponseTime');
|
||||
});
|
||||
|
||||
it('should calculate success rate correctly', () => {
|
||||
monitor.recordSuccess(100);
|
||||
monitor.recordSuccess(100);
|
||||
monitor.recordFailure(new Error('Test'));
|
||||
|
||||
const health = monitor.getHealth();
|
||||
const successRate = health.successfulRequests / health.totalRequests;
|
||||
expect(successRate).toBeCloseTo(2/3, 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAvailable', () => {
|
||||
it('should return true when healthy', () => {
|
||||
// Record some successful requests
|
||||
for (let i = 0; i < 5; i++) {
|
||||
monitor.recordSuccess(100);
|
||||
}
|
||||
|
||||
expect(monitor.isAvailable()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when many failures occur', () => {
|
||||
// Record many failures
|
||||
for (let i = 0; i < 10; i++) {
|
||||
monitor.recordFailure(new Error('Test'));
|
||||
}
|
||||
|
||||
expect(monitor.isAvailable()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReliability', () => {
|
||||
it('should return reliability score', () => {
|
||||
monitor.recordSuccess(100);
|
||||
monitor.recordSuccess(100);
|
||||
monitor.recordSuccess(100);
|
||||
monitor.recordFailure(new Error('Test'));
|
||||
|
||||
const reliability = monitor.getReliability();
|
||||
expect(reliability).toBeGreaterThanOrEqual(0);
|
||||
expect(reliability).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
it('should return 1 for perfect reliability', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
monitor.recordSuccess(100);
|
||||
}
|
||||
|
||||
expect(monitor.getReliability()).toBe(100);
|
||||
});
|
||||
|
||||
it('should return 0 for complete failure', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
monitor.recordFailure(new Error('Test'));
|
||||
}
|
||||
|
||||
expect(monitor.getReliability()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('performHealthCheck', () => {
|
||||
it('should return health check result', async () => {
|
||||
const result = await monitor.performHealthCheck();
|
||||
expect(result).toHaveProperty('timestamp');
|
||||
expect(result).toHaveProperty('healthy');
|
||||
expect(result).toHaveProperty('responseTime');
|
||||
});
|
||||
});
|
||||
});
|
||||
272
apps/website/lib/api/base/ApiError.test.ts
Normal file
272
apps/website/lib/api/base/ApiError.test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ApiError, isApiError, isNetworkError, isAuthError, isRetryableError } from './ApiError';
|
||||
import type { ApiErrorType, ApiErrorContext } from './ApiError';
|
||||
|
||||
describe('ApiError', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create an ApiError with correct properties', () => {
|
||||
const context: ApiErrorContext = {
|
||||
endpoint: '/api/test',
|
||||
method: 'GET',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
statusCode: 500,
|
||||
};
|
||||
|
||||
const error = new ApiError('Test error', 'SERVER_ERROR', context);
|
||||
|
||||
expect(error.message).toBe('Test error');
|
||||
expect(error.type).toBe('SERVER_ERROR');
|
||||
expect(error.context).toEqual(context);
|
||||
expect(error.name).toBe('ApiError');
|
||||
});
|
||||
|
||||
it('should accept an optional originalError', () => {
|
||||
const originalError = new Error('Original');
|
||||
const context: ApiErrorContext = { timestamp: '2024-01-01T00:00:00Z' };
|
||||
|
||||
const error = new ApiError('Wrapped', 'NETWORK_ERROR', context, originalError);
|
||||
|
||||
expect(error.originalError).toBe(originalError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserMessage', () => {
|
||||
it('should return correct user message for NETWORK_ERROR', () => {
|
||||
const error = new ApiError('Connection failed', 'NETWORK_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('Unable to connect to the server. Please check your internet connection.');
|
||||
});
|
||||
|
||||
it('should return correct user message for AUTH_ERROR', () => {
|
||||
const error = new ApiError('Unauthorized', 'AUTH_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('Authentication required. Please log in again.');
|
||||
});
|
||||
|
||||
it('should return correct user message for VALIDATION_ERROR', () => {
|
||||
const error = new ApiError('Invalid data', 'VALIDATION_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('The data you provided is invalid. Please check your input.');
|
||||
});
|
||||
|
||||
it('should return correct user message for NOT_FOUND', () => {
|
||||
const error = new ApiError('Not found', 'NOT_FOUND', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('The requested resource was not found.');
|
||||
});
|
||||
|
||||
it('should return correct user message for SERVER_ERROR', () => {
|
||||
const error = new ApiError('Server error', 'SERVER_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('Server is experiencing issues. Please try again later.');
|
||||
});
|
||||
|
||||
it('should return correct user message for RATE_LIMIT_ERROR', () => {
|
||||
const error = new ApiError('Rate limited', 'RATE_LIMIT_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('Too many requests. Please wait a moment and try again.');
|
||||
});
|
||||
|
||||
it('should return correct user message for TIMEOUT_ERROR', () => {
|
||||
const error = new ApiError('Timeout', 'TIMEOUT_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('Request timed out. Please try again.');
|
||||
});
|
||||
|
||||
it('should return correct user message for CANCELED_ERROR', () => {
|
||||
const error = new ApiError('Canceled', 'CANCELED_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('Request was canceled.');
|
||||
});
|
||||
|
||||
it('should return correct user message for UNKNOWN_ERROR', () => {
|
||||
const error = new ApiError('Unknown', 'UNKNOWN_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getUserMessage()).toBe('An unexpected error occurred. Please try again.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeveloperMessage', () => {
|
||||
it('should return developer message with type and message', () => {
|
||||
const error = new ApiError('Test error', 'NETWORK_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getDeveloperMessage()).toBe('[NETWORK_ERROR] Test error');
|
||||
});
|
||||
|
||||
it('should include endpoint and method when available', () => {
|
||||
const context: ApiErrorContext = {
|
||||
endpoint: '/api/users',
|
||||
method: 'POST',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
const error = new ApiError('Test error', 'SERVER_ERROR', context);
|
||||
expect(error.getDeveloperMessage()).toBe('[SERVER_ERROR] Test error POST /api/users');
|
||||
});
|
||||
|
||||
it('should include status code when available', () => {
|
||||
const context: ApiErrorContext = {
|
||||
endpoint: '/api/users',
|
||||
method: 'GET',
|
||||
statusCode: 404,
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
const error = new ApiError('Not found', 'NOT_FOUND', context);
|
||||
expect(error.getDeveloperMessage()).toBe('[NOT_FOUND] Not found GET /api/users status:404');
|
||||
});
|
||||
|
||||
it('should include retry count when available', () => {
|
||||
const context: ApiErrorContext = {
|
||||
endpoint: '/api/users',
|
||||
method: 'GET',
|
||||
retryCount: 3,
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
const error = new ApiError('Failed', 'NETWORK_ERROR', context);
|
||||
expect(error.getDeveloperMessage()).toBe('[NETWORK_ERROR] Failed GET /api/users retry:3');
|
||||
});
|
||||
|
||||
it('should include all context fields when available', () => {
|
||||
const context: ApiErrorContext = {
|
||||
endpoint: '/api/users',
|
||||
method: 'POST',
|
||||
statusCode: 500,
|
||||
retryCount: 2,
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
const error = new ApiError('Server error', 'SERVER_ERROR', context);
|
||||
expect(error.getDeveloperMessage()).toBe('[SERVER_ERROR] Server error POST /api/users status:500 retry:2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRetryable', () => {
|
||||
it('should return true for retryable error types', () => {
|
||||
const retryableTypes = ['NETWORK_ERROR', 'SERVER_ERROR', 'RATE_LIMIT_ERROR', 'TIMEOUT_ERROR'];
|
||||
|
||||
retryableTypes.forEach(type => {
|
||||
const error = new ApiError('Test', type as ApiErrorType, { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.isRetryable()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for non-retryable error types', () => {
|
||||
const nonRetryableTypes = ['AUTH_ERROR', 'VALIDATION_ERROR', 'NOT_FOUND', 'CANCELED_ERROR', 'UNKNOWN_ERROR'];
|
||||
|
||||
nonRetryableTypes.forEach(type => {
|
||||
const error = new ApiError('Test', type as ApiErrorType, { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.isRetryable()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConnectivityIssue', () => {
|
||||
it('should return true for NETWORK_ERROR', () => {
|
||||
const error = new ApiError('Network', 'NETWORK_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.isConnectivityIssue()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for TIMEOUT_ERROR', () => {
|
||||
const error = new ApiError('Timeout', 'TIMEOUT_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.isConnectivityIssue()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other error types', () => {
|
||||
const otherTypes = ['AUTH_ERROR', 'VALIDATION_ERROR', 'NOT_FOUND', 'SERVER_ERROR', 'RATE_LIMIT_ERROR', 'CANCELED_ERROR', 'UNKNOWN_ERROR'];
|
||||
|
||||
otherTypes.forEach(type => {
|
||||
const error = new ApiError('Test', type as ApiErrorType, { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.isConnectivityIssue()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeverity', () => {
|
||||
it('should return "warn" for AUTH_ERROR', () => {
|
||||
const error = new ApiError('Auth', 'AUTH_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getSeverity()).toBe('warn');
|
||||
});
|
||||
|
||||
it('should return "warn" for VALIDATION_ERROR', () => {
|
||||
const error = new ApiError('Validation', 'VALIDATION_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getSeverity()).toBe('warn');
|
||||
});
|
||||
|
||||
it('should return "warn" for NOT_FOUND', () => {
|
||||
const error = new ApiError('Not found', 'NOT_FOUND', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getSeverity()).toBe('warn');
|
||||
});
|
||||
|
||||
it('should return "info" for RATE_LIMIT_ERROR', () => {
|
||||
const error = new ApiError('Rate limited', 'RATE_LIMIT_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getSeverity()).toBe('info');
|
||||
});
|
||||
|
||||
it('should return "info" for CANCELED_ERROR', () => {
|
||||
const error = new ApiError('Canceled', 'CANCELED_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getSeverity()).toBe('info');
|
||||
});
|
||||
|
||||
it('should return "error" for other error types', () => {
|
||||
const errorTypes = ['NETWORK_ERROR', 'SERVER_ERROR', 'TIMEOUT_ERROR', 'UNKNOWN_ERROR'];
|
||||
|
||||
errorTypes.forEach(type => {
|
||||
const error = new ApiError('Test', type as ApiErrorType, { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(error.getSeverity()).toBe('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type guards', () => {
|
||||
describe('isApiError', () => {
|
||||
it('should return true for ApiError instances', () => {
|
||||
const error = new ApiError('Test', 'NETWORK_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(isApiError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-ApiError instances', () => {
|
||||
expect(isApiError(new Error('Test'))).toBe(false);
|
||||
expect(isApiError('string')).toBe(false);
|
||||
expect(isApiError(null)).toBe(false);
|
||||
expect(isApiError(undefined)).toBe(false);
|
||||
expect(isApiError({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNetworkError', () => {
|
||||
it('should return true for NETWORK_ERROR ApiError', () => {
|
||||
const error = new ApiError('Network', 'NETWORK_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(isNetworkError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other error types', () => {
|
||||
const error = new ApiError('Auth', 'AUTH_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(isNetworkError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-ApiError', () => {
|
||||
expect(isNetworkError(new Error('Test'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAuthError', () => {
|
||||
it('should return true for AUTH_ERROR ApiError', () => {
|
||||
const error = new ApiError('Auth', 'AUTH_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(isAuthError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other error types', () => {
|
||||
const error = new ApiError('Network', 'NETWORK_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(isAuthError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-ApiError', () => {
|
||||
expect(isAuthError(new Error('Test'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRetryableError', () => {
|
||||
it('should return true for retryable ApiError', () => {
|
||||
const error = new ApiError('Server', 'SERVER_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(isRetryableError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-retryable ApiError', () => {
|
||||
const error = new ApiError('Auth', 'AUTH_ERROR', { timestamp: '2024-01-01T00:00:00Z' });
|
||||
expect(isRetryableError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-ApiError', () => {
|
||||
expect(isRetryableError(new Error('Test'))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -86,9 +86,11 @@ export class ApiError extends Error {
|
||||
this.context.endpoint,
|
||||
this.context.statusCode ? `status:${this.context.statusCode}` : null,
|
||||
this.context.retryCount ? `retry:${this.context.retryCount}` : null,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return `${base} ${ctx ? `(${ctx})` : ''}`;
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return ctx ? `${base} ${ctx}` : base;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,4 +148,4 @@ export function isAuthError(error: unknown): boolean {
|
||||
|
||||
export function isRetryableError(error: unknown): boolean {
|
||||
return isApiError(error) && error.isRetryable();
|
||||
}
|
||||
}
|
||||
|
||||
8
apps/website/lib/api/base/BaseApiClient.test.ts
Normal file
8
apps/website/lib/api/base/BaseApiClient.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BaseApiClient } from './BaseApiClient';
|
||||
|
||||
describe('BaseApiClient', () => {
|
||||
it('should be defined', () => {
|
||||
expect(BaseApiClient).toBeDefined();
|
||||
});
|
||||
});
|
||||
10
apps/website/lib/api/base/GracefulDegradation.test.ts
Normal file
10
apps/website/lib/api/base/GracefulDegradation.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { GracefulService, responseCache, withGracefulDegradation } from './GracefulDegradation';
|
||||
|
||||
describe('GracefulDegradation', () => {
|
||||
it('should export graceful degradation utilities', () => {
|
||||
expect(withGracefulDegradation).toBeDefined();
|
||||
expect(responseCache).toBeDefined();
|
||||
expect(GracefulService).toBeDefined();
|
||||
});
|
||||
});
|
||||
126
apps/website/lib/api/base/RetryHandler.test.ts
Normal file
126
apps/website/lib/api/base/RetryHandler.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { RetryHandler, CircuitBreaker, CircuitBreakerRegistry, DEFAULT_RETRY_CONFIG } from './RetryHandler';
|
||||
|
||||
describe('RetryHandler', () => {
|
||||
let handler: RetryHandler;
|
||||
const fastConfig = {
|
||||
...DEFAULT_RETRY_CONFIG,
|
||||
baseDelay: 1,
|
||||
maxDelay: 1,
|
||||
backoffMultiplier: 1,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new RetryHandler(fastConfig);
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute function successfully without retry', async () => {
|
||||
const fn = vi.fn().mockResolvedValue('success');
|
||||
const result = await handler.execute(fn);
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should retry on failure and eventually succeed', async () => {
|
||||
const fn = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('First attempt'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const result = await handler.execute(fn);
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should exhaust retries and throw final error', async () => {
|
||||
const fn = vi.fn().mockRejectedValue(new Error('Always fails'));
|
||||
|
||||
await expect(handler.execute(fn)).rejects.toThrow('Always fails');
|
||||
expect(fn).toHaveBeenCalledTimes(fastConfig.maxRetries + 1);
|
||||
});
|
||||
|
||||
it('should respect custom retry config', async () => {
|
||||
const customConfig = { ...fastConfig, maxRetries: 2 };
|
||||
const customHandler = new RetryHandler(customConfig);
|
||||
const fn = vi.fn().mockRejectedValue(new Error('Fail'));
|
||||
|
||||
await expect(customHandler.execute(fn)).rejects.toThrow('Fail');
|
||||
expect(fn).toHaveBeenCalledTimes(3); // 2 retries + 1 initial
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CircuitBreaker', () => {
|
||||
let breaker: CircuitBreaker;
|
||||
|
||||
beforeEach(() => {
|
||||
breaker = new CircuitBreaker({ failureThreshold: 3, successThreshold: 1, timeout: 1000 });
|
||||
});
|
||||
|
||||
describe('canExecute', () => {
|
||||
it('should allow execution when closed', () => {
|
||||
expect(breaker.canExecute()).toBe(true);
|
||||
});
|
||||
|
||||
it('should prevent execution when open', () => {
|
||||
breaker.recordFailure();
|
||||
breaker.recordFailure();
|
||||
breaker.recordFailure();
|
||||
|
||||
expect(breaker.canExecute()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordSuccess', () => {
|
||||
it('should reset failure count on success', () => {
|
||||
breaker.recordFailure();
|
||||
breaker.recordFailure();
|
||||
breaker.recordSuccess();
|
||||
|
||||
// Should be closed again
|
||||
expect(breaker.canExecute()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordFailure', () => {
|
||||
it('should increment failure count', () => {
|
||||
breaker.recordFailure();
|
||||
expect(breaker.canExecute()).toBe(true);
|
||||
|
||||
breaker.recordFailure();
|
||||
expect(breaker.canExecute()).toBe(true);
|
||||
|
||||
breaker.recordFailure();
|
||||
expect(breaker.canExecute()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CircuitBreakerRegistry', () => {
|
||||
it('should return singleton instance', () => {
|
||||
const registry1 = CircuitBreakerRegistry.getInstance();
|
||||
const registry2 = CircuitBreakerRegistry.getInstance();
|
||||
expect(registry1).toBe(registry2);
|
||||
});
|
||||
|
||||
it('should return same breaker for same path', () => {
|
||||
const registry = CircuitBreakerRegistry.getInstance();
|
||||
const breaker1 = registry.getBreaker('/api/test');
|
||||
const breaker2 = registry.getBreaker('/api/test');
|
||||
expect(breaker1).toBe(breaker2);
|
||||
});
|
||||
|
||||
it('should return different breakers for different paths', () => {
|
||||
const registry = CircuitBreakerRegistry.getInstance();
|
||||
const breaker1 = registry.getBreaker('/api/test1');
|
||||
const breaker2 = registry.getBreaker('/api/test2');
|
||||
expect(breaker1).not.toBe(breaker2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user