Skip to content

Commit 0e7609b

Browse files
authored
fix: handle private profiles and empty repositories during badge gene… (JhaSourav07#1908)
## Description Fixes badge generation failures (HTTP 500) for GitHub users with private profiles, no public repositories, or no visible contribution data. I've added null-safe data handling for GraphQL responses in `lib/github.ts` and defensive validation across `lib/calculate.ts` so the system smoothly returns an empty-state badge instead of crashing. All 1,143 unit tests including the new edge-case scenarios passed successfully. Fixes JhaSourav07#1902 ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview *(No visual changes to standard badges; empty users will now correctly render an empty state badge instead of crashing.)* ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents 55dfc62 + b7e355e commit 0e7609b

3 files changed

Lines changed: 71 additions & 12 deletions

File tree

app/api/streak/route.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,54 @@ describe('GET /api/streak', () => {
291291
});
292292
});
293293

294+
describe('edge cases for empty/private profiles', () => {
295+
it('Scenario 1: Normal active GitHub user', async () => {
296+
const response = await GET(makeRequest({ user: 'octocat' }));
297+
expect(response.status).toBe(200);
298+
const body = await response.text();
299+
expect(body).toContain('<svg');
300+
});
301+
302+
it('Scenario 2 & 3: User with 0 public repositories or private profile (empty calendar)', async () => {
303+
vi.mocked(fetchGitHubContributions).mockResolvedValue({
304+
calendar: {
305+
totalContributions: 0,
306+
weeks: [],
307+
},
308+
repoContributions: [],
309+
} as unknown as ExtendedContributionData);
310+
311+
const response = await GET(makeRequest({ user: 'private-user' }));
312+
expect(response.status).toBe(200);
313+
const body = await response.text();
314+
expect(body).toContain('<svg');
315+
// Should show 0 contributions and streaks
316+
expect(body).toContain('>0<');
317+
});
318+
319+
it('Scenario 4: Nonexistent username', async () => {
320+
vi.mocked(fetchGitHubContributions).mockRejectedValue(
321+
new Error('GitHub user "nonexistent" not found')
322+
);
323+
324+
const response = await GET(makeRequest({ user: 'nonexistent' }));
325+
expect(response.status).toBe(404);
326+
const body = await response.text();
327+
expect(body).toContain('<svg');
328+
expect(body).toContain('NOT FOUND');
329+
});
330+
331+
it('Scenario 5: GitHub API failure', async () => {
332+
vi.mocked(fetchGitHubContributions).mockRejectedValue(new Error('API Rate Limit Exceeded'));
333+
334+
const response = await GET(makeRequest({ user: 'octocat' }));
335+
expect(response.status).toBe(429);
336+
const body = await response.text();
337+
expect(body).toContain('<svg');
338+
expect(body).toContain('API RATE LIMIT');
339+
});
340+
});
341+
294342
describe('cache-control header', () => {
295343
it('caches until UTC midnight by default, using the value from getSecondsUntilUTCMidnight', async () => {
296344
const response = await GET(makeRequest({ user: 'octocat' }));

lib/calculate.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export function calculateStreak(
2828
now: Date = new Date(),
2929
grace: number = 1
3030
): StreakStats {
31-
const weeks = calendar.weeks;
32-
const days = weeks.flatMap((week) => week.contributionDays);
31+
const weeks = calendar?.weeks || [];
32+
const days = weeks.flatMap((week) => week?.contributionDays || []);
3333

3434
let currentStreak = 0;
3535
let longestStreak = 0;
@@ -95,7 +95,8 @@ export function calculateMonthlyStats(
9595
timezone: string = 'UTC',
9696
now: Date = new Date()
9797
): MonthlyStats {
98-
const days = calendar.weeks.flatMap((week) => week.contributionDays);
98+
const weeks = calendar?.weeks || [];
99+
const days = weeks.flatMap((week) => week?.contributionDays || []);
99100

100101
const localTodayStr = new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format(now);
101102
const [currentYearStr, currentMonthStr] = localTodayStr.split('-');
@@ -171,13 +172,13 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut
171172
// Find the calendar with the most weeks to serve as our structural base
172173
let baseCalendar = calendars[0];
173174
for (const cal of calendars) {
174-
if (cal.weeks.length > baseCalendar.weeks.length) {
175+
if ((cal.weeks?.length || 0) > (baseCalendar.weeks?.length || 0)) {
175176
baseCalendar = cal;
176177
}
177178

178179
// Populate the Map with all contributions from all calendars
179-
cal.weeks.forEach((week) => {
180-
week.contributionDays.forEach((day) => {
180+
(cal.weeks || []).forEach((week) => {
181+
(week?.contributionDays || []).forEach((day) => {
181182
const currentCount = dateMap.get(day.date) || 0;
182183
dateMap.set(day.date, currentCount + day.contributionCount);
183184
});
@@ -190,8 +191,8 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut
190191
aggregatedBase.totalContributions = totalContributions;
191192

192193
// Re-map the structural base using our aggregated date map
193-
aggregatedBase.weeks.forEach((week) => {
194-
week.contributionDays.forEach((day) => {
194+
(aggregatedBase.weeks || []).forEach((week) => {
195+
(week?.contributionDays || []).forEach((day) => {
195196
day.contributionCount = dateMap.get(day.date) || 0;
196197
});
197198
});
@@ -202,7 +203,8 @@ export function aggregateCalendars(calendars: ContributionCalendar[]): Contribut
202203
* Processes a calendar to generate deep insights for "GitHub Wrapped"
203204
*/
204205
export function calculateWrappedStats(calendar: ContributionCalendar) {
205-
const days = calendar.weeks.flatMap((w) => w.contributionDays);
206+
const weeks = calendar?.weeks || [];
207+
const days = weeks.flatMap((w) => w?.contributionDays || []);
206208

207209
let mostActiveDay = { date: '', count: 0 };
208210
const monthCounts: Record<string, number> = {};

lib/github.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,16 @@ export async function fetchGitHubContributions(
469469
throw new Error(`GitHub user "${username}" not found`);
470470
}
471471

472-
let calendar = data.data.user.contributionsCollection.contributionCalendar;
472+
let calendar = data.data.user.contributionsCollection?.contributionCalendar;
473+
const repoContributions =
474+
data.data.user.contributionsCollection?.commitContributionsByRepository || [];
475+
476+
if (!calendar || !calendar.weeks) {
477+
calendar = {
478+
totalContributions: 0,
479+
weeks: [],
480+
};
481+
}
473482

474483
if (isDeltaSync && cached) {
475484
calendar = mergeCalendars(cached.calendar, calendar);
@@ -514,14 +523,14 @@ export async function fetchGitHubContributions(
514523
key,
515524
{
516525
calendar,
517-
repoContributions: data.data.user.contributionsCollection.commitContributionsByRepository,
526+
repoContributions,
518527
},
519528
LONG_CACHE_TTL
520529
);
521530
}
522531
return {
523532
calendar,
524-
repoContributions: data.data.user.contributionsCollection.commitContributionsByRepository,
533+
repoContributions,
525534
};
526535
};
527536

0 commit comments

Comments
 (0)