docs: add missing OpenAPI annotations for signup/verify, billing/success, billing/webhook
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 16m15s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 16m15s
This commit is contained in:
parent
427ec8e894
commit
8b31d11e74
15 changed files with 2167 additions and 128 deletions
515
dist/__tests__/api.test.js
vendored
515
dist/__tests__/api.test.js
vendored
|
|
@ -1,24 +1,20 @@
|
|||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { app } from "../index.js";
|
||||
// Note: These tests require Puppeteer/Chrome to be available
|
||||
// For CI, use the Dockerfile which includes Chrome
|
||||
const BASE = "http://localhost:3199";
|
||||
let server;
|
||||
beforeAll(async () => {
|
||||
process.env.API_KEYS = "test-key";
|
||||
process.env.PORT = "3199";
|
||||
// Import fresh to pick up env
|
||||
server = app.listen(3199);
|
||||
// Wait for browser init
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
});
|
||||
afterAll(async () => {
|
||||
server?.close();
|
||||
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`, {
|
||||
|
|
@ -26,6 +22,8 @@ describe("Auth", () => {
|
|||
headers: { Authorization: "Bearer wrong-key" },
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
const data = await res.json();
|
||||
expect(data.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
describe("Health", () => {
|
||||
|
|
@ -35,51 +33,243 @@ 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(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",
|
||||
},
|
||||
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(100);
|
||||
// PDF magic bytes
|
||||
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",
|
||||
},
|
||||
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",
|
||||
},
|
||||
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`, {
|
||||
|
|
@ -93,10 +283,7 @@ describe("Templates", () => {
|
|||
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",
|
||||
},
|
||||
headers: { Authorization: "Bearer test-key", "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
invoiceNumber: "TEST-001",
|
||||
date: "2026-02-14",
|
||||
|
|
@ -111,12 +298,280 @@ describe("Templates", () => {
|
|||
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",
|
||||
},
|
||||
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");
|
||||
});
|
||||
});
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue