Skip to content

Commit 7d41b24

Browse files
committed
test(github): add fetchWithRetry unit tests for 429 and 5xx retry logic
1 parent e15a46a commit 7d41b24

1 file changed

Lines changed: 71 additions & 0 deletions

File tree

lib/github.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
clearGitHubApiCacheForTests,
1010
GITHUB_CACHE_TTL_MS,
1111
validateGitHubUsername,
12+
fetchWithRetry,
1213
} from './github';
1314
import type { ContributionCalendar } from '../types';
1415

@@ -475,3 +476,73 @@ describe('validateGitHubUsername', () => {
475476
expect(validateGitHubUsername('in--valid')).toBe(false);
476477
});
477478
});
479+
describe('fetchWithRetry', () => {
480+
beforeEach(() => {
481+
vi.spyOn(global, 'fetch');
482+
});
483+
484+
afterEach(() => {
485+
vi.restoreAllMocks();
486+
});
487+
488+
it('resolves immediately on first attempt when fetch returns 200', async () => {
489+
vi.mocked(fetch).mockResolvedValueOnce(mockResponse({ ok: true }, 200));
490+
491+
const res = await fetchWithRetry('https://api.github.com/rest', {});
492+
493+
expect(res.status).toBe(200);
494+
expect(fetch).toHaveBeenCalledTimes(1);
495+
});
496+
497+
it('retries once on 429 (rate limit) and resolves on second attempt', async () => {
498+
vi.useFakeTimers();
499+
vi.mocked(fetch)
500+
.mockResolvedValueOnce(mockResponse({}, 429))
501+
.mockResolvedValueOnce(mockResponse({}, 200));
502+
503+
const promise = fetchWithRetry('https://api.github.com/rest', {});
504+
await vi.runAllTimersAsync();
505+
const res = await promise;
506+
507+
expect(res.status).toBe(200);
508+
expect(fetch).toHaveBeenCalledTimes(2);
509+
vi.useRealTimers();
510+
});
511+
512+
it('retries once on 500 (server error) and resolves on second attempt', async () => {
513+
vi.useFakeTimers();
514+
vi.mocked(fetch)
515+
.mockResolvedValueOnce(mockResponse({}, 500))
516+
.mockResolvedValueOnce(mockResponse({}, 200));
517+
518+
const promise = fetchWithRetry('https://api.github.com/rest', {});
519+
await vi.runAllTimersAsync();
520+
const res = await promise;
521+
522+
expect(res.status).toBe(200);
523+
expect(fetch).toHaveBeenCalledTimes(2);
524+
vi.useRealTimers();
525+
});
526+
527+
it('returns the bad response after exhausting all retries on 429', async () => {
528+
vi.useFakeTimers();
529+
vi.mocked(fetch).mockResolvedValue(mockResponse({}, 429));
530+
531+
const promise = fetchWithRetry('https://api.github.com/rest', {});
532+
await vi.runAllTimersAsync();
533+
const res = await promise;
534+
535+
expect(res.status).toBe(429);
536+
vi.useRealTimers();
537+
});
538+
539+
it('propagates network error after exhausting all retries', async () => {
540+
// Use real timers so retry delays actually fire and resolve cleanly
541+
vi.useRealTimers();
542+
vi.mocked(fetch).mockRejectedValue(new Error('network failure'));
543+
544+
await expect(fetchWithRetry('https://api.github.com/rest', {})).rejects.toThrow(
545+
'network failure'
546+
);
547+
});
548+
});

0 commit comments

Comments
 (0)