103 lines
6.6 KiB
TypeScript
103 lines
6.6 KiB
TypeScript
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<void>((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);
|
|
});
|
|
});
|