import test from "node:test"; import assert from "node:assert/strict"; import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { VIEW_NAMES } from "yt-dlp-transcript-common/views/names"; // Run with: // pnpm -C editor exec tsx --test "app/api/view/[name]/route.test.ts" // // ONE POLLING ROUTE, AND THE OLD PATHS THAT STILL ANSWER — the parts of that // contract a handler call can check. They were e2e (view-route.spec.ts) and // booted a server to ask a dispatcher for a 404. // // - the eight old paths are REWRITES to their view, and nothing else is: the // table in next.config.ts, read as data. A rewrite to the right view is // the same endpoint by construction, which is what the e2e compared bodies // to prove. // - an unknown name, a near-miss and every /api/test/* harness name are 404s // from the dispatcher, before any input is built. // - /api/widget/presets keeps its own route and answers. // // What stays e2e is the one thing only a running Next can show: that a // rewrite carries the QUERY STRING (`/api/pulse?rev=` — e2e/view-route.spec.ts). const HERE = path.dirname(fileURLToPath(import.meta.url)); const EDITOR = path.resolve(HERE, "..", "..", "..", ".."); const ROOT = await mkdtemp(path.join(os.tmpdir(), "view-route-")); const CORPUS = path.join(ROOT, "transcripts"); await mkdir(path.join(CORPUS, "channels"), { recursive: true }); // Set before anything that caches getPaths() is first imported. process.env.TRANSCRIPTS_DIR = CORPUS; process.env.SETTINGS_FILE = path.join(ROOT, "settings.json"); const { GET } = await import("./route"); const { GET: presetsGET } = await import("../../widget/presets/route"); // next.config.ts says `__dirname`, which Next's config loader provides and an // ES module does not; a global of that name is what the free identifier finds. (globalThis as { __dirname?: string }).__dirname = EDITOR; const { default: nextConfig } = await import("../../../../next.config"); delete (globalThis as { __dirname?: string }).__dirname; test.after(() => rm(ROOT, { recursive: true, force: true })); const PAIRS: Array<[string, string]> = [ ["/api/pulse", "/api/view/pulse"], ["/api/jobs/active", "/api/view/activeJobs"], ["/api/workers", "/api/view/workers"], ["/api/auto-queue/status", "/api/view/autoQueueStatus"], ["/api/scheduler/status", "/api/view/schedulerStatus"], ["/api/widget/sync", "/api/view/widgetSync"], ["/api/widget/actionable", "/api/view/widgetActionable"], ["/api/widget/cleanable", "/api/view/cleanable"], ]; async function view(name: string): Promise { const res = await GET(new Request(`http://localhost/api/view/${name}`), { params: Promise.resolve({ name }), }); return res.status; } type Rewrite = { source: string; destination: string }; async function rewrites(): Promise { const r = await nextConfig.rewrites!(); // The array form is `afterFiles`; the object form would split it. assert.ok(Array.isArray(r), "next.config rewrites() is the array form"); return r as Rewrite[]; } test("each old path is a rewrite to its view, and every view has one", async () => { const table = await rewrites(); const apiRewrites = table.filter((r) => r.source.startsWith("/api/")); assert.deepEqual( apiRewrites.map((r) => [r.source, r.destination]), PAIRS, ); // Every destination is a name the dispatcher serves — a typo here would be a // rewrite to a 404. for (const [, viewPath] of PAIRS) { const name = viewPath.slice("/api/view/".length); assert.ok( (VIEW_NAMES as readonly string[]).includes(name), `${viewPath} is not a view`, ); } assert.deepEqual( [...VIEW_NAMES].sort(), PAIRS.map(([, v]) => v.slice("/api/view/".length)).sort(), ); }); test("an unknown view name is 404, not 500", async () => { for (const name of ["nope", "Pulse", "activejobs", "presets"]) { assert.equal(await view(name), 404, `/api/view/${name}`); } }); // The dispatcher has no guard by design (these are read-only polls), but the // harness routes DO — and none may be reachable through it. test("no test-harness name is a view", async () => { const harness = ( await readdir(path.join(EDITOR, "app", "api", "test"), { withFileTypes: true, }) ) .filter((d) => d.isDirectory()) .map((d) => d.name); assert.ok(harness.includes("invalidate-cache"), "the harness dir was read"); for (const name of harness) { assert.equal(await view(name), 404, `/api/view/${name}`); } }); // /api/widget/presets is a menu fetch on open, not a poll: it is not a view, // it keeps its own route, and no rewrite shadows it. test("/api/widget/presets is untouched", async () => { const table = await rewrites(); assert.ok(!table.some((r) => r.source === "/api/widget/presets")); const res = await presetsGET(); assert.equal(res.status, 200); const body = (await res.json()) as { builtIn: unknown; saved: unknown }; assert.ok(Array.isArray(body.builtIn)); assert.ok(Array.isArray(body.saved)); });