Add landing page, tests, Docker deployment, nginx config
This commit is contained in:
parent
feee0317ae
commit
8e03b8ab3c
5 changed files with 394 additions and 2 deletions
136
src/__tests__/api.test.ts
Normal file
136
src/__tests__/api.test.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import express from "express";
|
||||
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: any;
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
server?.close();
|
||||
});
|
||||
|
||||
describe("Auth", () => {
|
||||
it("rejects requests without API key", async () => {
|
||||
const res = await fetch(`${BASE}/v1/convert/html`, { method: "POST" });
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
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(100);
|
||||
// PDF magic bytes
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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("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);
|
||||
});
|
||||
});
|
||||
10
src/index.ts
10
src/index.ts
|
|
@ -1,5 +1,7 @@
|
|||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import { convertRouter } from "./routes/convert.js";
|
||||
import { templatesRouter } from "./routes/templates.js";
|
||||
|
|
@ -30,8 +32,12 @@ app.use("/health", healthRouter);
|
|||
app.use("/v1/convert", authMiddleware, convertRouter);
|
||||
app.use("/v1/templates", authMiddleware, templatesRouter);
|
||||
|
||||
// Root
|
||||
app.get("/", (_req, res) => {
|
||||
// Landing page
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
app.use(express.static(path.join(__dirname, "../public")));
|
||||
|
||||
// API root (for programmatic discovery)
|
||||
app.get("/api", (_req, res) => {
|
||||
res.json({
|
||||
name: "DocFast API",
|
||||
version: "0.1.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue