import { describe, it, expect, vi, beforeAll } from "vitest"; import express from "express"; import request from "supertest"; // Mock heavy deps so we don't need DB vi.mock("../services/browser.js", () => ({ renderPdf: vi.fn(), renderUrlPdf: vi.fn(), initBrowser: vi.fn(), closeBrowser: vi.fn(), })); vi.mock("../services/keys.js", () => ({ loadKeys: vi.fn(), getAllKeys: vi.fn().mockReturnValue([]), isValidKey: vi.fn().mockReturnValue(false), getKeyInfo: vi.fn(), isProKey: vi.fn(), keyStore: new Map(), })); vi.mock("../services/db.js", () => ({ initDatabase: vi.fn(), pool: { query: vi.fn(), end: vi.fn() }, queryWithRetry: vi.fn(), connectWithRetry: vi.fn(), cleanupStaleData: vi.fn(), })); vi.mock("../services/verification.js", () => ({ verifyToken: vi.fn(), loadVerifications: vi.fn(), })); vi.mock("../middleware/usage.js", () => ({ usageMiddleware: (_req: any, _res: any, next: any) => next(), loadUsageData: vi.fn(), getUsageStats: vi.fn().mockReturnValue({}), getUsageForKey: vi.fn().mockReturnValue({ count: 0, monthKey: "2026-03" }), flushDirtyEntries: vi.fn(), })); vi.mock("../middleware/pdfRateLimit.js", () => ({ pdfRateLimitMiddleware: (_req: any, _res: any, next: any) => next(), getConcurrencyStats: vi.fn().mockReturnValue({}), })); describe("Pages integration tests", () => { let app: express.Express; // Use a fresh import per suite to avoid cross-test pollution beforeAll(async () => { const { pagesRouter } = await import("../routes/pages.js"); app = express(); app.use(pagesRouter); }); describe("GET /favicon.ico", () => { it("returns SVG with correct Content-Type and Cache-Control", async () => { const res = await request(app).get("/favicon.ico"); expect(res.status).toBe(200); expect(res.headers["content-type"]).toMatch(/image\/svg\+xml/); expect(res.headers["cache-control"]).toContain("public"); expect(res.headers["cache-control"]).toContain("max-age=604800"); }); }); describe("GET /openapi.json", () => { it("returns valid JSON with paths", async () => { const res = await request(app).get("/openapi.json"); expect(res.status).toBe(200); expect(res.headers["content-type"]).toMatch(/json/); expect(res.body.paths).toBeDefined(); expect(typeof res.body.paths).toBe("object"); }); }); describe("GET /docs", () => { it("returns HTML with CSP header", async () => { const res = await request(app).get("/docs"); expect(res.status).toBe(200); expect(res.headers["content-type"]).toMatch(/html/); expect(res.headers["content-security-policy"]).toBeDefined(); expect(res.headers["content-security-policy"]).toContain("unsafe-eval"); expect(res.headers["cache-control"]).toContain("max-age=86400"); }); }); describe("GET /", () => { it("returns HTML with Cache-Control", async () => { const res = await request(app).get("/"); expect(res.status).toBe(200); expect(res.headers["content-type"]).toMatch(/html/); expect(res.headers["cache-control"]).toContain("public"); expect(res.headers["cache-control"]).toContain("max-age=3600"); }); }); describe("Static pages", () => { const pages = ["/impressum", "/privacy", "/terms", "/examples"]; for (const page of pages) { it(`GET ${page} returns 200 with 24h cache`, async () => { const res = await request(app).get(page); expect(res.status).toBe(200); expect(res.headers["cache-control"]).toContain("public"); expect(res.headers["cache-control"]).toContain("max-age=86400"); }); } }); describe("GET /status", () => { it("returns 200 with short cache", async () => { const res = await request(app).get("/status"); expect(res.status).toBe(200); expect(res.headers["cache-control"]).toContain("max-age=60"); }); }); describe("GET /api", () => { it("returns JSON with version and endpoints", async () => { const res = await request(app).get("/api"); expect(res.status).toBe(200); expect(res.headers["content-type"]).toMatch(/json/); expect(res.body.name).toBe("DocFast API"); expect(typeof res.body.version).toBe("string"); expect(Array.isArray(res.body.endpoints)).toBe(true); expect(res.body.endpoints.length).toBeGreaterThan(0); }); }); });