Skip to content

Commit f3901e9

Browse files
committed
test: add comprehensive tests for GitHub data fetching and scoring algorithms
1 parent 1f64cc6 commit f3901e9

9 files changed

Lines changed: 734 additions & 113 deletions

test/api/compare.route.test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { beforeEach, describe, expect, test, vi } from "vitest";
2+
import { GitHubApiError } from "@/lib/github-graphql-client";
3+
4+
const mocks = vi.hoisted(() => ({
5+
fetchGitHubUserData: vi.fn(),
6+
calculateUserScore: vi.fn(),
7+
}));
8+
9+
vi.mock("@/lib/github", () => ({
10+
fetchGitHubUserData: mocks.fetchGitHubUserData,
11+
}));
12+
13+
vi.mock("@/lib/score", () => ({
14+
calculateUserScore: mocks.calculateUserScore,
15+
}));
16+
17+
import { GET } from "@/app/api/compare/route";
18+
19+
function makeRequest(params: Record<string, string | string[]>): Request {
20+
const search = new URLSearchParams();
21+
for (const [key, value] of Object.entries(params)) {
22+
if (Array.isArray(value)) {
23+
for (const item of value) {
24+
search.append(key, item);
25+
}
26+
} else {
27+
search.append(key, value);
28+
}
29+
}
30+
31+
return new Request(`http://localhost/api/compare?${search.toString()}`, {
32+
method: "GET",
33+
});
34+
}
35+
36+
function makeScore(finalScore: number) {
37+
return {
38+
repoScore: 10,
39+
prScore: 20,
40+
contributionScore: 3,
41+
finalScore,
42+
normalizedRepoScore: 30,
43+
normalizedPRScore: 40,
44+
normalizedContributionScore: 5,
45+
normalizedFinalScore: 35,
46+
topRepos: [],
47+
topPullRequests: [],
48+
topCommunityContributions: [],
49+
languageScores: undefined,
50+
signals: {
51+
reposAnalyzed: 1,
52+
pullRequestsAnalyzed: 1,
53+
mergedExternalPRs: 1,
54+
ownRepoPRsIgnored: 0,
55+
unmergedPRsIgnored: 0,
56+
uniqueExternalPRRepos: 1,
57+
issuesAnalyzed: 0,
58+
externalIssuesCounted: 0,
59+
discussionsAnalyzed: 0,
60+
externalDiscussionsCounted: 0,
61+
},
62+
explanations: {
63+
repo: [],
64+
pr: [],
65+
contribution: [],
66+
overall: [],
67+
},
68+
};
69+
}
70+
71+
describe("GET /api/compare", () => {
72+
beforeEach(() => {
73+
mocks.fetchGitHubUserData.mockReset();
74+
mocks.calculateUserScore.mockReset();
75+
});
76+
77+
test("returns structured friendly error when GitHub rate limit is hit", async () => {
78+
mocks.fetchGitHubUserData.mockRejectedValueOnce(
79+
new GitHubApiError({
80+
message: "API rate limit exceeded for user.",
81+
kind: "PRIMARY_RATE_LIMIT",
82+
status: 200,
83+
rateLimit: {
84+
limit: 5000,
85+
remaining: 0,
86+
used: 5000,
87+
resetAt: Math.floor(Date.now() / 1000) + 60,
88+
resource: "graphql",
89+
},
90+
retryAfterMs: 60_000,
91+
}),
92+
);
93+
94+
const response = await GET(
95+
makeRequest({
96+
username: ["user-a", "user-b"],
97+
}),
98+
);
99+
const body = (await response.json()) as {
100+
success: boolean;
101+
error?: string;
102+
errorDetails?: { code?: string; retryAfterSeconds?: number };
103+
};
104+
105+
expect(response.status).toBe(429);
106+
expect(body.success).toBe(false);
107+
expect(body.errorDetails?.code).toBe("RATE_LIMITED");
108+
expect(body.errorDetails?.retryAfterSeconds).toBe(60);
109+
});
110+
111+
test("returns success payload when both users are processed", async () => {
112+
mocks.fetchGitHubUserData.mockResolvedValueOnce({
113+
name: "User A",
114+
avatarUrl: "https://example.com/a.png",
115+
repos: [],
116+
pullRequests: [],
117+
contributions: {
118+
totalCommitContributions: 0,
119+
totalPullRequestContributions: 0,
120+
totalIssueContributions: 0,
121+
},
122+
issues: [],
123+
discussions: [],
124+
});
125+
mocks.fetchGitHubUserData.mockResolvedValueOnce({
126+
name: "User B",
127+
avatarUrl: "https://example.com/b.png",
128+
repos: [],
129+
pullRequests: [],
130+
contributions: {
131+
totalCommitContributions: 0,
132+
totalPullRequestContributions: 0,
133+
totalIssueContributions: 0,
134+
},
135+
issues: [],
136+
discussions: [],
137+
});
138+
139+
mocks.calculateUserScore.mockReturnValueOnce(makeScore(20));
140+
mocks.calculateUserScore.mockReturnValueOnce(makeScore(10));
141+
142+
const response = await GET(
143+
makeRequest({
144+
username: ["user-a", "user-b"],
145+
}),
146+
);
147+
const body = (await response.json()) as {
148+
success: boolean;
149+
users?: Array<{ username: string }>;
150+
winner?: { username: string };
151+
};
152+
153+
expect(response.status).toBe(200);
154+
expect(body.success).toBe(true);
155+
expect(body.users).toHaveLength(2);
156+
expect(body.winner?.username).toBe("user-a");
157+
});
158+
});

test/fixtures/github.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type {
33
DiscussionNode,
44
IssueNode,
55
PullRequestNode,
6-
ReactionSummary,
76
RepoLanguages,
87
RepoNode,
98
} from "@/types/github";
@@ -56,22 +55,10 @@ const defaultContributions: ContributionTotals = {
5655
totalIssueContributions: 0,
5756
};
5857

59-
const defaultReactions: ReactionSummary = {
60-
thumbsUp: 0,
61-
thumbsDown: 0,
62-
heart: 0,
63-
hooray: 0,
64-
rocket: 0,
65-
eyes: 0,
66-
confused: 0,
67-
laugh: 0,
68-
};
69-
7058
const defaultIssue: IssueNode = {
7159
title: "Issue about improving docs",
7260
url: "https://example.com/external-owner/repo/issues/1",
7361
comments: { totalCount: 2 },
74-
reactions: defaultReactions,
7562
repository: {
7663
nameWithOwner: "external-owner/repo",
7764
stargazerCount: 10,
@@ -83,7 +70,6 @@ const defaultDiscussion: DiscussionNode = {
8370
title: "Discussion about roadmap",
8471
url: "https://example.com/external-owner/repo/discussions/1",
8572
comments: { totalCount: 2 },
86-
reactions: defaultReactions,
8773
repository: {
8874
nameWithOwner: "external-owner/repo",
8975
stargazerCount: 10,
@@ -134,7 +120,6 @@ export function makeIssue(overrides: Partial<IssueNode> = {}): IssueNode {
134120
...defaultIssue,
135121
...overrides,
136122
comments: overrides.comments ?? defaultIssue.comments,
137-
reactions: overrides.reactions ?? defaultIssue.reactions,
138123
repository: overrides.repository ?? defaultIssue.repository,
139124
};
140125
}
@@ -146,7 +131,6 @@ export function makeDiscussion(
146131
...defaultDiscussion,
147132
...overrides,
148133
comments: overrides.comments ?? defaultDiscussion.comments,
149-
reactions: overrides.reactions ?? defaultDiscussion.reactions,
150134
repository: overrides.repository ?? defaultDiscussion.repository,
151135
};
152136
}

0 commit comments

Comments
 (0)