All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m42s
144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import express from "express";
|
|
import request from "supertest";
|
|
|
|
let app: express.Express;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockResolvedValue(Buffer.from("%PDF-1.4 mock"));
|
|
|
|
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("GET /v1/templates", () => {
|
|
it("returns template list with id, name, description, fields", async () => {
|
|
const res = await request(app)
|
|
.get("/v1/templates")
|
|
.set("Authorization", "Bearer test-key");
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.templates).toBeInstanceOf(Array);
|
|
expect(res.body.templates.length).toBeGreaterThan(0);
|
|
|
|
const invoice = res.body.templates.find((t: any) => t.id === "invoice");
|
|
expect(invoice).toBeDefined();
|
|
expect(invoice.name).toBe("Invoice");
|
|
expect(invoice.description).toBeTruthy();
|
|
expect(invoice.fields).toBeInstanceOf(Array);
|
|
expect(invoice.fields.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("requires auth (401 without key)", async () => {
|
|
const res = await request(app).get("/v1/templates");
|
|
expect(res.status).toBe(401);
|
|
});
|
|
});
|
|
|
|
describe("POST /v1/templates/:id/render", () => {
|
|
const validInvoiceData = {
|
|
invoiceNumber: "INV-001",
|
|
date: "2026-01-15",
|
|
from: { name: "Acme Corp" },
|
|
to: { name: "Client Inc" },
|
|
items: [{ description: "Service", quantity: 1, unitPrice: 100 }],
|
|
};
|
|
|
|
it("returns 404 for unknown template", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/templates/nonexistent/render")
|
|
.set("Authorization", "Bearer test-key")
|
|
.send({ foo: "bar" });
|
|
|
|
expect(res.status).toBe(404);
|
|
expect(res.body.error).toContain("not found");
|
|
});
|
|
|
|
it("returns 400 with missing required fields listed", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/templates/invoice/render")
|
|
.set("Authorization", "Bearer test-key")
|
|
.send({ invoiceNumber: "INV-001" });
|
|
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.missing).toBeInstanceOf(Array);
|
|
expect(res.body.missing).toContain("date");
|
|
expect(res.body.missing).toContain("from");
|
|
});
|
|
|
|
it("renders invoice successfully, returns PDF", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/templates/invoice/render")
|
|
.set("Authorization", "Bearer test-key")
|
|
.send(validInvoiceData);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-type"]).toMatch(/application\/pdf/);
|
|
expect(res.headers["content-disposition"]).toContain("invoice.pdf");
|
|
});
|
|
|
|
it("accepts data in req.body.data wrapper", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/templates/invoice/render")
|
|
.set("Authorization", "Bearer test-key")
|
|
.send({ data: validInvoiceData });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-type"]).toMatch(/application\/pdf/);
|
|
});
|
|
|
|
it("passes _format, _margin to renderPdf", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
|
|
await request(app)
|
|
.post("/v1/templates/invoice/render")
|
|
.set("Authorization", "Bearer test-key")
|
|
.send({
|
|
...validInvoiceData,
|
|
_format: "Letter",
|
|
_margin: { top: "20mm", bottom: "20mm" },
|
|
});
|
|
|
|
expect(vi.mocked(renderPdf)).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
format: "Letter",
|
|
margin: { top: "20mm", bottom: "20mm" },
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sanitizes _filename in Content-Disposition", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/templates/invoice/render")
|
|
.set("Authorization", "Bearer test-key")
|
|
.send({ ...validInvoiceData, _filename: 'evil"file\nname.pdf' });
|
|
|
|
expect(res.status).toBe(200);
|
|
const disposition = res.headers["content-disposition"];
|
|
// The quote and newline in the input should be sanitized to underscores
|
|
expect(disposition).toContain("evil_file_name.pdf");
|
|
expect(disposition).not.toContain("\n");
|
|
});
|
|
|
|
it("returns 500 on render error", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockRejectedValue(new Error("Browser crashed"));
|
|
|
|
const res = await request(app)
|
|
.post("/v1/templates/invoice/render")
|
|
.set("Authorization", "Bearer test-key")
|
|
.send(validInvoiceData);
|
|
|
|
expect(res.status).toBe(500);
|
|
expect(res.body.error).toContain("failed");
|
|
});
|
|
});
|