import { afterEach, describe, expect, test } from "bun:test"; import { configureConversion } from "../src/convert/config.ts"; import { handleCapabilities, handleConvert } from "../src/convert/service.ts"; import { localBinaryConverter } from "../src/convert/local.ts"; import { resetConverterProbe } from "../src/convert/client.ts"; import { viewerPlugin } from "../src/plugin.ts"; import { resolve } from "../src/resolve.ts"; import type { ViewerConverter } from "../src/convert/types.ts"; const encoder = new TextEncoder(); function fakeConverter(overrides: Partial = {}): ViewerConverter { return { name: "fake", version: "1", supports: (_kind, ext) => ext === ".docx", outputType: (ext) => (ext === ".docx" ? "application/pdf" : undefined), toPreview: async () => ({ mime: "application/pdf", bytes: encoder.encode("%PDF-1.7 fake") }), ...overrides, }; } const request = (path: string) => new Request(`https://app.test${path}`); const convertRequest = (src: string, name: string) => request(`/api/viewer/preview?src=${encodeURIComponent(src)}&name=${encodeURIComponent(name)}`); afterEach(() => { configureConversion(undefined); resetConverterProbe(); }); describe("conversion routes", () => { test("everything 404s until conversion is configured", async () => { expect(handleCapabilities().status).toBe(404); expect((await handleConvert(convertRequest("/a.docx", "a.docx"), "preview")).status).toBe(404); }); test("capabilities advertise only what a converter claims", async () => { configureConversion({ converters: [fakeConverter()] }); const body = (await handleCapabilities().json()) as { converters: string[]; formats: Record; }; expect(body.converters).toEqual(["fake"]); expect(body.formats[".docx"]).toBe("application/pdf"); expect(body.formats[".psd"]).toBeUndefined(); }); test("unknown extensions and unsupported formats are refused, not attempted", async () => { configureConversion({ converters: [fakeConverter()] }); expect((await handleConvert(convertRequest("/a.foo", "a.foo"), "preview")).status).toBe(415); expect((await handleConvert(convertRequest("/a.psd", "a.psd"), "preview")).status).toBe(415); expect((await handleConvert(request("/api/viewer/preview"), "preview")).status).toBe(400); }); test("the default source policy refuses cross-origin URLs without fetching", async () => { let fetched = false; const original = globalThis.fetch; globalThis.fetch = (async () => { fetched = true; return new Response("x"); }) as unknown as typeof fetch; try { configureConversion({ converters: [fakeConverter()] }); const response = await handleConvert( convertRequest("https://evil.test/a.docx", "a.docx"), "preview", ); expect(response.status).toBe(403); expect(fetched).toBe(false); } finally { globalThis.fetch = original; } }); test("a converted result is returned, then served from cache", async () => { let conversions = 0; configureConversion({ converters: [ fakeConverter({ toPreview: async () => { conversions++; return { mime: "application/pdf", bytes: encoder.encode("%PDF-1.7 fake") }; }, }), ], resolveSource: async () => ({ body: new Response(encoder.encode("docx bytes")).body!, size: 10, }), }); const first = await handleConvert(convertRequest("/files/a.docx", "a.docx"), "preview"); expect(first.status).toBe(200); expect(first.headers.get("content-type")).toBe("application/pdf"); expect(first.headers.get("x-content-type-options")).toBe("nosniff"); expect(first.headers.get("x-viewer-cache")).toBe("miss"); expect(await first.text()).toContain("%PDF"); const second = await handleConvert(convertRequest("/files/a.docx", "a.docx"), "preview"); expect(second.headers.get("x-viewer-cache")).toBe("hit"); expect(conversions).toBe(1); }); test("oversized sources are refused before the converter runs", async () => { let ran = false; configureConversion({ maxInputBytes: 8, converters: [ fakeConverter({ toPreview: async () => { ran = true; return { mime: "application/pdf", bytes: new Uint8Array() }; }, }), ], resolveSource: async () => ({ body: new Response("x").body!, size: 999 }), }); expect((await handleConvert(convertRequest("/a.docx", "a.docx"), "preview")).status).toBe(413); expect(ran).toBe(false); }); test("a converter failure reports a generic error, never its own message", async () => { configureConversion({ converters: [ fakeConverter({ toPreview: async () => { throw new Error("soffice crashed at C:/secret/path"); }, }), ], resolveSource: async () => ({ body: new Response("x").body! }), }); const response = await handleConvert(convertRequest("/a.docx", "a.docx"), "preview"); expect(response.status).toBe(422); expect(await response.text()).not.toContain("secret"); }); }); describe("plugin wiring", () => { test("routes appear only when conversion is configured", () => { expect(viewerPlugin().routeEntries).toBeUndefined(); const routes = viewerPlugin({ convert: { converters: [fakeConverter()] } }).routeEntries as { path: string; }[]; expect(routes.map((route) => route.path)).toEqual([ "/api/viewer/capabilities", "/api/viewer/preview", "/api/viewer/thumb", "/api/viewer/text", ]); // Configuring the plugin is what arms the handlers. expect(handleCapabilities().status).toBe(200); }); }); describe("local binary converter", () => { test("a disabled binary makes its formats unsupported", () => { const converter = localBinaryConverter({ binaries: { soffice: false, magick: false, ffmpeg: false, "7z": false }, }); expect(converter.supports("doc", ".docx")).toBe(false); expect(converter.outputType(".psd")).toBeUndefined(); }); test("an unknown extension is never routed to a binary", () => { expect(localBinaryConverter().supports("binary", ".xyz")).toBe(false); }); test("conversion of an unsupported format rejects rather than spawning", async () => { const converter = localBinaryConverter({ binaries: { soffice: false } }); await expect( converter.toPreview(new Response("x").body!, { name: "a.docx", ext: ".docx", kind: "doc", signal: new AbortController().signal, }), ).rejects.toThrow("No local converter"); }); }); describe("tier 3 planning", () => { test("office and design files keep their extension kind despite generic bytes", async () => { // A .docx is a ZIP and a .psd is opaque binary; neither signature identifies // the content, so the extension must still choose the converter. const docx = await resolve({ name: "report.docx", blob: new Blob([new Uint8Array([0x50, 0x4b, 3, 4, 0, 0])]), }); expect(docx.kind).toBe("doc"); const psd = await resolve({ name: "art.psd", blob: new Blob([new Uint8Array([0x38, 0x42, 0x50, 0x53, 0, 1])]), }); expect(psd.kind).toBe("binary"); }); test("without a reachable converter they fall back and ask for no conversion", async () => { const plan = await resolve({ name: "report.docx", blob: new Blob([new Uint8Array([0x50, 0x4b, 3, 4, 0, 0])]), }); expect(plan.renderer.component).toBe("DownloadCard"); expect(plan.needsConvert).toBe(false); }); });