From a6527ca27e491bc937b2d41b2d88e0457b5bb13a Mon Sep 17 00:00:00 2001 From: Pari Sangamnerkar Date: Fri, 12 Jun 2026 13:25:38 +0530 Subject: [PATCH 1/2] test(stats): add validation and cache behavior coverage --- app/api/stats/route.validation.test.ts | 137 +++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 app/api/stats/route.validation.test.ts diff --git a/app/api/stats/route.validation.test.ts b/app/api/stats/route.validation.test.ts new file mode 100644 index 000000000..8af826b4b --- /dev/null +++ b/app/api/stats/route.validation.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { GET } from './route'; + +vi.mock('@/lib/github', () => ({ + fetchGitHubContributions: vi.fn(), +})); + +import { fetchGitHubContributions } from '@/lib/github'; +import type { ContributionCalendar } from '@/types'; +import { quotaMonitor } from '@/services/github/quota-monitor'; +import { refreshPolicy } from '@/services/github/refresh-policy'; +import { refreshRateLimiter } from '@/services/github/refresh-rate-limiter'; + +const mockCalendar: ContributionCalendar = { + totalContributions: 15, + weeks: [ + { + contributionDays: [ + { contributionCount: 5, date: '2024-06-10' }, + { contributionCount: 10, date: '2024-06-11' }, + ], + }, + ], +}; + +function makeRequest(params: Record = {}): Request { + const url = new URL('http://localhost/api/stats'); + + for (const [key, value] of Object.entries(params)) { + url.searchParams.set(key, value); + } + + return new Request(url.toString(), { + headers: new Headers({ + 'x-forwarded-for': '127.0.0.1', + }), + }); +} + +describe('GET /api/stats validation and cache coverage', () => { + beforeEach(() => { + vi.clearAllMocks(); + + quotaMonitor.reset(); + refreshPolicy.reset(); + refreshRateLimiter.reset(); + + vi.mocked(fetchGitHubContributions).mockResolvedValue({ + calendar: mockCalendar, + repoContributions: [], + }); + }); + + it('treats bypassCache=true as a refresh request', async () => { + const response = await GET( + makeRequest({ + user: 'octocat', + bypassCache: 'true', + }) + ); + + expect(response.status).toBe(200); + + expect(fetchGitHubContributions).toHaveBeenCalledWith('octocat', { + bypassCache: true, + }); + + expect(response.headers.get('X-Refresh-Status')).toBe('Fresh'); + }); + + it('returns cache HIT status for normal requests', async () => { + const response = await GET( + makeRequest({ + user: 'octocat', + }) + ); + + expect(response.status).toBe(200); + + expect(response.headers.get('X-Cache-Status')).toBe('HIT'); + expect(response.headers.get('X-Refresh-Status')).toBe('Cached'); + }); + + it('returns 404 when github lookup cannot resolve user', async () => { + vi.mocked(fetchGitHubContributions).mockRejectedValue(new Error('Could not resolve user')); + + const response = await GET( + makeRequest({ + user: 'missing-user', + }) + ); + + expect(response.status).toBe(404); + + const body = await response.json(); + + expect(body.error).toBe('User not found'); + }); + + it('returns rate limit headers when refresh limit is exceeded', async () => { + vi.spyOn(refreshRateLimiter, 'checkLimit').mockReturnValue({ + success: false, + limit: 10, + remaining: 0, + reset: 12345, + }); + + const response = await GET( + makeRequest({ + user: 'octocat', + refresh: 'true', + }) + ); + + expect(response.status).toBe(429); + + expect(response.headers.get('X-RateLimit-Limit')).toBe('10'); + expect(response.headers.get('X-RateLimit-Remaining')).toBe('0'); + expect(response.headers.get('X-RateLimit-Reset')).toBe('12345'); + }); + + it('returns github rate limit error when api limit is reached', async () => { + vi.mocked(fetchGitHubContributions).mockRejectedValue(new Error('status 403')); + + const response = await GET( + makeRequest({ + user: 'octocat', + }) + ); + + expect(response.status).toBe(429); + + const body = await response.json(); + + expect(body.error).toContain('GitHub API rate limit reached'); + }); +}); From d0a3bd218d4cb7837c6d60d0b177091c8327727d Mon Sep 17 00:00:00 2001 From: Pari Sangamnerkar Date: Fri, 12 Jun 2026 21:02:52 +0530 Subject: [PATCH 2/2] test(stats): improve runtime coverage --- app/api/stats/route.validation.test.ts | 57 +++++++++++++------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/app/api/stats/route.validation.test.ts b/app/api/stats/route.validation.test.ts index 8af826b4b..b15d2fc68 100644 --- a/app/api/stats/route.validation.test.ts +++ b/app/api/stats/route.validation.test.ts @@ -37,7 +37,7 @@ function makeRequest(params: Record = {}): Request { }); } -describe('GET /api/stats validation and cache coverage', () => { +describe('GET /api/stats additional runtime coverage', () => { beforeEach(() => { vi.clearAllMocks(); @@ -66,6 +66,7 @@ describe('GET /api/stats validation and cache coverage', () => { }); expect(response.headers.get('X-Refresh-Status')).toBe('Fresh'); + expect(response.headers.get('X-Cache-Status')).toBe('MISS'); }); it('returns cache HIT status for normal requests', async () => { @@ -81,57 +82,57 @@ describe('GET /api/stats validation and cache coverage', () => { expect(response.headers.get('X-Refresh-Status')).toBe('Cached'); }); - it('returns 404 when github lookup cannot resolve user', async () => { - vi.mocked(fetchGitHubContributions).mockRejectedValue(new Error('Could not resolve user')); - + it('returns no-store cache headers when bypassCache=true', async () => { const response = await GET( makeRequest({ - user: 'missing-user', + user: 'octocat', + bypassCache: 'true', }) ); - expect(response.status).toBe(404); - - const body = await response.json(); + expect(response.status).toBe(200); - expect(body.error).toBe('User not found'); + expect(response.headers.get('Cache-Control')).toBe('no-store, no-cache, must-revalidate'); + expect(response.headers.get('Pragma')).toBe('no-cache'); + expect(response.headers.get('Expires')).toBe('0'); }); - it('returns rate limit headers when refresh limit is exceeded', async () => { - vi.spyOn(refreshRateLimiter, 'checkLimit').mockReturnValue({ - success: false, - limit: 10, - remaining: 0, - reset: 12345, - }); - + it('returns standard cache headers for normal requests', async () => { const response = await GET( makeRequest({ user: 'octocat', - refresh: 'true', }) ); - expect(response.status).toBe(429); + expect(response.status).toBe(200); - expect(response.headers.get('X-RateLimit-Limit')).toBe('10'); - expect(response.headers.get('X-RateLimit-Remaining')).toBe('0'); - expect(response.headers.get('X-RateLimit-Reset')).toBe('12345'); + expect(response.headers.get('Cache-Control')).toBe( + 'public, s-maxage=3600, stale-while-revalidate=86400' + ); + expect(response.headers.get('Pragma')).toBeNull(); + expect(response.headers.get('Expires')).toBeNull(); }); - it('returns github rate limit error when api limit is reached', async () => { - vi.mocked(fetchGitHubContributions).mockRejectedValue(new Error('status 403')); + it('records refresh requests through bypassCache=true', async () => { + await GET( + makeRequest({ + user: 'octocat', + bypassCache: 'true', + }) + ); const response = await GET( makeRequest({ user: 'octocat', + bypassCache: 'true', }) ); - expect(response.status).toBe(429); - - const body = await response.json(); + expect(response.status).toBe(200); + expect(response.headers.get('X-Refresh-Status')).toBe('Cooldown-Served-Cached'); - expect(body.error).toContain('GitHub API rate limit reached'); + expect(fetchGitHubContributions).toHaveBeenLastCalledWith('octocat', { + bypassCache: false, + }); }); });