From b7e355e5955d0a78251e09e79178320267bda809 Mon Sep 17 00:00:00 2001 From: anshika1179 Date: Sun, 31 May 2026 02:41:41 +0530 Subject: [PATCH] fix: handle private profiles and empty repositories during badge generation --- app/api/streak/route.test.ts | 48 ++++++++++++++++++++++++++++++++++++ lib/calculate.ts | 20 ++++++++------- lib/github.ts | 15 ++++++++--- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/app/api/streak/route.test.ts b/app/api/streak/route.test.ts index 96420548..bb8cb78b 100644 --- a/app/api/streak/route.test.ts +++ b/app/api/streak/route.test.ts @@ -291,6 +291,54 @@ describe('GET /api/streak', () => { }); }); + describe('edge cases for empty/private profiles', () => { + it('Scenario 1: Normal active GitHub user', async () => { + const response = await GET(makeRequest({ user: 'octocat' })); + expect(response.status).toBe(200); + const body = await response.text(); + expect(body).toContain(' { + vi.mocked(fetchGitHubContributions).mockResolvedValue({ + calendar: { + totalContributions: 0, + weeks: [], + }, + repoContributions: [], + } as unknown as ExtendedContributionData); + + const response = await GET(makeRequest({ user: 'private-user' })); + expect(response.status).toBe(200); + const body = await response.text(); + expect(body).toContain('0<'); + }); + + it('Scenario 4: Nonexistent username', async () => { + vi.mocked(fetchGitHubContributions).mockRejectedValue( + new Error('GitHub user "nonexistent" not found') + ); + + const response = await GET(makeRequest({ user: 'nonexistent' })); + expect(response.status).toBe(404); + const body = await response.text(); + expect(body).toContain(' { + vi.mocked(fetchGitHubContributions).mockRejectedValue(new Error('API Rate Limit Exceeded')); + + const response = await GET(makeRequest({ user: 'octocat' })); + expect(response.status).toBe(429); + const body = await response.text(); + expect(body).toContain(' { it('caches until UTC midnight by default, using the value from getSecondsUntilUTCMidnight', async () => { const response = await GET(makeRequest({ user: 'octocat' })); diff --git a/lib/calculate.ts b/lib/calculate.ts index 158a921d..aa53943c 100644 --- a/lib/calculate.ts +++ b/lib/calculate.ts @@ -28,8 +28,8 @@ export function calculateStreak( now: Date = new Date(), grace: number = 1 ): StreakStats { - const weeks = calendar.weeks; - const days = weeks.flatMap((week) => week.contributionDays); + const weeks = calendar?.weeks || []; + const days = weeks.flatMap((week) => week?.contributionDays || []); let currentStreak = 0; let longestStreak = 0; @@ -95,7 +95,8 @@ export function calculateMonthlyStats( timezone: string = 'UTC', now: Date = new Date() ): MonthlyStats { - const days = calendar.weeks.flatMap((week) => week.contributionDays); + const weeks = calendar?.weeks || []; + const days = weeks.flatMap((week) => week?.contributionDays || []); const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format(now); const [currentYearStr, currentMonthStr] = localTodayStr.split('-'); @@ -171,13 +172,13 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut // Find the calendar with the most weeks to serve as our structural base let baseCalendar = calendars[0]; for (const cal of calendars) { - if (cal.weeks.length > baseCalendar.weeks.length) { + if ((cal.weeks?.length || 0) > (baseCalendar.weeks?.length || 0)) { baseCalendar = cal; } // Populate the Map with all contributions from all calendars - cal.weeks.forEach((week) => { - week.contributionDays.forEach((day) => { + (cal.weeks || []).forEach((week) => { + (week?.contributionDays || []).forEach((day) => { const currentCount = dateMap.get(day.date) || 0; dateMap.set(day.date, currentCount + day.contributionCount); }); @@ -190,8 +191,8 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut aggregatedBase.totalContributions = totalContributions; // Re-map the structural base using our aggregated date map - aggregatedBase.weeks.forEach((week) => { - week.contributionDays.forEach((day) => { + (aggregatedBase.weeks || []).forEach((week) => { + (week?.contributionDays || []).forEach((day) => { day.contributionCount = dateMap.get(day.date) || 0; }); }); @@ -202,7 +203,8 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut * Processes a calendar to generate deep insights for "GitHub Wrapped" */ export function calculateWrappedStats(calendar: ContributionCalendar) { - const days = calendar.weeks.flatMap((w) => w.contributionDays); + const weeks = calendar?.weeks || []; + const days = weeks.flatMap((w) => w?.contributionDays || []); let mostActiveDay = { date: '', count: 0 }; const monthCounts: Record = {}; diff --git a/lib/github.ts b/lib/github.ts index 2959e4fb..3c049cb7 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -469,7 +469,16 @@ export async function fetchGitHubContributions( throw new Error(`GitHub user "${username}" not found`); } - let calendar = data.data.user.contributionsCollection.contributionCalendar; + let calendar = data.data.user.contributionsCollection?.contributionCalendar; + const repoContributions = + data.data.user.contributionsCollection?.commitContributionsByRepository || []; + + if (!calendar || !calendar.weeks) { + calendar = { + totalContributions: 0, + weeks: [], + }; + } if (isDeltaSync && cached) { calendar = mergeCalendars(cached.calendar, calendar); @@ -514,14 +523,14 @@ export async function fetchGitHubContributions( key, { calendar, - repoContributions: data.data.user.contributionsCollection.commitContributionsByRepository, + repoContributions, }, LONG_CACHE_TTL ); } return { calendar, - repoContributions: data.data.user.contributionsCollection.commitContributionsByRepository, + repoContributions, }; };