Add input validation for waitUntil and size limits for headerTemplate/footerTemplate
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled

- Add waitUntil validation with allowed values: load, domcontentloaded, networkidle0, networkidle2
- Add size limit validation for headerTemplate and footerTemplate (100KB max)
- Follow TDD approach: 15 new failing tests, then implementation
- All 462 tests passing (was 447)
This commit is contained in:
OpenClaw Backend Subagent 2026-03-04 11:04:46 +01:00
parent 646a94dd6a
commit 7d44524ae0
2 changed files with 146 additions and 3 deletions

View file

@ -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 = "<html><head></head><body>Header</body></html>";
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 = "<html><head></head><body>Footer</body></html>";
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");
}
});
});
});

View file

@ -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<string, any>;
type ValidResult = { valid: true; sanitized: Record<string, any> };
@ -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 };