diff --git a/src/__tests__/pdf-options.test.ts b/src/__tests__/pdf-options.test.ts index 224886f..99347fe 100644 --- a/src/__tests__/pdf-options.test.ts +++ b/src/__tests__/pdf-options.test.ts @@ -159,4 +159,117 @@ describe("validatePdfOptions", () => { expect(validatePdfOptions({ pageRanges: "all" }).valid).toBe(false); }); }); + + // --- waitUntil --- + describe("waitUntil", () => { + const validValues = ["load", "domcontentloaded", "networkidle0", "networkidle2"]; + for (const value of validValues) { + it(`accepts "${value}"`, () => { + const result = validatePdfOptions({ waitUntil: value }); + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.sanitized.waitUntil).toBe(value); + } + }); + } + + it("rejects invalid string", () => { + const result = validatePdfOptions({ waitUntil: "invalid" }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toContain("waitUntil"); + expect(result.error).toContain("load"); + expect(result.error).toContain("domcontentloaded"); + expect(result.error).toContain("networkidle0"); + expect(result.error).toContain("networkidle2"); + } + }); + + it("rejects number", () => { + const result = validatePdfOptions({ waitUntil: 123 as any }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toContain("waitUntil"); + } + }); + + it("rejects boolean", () => { + const result = validatePdfOptions({ waitUntil: true as any }); + expect(result.valid).toBe(false); + }); + }); + + // --- headerTemplate --- + describe("headerTemplate", () => { + it("accepts string under size limit", () => { + const template = "Header"; + const result = validatePdfOptions({ headerTemplate: template }); + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.sanitized.headerTemplate).toBe(template); + } + }); + + it("accepts exactly 100KB", () => { + const template = "a".repeat(102400); // exactly 100KB + const result = validatePdfOptions({ headerTemplate: template }); + expect(result.valid).toBe(true); + }); + + it("rejects over 100KB", () => { + const template = "a".repeat(102401); // 100KB + 1 char + const result = validatePdfOptions({ headerTemplate: template }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toContain("headerTemplate"); + expect(result.error).toContain("100KB"); + } + }); + + it("rejects non-string", () => { + const result = validatePdfOptions({ headerTemplate: 123 as any }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toContain("headerTemplate"); + expect(result.error).toContain("string"); + } + }); + }); + + // --- footerTemplate --- + describe("footerTemplate", () => { + it("accepts string under size limit", () => { + const template = "Footer"; + const result = validatePdfOptions({ footerTemplate: template }); + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.sanitized.footerTemplate).toBe(template); + } + }); + + it("accepts exactly 100KB", () => { + const template = "a".repeat(102400); // exactly 100KB + const result = validatePdfOptions({ footerTemplate: template }); + expect(result.valid).toBe(true); + }); + + it("rejects over 100KB", () => { + const template = "a".repeat(102401); // 100KB + 1 char + const result = validatePdfOptions({ footerTemplate: template }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toContain("footerTemplate"); + expect(result.error).toContain("100KB"); + } + }); + + it("rejects non-string", () => { + const result = validatePdfOptions({ footerTemplate: 123 as any }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toContain("footerTemplate"); + expect(result.error).toContain("string"); + } + }); + }); }); diff --git a/src/utils/pdf-options.ts b/src/utils/pdf-options.ts index ec7fd05..4442fa2 100644 --- a/src/utils/pdf-options.ts +++ b/src/utils/pdf-options.ts @@ -2,6 +2,8 @@ const VALID_FORMATS = ["Letter", "Legal", "Tabloid", "Ledger", "A0", "A1", "A2", const FORMAT_MAP = new Map(VALID_FORMATS.map(f => [f.toLowerCase(), f])); const PAGE_RANGES_RE = /^\d+(-\d*)?(\s*,\s*\d+(-\d*)?)*$/; const MARGIN_KEYS = new Set(["top", "right", "bottom", "left"]); +const VALID_WAIT_UNTIL = ["load", "domcontentloaded", "networkidle0", "networkidle2"]; +const MAX_TEMPLATE_SIZE = 102400; // 100KB in characters type PdfInput = Record; type ValidResult = { valid: true; sanitized: Record }; @@ -79,9 +81,37 @@ export function validatePdfOptions(opts: PdfInput): ValidResult | InvalidResult sanitized.pageRanges = opts.pageRanges; } - // Pass through non-validated fields - for (const key of ["headerTemplate", "footerTemplate"]) { - if (opts[key] !== undefined) sanitized[key] = opts[key]; + // waitUntil + if (opts.waitUntil !== undefined) { + if (typeof opts.waitUntil !== "string") { + return { valid: false, error: "waitUntil must be a string" }; + } + if (!VALID_WAIT_UNTIL.includes(opts.waitUntil)) { + return { valid: false, error: `waitUntil must be one of: ${VALID_WAIT_UNTIL.join(", ")}` }; + } + sanitized.waitUntil = opts.waitUntil; + } + + // headerTemplate + if (opts.headerTemplate !== undefined) { + if (typeof opts.headerTemplate !== "string") { + return { valid: false, error: "headerTemplate must be a string" }; + } + if (opts.headerTemplate.length > MAX_TEMPLATE_SIZE) { + return { valid: false, error: "headerTemplate must not exceed 100KB" }; + } + sanitized.headerTemplate = opts.headerTemplate; + } + + // footerTemplate + if (opts.footerTemplate !== undefined) { + if (typeof opts.footerTemplate !== "string") { + return { valid: false, error: "footerTemplate must be a string" }; + } + if (opts.footerTemplate.length > MAX_TEMPLATE_SIZE) { + return { valid: false, error: "footerTemplate must not exceed 100KB" }; + } + sanitized.footerTemplate = opts.footerTemplate; } return { valid: true, sanitized };