diff --git a/app/api/compare/route.test.ts b/app/api/compare/route.test.ts index e3f4683c..eb76db5b 100644 --- a/app/api/compare/route.test.ts +++ b/app/api/compare/route.test.ts @@ -90,4 +90,38 @@ describe('GET /api/compare', () => { const res = await GET(makeRequest('user1=octocat&user2=ghost123')); expect(res.status).toBe(404); }); + + it('returns 403 when the user1 fetch hits a GitHub rate limit', async () => { + vi.mocked(getFullDashboardData).mockRejectedValueOnce(new Error('API Rate Limit Exceeded')); + + const res = await GET(makeRequest('user1=octocat&user2=torvalds')); + + expect(res.status).toBe(403); + const data = await res.json(); + expect(data.error).toBe('GitHub API rate limit reached. Please configure GITHUB_TOKEN.'); + }); + + it('returns 403 when the user2 fetch hits a GitHub rate limit', async () => { + vi.mocked(getFullDashboardData) + .mockResolvedValueOnce({ calendar: { totalContributions: 0, weeks: [] } } as never) + .mockRejectedValueOnce(new Error('GitHub GraphQL API returned status 403')); + + const res = await GET(makeRequest('user1=octocat&user2=torvalds')); + + expect(res.status).toBe(403); + const data = await res.json(); + expect(data.error).toBe('GitHub API rate limit reached. Please configure GITHUB_TOKEN.'); + }); + + it('returns 500 for non-rate-limit upstream failures instead of reporting not found', async () => { + vi.mocked(getFullDashboardData).mockRejectedValueOnce( + new Error('GitHub GraphQL API returned status 500') + ); + + const res = await GET(makeRequest('user1=octocat&user2=torvalds')); + + expect(res.status).toBe(500); + const data = await res.json(); + expect(data.error).toContain('GitHub GraphQL API returned status 500'); + }); }); diff --git a/app/api/compare/route.ts b/app/api/compare/route.ts index 4ca26c15..61e9daff 100644 --- a/app/api/compare/route.ts +++ b/app/api/compare/route.ts @@ -4,6 +4,38 @@ import { compareParamsSchema } from '@/lib/validations'; export const revalidate = 3600; +function buildCompareFetchErrorResponse(user: string, reason: unknown): NextResponse { + const message = reason instanceof Error ? reason.message : 'Unknown error'; + const lowerMessage = message.toLowerCase(); + + if (lowerMessage.includes('not found') || lowerMessage.includes('could not resolve')) { + return NextResponse.json( + { + error: `Failed to fetch data for "${user}": ${message}`, + }, + { status: 404 } + ); + } + + if ( + lowerMessage.includes('rate limit') || + message.includes('API limit reached') || + message.includes('status 403') + ) { + return NextResponse.json( + { error: 'GitHub API rate limit reached. Please configure GITHUB_TOKEN.' }, + { status: 403 } + ); + } + + return NextResponse.json( + { + error: `Failed to fetch data for "${user}": ${message}`, + }, + { status: 500 } + ); +} + export async function GET(request: Request) { const { searchParams } = new URL(request.url); @@ -26,21 +58,11 @@ export async function GET(request: Request) { ]); if (result1.status === 'rejected') { - return NextResponse.json( - { - error: `Failed to fetch data for "${user1}": ${result1.reason?.message || 'Unknown error'}`, - }, - { status: 404 } - ); + return buildCompareFetchErrorResponse(user1, result1.reason); } if (result2.status === 'rejected') { - return NextResponse.json( - { - error: `Failed to fetch data for "${user2}": ${result2.reason?.message || 'Unknown error'}`, - }, - { status: 404 } - ); + return buildCompareFetchErrorResponse(user2, result2.reason); } return NextResponse.json({