import { describe, expect, it } from "vitest"; import { CmsConnection } from "../src/cms/cms-connection.js"; const dramaId = "11111111-1111-4111-8111-111111111111"; const episodeId = "22222222-2222-4222-8222-222222222222"; const userId = "33333333-3333-4333-8333-333333333333"; const token = "test-access-secret"; function server(role = "admin") { const requests: { url: URL; init: RequestInit | undefined }[] = []; const fetcher: typeof fetch = (input, init) => { const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url); requests.push({ url, init }); const user = { id: userId, aud: "authenticated", role: "authenticated", email: "fixture@example.com", app_metadata: { role }, user_metadata: { role: "admin" }, created_at: "2026-01-01T00:00:00Z" }; let data: unknown; if (url.pathname === "/auth/v1/token") data = { access_token: token, refresh_token: "refresh-secret", token_type: "bearer", expires_in: 3600, user }; else if (url.pathname === "/auth/v1/user") data = user; else if (url.pathname === "/rest/v1/drama") data = [{ id: dramaId, title: "Test drama", synopsis: "Fixture only", language: "en", total_episodes: 2, status: "published" }]; else if (url.pathname === "/rest/v1/episode") data = [{ id: episodeId, drama_id: dramaId, episode_no: 1, title: "One", duration_seconds: 90, status: "published" }]; else if (url.pathname === "/rest/v1/drama_category") data = [{drama_id:dramaId,category:{name:"Mystery"}}]; else if (url.pathname === "/rest/v1/episode_source") data = [{ episode_id: episodeId, provider: "fixture", playback_path: "private/fixture.m3u8" }]; else if (url.pathname === "/rest/v1/subtitle_track") data = [{ episode_id: episodeId, locale: "en", path: "private/fixture.srt" }]; else if (url.pathname.endsWith("/subtitles/en.vtt")) return Promise.resolve(new Response("WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nFixture", { headers: { "content-type": "text/vtt" } })); else return Promise.resolve(new Response("unexpected destination", { status: 500 })); return Promise.resolve(new Response(JSON.stringify(data), { headers: { "content-type": "application/json" } })); }; return { requests, fetcher }; } describe("CMS connection", () => { it("binds signing to the selected drama and keeps it on the fixed CMS admin route", async () => { const fixture = server(); const cms = new CmsConnection({publishableKey:"sb_publishable_test",fetch:async(input,init)=>{ const url=new URL(typeof input==="string"?input:input instanceof URL?input.href:input.url); if(url.pathname.endsWith(`/cms/playback/${episodeId}`)) { expect(url.origin).toBe("https://swave.pencil-stub.com"); expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${token}`); return new Response(JSON.stringify({url:"https://video.pencil-stub.com/fixture/master.m3u8?auth_key=private",expires_at:new Date(Date.now()+600000).toISOString()})); } return fixture.fetcher(input,init); }}); await cms.login("fixture@example.com","secret"); await expect(cms.playback(dramaId,userId)).rejects.toMatchObject({code:"CMS_EPISODE_NOT_FOUND"}); expect((await cms.playback(dramaId,episodeId)).url).toContain("https://video.pencil-stub.com/"); expect(JSON.stringify(cms.status())).not.toContain("auth_key"); }); it("blocks content before login and never sends credentials to a model-facing status", async () => { const { fetcher } = server(); const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: fetcher }); await expect(cms.listDramas("", 0)).rejects.toMatchObject({ code: "CMS_LOGIN_REQUIRED" }); await cms.login("fixture@example.com", "password-secret"); expect(cms.status()).toMatchObject({ configured: true, connected: true }); expect(JSON.stringify(cms.status())).not.toMatch(/password-secret|test-access-secret|refresh-secret|fixture@example/); expect((await cms.listDramas("", 0)).dramas[0]?.title).toBe("Test drama"); cms.clear(); expect(cms.status().connected).toBe(false); await expect(cms.listDramas("", 0)).rejects.toMatchObject({ code: "CMS_LOGIN_REQUIRED" }); }); it("rejects user-editable administrator claims and discards the session", async () => { const { fetcher } = server("viewer"); const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: fetcher }); await expect(cms.login("fixture@example.com", "secret")).rejects.toMatchObject({ code: "CMS_ADMIN_REQUIRED" }); expect(cms.status().connected).toBe(false); }); it("reports incomplete inventory without leaking playback or subtitle paths", async () => { const { fetcher } = server(); const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: fetcher }); await cms.login("fixture@example.com", "secret"); const inventory = await cms.inventory(dramaId); expect(inventory.complete).toBe(false); expect(inventory.drama.tags).toEqual(["Mystery"]); expect(inventory.missingEpisodeNumbers).toEqual([2]); expect(inventory.episodes[0]).toMatchObject({ episodeNo: 1, hasSource: true, subtitleLocales: ["en"] }); expect(JSON.stringify(inventory)).not.toContain("private/"); expect(inventory.fetchedAt).toMatch(/^\d{4}-/); }); it("binds subtitle access to an actual episode in the chosen drama", async () => { const { fetcher, requests } = server(); const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: fetcher }); await cms.login("fixture@example.com", "secret"); await expect(cms.subtitle(dramaId, userId, "en")).rejects.toMatchObject({ code: "CMS_EPISODE_NOT_FOUND" }); expect((await cms.subtitle(dramaId, episodeId, "en")).text).toContain("WEBVTT"); const subtitleRequest = requests.find((request) => request.url.pathname.endsWith(".vtt")); expect(subtitleRequest?.url.origin).toBe("https://swave.pencil-stub.com"); expect(subtitleRequest?.init?.redirect).toBe("error"); expect(new Headers(subtitleRequest?.init?.headers).get("authorization")).toBe(`Bearer ${token}`); }); it("rejects a late login result after the connection was cleared", async () => { const { fetcher } = server(); let release: (() => void) | undefined; const gate = new Promise((resolve) => { release = resolve; }); const cms = new CmsConnection({ publishableKey: "sb_publishable_test", fetch: async (input, init) => { await gate; return fetcher(input, init); } }); const pending = cms.login("fixture@example.com", "secret"); cms.clear(); release?.(); await expect(pending).rejects.toMatchObject({ code: "CMS_LOGIN_CANCELLED" }); expect(cms.status().connected).toBe(false); }); });