From 7feb9d7e04d46eda20c9ea8f9bb293d9d7c579e2 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 24 Sep 2026 17:29:41 -0700 Subject: [PATCH 1/2] Refresh Twitch channel IDs after username changes --- src/utils/twitch-server.ts | 21 ++++++++++------- src/utils/twitch-user.test.ts | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 src/utils/twitch-user.test.ts diff --git a/src/utils/twitch-server.ts b/src/utils/twitch-server.ts index 5fb5577..fde4c18 100644 --- a/src/utils/twitch-server.ts +++ b/src/utils/twitch-server.ts @@ -11,20 +11,23 @@ export const generateTwitchRequestHeaders = (accessToken: string) => { }; export const getTwitchUserId = async (userName: string, token: string) => { - const res = await fetch( - `https://api.twitch.tv/helix/users?login=${userName}`, + const response = await fetch( + `https://api.twitch.tv/helix/users?${new URLSearchParams({ + login: userName, + })}`, { method: "GET", headers: generateTwitchRequestHeaders(token), - next: { revalidate: Infinity }, // These should never change + // Twitch logins can change or be reassigned to another account. + next: { revalidate: 60 }, } - ).then((response) => response.json()); - if (res.error === "Unauthorized") throw new Error("Unauthorized"); - - const responseId = (res as any)?.data[0]?.id as string; - if (!responseId) return null; + ); + if (!response.ok) { + throw new Error(`Could not look up Twitch user: ${response.status}`); + } - return responseId; + const result = (await response.json()) as { data?: { id: string }[] }; + return result.data?.[0]?.id ?? null; }; export type VOD = { diff --git a/src/utils/twitch-user.test.ts b/src/utils/twitch-user.test.ts new file mode 100644 index 0000000..88e9834 --- /dev/null +++ b/src/utils/twitch-user.test.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getTwitchUserId } from "./twitch-server"; + +vi.mock("@clerk/nextjs/server", () => ({ clerkClient: vi.fn() })); + +afterEach(() => vi.unstubAllGlobals()); + +describe("getTwitchUserId", () => { + it("encodes the login and returns the matching ID", async () => { + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ data: [{ id: "123" }] }), { status: 200 }) + ); + vi.stubGlobal("fetch", fetchMock); + + await expect(getTwitchUserId("name&other", "app-token")).resolves.toBe( + "123" + ); + expect(fetchMock.mock.calls[0]![0]).toBe( + "https://api.twitch.tv/helix/users?login=name%26other" + ); + }); + + it("returns null when Twitch has no account for the login", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(JSON.stringify({ data: [] }))) + ); + + await expect(getTwitchUserId("missing", "app-token")).resolves.toBeNull(); + }); + + it("reports an API failure instead of treating it as a missing account", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(null, { status: 401 })) + ); + + await expect(getTwitchUserId("creator", "bad-token")).rejects.toThrow( + "Could not look up Twitch user: 401" + ); + }); +}); From 553676f253ca89eae7966e6a8d9249f172340cbd Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 24 Sep 2026 21:07:33 -0700 Subject: [PATCH 2/2] Name the Twitch failure test for what it checks Main already threw on Twitch failures, so the old name described a behavior change that did not happen. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/utils/twitch-user.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/twitch-user.test.ts b/src/utils/twitch-user.test.ts index 88e9834..036e7a3 100644 --- a/src/utils/twitch-user.test.ts +++ b/src/utils/twitch-user.test.ts @@ -31,7 +31,7 @@ describe("getTwitchUserId", () => { await expect(getTwitchUserId("missing", "app-token")).resolves.toBeNull(); }); - it("reports an API failure instead of treating it as a missing account", async () => { + it("throws a clear error when the Twitch request fails", async () => { vi.stubGlobal( "fetch", vi.fn().mockResolvedValue(new Response(null, { status: 401 }))