test: add comprehensive unit tests for utils and security
- Add security.test.ts: 36 tests for webhook validation, rate limiting - Add circuit-breaker.test.ts: 31 tests for state transitions - Add retry.test.ts: 25 tests for exponential backoff - Add api-helper.test.ts: 25 tests for API abstraction - Add optimistic-lock.test.ts: 11 tests for concurrency control - Add summary-service.test.ts: 29 tests for profile system Total: 157 new test cases (222 passing overall) - Fix setup.ts D1 schema initialization for Miniflare - Update vitest.config.ts to exclude demo files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
610
tests/retry.test.ts
Normal file
610
tests/retry.test.ts
Normal file
@@ -0,0 +1,610 @@
|
||||
/**
|
||||
* Comprehensive Unit Tests for retry.ts
|
||||
*
|
||||
* 테스트 범위:
|
||||
* 1. 즉시 성공 (재시도 없음)
|
||||
* 2. 재시도 후 성공 (N번 실패 후)
|
||||
* 3. 모든 재시도 실패 (RetryError)
|
||||
* 4. 지수 백오프 검증 (delay 증가)
|
||||
* 5. maxDelay 상한선 확인
|
||||
* 6. Jitter 적용 확인
|
||||
* 7. 커스텀 옵션 테스트
|
||||
* 8. RetryError 속성 검증
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { retryWithBackoff, RetryError, RetryOptions } from '../src/utils/retry';
|
||||
import { metrics } from '../src/utils/metrics';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../src/utils/metrics', () => ({
|
||||
metrics: {
|
||||
increment: vi.fn(),
|
||||
gauge: vi.fn(),
|
||||
timing: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../src/services/notification', () => ({
|
||||
notifyAdmin: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../src/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
startTimer: vi.fn(() => vi.fn()),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('retryWithBackoff', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('즉시 성공', () => {
|
||||
it('should return result on first attempt', async () => {
|
||||
const mockFn = vi.fn().mockResolvedValue('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn);
|
||||
await vi.runAllTimersAsync();
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not add any delays on immediate success', async () => {
|
||||
const mockFn = vi.fn().mockResolvedValue(42);
|
||||
|
||||
const promise = retryWithBackoff(mockFn);
|
||||
await vi.runAllTimersAsync();
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toBe(42);
|
||||
expect(vi.getTimerCount()).toBe(0); // No pending timers
|
||||
});
|
||||
|
||||
it('should handle complex return types', async () => {
|
||||
const mockData = { id: 1, name: 'test', values: [1, 2, 3] };
|
||||
const mockFn = vi.fn().mockResolvedValue(mockData);
|
||||
|
||||
const promise = retryWithBackoff(mockFn);
|
||||
await vi.runAllTimersAsync();
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('재시도 후 성공', () => {
|
||||
it('should succeed after 1 failure', async () => {
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('First failure'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 3 });
|
||||
|
||||
// Fast-forward through delays
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(mockFn).toHaveBeenCalledTimes(2); // Initial + 1 retry
|
||||
});
|
||||
|
||||
it('should succeed after 2 failures', async () => {
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('First failure'))
|
||||
.mockRejectedValueOnce(new Error('Second failure'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 3 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(mockFn).toHaveBeenCalledTimes(3); // Initial + 2 retries
|
||||
});
|
||||
|
||||
it('should succeed on last attempt', async () => {
|
||||
const maxRetries = 3;
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Fail 2'))
|
||||
.mockRejectedValueOnce(new Error('Fail 3'))
|
||||
.mockResolvedValueOnce('success on last');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toBe('success on last');
|
||||
expect(mockFn).toHaveBeenCalledTimes(4); // Initial + 3 retries
|
||||
});
|
||||
});
|
||||
|
||||
describe('모든 재시도 실패', () => {
|
||||
it('should throw RetryError after exhausting all retries', async () => {
|
||||
const originalError = new Error('Persistent failure');
|
||||
const mockFn = vi.fn().mockRejectedValue(originalError);
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 2 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
await expect(promise).rejects.toThrow(RetryError);
|
||||
expect(mockFn).toHaveBeenCalledTimes(3); // Initial + 2 retries
|
||||
});
|
||||
|
||||
it('should preserve original error in RetryError', async () => {
|
||||
const originalError = new Error('Original failure');
|
||||
originalError.stack = 'Original stack trace';
|
||||
|
||||
const mockFn = vi.fn().mockRejectedValue(originalError);
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 1 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
try {
|
||||
await promise;
|
||||
expect.fail('Should have thrown RetryError');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(RetryError);
|
||||
|
||||
const retryError = error as RetryError;
|
||||
expect(retryError.name).toBe('RetryError');
|
||||
expect(retryError.attempts).toBe(2); // Initial + 1 retry
|
||||
expect(retryError.lastError).toBe(originalError);
|
||||
expect(retryError.lastError.message).toBe('Original failure');
|
||||
expect(retryError.lastError.stack).toBe('Original stack trace');
|
||||
}
|
||||
});
|
||||
|
||||
it('should include attempts count in RetryError message', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('API error'));
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 2 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
try {
|
||||
await promise;
|
||||
expect.fail('Should have thrown RetryError');
|
||||
} catch (error) {
|
||||
const retryError = error as RetryError;
|
||||
expect(retryError.message).toContain('3 attempts'); // Initial + 2 retries
|
||||
expect(retryError.message).toContain('API error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('지수 백오프 검증', () => {
|
||||
it('should apply exponential backoff with default multiplier (2x)', async () => {
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Fail 2'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const initialDelay = 100;
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: 3,
|
||||
initialDelayMs: initialDelay,
|
||||
jitter: false, // Disable jitter for predictable testing
|
||||
});
|
||||
|
||||
// First failure -> delay ~100ms (2^0 * 100)
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(mockFn).toHaveBeenCalledTimes(2); // Initial + 1st retry
|
||||
|
||||
// Second failure -> delay ~200ms (2^1 * 100)
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
expect(mockFn).toHaveBeenCalledTimes(3); // Initial + 2nd retry
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('success');
|
||||
});
|
||||
|
||||
it('should respect custom backoff multiplier', async () => {
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Fail 2'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const initialDelay = 100;
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: 3,
|
||||
initialDelayMs: initialDelay,
|
||||
backoffMultiplier: 3, // 3x instead of 2x
|
||||
jitter: false,
|
||||
});
|
||||
|
||||
// First failure -> delay ~100ms (3^0 * 100)
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(mockFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Second failure -> delay ~300ms (3^1 * 100)
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxDelay 상한선 확인', () => {
|
||||
it('should cap delay at maxDelayMs', async () => {
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Fail 2'))
|
||||
.mockRejectedValueOnce(new Error('Fail 3'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const maxDelay = 500;
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: 5,
|
||||
initialDelayMs: 100,
|
||||
maxDelayMs: maxDelay,
|
||||
jitter: false,
|
||||
});
|
||||
|
||||
// First retry: 100ms (2^0 * 100)
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(mockFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Second retry: 200ms (2^1 * 100)
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Third retry: Should be 400ms (2^2 * 100), still under 500ms cap
|
||||
await vi.advanceTimersByTimeAsync(400);
|
||||
expect(mockFn).toHaveBeenCalledTimes(4);
|
||||
|
||||
// Fourth retry: Would be 800ms (2^3 * 100), but capped at 500ms
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('success');
|
||||
});
|
||||
|
||||
it('should use default maxDelay (10000ms)', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Always fail'));
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: 20, // Many retries to test cap
|
||||
initialDelayMs: 1000,
|
||||
jitter: false,
|
||||
});
|
||||
|
||||
// After several retries, delay should cap at 10000ms
|
||||
// Let's verify it doesn't exceed 10000ms by checking timer behavior
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
await expect(promise).rejects.toThrow(RetryError);
|
||||
expect(mockFn).toHaveBeenCalledTimes(21); // Initial + 20 retries
|
||||
});
|
||||
});
|
||||
|
||||
describe('Jitter 적용 확인', () => {
|
||||
it('should apply jitter by default (±20%)', async () => {
|
||||
const delays: number[] = [];
|
||||
let callCount = 0;
|
||||
|
||||
const mockFn = vi.fn().mockImplementation(async () => {
|
||||
callCount++;
|
||||
if (callCount < 4) {
|
||||
// Record the delay before this call (if not first)
|
||||
throw new Error(`Fail ${callCount}`);
|
||||
}
|
||||
return 'success';
|
||||
});
|
||||
|
||||
// Spy on setTimeout to capture actual delays
|
||||
const originalSetTimeout = global.setTimeout;
|
||||
const setTimeoutSpy = vi.spyOn(global, 'setTimeout').mockImplementation((callback, ms) => {
|
||||
if (typeof ms === 'number' && ms > 0) {
|
||||
delays.push(ms);
|
||||
}
|
||||
return originalSetTimeout(callback as () => void, ms);
|
||||
});
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: 5,
|
||||
initialDelayMs: 1000,
|
||||
jitter: true, // Enable jitter (default)
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
setTimeoutSpy.mockRestore();
|
||||
|
||||
// With jitter, delays should vary within ±20% of expected values
|
||||
// Expected base delays (without jitter): [1000, 2000, 4000]
|
||||
expect(delays.length).toBeGreaterThan(0);
|
||||
|
||||
// First delay should be ~1000ms ±20% (800-1200ms)
|
||||
if (delays[0]) {
|
||||
expect(delays[0]).toBeGreaterThanOrEqual(800);
|
||||
expect(delays[0]).toBeLessThanOrEqual(1200);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not apply jitter when disabled', async () => {
|
||||
const delays: number[] = [];
|
||||
let callCount = 0;
|
||||
|
||||
const mockFn = vi.fn().mockImplementation(async () => {
|
||||
callCount++;
|
||||
if (callCount < 3) {
|
||||
throw new Error(`Fail ${callCount}`);
|
||||
}
|
||||
return 'success';
|
||||
});
|
||||
|
||||
const originalSetTimeout = global.setTimeout;
|
||||
const setTimeoutSpy = vi.spyOn(global, 'setTimeout').mockImplementation((callback, ms) => {
|
||||
if (typeof ms === 'number' && ms > 0) {
|
||||
delays.push(ms);
|
||||
}
|
||||
return originalSetTimeout(callback as () => void, ms);
|
||||
});
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 1000,
|
||||
jitter: false,
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
setTimeoutSpy.mockRestore();
|
||||
|
||||
// Without jitter, delays should be exact powers of 2
|
||||
// Expected: [1000, 2000]
|
||||
expect(delays[0]).toBe(1000); // 2^0 * 1000
|
||||
expect(delays[1]).toBe(2000); // 2^1 * 1000
|
||||
});
|
||||
});
|
||||
|
||||
describe('커스텀 옵션 테스트', () => {
|
||||
it('should respect custom maxRetries', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Always fail'));
|
||||
|
||||
const customMaxRetries = 5;
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: customMaxRetries,
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
await expect(promise).rejects.toThrow(RetryError);
|
||||
expect(mockFn).toHaveBeenCalledTimes(customMaxRetries + 1); // Initial + retries
|
||||
});
|
||||
|
||||
it('should respect custom initialDelayMs', async () => {
|
||||
const delays: number[] = [];
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Fail 1'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const originalSetTimeout = global.setTimeout;
|
||||
const setTimeoutSpy = vi.spyOn(global, 'setTimeout').mockImplementation((callback, ms) => {
|
||||
if (typeof ms === 'number' && ms > 0) {
|
||||
delays.push(ms);
|
||||
}
|
||||
return originalSetTimeout(callback as () => void, ms);
|
||||
});
|
||||
|
||||
const customInitialDelay = 500;
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
initialDelayMs: customInitialDelay,
|
||||
jitter: false,
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
setTimeoutSpy.mockRestore();
|
||||
|
||||
expect(delays[0]).toBe(customInitialDelay);
|
||||
});
|
||||
|
||||
it('should respect custom maxDelayMs', async () => {
|
||||
const delays: number[] = [];
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Fail 2'))
|
||||
.mockRejectedValueOnce(new Error('Fail 3'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const originalSetTimeout = global.setTimeout;
|
||||
const setTimeoutSpy = vi.spyOn(global, 'setTimeout').mockImplementation((callback, ms) => {
|
||||
if (typeof ms === 'number' && ms > 0) {
|
||||
delays.push(ms);
|
||||
}
|
||||
return originalSetTimeout(callback as () => void, ms);
|
||||
});
|
||||
|
||||
const customMaxDelay = 300;
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
initialDelayMs: 100,
|
||||
maxDelayMs: customMaxDelay,
|
||||
jitter: false,
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
setTimeoutSpy.mockRestore();
|
||||
|
||||
// Delays: [100, 200, 300] - third should be capped at 300 (not 400)
|
||||
expect(delays[0]).toBe(100);
|
||||
expect(delays[1]).toBe(200);
|
||||
expect(delays[2]).toBe(300); // Capped
|
||||
});
|
||||
|
||||
it('should work with maxRetries = 0 (no retries)', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Immediate fail'));
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 0 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
await expect(promise).rejects.toThrow(RetryError);
|
||||
expect(mockFn).toHaveBeenCalledTimes(1); // Only initial attempt
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle non-Error rejections', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue('String error');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 1 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
try {
|
||||
await promise;
|
||||
expect.fail('Should have thrown RetryError');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(RetryError);
|
||||
const retryError = error as RetryError;
|
||||
expect(retryError.lastError).toBeInstanceOf(Error);
|
||||
expect(retryError.lastError.message).toBe('String error');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle undefined rejections', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(undefined);
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 1 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
try {
|
||||
await promise;
|
||||
expect.fail('Should have thrown RetryError');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(RetryError);
|
||||
const retryError = error as RetryError;
|
||||
expect(retryError.lastError).toBeInstanceOf(Error);
|
||||
expect(retryError.lastError.message).toBe('undefined');
|
||||
}
|
||||
});
|
||||
|
||||
it('should work with serviceName for metrics tracking', async () => {
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Fail'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: 2,
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
// Note: metrics.increment is NOT called because we succeed on the retry
|
||||
// The metric is only tracked AFTER a retry fails (attempt > 0 && still failing)
|
||||
// In this case: attempt 0 fails -> no metric, attempt 1 succeeds -> no metric
|
||||
// Let's test a case where metrics ARE tracked
|
||||
});
|
||||
|
||||
it('should track metrics for failed retries', async () => {
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('Fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Fail 2'))
|
||||
.mockResolvedValueOnce('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
maxRetries: 3,
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await promise;
|
||||
|
||||
// Metrics should be tracked for attempt 1 (which failed)
|
||||
expect(metrics.increment).toHaveBeenCalledWith(
|
||||
'retry_count',
|
||||
expect.objectContaining({
|
||||
service: 'test-service',
|
||||
attempt: '1',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RetryError 속성 검증', () => {
|
||||
it('should have correct RetryError properties', async () => {
|
||||
const originalError = new Error('Test error');
|
||||
const mockFn = vi.fn().mockRejectedValue(originalError);
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 2 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
try {
|
||||
await promise;
|
||||
expect.fail('Should have thrown RetryError');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(RetryError);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
|
||||
const retryError = error as RetryError;
|
||||
expect(retryError.name).toBe('RetryError');
|
||||
expect(retryError.attempts).toBe(3); // Initial + 2 retries
|
||||
expect(retryError.lastError).toBe(originalError);
|
||||
expect(retryError.message).toContain('3 attempts');
|
||||
expect(retryError.message).toContain('Test error');
|
||||
}
|
||||
});
|
||||
|
||||
it('should be catchable as Error', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Fail'));
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxRetries: 0 });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
let caughtError: Error | null = null;
|
||||
|
||||
try {
|
||||
await promise;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
caughtError = error;
|
||||
}
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull();
|
||||
expect(caughtError).toBeInstanceOf(Error);
|
||||
expect(caughtError).toBeInstanceOf(RetryError);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user