import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import fs from "node:fs"; import fsp from "node:fs/promises"; import { Renderer } from "@pagenull/renderer"; import { setRenderer } from "@/lib/renderer"; import { logMeta, setLogSink } from "@/lib/log"; import { createServer, type Server } from "node:http"; import type { AddressInfo } from "node:net"; import { verifyReceipt } from "@/lib/receipt"; import { setDeliveryAllowForTests } from "@/lib/deliver"; import { usageFor } from "@/lib/quota"; import { POST as pdf } from "@/app/v1/pdf/route"; import { POST as renderTemplate } from "@/app/v1/templates/[id]/render/route"; import { POST as createTemplateRoute } from "@/app/v1/templates/route"; import { apiRequest, dumpDatabase, freshDb, makeAccount } from "./helpers"; /** * The privacy promise, as tests: after a render, the request's content must not * exist anywhere we control — not in any database table, not in logs, not on disk. */ const renderer = new Renderer({ concurrency: 2 }); const logLines: string[] = []; beforeAll(async () => { await freshDb(); setRenderer(renderer); setLogSink((l) => logLines.push(l)); }); afterAll(() => renderer.close()); const marker = () => `SECRET-${Math.random().toString(36).slice(2)}-PATIENT-DATA`; function spyOnDiskWrites() { const spies = [ vi.spyOn(fs, "writeFileSync"), vi.spyOn(fs, "writeFile"), vi.spyOn(fs, "appendFileSync"), vi.spyOn(fs, "createWriteStream"), vi.spyOn(fsp, "writeFile"), vi.spyOn(fsp, "appendFile"), ]; return { calls: () => spies.reduce((n, s) => n + s.mock.calls.length, 0), restore: () => spies.forEach((s) => s.mockRestore()), }; } describe("zero retention", () => { it("HTML sent to /v1/pdf is not stored, logged or written to disk", async () => { const { key } = await makeAccount(); const secret = marker(); const disk = spyOnDiskWrites(); const consoleSpy = vi.spyOn(console, "log"); const res = await pdf(apiRequest("/v1/pdf", key, { html: `
Dear {{name}}
" })); const { id } = await created.json(); const res = await renderTemplate(apiRequest(`/v1/templates/${id}/render`, key, { data: { name: secret } }), { params: Promise.resolve({ id }) } as never); expect(res.status).toBe(200); expect(await dumpDatabase()).not.toContain(secret); expect(logLines.join("\n")).not.toContain(secret); }); it("saved templates are encrypted at rest", async () => { const { key } = await makeAccount(); const secret = marker(); const res = await createTemplateRoute( apiRequest("/v1/templates", key, { name: "Encrypted", html: `${secret}
`, sampleData: { ssn: secret } }), ); expect(res.status).toBe(201); expect(await dumpDatabase()).not.toContain(secret); }); it("URLs are not stored or logged", async () => { const { key } = await makeAccount(); const secret = marker().toLowerCase(); await pdf(apiRequest("/v1/pdf", key, { url: `http://127.0.0.1/${secret}` })); // blocked, but still must not be recorded expect(await dumpDatabase()).not.toContain(secret); expect(logLines.join("\n")).not.toContain(secret); }); it("the logger drops any field that isn't metadata", () => { const secret = marker(); logMeta({ event: "x", status: 200, ...({ html: secret, body: secret, url: secret } as object) } as never); expect(logLines.at(-1)).not.toContain(secret); }); }); describe("no storage option exists", () => { it("rejects the old store/expiresIn options with a pointer to deliverTo", async () => { const { key } = await makeAccount("starter"); for (const body of [{ html: "x
", store: true }, { html: "x
", expiresIn: 60 }]) { const res = await pdf(apiRequest("/v1/pdf", key, body)); expect(res.status).toBe(400); expect((await res.json()).error.message).toMatch(/never stores output.*deliverTo/); } }); }); describe("deliverTo (bring your own storage)", () => { let bucket: Server; let bucketUrl = ""; let received: Buffer | null = null; beforeAll(async () => { bucket = createServer((req, res) => { const chunks: Buffer[] = []; req.on("data", (c) => chunks.push(c)); req.on("end", () => { received = Buffer.concat(chunks); res.writeHead(200).end(); }); }); await new Promisex
", deliverTo: url })); expect([400, 422]).toContain(res.status); } }); }); describe("retention headers and signed receipts", () => { it("every render carries retention, region, render id, renderer and a valid receipt for the exact bytes", async () => { const { key } = await makeAccount(); const res = await pdf(apiRequest("/v1/pdf", key, { html: "receipt
" })); const bytes = Buffer.from(await res.arrayBuffer()); expect(res.headers.get("pagenull-retention")).toBe("none"); expect(res.headers.get("pagenull-region")).toBeTruthy(); expect(res.headers.get("pagenull-render-id")).toMatch(/^[0-9a-f-]{36}$/); expect(res.headers.get("pagenull-renderer")).toBeTruthy(); const v = verifyReceipt(res.headers.get("pagenull-receipt")!, bytes); expect(v.ok).toBe(true); if (v.ok) { expect(v.fileMatches).toBe(true); expect(v.payload.stored).toBe(false); expect(v.payload.render_id).toBe(res.headers.get("pagenull-render-id")); } // A different file, or a tampered receipt, fails. const other = verifyReceipt(res.headers.get("pagenull-receipt")!, Buffer.from("%PDF-1.7 other")); expect(other.ok && other.fileMatches).toBe(false); const r = res.headers.get("pagenull-receipt")!; const tampered = r.slice(0, -4) + (r.endsWith("AAAA") ? "BBBB" : "AAAA"); expect(verifyReceipt(tampered).ok).toBe(false); }); it("test renders are free and watermarked", async () => { const { db, user, key } = await makeAccount(); const res = await pdf(apiRequest("/v1/pdf", key, { html: "t
", test: true })); expect(res.status).toBe(200); expect(await usageFor(db, user.id)).toBe(0); }); });