feat: validate PDF options with TDD tests
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 9m38s
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 9m38s
This commit is contained in:
parent
0e03e39ec7
commit
f89a3181f7
4 changed files with 335 additions and 0 deletions
|
|
@ -182,3 +182,66 @@ describe("POST /v1/convert/url", () => {
|
|||
expect(res.headers["content-type"]).toMatch(/application\/pdf/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PDF option validation (all endpoints)", () => {
|
||||
const endpoints = [
|
||||
{ path: "/v1/convert/html", body: { html: "<h1>Hi</h1>" } },
|
||||
{ path: "/v1/convert/markdown", body: { markdown: "# Hi" } },
|
||||
];
|
||||
|
||||
for (const { path, body } of endpoints) {
|
||||
it(`${path} returns 400 for invalid scale`, async () => {
|
||||
const res = await request(app)
|
||||
.post(path)
|
||||
.set("content-type", "application/json")
|
||||
.send({ ...body, scale: 5 });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("scale");
|
||||
});
|
||||
|
||||
it(`${path} returns 400 for invalid format`, async () => {
|
||||
const res = await request(app)
|
||||
.post(path)
|
||||
.set("content-type", "application/json")
|
||||
.send({ ...body, format: "B5" });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("format");
|
||||
});
|
||||
|
||||
it(`${path} returns 400 for non-boolean landscape`, async () => {
|
||||
const res = await request(app)
|
||||
.post(path)
|
||||
.set("content-type", "application/json")
|
||||
.send({ ...body, landscape: "yes" });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("landscape");
|
||||
});
|
||||
|
||||
it(`${path} returns 400 for invalid pageRanges`, async () => {
|
||||
const res = await request(app)
|
||||
.post(path)
|
||||
.set("content-type", "application/json")
|
||||
.send({ ...body, pageRanges: "abc" });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("pageRanges");
|
||||
});
|
||||
|
||||
it(`${path} returns 400 for invalid margin`, async () => {
|
||||
const res = await request(app)
|
||||
.post(path)
|
||||
.set("content-type", "application/json")
|
||||
.send({ ...body, margin: "1cm" });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("margin");
|
||||
});
|
||||
}
|
||||
|
||||
it("/v1/convert/url returns 400 for invalid scale", async () => {
|
||||
const res = await request(app)
|
||||
.post("/v1/convert/url")
|
||||
.set("content-type", "application/json")
|
||||
.send({ url: "https://example.com", scale: 5 });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("scale");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
162
src/__tests__/pdf-options.test.ts
Normal file
162
src/__tests__/pdf-options.test.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { validatePdfOptions } from "../utils/pdf-options.js";
|
||||
|
||||
describe("validatePdfOptions", () => {
|
||||
// --- Happy path ---
|
||||
it("accepts empty options", () => {
|
||||
const result = validatePdfOptions({});
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts undefined", () => {
|
||||
const result = validatePdfOptions(undefined as any);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts all valid options together", () => {
|
||||
const result = validatePdfOptions({
|
||||
scale: 1.5,
|
||||
format: "A4",
|
||||
landscape: true,
|
||||
printBackground: false,
|
||||
displayHeaderFooter: true,
|
||||
preferCSSPageSize: false,
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" },
|
||||
pageRanges: "1-5",
|
||||
});
|
||||
expect(result.valid).toBe(true);
|
||||
if (result.valid) {
|
||||
expect(result.sanitized.scale).toBe(1.5);
|
||||
expect(result.sanitized.format).toBe("A4");
|
||||
}
|
||||
});
|
||||
|
||||
// --- scale ---
|
||||
describe("scale", () => {
|
||||
it("accepts 0.1", () => {
|
||||
expect(validatePdfOptions({ scale: 0.1 }).valid).toBe(true);
|
||||
});
|
||||
it("accepts 2.0", () => {
|
||||
expect(validatePdfOptions({ scale: 2.0 }).valid).toBe(true);
|
||||
});
|
||||
it("rejects 0.05", () => {
|
||||
const r = validatePdfOptions({ scale: 0.05 });
|
||||
expect(r.valid).toBe(false);
|
||||
if (!r.valid) expect(r.error).toContain("scale");
|
||||
});
|
||||
it("rejects 2.5", () => {
|
||||
expect(validatePdfOptions({ scale: 2.5 }).valid).toBe(false);
|
||||
});
|
||||
it("rejects non-number", () => {
|
||||
expect(validatePdfOptions({ scale: "big" as any }).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// --- format ---
|
||||
describe("format", () => {
|
||||
const validFormats = ["Letter", "Legal", "Tabloid", "Ledger", "A0", "A1", "A2", "A3", "A4", "A5", "A6"];
|
||||
for (const f of validFormats) {
|
||||
it(`accepts ${f}`, () => {
|
||||
expect(validatePdfOptions({ format: f }).valid).toBe(true);
|
||||
});
|
||||
}
|
||||
it("accepts case-insensitive (a4)", () => {
|
||||
const r = validatePdfOptions({ format: "a4" });
|
||||
expect(r.valid).toBe(true);
|
||||
if (r.valid) expect(r.sanitized.format).toBe("A4");
|
||||
});
|
||||
it("accepts case-insensitive (letter)", () => {
|
||||
const r = validatePdfOptions({ format: "letter" });
|
||||
expect(r.valid).toBe(true);
|
||||
if (r.valid) expect(r.sanitized.format).toBe("Letter");
|
||||
});
|
||||
it("rejects invalid format", () => {
|
||||
const r = validatePdfOptions({ format: "B5" });
|
||||
expect(r.valid).toBe(false);
|
||||
if (!r.valid) expect(r.error).toContain("format");
|
||||
});
|
||||
});
|
||||
|
||||
// --- booleans ---
|
||||
for (const field of ["landscape", "printBackground", "displayHeaderFooter", "preferCSSPageSize"] as const) {
|
||||
describe(field, () => {
|
||||
it("accepts true", () => {
|
||||
expect(validatePdfOptions({ [field]: true }).valid).toBe(true);
|
||||
});
|
||||
it("accepts false", () => {
|
||||
expect(validatePdfOptions({ [field]: false }).valid).toBe(true);
|
||||
});
|
||||
it("rejects string", () => {
|
||||
const r = validatePdfOptions({ [field]: "yes" as any });
|
||||
expect(r.valid).toBe(false);
|
||||
if (!r.valid) expect(r.error).toContain(field);
|
||||
});
|
||||
it("rejects number", () => {
|
||||
expect(validatePdfOptions({ [field]: 1 as any }).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- width/height ---
|
||||
for (const field of ["width", "height"] as const) {
|
||||
describe(field, () => {
|
||||
it("accepts string", () => {
|
||||
expect(validatePdfOptions({ [field]: "210mm" }).valid).toBe(true);
|
||||
});
|
||||
it("rejects number", () => {
|
||||
expect(validatePdfOptions({ [field]: 210 as any }).valid).toBe(false);
|
||||
const r = validatePdfOptions({ [field]: 210 as any });
|
||||
if (!r.valid) expect(r.error).toContain(field);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- margin ---
|
||||
describe("margin", () => {
|
||||
it("accepts valid margin object", () => {
|
||||
expect(validatePdfOptions({ margin: { top: "1cm", bottom: "2cm" } }).valid).toBe(true);
|
||||
});
|
||||
it("accepts empty margin object", () => {
|
||||
expect(validatePdfOptions({ margin: {} }).valid).toBe(true);
|
||||
});
|
||||
it("rejects non-object margin", () => {
|
||||
expect(validatePdfOptions({ margin: "1cm" as any }).valid).toBe(false);
|
||||
});
|
||||
it("rejects margin with non-string values", () => {
|
||||
expect(validatePdfOptions({ margin: { top: 10 } as any }).valid).toBe(false);
|
||||
});
|
||||
it("rejects margin with unknown keys", () => {
|
||||
expect(validatePdfOptions({ margin: { top: "1cm", padding: "2cm" } as any }).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// --- pageRanges ---
|
||||
describe("pageRanges", () => {
|
||||
it("accepts '1-5'", () => {
|
||||
expect(validatePdfOptions({ pageRanges: "1-5" }).valid).toBe(true);
|
||||
});
|
||||
it("accepts '1,3,5'", () => {
|
||||
expect(validatePdfOptions({ pageRanges: "1,3,5" }).valid).toBe(true);
|
||||
});
|
||||
it("accepts '2-'", () => {
|
||||
expect(validatePdfOptions({ pageRanges: "2-" }).valid).toBe(true);
|
||||
});
|
||||
it("accepts '1-3,5,7-9'", () => {
|
||||
expect(validatePdfOptions({ pageRanges: "1-3,5,7-9" }).valid).toBe(true);
|
||||
});
|
||||
it("accepts single page '3'", () => {
|
||||
expect(validatePdfOptions({ pageRanges: "3" }).valid).toBe(true);
|
||||
});
|
||||
it("rejects non-string", () => {
|
||||
expect(validatePdfOptions({ pageRanges: 5 as any }).valid).toBe(false);
|
||||
});
|
||||
it("rejects invalid pattern", () => {
|
||||
expect(validatePdfOptions({ pageRanges: "abc" }).valid).toBe(false);
|
||||
});
|
||||
it("rejects 'all'", () => {
|
||||
expect(validatePdfOptions({ pageRanges: "all" }).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue