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
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:
parent
ca72f04b6b
commit
52e9b860cf
1 changed files with 266 additions and 0 deletions
|
|
@ -43,6 +43,35 @@ describe("Health", () => {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
expect(data.status).toBe("ok");
|
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", () => {
|
describe("HTML to PDF", () => {
|
||||||
|
|
@ -75,6 +104,93 @@ describe("HTML to PDF", () => {
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(400);
|
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", () => {
|
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", () => {
|
describe("Templates", () => {
|
||||||
it("lists templates", async () => {
|
it("lists templates", async () => {
|
||||||
const res = await fetch(`${BASE}/v1/templates`, {
|
const res = await fetch(`${BASE}/v1/templates`, {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue