Skip to content
Open
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
116 changes: 113 additions & 3 deletions src/lib/__tests__/githubYearInReview.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";

import { getMostActiveDayFromCalendar, getMostActiveHour } from "@/lib/yearInReviewUtils";
import { fetchYearInReviewData } from "@/lib/githubYearInReview";
import { GitHubApiError } from "@/lib/types";
import { GitHubApiError, RateLimitError, UserNotFoundError } from "@/lib/types";

// "server-only" を事前にモック
vi.mock("server-only", () => ({}));

export const mockFetch = vi.fn();

beforeEach(() => {
vi.stubGlobal("fetch", mockFetch);
mockFetch.mockReset();
});

afterEach(() => {
vi.unstubAllGlobals();
});
Comment thread
is0692vs marked this conversation as resolved.

export function jsonResponse(data: unknown, status = 200, headers: Record<string, string> = {}): Response {
Comment thread
is0692vs marked this conversation as resolved.
return {
ok: status >= 200 && status < 300,
status,
headers: new Headers(headers),
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
} as unknown as Response;
}
Comment thread
is0692vs marked this conversation as resolved.

describe("githubYearInReview helpers", () => {
it("returns the hour with the highest summed activity", () => {
Expand All @@ -26,7 +50,7 @@ describe("githubYearInReview helpers", () => {
});
});

describe("fetchYearInReviewData", () => {
describe("fetchYearInReviewData error paths", () => {
it("throws GitHubApiError when token is not provided", async () => {
await expect(fetchYearInReviewData("testuser", 2024)).rejects.toThrow(GitHubApiError);

Expand All @@ -38,4 +62,90 @@ describe("fetchYearInReviewData", () => {
expect((error as GitHubApiError).status).toBe(401);
}
});

it("throws UserNotFoundError when statsResponse.user is null", async () => {
let callCount = 0;
mockFetch.mockImplementation((url: string | URL | Request) => {
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : (url as Request).url;
if (urlStr.includes("/graphql")) {
callCount++;
if (callCount === 1) {
return Promise.resolve(jsonResponse({ data: { user: null } })); // statsPromise
}
if (callCount === 2) {
return Promise.resolve(jsonResponse({
data: {
user: {
contributionsCollection: {
commitContributionsByRepository: []
}
}
}
})); // reposResponse
}
}
return Promise.resolve(jsonResponse([], 200));
});
Comment thread
is0692vs marked this conversation as resolved.

await expect(fetchYearInReviewData("nonexistent", 2024, "fake-token")).rejects.toThrow(UserNotFoundError);
});

it("throws UserNotFoundError when reposResponse.user is null", async () => {
let callCount = 0;
mockFetch.mockImplementation((url: string | URL | Request) => {
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : (url as Request).url;
if (urlStr.includes("/graphql")) {
callCount++;
if (callCount === 1) {
return Promise.resolve(jsonResponse({ data: { user: { contributionsCollection: {} } } }));
}
if (callCount === 2) {
return Promise.resolve(jsonResponse({ data: { user: null } }));
}
}
return Promise.resolve(jsonResponse([], 200));
});

await expect(fetchYearInReviewData("nonexistent", 2024, "fake-token")).rejects.toThrow(UserNotFoundError);
});

it("throws RateLimitError when API returns 403", async () => {
let callCount = 0;
mockFetch.mockImplementation((url: string | URL | Request) => {
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : (url as Request).url;
if (urlStr.includes("/graphql")) {
callCount++;
// Let statsPromise succeed so it doesn't cause unhandled rejection
if (callCount === 1) {
return Promise.resolve(jsonResponse({ data: { user: { contributionsCollection: {} } } }));
}
// Let reposResponse fail
if (callCount === 2) {
return Promise.resolve(jsonResponse(null, 403, { "X-RateLimit-Reset": "1700000000" }));
}
}
return Promise.resolve(jsonResponse([], 200));
});

await expect(fetchYearInReviewData("testuser", 2024, "fake-token")).rejects.toThrow(RateLimitError);
});

it("throws GitHubApiError when API returns other errors", async () => {
let callCount = 0;
mockFetch.mockImplementation((url: string | URL | Request) => {
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : (url as Request).url;
if (urlStr.includes("/graphql")) {
callCount++;
if (callCount === 1) {
return Promise.resolve(jsonResponse({ data: { user: { contributionsCollection: {} } } }));
}
if (callCount === 2) {
return Promise.resolve(jsonResponse(null, 500));
}
}
return Promise.resolve(jsonResponse([], 200));
});

await expect(fetchYearInReviewData("testuser", 2024, "fake-token")).rejects.toThrow(GitHubApiError);
});
});
Loading