From b46cfcf6bae3292b5828a2d93f77dc9e103257c0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:44:25 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A7=AA=20Add=20error=20path=20tests?= =?UTF-8?q?=20for=20fetchYearInReviewData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: is0692vs <135803462+is0692vs@users.noreply.github.com> --- src/lib/__tests__/githubYearInReview.test.ts | 116 ++++++++++++++++++- 1 file changed, 113 insertions(+), 3 deletions(-) diff --git a/src/lib/__tests__/githubYearInReview.test.ts b/src/lib/__tests__/githubYearInReview.test.ts index 456afee..8e9fbf2 100644 --- a/src/lib/__tests__/githubYearInReview.test.ts +++ b/src/lib/__tests__/githubYearInReview.test.ts @@ -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(); +}); + +export function jsonResponse(data: unknown, status = 200, headers: Record = {}): Response { + 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; +} describe("githubYearInReview helpers", () => { it("returns the hour with the highest summed activity", () => { @@ -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); @@ -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)); + }); + + 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); + }); }); From 0b5a67b14ab726294daa7407b04b4d10897214dd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:38:12 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A7=AA=20Add=20error=20path=20tests?= =?UTF-8?q?=20for=20fetchYearInReviewData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: is0692vs <135803462+is0692vs@users.noreply.github.com>