diff --git a/src/__tests__/demo.test.ts b/src/__tests__/demo.test.ts
new file mode 100644
index 0000000..e613bfe
--- /dev/null
+++ b/src/__tests__/demo.test.ts
@@ -0,0 +1,148 @@
+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(Buffer.from("%PDF-1.4 mock"));
+
+ 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=
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/html")
+ .set("content-type", "application/json")
+ .send({ html: "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/html")
+ .set("content-type", "application/json")
+ .send({ html: "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/html")
+ .set("content-type", "application/json")
+ .send({ html: "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/html")
+ .set("content-type", "application/json")
+ .send({ html: "Hello
" });
+ 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");
+ });
+});
+
+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");
+ });
+});