All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m8s
- Refactored demo routes to use shared handlePdfRoute utility - Added handleDemoPdfRoute wrapper to preserve attachment disposition - Preserved watermark injection and demo.pdf default filename - Added comprehensive TDD tests for Content-Disposition behavior - Reduced demo.ts from 269 to 238 lines (31 lines removed) - All 628 tests pass including 6 new behavioral tests Fixes duplicated error handling, validation, and concurrency logic while maintaining existing demo route behavior.
272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import express from "express";
|
|
import request from "supertest";
|
|
|
|
vi.mock("../services/browser.js", () => ({
|
|
renderPdf: vi.fn(),
|
|
renderUrlPdf: vi.fn(),
|
|
}));
|
|
|
|
let app: express.Express;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
vi.resetModules();
|
|
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockResolvedValue({ pdf: Buffer.from("%PDF-1.4 mock"), durationMs: 10 });
|
|
|
|
const { demoRouter } = await import("../routes/demo.js");
|
|
app = express();
|
|
app.use(express.json({ limit: "500kb" }));
|
|
app.use("/v1/demo", demoRouter);
|
|
});
|
|
|
|
describe("POST /v1/demo/html", () => {
|
|
it("returns 400 for missing html", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("returns 415 for wrong content-type", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "text/plain")
|
|
.send("html=<h1>hi</h1>");
|
|
expect(res.status).toBe(415);
|
|
});
|
|
|
|
it("returns 503 on QUEUE_FULL", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockRejectedValue(new Error("QUEUE_FULL"));
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>" });
|
|
expect(res.status).toBe(503);
|
|
});
|
|
|
|
it("returns 504 on PDF_TIMEOUT", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockRejectedValue(new Error("PDF_TIMEOUT"));
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>" });
|
|
expect(res.status).toBe(504);
|
|
});
|
|
|
|
it("returns 500 on unexpected error", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockRejectedValue(new Error("something broke"));
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>" });
|
|
expect(res.status).toBe(500);
|
|
expect(res.body.error).toMatch(/PDF generation failed/);
|
|
});
|
|
|
|
it("returns PDF with watermark on success", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>" });
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-type"]).toMatch(/application\/pdf/);
|
|
// Verify watermark was injected into the HTML passed to renderPdf
|
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
|
expect(calledHtml).toContain("DEMO");
|
|
expect(calledHtml).toContain("docfast.dev");
|
|
});
|
|
|
|
it("returns 400 for invalid scale", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>", scale: 99 });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toMatch(/scale/);
|
|
});
|
|
|
|
it("returns 400 for invalid format", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>", format: "INVALID" });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toMatch(/format/);
|
|
});
|
|
|
|
it("returns 400 for non-boolean landscape", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>", landscape: "yes" });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toMatch(/landscape/);
|
|
});
|
|
|
|
it("returns 400 for invalid margin", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>", margin: "10px" });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toMatch(/margin/);
|
|
});
|
|
});
|
|
|
|
describe("POST /v1/demo/markdown", () => {
|
|
it("returns 400 for missing markdown", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("returns 415 for wrong content-type", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "text/plain")
|
|
.send("markdown=# hi");
|
|
expect(res.status).toBe(415);
|
|
});
|
|
|
|
it("returns 503 on QUEUE_FULL", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockRejectedValue(new Error("QUEUE_FULL"));
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello" });
|
|
expect(res.status).toBe(503);
|
|
});
|
|
|
|
it("returns 504 on PDF_TIMEOUT", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockRejectedValue(new Error("PDF_TIMEOUT"));
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello" });
|
|
expect(res.status).toBe(504);
|
|
});
|
|
|
|
it("returns 500 on unexpected error", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
vi.mocked(renderPdf).mockRejectedValue(new Error("something broke"));
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello" });
|
|
expect(res.status).toBe(500);
|
|
expect(res.body.error).toMatch(/PDF generation failed/);
|
|
});
|
|
|
|
it("returns PDF with watermark on success", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello World" });
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-type"]).toMatch(/application\/pdf/);
|
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
|
expect(calledHtml).toContain("DEMO");
|
|
expect(calledHtml).toContain("docfast.dev");
|
|
});
|
|
|
|
it("returns 400 for invalid scale", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello", scale: 99 });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toMatch(/scale/);
|
|
});
|
|
|
|
it("returns 400 for invalid format", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello", format: "INVALID" });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.error).toMatch(/format/);
|
|
});
|
|
|
|
// NEW TDD TESTS - These should verify current behavior before refactoring
|
|
it("returns Content-Disposition attachment header", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello" });
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-disposition"]).toMatch(/attachment/);
|
|
expect(res.headers["content-disposition"]).toMatch(/filename="demo\.pdf"/);
|
|
});
|
|
|
|
it("returns custom filename in attachment header", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello", filename: "custom.pdf" });
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-disposition"]).toMatch(/attachment/);
|
|
expect(res.headers["content-disposition"]).toMatch(/filename="custom\.pdf"/);
|
|
});
|
|
|
|
it("injects watermark into HTML content", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
const res = await request(app)
|
|
.post("/v1/demo/markdown")
|
|
.set("content-type", "application/json")
|
|
.send({ markdown: "# Hello" });
|
|
expect(res.status).toBe(200);
|
|
|
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
|
expect(calledHtml).toContain("Generated by DocFast — docfast.dev");
|
|
expect(calledHtml).toContain("Upgrade to Pro for clean PDFs");
|
|
expect(calledHtml).toContain("position:fixed;top:0;left:0;width:100%;height:100%"); // watermark overlay
|
|
});
|
|
|
|
// NEW TDD TESTS - These should verify current behavior before refactoring
|
|
it("returns Content-Disposition attachment header", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>" });
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-disposition"]).toMatch(/attachment/);
|
|
expect(res.headers["content-disposition"]).toMatch(/filename="demo\.pdf"/);
|
|
});
|
|
|
|
it("returns custom filename in attachment header", async () => {
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>", filename: "custom.pdf" });
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-disposition"]).toMatch(/attachment/);
|
|
expect(res.headers["content-disposition"]).toMatch(/filename="custom\.pdf"/);
|
|
});
|
|
|
|
it("injects watermark into HTML content", async () => {
|
|
const { renderPdf } = await import("../services/browser.js");
|
|
const res = await request(app)
|
|
.post("/v1/demo/html")
|
|
.set("content-type", "application/json")
|
|
.send({ html: "<h1>Hello</h1>" });
|
|
expect(res.status).toBe(200);
|
|
|
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
|
expect(calledHtml).toContain("Generated by DocFast — docfast.dev");
|
|
expect(calledHtml).toContain("Upgrade to Pro for clean PDFs");
|
|
expect(calledHtml).toContain("position:fixed;top:0;left:0;width:100%;height:100%"); // watermark overlay
|
|
});
|
|
});
|