import { afterEach, describe, expect, test } from "bun:test"; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { configureStorage } from "@wrnexus/uploader"; import { configureUploads } from "../src/uploads/config.ts"; import { handleFile, handleSource, uploadedConversionSource } from "../src/uploads/service.ts"; import { viewerPlugin } from "../src/plugin.ts"; import { Window } from "happy-dom"; import { mountViewer } from "../src/runtime.ts"; const SECRET = "test-secret-value-32-chars-minimum!!"; const root = mkdtempSync(join(tmpdir(), "wrn-viewer-uploads-")); function storage() { for (const dir of ["public", "private"]) mkdirSync(join(root, "uploads", dir), { recursive: true }); writeFileSync(join(root, "uploads", "public", "logo.png"), Buffer.from([137, 80, 78, 71])); writeFileSync(join(root, "uploads", "private", "report.pdf"), Buffer.from("%PDF-1.7 private")); configureStorage( { default: "public", stores: { public: { driver: "local", access: "public", dir: "uploads/public" }, private: { driver: "local", access: "private", dir: "uploads/private" }, secret: { driver: "local", access: "private", dir: "uploads/private" }, }, }, root, ); } storage(); const ask = (query: string) => new Request(`https://app.test/api/viewer/source?${query}`); const body = async (response: Response) => (await response.json()) as { src: string; name: string; mime?: string }; afterEach(() => configureUploads(undefined)); describe("resolving a storage key", () => { test("nothing resolves until uploads are enabled", async () => { expect((await handleSource(ask("key=logo.png"))).status).toBe(404); }); test("a public object answers with its own URL, no token", async () => { configureUploads({ stores: ["public"] }); const response = await handleSource(ask("key=logo.png&store=public")); expect(response.status).toBe(200); const descriptor = await body(response); expect(descriptor.src).toContain("logo.png"); expect(descriptor.src).not.toContain("token="); expect(descriptor.name).toBe("logo.png"); // The MIME comes from the uploader's table, not a second copy of it. expect(descriptor.mime).toBe("image/png"); }); test("stores outside the allow-list are refused", async () => { configureUploads({ stores: ["public"], secret: SECRET, authorize: () => true }); expect((await handleSource(ask("key=report.pdf&store=private"))).status).toBe(403); }); test("keys that try to escape the store are refused", async () => { configureUploads({ stores: ["public"] }); for (const key of ["../secrets.env", "/etc/passwd", "a/../../b"]) expect((await handleSource(ask(`key=${encodeURIComponent(key)}&store=public`))).status).toBe( 400, ); }); test("a private object needs both a secret and an authorize hook", async () => { configureUploads({ stores: ["private"] }); expect((await handleSource(ask("key=report.pdf&store=private"))).status).toBe(403); configureUploads({ stores: ["private"], secret: SECRET }); expect((await handleSource(ask("key=report.pdf&store=private"))).status).toBe(403); }); test("authorize decides, and a refusal mints nothing", async () => { const seen: { store: string; key: string }[] = []; configureUploads({ stores: ["private"], secret: SECRET, authorize: (_request, params) => { seen.push(params); return false; }, }); expect((await handleSource(ask("key=report.pdf&store=private"))).status).toBe(403); expect(seen).toEqual([{ store: "private", key: "report.pdf" }]); }); }); describe("serving a signed link", () => { const signedFor = async (key: string, store = "private") => { configureUploads({ stores: [store], secret: SECRET, authorize: () => true }); const descriptor = await body(await handleSource(ask(`key=${key}&store=${store}`))); return new URL(descriptor.src, "https://app.test").searchParams.get("token")!; }; const fetchFile = (token: string, init: RequestInit = {}) => handleFile( new Request(`https://app.test/api/viewer/file?token=${encodeURIComponent(token)}`, init), ); test("a valid link streams the object, uncacheable and inline", async () => { const response = await fetchFile(await signedFor("report.pdf")); expect(response.status).toBe(200); expect(response.headers.get("cache-control")).toBe("private, no-store"); expect(response.headers.get("x-content-type-options")).toBe("nosniff"); expect(response.headers.get("content-disposition")).toBe("inline"); expect(await response.text()).toContain("%PDF"); }); test("HEAD answers with the same headers and no body", async () => { const response = await fetchFile(await signedFor("report.pdf"), { method: "HEAD" }); expect(response.status).toBe(200); expect(response.headers.get("content-length")).toBe("16"); expect(await response.text()).toBe(""); }); test("ranges are served so media can seek", async () => { const response = await fetchFile(await signedFor("report.pdf"), { headers: { range: "bytes=0-3" }, }); expect(response.status).toBe(206); expect(response.headers.get("content-range")).toBe("bytes 0-3/16"); expect(await response.text()).toBe("%PDF"); }); test("a tampered, unsigned or expired link is indistinguishable from a miss", async () => { const token = await signedFor("report.pdf"); expect((await fetchFile(token.slice(0, -2) + "xx")).status).toBe(404); expect((await fetchFile("not-a-token")).status).toBe(404); expect((await handleFile(new Request("https://app.test/api/viewer/file"))).status).toBe(400); // A link signed with a different secret must not open under this one. configureUploads({ stores: ["private"], secret: "another-secret-entirely-x", authorize: () => true, }); expect((await fetchFile(token)).status).toBe(404); }); test("a link for one store cannot be replayed after that store is withdrawn", async () => { const token = await signedFor("report.pdf"); configureUploads({ stores: ["public"], secret: SECRET, authorize: () => true }); expect((await fetchFile(token)).status).toBe(404); }); }); describe("conversion inherits the same check", () => { test("only a valid signed link opens an object for a converter", async () => { configureUploads({ stores: ["private"], secret: SECRET, authorize: () => true }); const descriptor = await body(await handleSource(ask("key=report.pdf&store=private"))); const open = uploadedConversionSource(); const request = new Request("https://app.test/api/viewer/preview"); const allowed = await open(request, { src: descriptor.src, name: "report.pdf" }); expect(allowed).toBeDefined(); expect(allowed!.size).toBe(16); // A bare key, a foreign URL, or a forged token opens nothing. expect(await open(request, { src: "report.pdf", name: "report.pdf" })).toBeUndefined(); expect( await open(request, { src: "https://evil.test/x?token=abc", name: "report.pdf" }), ).toBeUndefined(); }); }); describe("plugin wiring", () => { test("uploader routes appear only when uploads are configured", () => { const paths = (plugin: ReturnType) => ((plugin.routeEntries ?? []) as { path: string }[]).map((route) => route.path); expect(paths(viewerPlugin())).toEqual([]); expect(paths(viewerPlugin({ uploads: { stores: ["public"] } }))).toEqual([ "/api/viewer/source", "/api/viewer/file", ]); // Enabling one feature must not publish the other's endpoints. expect(paths(viewerPlugin({ uploads: { stores: ["public"] } }))).not.toContain( "/api/viewer/preview", ); }); }); describe("the browser side of a key", () => { async function mountKeyed(descriptor: unknown, body: string, componentName = "") { const window = new Window({ url: "https://app.test/" }); const host = window.document.createElement("section") as unknown as HTMLElement; window.document.body.append(host as never); const asked: string[] = []; const original = globalThis.fetch; globalThis.fetch = (async (input: string | URL | Request) => { const url = String(input); asked.push(url); if (url.includes("/api/viewer/source")) return descriptor ? Response.json(descriptor) : new Response("no", { status: 403 }); return new Response(body, { headers: { "content-type": "text/csv" } }); }) as unknown as typeof fetch; try { const dispose = mountViewer(host, { key: "regional-figures.csv", store: "reports", name: componentName, }); for (let i = 0; i < 20; i++) await new Promise((done) => setTimeout(done, 5)); return { host, asked, dispose, window }; } finally { globalThis.fetch = original; } } test("a key is exchanged for a link, and the store travels with it", async () => { const { host, asked, dispose, window } = await mountKeyed( { src: "/api/viewer/file?token=abc", name: "regional-figures.csv", mime: "text/csv" }, "region,units\nNorth,120\n", ); const source = asked.find((url) => url.includes("/api/viewer/source"))!; expect(source).toContain("key=regional-figures.csv"); expect(source).toContain("store=reports"); // The descriptor's filename must reach classification: without the extension // the renderer for this kind cannot be found at all. expect(host.querySelector("table")).not.toBeNull(); expect(host.querySelector(".wrn-viewer-toolbar span")!.textContent).toBe( "regional-figures.csv", ); dispose(); await window.happyDOM.close(); }); test("an explicit name on the component still wins", async () => { const { host, dispose, window } = await mountKeyed( { src: "/api/viewer/file?token=abc", name: "regional-figures.csv", mime: "text/csv" }, "region,units\nNorth,120\n", "Q3 figures.csv", ); expect(host.querySelector(".wrn-viewer-toolbar span")!.textContent).toBe("Q3 figures.csv"); dispose(); await window.happyDOM.close(); }); test("a refused key shows the fallback, and no file is fetched", async () => { const { host, asked, dispose, window } = await mountKeyed(undefined, ""); expect(host.textContent).toContain("This file is not available to preview"); expect(asked.every((url) => url.includes("/api/viewer/source"))).toBe(true); dispose(); await window.happyDOM.close(); }); }); process.on("exit", () => rmSync(root, { recursive: true, force: true }));