UNPKG

@accounter/client

Version:
121 lines (91 loc) 4.73 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { retryWithBackoff } from '../auth-callback.js'; // auth-callback.tsx imports Button, which in turn pulls in Radix/UI and DOM-dependent code. // In this non-jsdom Node test environment, mock Button so those dependencies are never loaded. vi.mock('../../ui/button.js', () => ({})); // isNetworkError (from auth0-errors) uses `error.error === 'network_error'` or message // keywords 'network' / 'fetch'. We replicate the same pattern in our test helpers. function makeNetworkError(): Error & { error: string } { return Object.assign(new Error('fetch failed'), { error: 'network_error' }); } describe('retryWithBackoff', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('resolves immediately when the first attempt succeeds', async () => { const fn = vi.fn().mockResolvedValue({ appState: { returnTo: '/charges' } }); const result = await retryWithBackoff(fn); expect(fn).toHaveBeenCalledTimes(1); expect(result).toEqual({ appState: { returnTo: '/charges' } }); }); it('retries on transient network errors and resolves on subsequent attempt', async () => { const fn = vi .fn() .mockRejectedValueOnce(makeNetworkError()) .mockRejectedValueOnce(makeNetworkError()) .mockResolvedValueOnce({ appState: { returnTo: '/charges' } }); const promise = retryWithBackoff(fn); // Attempt 0 fails → backoff 750 ms await vi.advanceTimersByTimeAsync(750); // Attempt 1 fails → backoff 1500 ms await vi.advanceTimersByTimeAsync(1500); // Attempt 2 succeeds const result = await promise; expect(fn).toHaveBeenCalledTimes(3); expect(result).toEqual({ appState: { returnTo: '/charges' } }); }); it('throws after all attempts (maxAttempts = 2) are exhausted', async () => { const fn = vi.fn().mockRejectedValue(makeNetworkError()); const promise = retryWithBackoff(fn); // Silence the "unhandled rejection" Node warning while timers advance void promise.catch(() => undefined); // Advance through both backoff windows await vi.advanceTimersByTimeAsync(750); // after attempt 0 await vi.advanceTimersByTimeAsync(1500); // after attempt 1 // Attempt 2 fails and no more retries remain await expect(promise).rejects.toMatchObject({ error: 'network_error' }); // Three total calls: attempts 0, 1, 2 expect(fn).toHaveBeenCalledTimes(3); }); it('does not retry non-network errors and throws immediately', async () => { const fn = vi.fn().mockRejectedValue(new Error('access_denied: user closed the tab')); const promise = retryWithBackoff(fn); // Silence the "unhandled rejection" Node warning while timers advance void promise.catch(() => undefined); // No timers should advance because no retry was scheduled await vi.runAllTimersAsync(); await expect(promise).rejects.toThrow('access_denied: user closed the tab'); expect(fn).toHaveBeenCalledTimes(1); }); it('applies linear (not exponential) back-off: delay 750 ms × (attempt + 1)', async () => { const fn = vi.fn().mockRejectedValue(makeNetworkError()); const promise = retryWithBackoff(fn); // Silence the "unhandled rejection" Node warning while timers advance void promise.catch(() => undefined); // After attempt 0, expect the 750 ms × 1 = 750 ms delay to fire expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(749); // just under – should NOT trigger retry 1 expect(fn).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(1); // reaches 750 ms → retry 1 fires expect(fn).toHaveBeenCalledTimes(2); // After attempt 1, expect the 750 ms × 2 = 1500 ms delay to fire await vi.advanceTimersByTimeAsync(1499); // just under – should NOT trigger retry 2 expect(fn).toHaveBeenCalledTimes(2); await vi.advanceTimersByTimeAsync(1); // reaches 1500 ms → retry 2 fires expect(fn).toHaveBeenCalledTimes(3); // All attempts exhausted await expect(promise).rejects.toMatchObject({ error: 'network_error' }); }); it('respects a custom maxAttempts override', async () => { const fn = vi.fn().mockRejectedValue(makeNetworkError()); const promise = retryWithBackoff(fn, { maxAttempts: 1, retryDelayMs: 100 }); // Silence the "unhandled rejection" Node warning while the timer advances void promise.catch(() => undefined); await vi.advanceTimersByTimeAsync(100); // one retry only await expect(promise).rejects.toMatchObject({ error: 'network_error' }); expect(fn).toHaveBeenCalledTimes(2); // initial + 1 retry }); });