import { describe, it, expect, vi, beforeEach } from "vitest"; import express from "express"; import request from "supertest"; let app: express.Express; const baseData = { invoiceNumber: "INV-001", date: "2026-01-15", from: { name: "Acme Corp" }, to: { name: "Client Inc" }, items: [{ description: "Service", quantity: 1, unitPrice: 100 }], }; beforeEach(async () => { vi.clearAllMocks(); const { renderPdf } = await import("../services/browser.js"); vi.mocked(renderPdf).mockResolvedValue({ pdf: Buffer.from("%PDF-1.4 mock"), durationMs: 10 }); const { authMiddleware } = await import("../middleware/auth.js"); const { usageMiddleware } = await import("../middleware/usage.js"); const { templatesRouter } = await import("../routes/templates.js"); app = express(); app.use(express.json()); app.use("/v1/templates", authMiddleware, usageMiddleware, templatesRouter); }); describe("BUG-103: PDF option validation in template render", () => { it("should reject invalid _format with 400", async () => { const res = await request(app) .post("/v1/templates/invoice/render") .set("X-API-Key", "test-key") .send({ data: { ...baseData, _format: "EVIL_FORMAT" } }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/format/i); }); it("should accept and sanitize lowercase _format", async () => { const { renderPdf } = await import("../services/browser.js"); const res = await request(app) .post("/v1/templates/invoice/render") .set("X-API-Key", "test-key") .send({ data: { ...baseData, _format: "a4" } }); expect(res.status).toBe(200); expect(vi.mocked(renderPdf)).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ format: "A4" }) ); }); it("should reject _margin that is not an object", async () => { const res = await request(app) .post("/v1/templates/invoice/render") .set("X-API-Key", "test-key") .send({ data: { ...baseData, _margin: "10px" } }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/margin/i); }); it("should reject _margin with unknown keys", async () => { const res = await request(app) .post("/v1/templates/invoice/render") .set("X-API-Key", "test-key") .send({ data: { ...baseData, _margin: { top: "10px", bogus: "5px" } } }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/margin/i); }); it("should accept valid _margin and pass sanitized to renderPdf", async () => { const { renderPdf } = await import("../services/browser.js"); const res = await request(app) .post("/v1/templates/invoice/render") .set("X-API-Key", "test-key") .send({ data: { ...baseData, _margin: { top: "10px", bottom: "20px" } } }); expect(res.status).toBe(200); expect(vi.mocked(renderPdf)).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ margin: { top: "10px", bottom: "20px" } }) ); }); it("should default to A4 when no _format provided", async () => { const { renderPdf } = await import("../services/browser.js"); const res = await request(app) .post("/v1/templates/invoice/render") .set("X-API-Key", "test-key") .send({ data: { ...baseData } }); expect(res.status).toBe(200); expect(vi.mocked(renderPdf)).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ format: "A4" }) ); }); });