// IMP-56 (#90) u19 — vitest coverage for the vite POST /api/export // middleware and its supporting inlineAssetsAsDataUrls helper. // // Scope: // 1) inlineAssetsAsDataUrls (pure helper): // - no url(assets/...) refs → passthrough. // - single PNG ref → inlined as base64 data: URL with image/png mime. // - multiple refs → all inlined. // - SVG ref → image/svg+xml mime. // - missing asset file → left as-is (no throw, no rewrite). // - data:/http:/ URLs (non-asset) → untouched. // 2) handleExportStandalone (POST): // - method != POST → false (chain continues; next middleware may handle). // - invalid JSON / non-object body → 400. // - missing run_id → 400. // - invalid run_id (key gate / path traversal) → 400. // - final.html missing → 404. // - success → 200 with Content-Disposition: attachment; filename=..., // Content-Type: text/html; charset=utf-8, body = inlined HTML. // // Tests exercise the pure handler with mock req/res — no real vite server. import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { EventEmitter } from "node:events"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { handleExportStandalone, inlineAssetsAsDataUrls, } from "../../vite.config"; function makeMockRes() { const state = { statusCode: 0, headers: {} as Record, body: "", ended: false, }; return { state, res: { writeHead(status: number, headers?: Record) { state.statusCode = status; if (headers) state.headers = headers; }, end(body?: string) { state.body = body ?? ""; state.ended = true; }, }, }; } function makeMockReq(opts: { method?: string; }): EventEmitter & { method?: string; send: (body: string) => void } { const ee = new EventEmitter() as EventEmitter & { method?: string; send: (body: string) => void; }; ee.method = opts.method; ee.send = (body: string) => { if (body.length > 0) ee.emit("data", Buffer.from(body, "utf-8")); ee.emit("end"); }; return ee; } function seedRun( daRoot: string, runId: string, htmlBody: string, assets?: Record, ): string { const runDir = path.join(daRoot, "data", "runs", runId, "phase_z2"); fs.mkdirSync(runDir, { recursive: true }); const html = path.join(runDir, "final.html"); fs.writeFileSync(html, htmlBody, "utf-8"); if (assets) { for (const [rel, buf] of Object.entries(assets)) { const dst = path.join(runDir, "assets", rel); fs.mkdirSync(path.dirname(dst), { recursive: true }); fs.writeFileSync(dst, buf); } } return runDir; } describe("inlineAssetsAsDataUrls (IMP-56 #90 u19)", () => { let tmp: string; beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), "imp90-u19-inline-")); }); afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); }); it("returns html unchanged when no url(assets/...) refs are present", () => { const html = "hi"; expect(inlineAssetsAsDataUrls(html, tmp)).toBe(html); }); it("inlines a single PNG asset as a base64 data: URL with image/png mime", () => { fs.mkdirSync(path.join(tmp, "frame_x"), { recursive: true }); const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); fs.writeFileSync(path.join(tmp, "frame_x", "a.png"), pngBytes); const html = "background: url(assets/frame_x/a.png);"; const out = inlineAssetsAsDataUrls(html, tmp); expect(out).toContain(`url("data:image/png;base64,${pngBytes.toString("base64")}")`); expect(out).not.toContain("url(assets/frame_x/a.png)"); }); it("inlines multiple refs across the same HTML body", () => { fs.mkdirSync(path.join(tmp, "f"), { recursive: true }); fs.writeFileSync(path.join(tmp, "f", "one.png"), Buffer.from("ONE")); fs.writeFileSync(path.join(tmp, "f", "two.png"), Buffer.from("TWO")); const html = "a{background:url(assets/f/one.png)} b{background:url(assets/f/two.png)}"; const out = inlineAssetsAsDataUrls(html, tmp); expect(out).toContain(`data:image/png;base64,${Buffer.from("ONE").toString("base64")}`); expect(out).toContain(`data:image/png;base64,${Buffer.from("TWO").toString("base64")}`); }); it("uses image/svg+xml mime for .svg refs", () => { fs.mkdirSync(path.join(tmp, "f"), { recursive: true }); fs.writeFileSync(path.join(tmp, "f", "icon.svg"), "", "utf-8"); const html = "url(assets/f/icon.svg)"; const out = inlineAssetsAsDataUrls(html, tmp); expect(out).toContain("data:image/svg+xml;base64,"); }); it("leaves the ref untouched when the asset file is missing", () => { const html = "url(assets/missing/file.png)"; const out = inlineAssetsAsDataUrls(html, tmp); expect(out).toBe(html); }); it("does not touch data: or http(s): url() values (only matches assets/...)", () => { const html = "x{background:url(data:image/png;base64,AAA)} " + "y{background:url(https://cdn.x/a.png)}"; expect(inlineAssetsAsDataUrls(html, tmp)).toBe(html); }); it("handles quoted url(...) refs (single and double quotes)", () => { fs.mkdirSync(path.join(tmp, "q"), { recursive: true }); fs.writeFileSync(path.join(tmp, "q", "k.png"), Buffer.from("K")); const html = "a{background:url('assets/q/k.png')} b{background:url(\"assets/q/k.png\")}"; const out = inlineAssetsAsDataUrls(html, tmp); const data = `data:image/png;base64,${Buffer.from("K").toString("base64")}`; expect(out.split(data).length - 1).toBe(2); }); }); describe("handleExportStandalone (IMP-56 #90 u19)", () => { let daRoot: string; beforeEach(() => { daRoot = fs.mkdtempSync(path.join(os.tmpdir(), "imp90-u19-da-")); }); afterEach(() => { fs.rmSync(daRoot, { recursive: true, force: true }); }); it("returns false (next chained) when method != POST", () => { const req = makeMockReq({ method: "GET" }); const { res, state } = makeMockRes(); const handled = handleExportStandalone(req, res, daRoot); expect(handled).toBe(false); expect(state.ended).toBe(false); }); it("returns 400 on invalid JSON body", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); const handled = handleExportStandalone(req, res, daRoot); expect(handled).toBe(true); req.send("{nope"); expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("invalid JSON"); }); it("returns 400 when body is not a JSON object (array root)", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleExportStandalone(req, res, daRoot); req.send(JSON.stringify(["x"])); expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("body must be a JSON object"); }); it("returns 400 when run_id is missing", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleExportStandalone(req, res, daRoot); req.send(JSON.stringify({})); expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("missing run_id"); }); it("returns 400 when run_id contains path traversal", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleExportStandalone(req, res, daRoot); req.send(JSON.stringify({ run_id: "../escape" })); expect(state.statusCode).toBe(400); expect(JSON.parse(state.body).error).toBe("invalid run_id"); }); it("returns 404 when final.html does not exist for run_id", () => { const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleExportStandalone(req, res, daRoot); req.send(JSON.stringify({ run_id: "ghost_run" })); expect(state.statusCode).toBe(404); expect(JSON.parse(state.body).error).toBe("final.html not found"); }); it("returns 200 with text/html body + Content-Disposition on success", () => { seedRun(daRoot, "mdx03_run", "03"); const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleExportStandalone(req, res, daRoot); req.send(JSON.stringify({ run_id: "mdx03_run" })); expect(state.statusCode).toBe(200); expect(state.headers["Content-Type"]).toBe("text/html; charset=utf-8"); expect(state.headers["Content-Disposition"]).toBe( 'attachment; filename="mdx03_run.html"', ); expect(state.body).toBe("03"); }); it("inlines assets in final.html when run dir has assets/", () => { const pngBytes = Buffer.from("PNGDATA"); seedRun( daRoot, "mdx05_run", "
", { "f/x.png": pngBytes }, ); const req = makeMockReq({ method: "POST" }); const { res, state } = makeMockRes(); handleExportStandalone(req, res, daRoot); req.send(JSON.stringify({ run_id: "mdx05_run" })); expect(state.statusCode).toBe(200); expect(state.body).toContain( `data:image/png;base64,${pngBytes.toString("base64")}`, ); expect(state.body).not.toContain("url(assets/f/x.png)"); }); });