import test from "node:test"; import assert from "node:assert/strict"; import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; // Run with: // pnpm -C editor exec tsx --test "app/api/ops/rename-channel/route.test.ts" // // The channel lifecycle over ops — rename, delete, create, the membership and // exclusion half of channel-config, and the list — against a temp corpus with // two social channels and one site: every REFUSAL, which is answered before // anything is written. The successful writes end in revalidatePath, which needs // the request store a running Next server has and a bare tsx process has not // (safeRevalidate.ts); they are driven over HTTP in e2e/ops-api.spec.ts. const ROOT = await mkdtemp(path.join(os.tmpdir(), "channel-lifecycle-route-")); process.env.WORKER_TOKEN = "test-token"; process.env.TRANSCRIPTS_DIR = ROOT; process.env.SETTINGS_FILE = path.join(ROOT, "settings.json"); async function makeSocial(slug: string, handle: string): Promise { const dir = path.join(ROOT, "channels", slug); await mkdir(path.join(dir, "posts"), { recursive: true }); await writeFile( path.join(dir, "config.json"), JSON.stringify({ handling: "transcribe", sourceKind: "social", platform: "twitter", postFetcher: "x-gallery-dl", socialHandle: handle, name: `${handle} (X)`, url: `https://x.com/${handle}`, }), ); } await makeSocial("misspeled-x", "example_user"); await makeSocial("other-x", "other_user"); await mkdir(path.join(ROOT, "sites", "demo-site"), { recursive: true }); await writeFile( path.join(ROOT, "sites", "demo-site", "site.json"), JSON.stringify({ siteId: "demo-site", siteTitle: "Demo", channels: [] }), ); const rename = (await import("./route")).POST; const del = (await import("../delete-channel/route")).POST; const create = (await import("../create-channel/route")).POST; const config = (await import("../channel-config/route")).POST; const list = (await import("../channels/route")).GET; test.after(() => rm(ROOT, { recursive: true, force: true })); type Res = { status: number; json: Record }; async function call( handler: (r: Request) => Promise, body?: Record, ): Promise { const res = await handler( new Request("http://localhost/api/ops/x", { method: body ? "POST" : "GET", headers: { authorization: "Bearer test-token", "content-type": "application/json", }, ...(body ? { body: JSON.stringify(body) } : {}), }), ); return { status: res.status, json: (await res.json()) as Record }; } const exists = (p: string) => access(p).then(() => true, () => false); const readSite = async () => JSON.parse(await readFile(path.join(ROOT, "sites", "demo-site", "site.json"), "utf8")) as { channels: { slug: string }[]; }; test("rename refuses a taken slug and an invalid one, in the form's sentences", async () => { const taken = await call(rename, { slug: "misspeled-x", newSlug: "other-x" }); assert.equal(taken.status, 400); assert.equal(taken.json.error, 'Channel "other-x" already exists'); const bad = await call(rename, { slug: "misspeled-x", newSlug: "../escape" }); assert.equal(bad.status, 400); assert.match(String(bad.json.error), /not a valid channel slug/); const missing = await call(rename, { slug: "nope", newSlug: "nope-2" }); assert.equal(missing.status, 400); assert.equal(missing.json.error, 'Channel "nope" not found'); }); test("the list names every channel, its kind and its sites", async () => { const listed = await call(list); assert.equal(listed.status, 200); const rows = listed.json.channels as { slug: string; sites: string[]; kind: string }[]; assert.deepEqual(rows.map((c) => c.slug).sort(), ["misspeled-x", "other-x"]); assert.ok(rows.every((c) => c.kind === "social" && c.sites.length === 0)); }); test("channel-config refuses an unknown site and a malformed membership", async () => { const unknown = await call(config, { slug: "misspeled-x", sites: [{ siteId: "no-such-site" }] }); assert.equal(unknown.status, 400); assert.equal(unknown.json.error, 'no site "no-such-site" — known: demo-site'); const stray = await call(config, { slug: "misspeled-x", sites: [{ siteId: "demo-site", group: "a" }] }); assert.equal(stray.status, 400); assert.match(String(stray.json.error), /unknown key\(s\) in a "sites" entry: group/); const notArray = await call(config, { slug: "misspeled-x", sites: "demo-site" }); assert.equal(notArray.status, 400); assert.match(String(notArray.json.error), /"sites" must be an array/); // Nothing was written. assert.deepEqual((await readSite()).channels, []); }); test("channel-config refuses an empty body and a non-boolean exclusion", async () => { const none = await call(config, { slug: "misspeled-x" }); assert.equal(none.status, 400); assert.match(String(none.json.error), /nothing to change/); const notBool = await call(config, { slug: "misspeled-x", excludeFromBuild: "yes" }); assert.equal(notBool.status, 400); assert.equal(notBool.json.error, '"excludeFromBuild" must be a boolean'); const cfg = JSON.parse( await readFile(path.join(ROOT, "channels", "misspeled-x", "config.json"), "utf8"), ); assert.equal(cfg.excludeFromBuild, undefined); }); test("create refuses what the New form refuses, before any channel exists", async () => { const noName = await call(create, { fields: { handling: "transcribe" } }); assert.equal(noName.status, 400); assert.equal(noName.json.error, "Name is required"); const badKey = await call(create, { fields: { name: "X", handlng: "youtube" } }); assert.equal(badKey.status, 400); assert.match(String(badKey.json.error), /"handlng" is not a channel config field/); const taken = await call(create, { slug: "other-x", fields: { name: "Other", handling: "transcribe" }, }); assert.equal(taken.status, 400); assert.equal(taken.json.error, 'Channel "other-x" already exists'); const unknownSite = await call(create, { slug: "made-x", fields: { name: "Made", handling: "transcribe" }, sites: [{ siteId: "nope" }], }); assert.equal(unknownSite.status, 400); assert.equal(await exists(path.join(ROOT, "channels", "made-x")), false); }); test("delete needs the slug repeated, and refuses before touching anything", async () => { const wrong = await call(del, { slug: "other-x", confirm: "other" }); assert.equal(wrong.status, 400); assert.equal(wrong.json.error, 'Type the channel slug "other-x" exactly to confirm deletion'); const missing = await call(del, { slug: "other-x" }); assert.equal(missing.status, 400); assert.match(String(missing.json.error), /"confirm" is required/); assert.equal(await exists(path.join(ROOT, "channels", "other-x", "config.json")), true); });