Expand test coverage: Add tests for demo endpoints, URL conversion, PDF options, error handling, and health details
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m37s

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.
This commit is contained in:
OpenClaw Agent 2026-02-22 07:05:54 +00:00
parent ca72f04b6b
commit 52e9b860cf

View file

@ -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: "<h1>A3 Test</h1>",
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: "<h1>Landscape Test</h1>",
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: "<h1>Margin Test</h1>",
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: "<h1>Test</h1>" }),
});
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: "<h1>Demo Test</h1>" }),
});
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: `<h1>Rate limit test ${i}</h1>` }),
});
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`, {