All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m9s
- Remove @openapi annotations from /v1/billing/webhook (Stripe-internal) - Remove @openapi annotations from /v1/billing/success (browser redirect) - Mark /v1/signup/verify as deprecated (returns 410) - Add 3 TDD tests in openapi-spec.test.ts - Update 2 existing tests in app-routes.test.ts - 530 tests passing (was 527)
592 lines
25 KiB
JavaScript
592 lines
25 KiB
JavaScript
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
import { app } from "../index.js";
|
|
const BASE = "http://localhost:3199";
|
|
let server;
|
|
beforeAll(async () => {
|
|
server = app.listen(3199);
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
});
|
|
afterAll(async () => {
|
|
await new Promise((resolve) => server?.close(() => resolve()));
|
|
});
|
|
describe("Auth", () => {
|
|
it("rejects requests without API key", async () => {
|
|
const res = await fetch(`${BASE}/v1/convert/html`, { method: "POST" });
|
|
expect(res.status).toBe(401);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
});
|
|
it("rejects invalid API key", async () => {
|
|
const res = await fetch(`${BASE}/v1/convert/html`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer wrong-key" },
|
|
});
|
|
expect(res.status).toBe(403);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
});
|
|
});
|
|
describe("Health", () => {
|
|
it("returns ok", async () => {
|
|
const res = await fetch(`${BASE}/health`);
|
|
expect(res.status).toBe(200);
|
|
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(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", () => {
|
|
it("converts simple HTML", 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>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(10);
|
|
const header = new Uint8Array(buf.slice(0, 5));
|
|
expect(String.fromCharCode(...header)).toBe("%PDF-");
|
|
});
|
|
it("rejects missing html field", async () => {
|
|
const res = await fetch(`${BASE}/v1/convert/html`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer test-key", "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
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");
|
|
});
|
|
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) - but validation may reject it
|
|
expect([200, 400]).toContain(res.status);
|
|
});
|
|
});
|
|
describe("Markdown to PDF", () => {
|
|
it("converts markdown", async () => {
|
|
const res = await fetch(`${BASE}/v1/convert/markdown`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer test-key", "Content-Type": "application/json" },
|
|
body: JSON.stringify({ markdown: "# Hello\n\nWorld" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get("content-type")).toBe("application/pdf");
|
|
});
|
|
});
|
|
describe("URL to 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("blocks 0.0.0.0 (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://0.0.0.0" }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
const data = await res.json();
|
|
expect(data.error).toContain("private");
|
|
});
|
|
it("returns default filename in Content-Disposition for /convert/html", async () => {
|
|
const res = await fetch(`${BASE}/v1/convert/html`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer test-key", "Content-Type": "application/json" },
|
|
body: JSON.stringify({ html: "<p>hello</p>" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const disposition = res.headers.get("content-disposition");
|
|
expect(disposition).toContain('filename="document.pdf"');
|
|
});
|
|
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");
|
|
});
|
|
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(10);
|
|
const header = new Uint8Array(buf.slice(0, 5));
|
|
expect(String.fromCharCode(...header)).toBe("%PDF-");
|
|
});
|
|
});
|
|
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(10);
|
|
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");
|
|
});
|
|
it("demo rejects missing html field", async () => {
|
|
const res = await fetch(`${BASE}/v1/demo/html`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
});
|
|
it("demo rejects wrong content-type", async () => {
|
|
const res = await fetch(`${BASE}/v1/demo/html`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "text/plain" },
|
|
body: "<h1>Test</h1>",
|
|
});
|
|
expect(res.status).toBe(415);
|
|
});
|
|
});
|
|
describe("Templates", () => {
|
|
it("lists templates", async () => {
|
|
const res = await fetch(`${BASE}/v1/templates`, {
|
|
headers: { Authorization: "Bearer test-key" },
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const data = await res.json();
|
|
expect(data.templates).toBeInstanceOf(Array);
|
|
expect(data.templates.length).toBeGreaterThan(0);
|
|
});
|
|
it("renders invoice template", async () => {
|
|
const res = await fetch(`${BASE}/v1/templates/invoice/render`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer test-key", "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
invoiceNumber: "TEST-001",
|
|
date: "2026-02-14",
|
|
from: { name: "Seller", email: "s@test.com" },
|
|
to: { name: "Buyer", email: "b@test.com" },
|
|
items: [{ description: "Widget", quantity: 2, unitPrice: 50, taxRate: 20 }],
|
|
}),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get("content-type")).toBe("application/pdf");
|
|
});
|
|
it("returns 404 for unknown template", async () => {
|
|
const res = await fetch(`${BASE}/v1/templates/nonexistent/render`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer test-key", "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|
|
// === NEW TESTS: Task 3 ===
|
|
describe("Signup endpoint (discontinued)", () => {
|
|
it("returns 410 Gone", async () => {
|
|
const res = await fetch(`${BASE}/v1/signup/free`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: "test@example.com" }),
|
|
});
|
|
expect(res.status).toBe(410);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
});
|
|
});
|
|
describe("Recovery endpoint validation", () => {
|
|
it("rejects missing email", async () => {
|
|
const res = await fetch(`${BASE}/v1/recover`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
});
|
|
it("rejects invalid email format", async () => {
|
|
const res = await fetch(`${BASE}/v1/recover`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: "not-an-email" }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
});
|
|
it("accepts valid email (always returns success)", async () => {
|
|
const res = await fetch(`${BASE}/v1/recover`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: "user@example.com" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const data = await res.json();
|
|
expect(data.status).toBe("recovery_sent");
|
|
});
|
|
it("verify rejects missing fields", async () => {
|
|
const res = await fetch(`${BASE}/v1/recover/verify`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
// May be 400 (validation) or 429 (rate limited from previous recover calls)
|
|
expect([400, 429]).toContain(res.status);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
});
|
|
});
|
|
describe("CORS headers", () => {
|
|
it("sets Access-Control-Allow-Origin to * for API routes", async () => {
|
|
const res = await fetch(`${BASE}/v1/convert/html`, {
|
|
method: "OPTIONS",
|
|
});
|
|
expect(res.status).toBe(204);
|
|
expect(res.headers.get("access-control-allow-origin")).toBe("*");
|
|
});
|
|
it("restricts CORS for signup/billing/demo routes to docfast.dev", async () => {
|
|
const res = await fetch(`${BASE}/v1/demo/html`, {
|
|
method: "OPTIONS",
|
|
});
|
|
expect(res.status).toBe(204);
|
|
expect(res.headers.get("access-control-allow-origin")).toBe("https://docfast.dev");
|
|
});
|
|
it("includes correct allowed methods", async () => {
|
|
const res = await fetch(`${BASE}/health`, { method: "OPTIONS" });
|
|
const methods = res.headers.get("access-control-allow-methods");
|
|
expect(methods).toContain("GET");
|
|
expect(methods).toContain("POST");
|
|
});
|
|
});
|
|
describe("Error response format consistency", () => {
|
|
it("401 returns {error: string}", async () => {
|
|
const res = await fetch(`${BASE}/v1/convert/html`, { method: "POST" });
|
|
expect(res.status).toBe(401);
|
|
const data = await res.json();
|
|
expect(typeof data.error).toBe("string");
|
|
});
|
|
it("403 returns {error: string}", async () => {
|
|
const res = await fetch(`${BASE}/v1/convert/html`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer bad-key" },
|
|
});
|
|
expect(res.status).toBe(403);
|
|
const data = await res.json();
|
|
expect(typeof data.error).toBe("string");
|
|
});
|
|
it("404 API returns {error: string}", async () => {
|
|
const res = await fetch(`${BASE}/v1/nonexistent`);
|
|
expect(res.status).toBe(404);
|
|
const data = await res.json();
|
|
expect(typeof data.error).toBe("string");
|
|
});
|
|
it("410 returns {error: string}", async () => {
|
|
const res = await fetch(`${BASE}/v1/signup/free`, { method: "POST" });
|
|
expect(res.status).toBe(410);
|
|
const data = await res.json();
|
|
expect(typeof data.error).toBe("string");
|
|
});
|
|
});
|
|
describe("Rate limiting (global)", () => {
|
|
it("includes rate limit headers", async () => {
|
|
const res = await fetch(`${BASE}/health`);
|
|
// express-rate-limit with standardHeaders:true uses RateLimit-* headers
|
|
const limit = res.headers.get("ratelimit-limit");
|
|
expect(limit).toBeDefined();
|
|
});
|
|
});
|
|
describe("API root", () => {
|
|
it("returns API info", async () => {
|
|
const res = await fetch(`${BASE}/api`);
|
|
expect(res.status).toBe(200);
|
|
const data = await res.json();
|
|
expect(data.name).toBe("DocFast API");
|
|
expect(data.version).toBeDefined();
|
|
expect(data.endpoints).toBeInstanceOf(Array);
|
|
});
|
|
});
|
|
describe("JS minification", () => {
|
|
it("serves minified JS files in homepage HTML", async () => {
|
|
const res = await fetch(`${BASE}/`);
|
|
expect(res.status).toBe(200);
|
|
const html = await res.text();
|
|
// Check that HTML references app.js and status.js
|
|
expect(html).toContain('src="/app.js"');
|
|
// Fetch the JS file and verify it's minified (no excessive whitespace)
|
|
const jsRes = await fetch(`${BASE}/app.js`);
|
|
expect(jsRes.status).toBe(200);
|
|
const jsContent = await jsRes.text();
|
|
// Minified JS should not have excessive whitespace or comments
|
|
// Basic check: line count should be reasonable for minified code
|
|
const lineCount = jsContent.split('\n').length;
|
|
expect(lineCount).toBeLessThan(50); // Original has ~400+ lines, minified should be much less
|
|
// Should not contain developer comments (/* ... */)
|
|
expect(jsContent).not.toMatch(/\/\*[\s\S]*?\*\//);
|
|
});
|
|
});
|
|
describe("Usage endpoint", () => {
|
|
it("requires authentication (401 without key)", async () => {
|
|
const res = await fetch(`${BASE}/v1/usage`);
|
|
expect(res.status).toBe(401);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
expect(typeof data.error).toBe("string");
|
|
});
|
|
it("requires admin key (503 when not configured)", async () => {
|
|
const res = await fetch(`${BASE}/v1/usage`, {
|
|
headers: { Authorization: "Bearer test-key" },
|
|
});
|
|
expect(res.status).toBe(503);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
expect(data.error).toContain("Admin access not configured");
|
|
});
|
|
it("returns usage data with admin key", async () => {
|
|
// This test will likely fail since we don't have an admin key set in test environment
|
|
// But it documents the expected behavior
|
|
const res = await fetch(`${BASE}/v1/usage`, {
|
|
headers: { Authorization: "Bearer admin-key" },
|
|
});
|
|
// Could be 503 (admin access not configured) or 403 (admin access required)
|
|
expect([403, 503]).toContain(res.status);
|
|
});
|
|
});
|
|
describe("Billing checkout", () => {
|
|
it("has rate limiting headers", async () => {
|
|
const res = await fetch(`${BASE}/v1/billing/checkout`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
// Check rate limit headers are present (express-rate-limit should add these)
|
|
const limitHeader = res.headers.get("ratelimit-limit");
|
|
const remainingHeader = res.headers.get("ratelimit-remaining");
|
|
const resetHeader = res.headers.get("ratelimit-reset");
|
|
expect(limitHeader).toBeDefined();
|
|
expect(remainingHeader).toBeDefined();
|
|
expect(resetHeader).toBeDefined();
|
|
});
|
|
it("fails when Stripe not configured", async () => {
|
|
const res = await fetch(`${BASE}/v1/billing/checkout`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
});
|
|
// Returns 500 due to missing STRIPE_SECRET_KEY in test environment
|
|
expect(res.status).toBe(500);
|
|
const data = await res.json();
|
|
expect(data.error).toBeDefined();
|
|
});
|
|
});
|
|
describe("Rate limit headers on PDF endpoints", () => {
|
|
it("includes rate limit headers on HTML conversion", 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>Test</h1>" }),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
// Check for rate limit headers
|
|
const limitHeader = res.headers.get("ratelimit-limit");
|
|
const remainingHeader = res.headers.get("ratelimit-remaining");
|
|
const resetHeader = res.headers.get("ratelimit-reset");
|
|
expect(limitHeader).toBeDefined();
|
|
expect(remainingHeader).toBeDefined();
|
|
expect(resetHeader).toBeDefined();
|
|
});
|
|
it("includes rate limit headers on demo endpoint", 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);
|
|
// Check for rate limit headers
|
|
const limitHeader = res.headers.get("ratelimit-limit");
|
|
const remainingHeader = res.headers.get("ratelimit-remaining");
|
|
const resetHeader = res.headers.get("ratelimit-reset");
|
|
expect(limitHeader).toBeDefined();
|
|
expect(remainingHeader).toBeDefined();
|
|
expect(resetHeader).toBeDefined();
|
|
});
|
|
});
|
|
describe("OpenAPI spec", () => {
|
|
it("returns a valid OpenAPI 3.0 spec with paths", async () => {
|
|
const res = await fetch(`${BASE}/openapi.json`);
|
|
expect(res.status).toBe(200);
|
|
const spec = await res.json();
|
|
expect(spec.openapi).toBe("3.0.3");
|
|
expect(spec.info).toBeDefined();
|
|
expect(spec.info.title).toBe("DocFast API");
|
|
expect(Object.keys(spec.paths).length).toBeGreaterThanOrEqual(8);
|
|
});
|
|
it("includes all major endpoint groups", async () => {
|
|
const res = await fetch(`${BASE}/openapi.json`);
|
|
const spec = await res.json();
|
|
const paths = Object.keys(spec.paths);
|
|
expect(paths).toContain("/v1/convert/html");
|
|
expect(paths).toContain("/v1/convert/markdown");
|
|
expect(paths).toContain("/health");
|
|
});
|
|
it("PdfOptions schema includes all valid format values and waitUntil field", async () => {
|
|
const res = await fetch(`${BASE}/openapi.json`);
|
|
const spec = await res.json();
|
|
const pdfOptions = spec.components.schemas.PdfOptions;
|
|
expect(pdfOptions).toBeDefined();
|
|
// Check that all 11 format values are included
|
|
const expectedFormats = ["Letter", "Legal", "Tabloid", "Ledger", "A0", "A1", "A2", "A3", "A4", "A5", "A6"];
|
|
expect(pdfOptions.properties.format.enum).toEqual(expectedFormats);
|
|
// Check that waitUntil field exists with correct enum values
|
|
expect(pdfOptions.properties.waitUntil).toBeDefined();
|
|
expect(pdfOptions.properties.waitUntil.enum).toEqual(["load", "domcontentloaded", "networkidle0", "networkidle2"]);
|
|
// Check that headerTemplate and footerTemplate descriptions mention 100KB limit
|
|
expect(pdfOptions.properties.headerTemplate.description).toContain("100KB");
|
|
expect(pdfOptions.properties.footerTemplate.description).toContain("100KB");
|
|
});
|
|
});
|
|
describe("404 handler", () => {
|
|
it("returns proper JSON error format for API routes", async () => {
|
|
const res = await fetch(`${BASE}/v1/nonexistent-endpoint`);
|
|
expect(res.status).toBe(404);
|
|
const data = await res.json();
|
|
expect(typeof data.error).toBe("string");
|
|
expect(data.error).toContain("Not Found");
|
|
expect(data.error).toContain("GET");
|
|
expect(data.error).toContain("/v1/nonexistent-endpoint");
|
|
});
|
|
it("returns HTML 404 for non-API routes", async () => {
|
|
const res = await fetch(`${BASE}/nonexistent-page`);
|
|
expect(res.status).toBe(404);
|
|
const html = await res.text();
|
|
expect(html).toContain("<!DOCTYPE html>");
|
|
expect(html).toContain("404");
|
|
expect(html).toContain("Page Not Found");
|
|
});
|
|
});
|