Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"eslint": "^9",
"eslint-config-next": "^16.1.6",
"jsdom": "^28.1.0",
"mitata": "^1.0.34",
"server-only": "^0.0.1",
"tailwindcss": "^4",
"typescript": "^5",
Expand Down
141 changes: 127 additions & 14 deletions src/lib/__tests__/githubYearInReview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,21 @@ describe("fetchYearInReviewData safety checks", () => {
});

describe("fetchYearInReviewData additional coverage", () => {
it("throws 500 GitHubApiError for non-Error thrown objects", async () => {
mockFetch.mockImplementation(() => {
throw "String error";
});

await expect(fetchYearInReviewData("testuser", 2024, "fake-token")).rejects.toThrow(GitHubApiError);
try {
await fetchYearInReviewData("testuser", 2024, "fake-token");
} catch (error) {
expect(error).toBeInstanceOf(GitHubApiError);
expect((error as GitHubApiError).status).toBe(500);
expect((error as GitHubApiError).message).toBe("Failed to fetch year in review data");
}
});

it("throws 500 GitHubApiError for unknown errors", async () => {
mockFetch.mockImplementation(() => {
throw new Error("Unexpected crash");
Expand Down Expand Up @@ -592,25 +607,123 @@ describe("githubYearInReview additional edge cases", () => {
});

it("fetchCommitDatesForTopRepos handles missing defaultBranchRef or history", async () => {
mockFetch.mockImplementation(() => {
return Promise.resolve(jsonResponse({
data: {
user: {
id: "123",
contributionsCollection: {
totalCommitContributions: 1,
contributionCalendar: { totalContributions: 1, weeks: [] },
commitContributionsByRepository: [
{ repository: { owner: { login: "u" }, name: "r" }, contributions: { totalCount: 1 } }
]
let callCount = 0;
mockFetch.mockImplementation((url) => {
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url;
if (urlStr.includes("/graphql")) {
callCount++;
if (callCount === 1) {
return Promise.resolve(jsonResponse({
data: {
user: {
id: "u1",
contributionsCollection: {
totalCommitContributions: 10,
totalPullRequestContributions: 0,
totalIssueContributions: 0,
totalPullRequestReviewContributions: 0,
contributionCalendar: {
totalContributions: 10,
weeks: []
},
commitContributionsByRepository: [
{ repository: { name: "repo", owner: { login: "u" } }, contributions: { totalCount: 10 } }
]
}
}
}
},
repo0: { defaultBranchRef: null } // Missing defaultBranchRef
}));
}
}));
return Promise.resolve(jsonResponse({
data: {
repo0: { defaultBranchRef: null }
}
}));
}
return Promise.resolve(jsonResponse([], 200));
});

const data = await fetchYearInReviewData("user", 2024, "token");
expect(data.year).toBe(2024);
});

it("fetchCommitDatesForTopRepos handles nodes without author or date", async () => {
let callCount = 0;
mockFetch.mockImplementation((url) => {
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url;
if (urlStr.includes("/graphql")) {
callCount++;
if (callCount === 1) {
return Promise.resolve(jsonResponse({
data: {
user: {
id: "u1",
contributionsCollection: {
totalCommitContributions: 10,
totalPullRequestContributions: 0,
totalIssueContributions: 0,
totalPullRequestReviewContributions: 0,
contributionCalendar: {
totalContributions: 10,
weeks: []
},
commitContributionsByRepository: [
{ repository: { name: "repo", owner: { login: "u" } }, contributions: { totalCount: 10 } }
]
}
}
}
}));
}
return Promise.resolve(jsonResponse({
data: {
repo0: { defaultBranchRef: { target: { history: { nodes: [{ author: null }, { author: { date: null } }, { author: { date: "2024-01-01T12:00:00Z" } }] } } } }
}
}));
}
return Promise.resolve(jsonResponse([], 200));
});
const data = await fetchYearInReviewData("user", 2024, "token");
expect(data.year).toBe(2024);
});

it("fetchCommitDatesForTopRepos handles null nodes in history", async () => {
let callCount = 0;
mockFetch.mockImplementation((url) => {
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url;
if (urlStr.includes("/graphql")) {
callCount++;
if (callCount === 1) {
return Promise.resolve(jsonResponse({
data: {
user: {
id: "u1",
contributionsCollection: {
totalCommitContributions: 10,
totalPullRequestContributions: 0,
totalIssueContributions: 0,
totalPullRequestReviewContributions: 0,
contributionCalendar: {
totalContributions: 10,
weeks: []
},
commitContributionsByRepository: [
{ repository: { name: "repo", owner: { login: "u" } }, contributions: { totalCount: 10 } }
]
}
}
}
}));
}
return Promise.resolve(jsonResponse({
data: {
repo0: { defaultBranchRef: { target: { history: { nodes: [null, { author: { date: "2024-01-01T12:00:00Z" } }] } } } }
}
}));
}
return Promise.resolve(jsonResponse([], 200));
});
const data = await fetchYearInReviewData("user", 2024, "token");
expect(data.year).toBe(2024);
});
});
19 changes: 10 additions & 9 deletions src/lib/githubYearInReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,17 @@ async function fetchCommitDatesForTopRepos(
const response = await graphql<Record<string, unknown>>(query, token, variables);
const dates: string[] = [];

type RepoGraphQLNode = {
defaultBranchRef?: { target?: { history?: { nodes?: Array<{ author?: { date?: string } }> } } };
};

for (let i = 0; i < candidates.length; i++) {
const repoData = response[`repo${i}`] as Record<string, unknown> | undefined;
const defaultBranchRef = repoData?.defaultBranchRef as Record<string, unknown> | undefined;
const target = defaultBranchRef?.target as Record<string, unknown> | undefined;
const history = target?.history as Record<string, unknown> | undefined;
const historyNodes = (history?.nodes as Array<Record<string, unknown>> | undefined) || [];
for (const node of historyNodes) {
const author = node?.author as Record<string, unknown> | undefined;
if (typeof author?.date === 'string') {
dates.push(author.date);
const nodes = (response[`repo${i}`] as RepoGraphQLNode | undefined)?.defaultBranchRef?.target?.history?.nodes;
if (!nodes) continue;
for (let j = 0; j < nodes.length; j++) {
const date = nodes[j]?.author?.date;
if (typeof date === 'string') {
dates.push(date);
}
}
}
Expand Down
Loading