diff --git a/src/utils/twitch-server.test.ts b/src/utils/twitch-server.test.ts new file mode 100644 index 0000000..7333644 --- /dev/null +++ b/src/utils/twitch-server.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getVodWithMarkers } from "./twitch-server"; + +// The creator has signed in before, so Clerk has their Twitch token +vi.mock("@clerk/nextjs/server", () => ({ + clerkClient: async () => ({ + users: { + getUserList: async () => ({ data: [{ id: "creator" }] }), + getUserOauthAccessToken: async () => ({ + data: [{ token: "creator-token" }], + }), + }, + }), +})); + +const vod = { + created_at: "2026-09-24T00:00:00Z", + duration: "1h0m0s", + user_login: "creator", +}; + +// Stubs the videos request, then the markers request +const stubTwitch = (markersResponse: Response) => + vi.stubGlobal( + "fetch", + vi + .fn() + .mockResolvedValueOnce(Response.json({ data: [vod] })) + .mockResolvedValueOnce(markersResponse) + ); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("getVodWithMarkers", () => { + it("throws when the markers request fails", async () => { + stubTwitch( + Response.json({ error: "Unauthorized", status: 401 }, { status: 401 }) + ); + + await expect(getVodWithMarkers("123", "viewer-token")).rejects.toThrow( + "Twitch markers request failed: 401" + ); + }); + + it("gives no markers when Twitch sends an empty videos list", async () => { + stubTwitch(Response.json({ data: [{ videos: [] }] })); + + await expect(getVodWithMarkers("123", "viewer-token")).resolves.toEqual({ + ...vod, + markers: [], + }); + }); +}); diff --git a/src/utils/twitch-server.ts b/src/utils/twitch-server.ts index 5fb5577..d91ad7c 100644 --- a/src/utils/twitch-server.ts +++ b/src/utils/twitch-server.ts @@ -39,7 +39,7 @@ export type VOD = { duration: string; }; -// Used for vod markers +// Used for VOD markers and the live topic embed const getValidTokenForCreator = async (creatorName: string) => { // Get token for the input displayName IF THEY HAVE SIGNED IN BEFORE const clerk = await clerkClient(); @@ -49,8 +49,6 @@ const getValidTokenForCreator = async (creatorName: string) => { const creatorFoundInClerk = response.data[0]; - console.log("found in clerk?", creatorFoundInClerk); - // Early escape if we don't find this user in Clerk if (!creatorFoundInClerk) { throw new Error("User not found in Clerk"); @@ -59,6 +57,9 @@ const getValidTokenForCreator = async (creatorName: string) => { return await getTwitchTokenFromClerk(creatorFoundInClerk.id); }; +// Loads a VOD and its markers. Markers are read with the creator's stored +// token, so any signed-in user can view a connected creator's markers. +// Throws on Twitch errors, so a failed request is not read as "no markers". export const getVodWithMarkers = async (vodId: string, token: string) => { const vodResponse = await fetch( `https://api.twitch.tv/helix/videos?id=${vodId}`, @@ -69,16 +70,18 @@ export const getVodWithMarkers = async (vodId: string, token: string) => { cache: "no-store", } ); - console.log("VOD RESPONSE", vodResponse.status); - - const vodData = await vodResponse.json(); - console.log("VOD DATA", vodData); + if (!vodResponse.ok) { + throw new Error(`Twitch videos request failed: ${vodResponse.status}`); + } - const creatorName = vodData?.data?.[0]?.user_login; + const vodData = (await vodResponse.json()) as { + data?: (Omit & { user_login: string })[]; + }; + const vod = vodData.data?.[0]; - if (!creatorName) throw new Error("could not find vod data or user login"); + if (!vod) throw new Error("could not find vod data or user login"); - const tokenForMarkers = await getValidTokenForCreator(creatorName); + const tokenForMarkers = await getValidTokenForCreator(vod.user_login); const markersResponse = await fetch( `https://api.twitch.tv/helix/streams/markers?video_id=${vodId}&first=100`, @@ -88,15 +91,14 @@ export const getVodWithMarkers = async (vodId: string, token: string) => { next: { revalidate: 60 }, } ); + if (!markersResponse.ok) { + throw new Error(`Twitch markers request failed: ${markersResponse.status}`); + } - console.log("MARKER RESPONSE", markersResponse.status); - - const markersData = await markersResponse.json(); - console.log("MARKER DATA", markersData); - - const markers = markersData?.data?.[0]?.videos?.[0]["markers"] ?? []; + const markersData = (await markersResponse.json()) as TwitchMarkersResponse; + const markers = markersData.data?.[0]?.videos?.[0]?.markers ?? []; - return { ...vodData?.data?.[0], markers } as VOD; + return { ...vod, markers }; }; export const getTwitchTokenFromClerk = async (clerkUserId: string) => {