All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 19m33s
- admin-integration.test.ts: 14 tests covering /v1/usage/me, /v1/usage, /v1/concurrency, /admin/cleanup, auth middleware (401/403/503) - pages-integration.test.ts: 10 tests covering favicon, openapi.json, docs, landing page, static pages, status, /api Both files now at 100% function/statement/branch/line coverage. All 696 tests pass.
131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|