From 52e9b860cfd650335c14620203545efdc56b6b9f Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sun, 22 Feb 2026 07:05:54 +0000 Subject: [PATCH] Expand test coverage: Add tests for demo endpoints, URL conversion, PDF options, error handling, and health details Added comprehensive tests for previously untested areas: 1. Demo Endpoints (no auth): - POST /v1/demo/html - converts HTML to watermarked PDF - POST /v1/demo/markdown - converts markdown to PDF - Rate limiting (5 requests/hour) validation 2. URL to PDF Conversion: - Valid URL conversion - Missing url field validation - SSRF protection (blocks private IPs like 127.0.0.1, localhost) - Invalid protocol rejection (ftp://) - Invalid URL format handling 3. PDF Options: - A3 format conversion - Landscape orientation - Custom margins 4. Error Handling: - Invalid JSON body - Wrong Content-Type header (415 expected) - Empty HTML string handling 5. Health Endpoint Details: - Verify database field presence - Verify pool stats (size, active, available) - Verify version field Total tests: 27 (3 passed locally, 24 require Docker/Chrome/DB) Tests that need Docker to pass: All PDF generation and DB-dependent tests Note: Local failures are expected without PostgreSQL and Chromium. CI will run these in Docker with all dependencies. --- src/__tests__/api.test.ts | 266 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) diff --git a/src/__tests__/api.test.ts b/src/__tests__/api.test.ts index fd23638..e16927e 100644 --- a/src/__tests__/api.test.ts +++ b/src/__tests__/api.test.ts @@ -43,6 +43,35 @@ describe("Health", () => { const data = await res.json(); expect(data.status).toBe("ok"); }); + + it("includes database field", async () => { + const res = await fetch(`${BASE}/health`); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.database).toBeDefined(); + expect(data.database.status).toBeDefined(); + }); + + it("includes pool field with size, active, available", async () => { + const res = await fetch(`${BASE}/health`); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.pool).toBeDefined(); + expect(data.pool.size).toBeDefined(); + expect(data.pool.active).toBeDefined(); + expect(data.pool.available).toBeDefined(); + expect(typeof data.pool.size).toBe("number"); + expect(typeof data.pool.active).toBe("number"); + expect(typeof data.pool.available).toBe("number"); + }); + + it("includes version field", async () => { + const res = await fetch(`${BASE}/health`); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.version).toBeDefined(); + expect(typeof data.version).toBe("string"); + }); }); describe("HTML to PDF", () => { @@ -75,6 +104,93 @@ describe("HTML to PDF", () => { }); expect(res.status).toBe(400); }); + + it("converts HTML with A3 format option", async () => { + const res = await fetch(`${BASE}/v1/convert/html`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + html: "

A3 Test

", + options: { format: "A3" }, + }), + }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("application/pdf"); + const buf = await res.arrayBuffer(); + expect(buf.byteLength).toBeGreaterThan(100); + }); + + it("converts HTML with landscape option", async () => { + const res = await fetch(`${BASE}/v1/convert/html`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + html: "

Landscape Test

", + options: { landscape: true }, + }), + }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("application/pdf"); + }); + + it("converts HTML with margin options", async () => { + const res = await fetch(`${BASE}/v1/convert/html`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + html: "

Margin Test

", + options: { margin: { top: "2cm" } }, + }), + }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("application/pdf"); + }); + + it("rejects invalid JSON body", async () => { + const res = await fetch(`${BASE}/v1/convert/html`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: "invalid json{", + }); + expect(res.status).toBe(400); + }); + + it("rejects wrong content-type header", async () => { + const res = await fetch(`${BASE}/v1/convert/html`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "text/plain", + }, + body: JSON.stringify({ html: "

Test

" }), + }); + expect(res.status).toBe(415); + }); + + it("handles empty html string", async () => { + const res = await fetch(`${BASE}/v1/convert/html`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ html: "" }), + }); + // Empty HTML should still generate a PDF (just blank) + expect(res.status).toBe(200); + }); }); describe("Markdown to PDF", () => { @@ -92,6 +208,156 @@ describe("Markdown to PDF", () => { }); }); +describe("URL to PDF", () => { + it("converts valid URL to PDF", async () => { + const res = await fetch(`${BASE}/v1/convert/url`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ url: "https://example.com" }), + }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("application/pdf"); + const buf = await res.arrayBuffer(); + expect(buf.byteLength).toBeGreaterThan(100); + const header = new Uint8Array(buf.slice(0, 5)); + expect(String.fromCharCode(...header)).toBe("%PDF-"); + }); + + it("rejects missing url field", async () => { + const res = await fetch(`${BASE}/v1/convert/url`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toContain("url"); + }); + + it("blocks private IP addresses (SSRF protection)", async () => { + const res = await fetch(`${BASE}/v1/convert/url`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ url: "http://127.0.0.1" }), + }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toContain("private"); + }); + + it("blocks localhost (SSRF protection)", async () => { + const res = await fetch(`${BASE}/v1/convert/url`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ url: "http://localhost" }), + }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toContain("private"); + }); + + it("rejects invalid protocol (ftp)", async () => { + const res = await fetch(`${BASE}/v1/convert/url`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ url: "ftp://example.com" }), + }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toContain("http"); + }); + + it("rejects invalid URL format", async () => { + const res = await fetch(`${BASE}/v1/convert/url`, { + method: "POST", + headers: { + Authorization: "Bearer test-key", + "Content-Type": "application/json", + }, + body: JSON.stringify({ url: "not-a-url" }), + }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toContain("Invalid"); + }); +}); + +describe("Demo Endpoints", () => { + it("demo/html converts HTML to PDF without auth", async () => { + const res = await fetch(`${BASE}/v1/demo/html`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ html: "

Demo Test

" }), + }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("application/pdf"); + const buf = await res.arrayBuffer(); + expect(buf.byteLength).toBeGreaterThan(100); + const header = new Uint8Array(buf.slice(0, 5)); + expect(String.fromCharCode(...header)).toBe("%PDF-"); + }); + + it("demo/markdown converts markdown to PDF without auth", async () => { + const res = await fetch(`${BASE}/v1/demo/markdown`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ markdown: "# Demo Markdown\n\nTest content" }), + }); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toBe("application/pdf"); + const buf = await res.arrayBuffer(); + expect(buf.byteLength).toBeGreaterThan(100); + }); + + it("demo endpoints are rate-limited", async () => { + // Rate limit is 5 requests per hour + // Make 6 rapid requests - the 6th should fail with 429 + const requests = []; + for (let i = 0; i < 6; i++) { + const promise = fetch(`${BASE}/v1/demo/html`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ html: `

Rate limit test ${i}

` }), + }); + requests.push(promise); + } + + const responses = await Promise.all(requests); + const statuses = responses.map((r) => r.status); + + // At least one should be 429 (rate limited) + expect(statuses).toContain(429); + + // Find the 429 response and check the error message + const rateLimitedResponse = responses.find((r) => r.status === 429); + if (rateLimitedResponse) { + const data = await rateLimitedResponse.json(); + expect(data.error).toContain("limit"); + } + }, 30000); // Increase timeout for this test +}); + describe("Templates", () => { it("lists templates", async () => { const res = await fetch(`${BASE}/v1/templates`, {