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.
187 lines
6.3 KiB
TypeScript
187 lines
6.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import express from "express";
|
|
import request from "supertest";
|
|
|
|
// Mock all heavy dependencies
|
|
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(),
|
|
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({ "test-key": { count: 42, month: "2026-03" } }),
|
|
getUsageForKey: vi.fn().mockReturnValue({ count: 10, monthKey: "2026-03" }),
|
|
flushDirtyEntries: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../middleware/pdfRateLimit.js", () => ({
|
|
pdfRateLimitMiddleware: (_req: any, _res: any, next: any) => next(),
|
|
getConcurrencyStats: vi.fn().mockReturnValue({ active: 2, queued: 0, maxConcurrent: 10 }),
|
|
}));
|
|
|
|
const TEST_KEY = "test-key-123";
|
|
const ADMIN_KEY = "admin-key-456";
|
|
|
|
describe("Admin integration tests", () => {
|
|
let app: express.Express;
|
|
let originalAdminKey: string | undefined;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
vi.resetModules();
|
|
|
|
originalAdminKey = process.env.ADMIN_API_KEY;
|
|
process.env.ADMIN_API_KEY = ADMIN_KEY;
|
|
|
|
// Set up key mocks
|
|
const keys = await import("../services/keys.js");
|
|
vi.mocked(keys.isValidKey).mockImplementation((k: string) => k === TEST_KEY || k === ADMIN_KEY);
|
|
vi.mocked(keys.getKeyInfo).mockImplementation((k: string) => {
|
|
if (k === TEST_KEY) return { key: TEST_KEY, tier: "free" as const, email: "test@test.com", createdAt: "2026-01-01" };
|
|
if (k === ADMIN_KEY) return { key: ADMIN_KEY, tier: "pro" as const, email: "admin@test.com", createdAt: "2026-01-01" };
|
|
return undefined;
|
|
});
|
|
vi.mocked(keys.isProKey).mockImplementation((k: string) => k === ADMIN_KEY);
|
|
|
|
const { adminRouter } = await import("../routes/admin.js");
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use(adminRouter);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalAdminKey !== undefined) {
|
|
process.env.ADMIN_API_KEY = originalAdminKey;
|
|
} else {
|
|
delete process.env.ADMIN_API_KEY;
|
|
}
|
|
});
|
|
|
|
describe("GET /v1/usage/me", () => {
|
|
it("returns 401 without auth", async () => {
|
|
const res = await request(app).get("/v1/usage/me");
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it("returns 403 with invalid key", async () => {
|
|
const res = await request(app).get("/v1/usage/me").set("X-API-Key", "bad-key");
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it("returns usage stats with Bearer auth", async () => {
|
|
const res = await request(app).get("/v1/usage/me").set("Authorization", `Bearer ${TEST_KEY}`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toMatchObject({
|
|
used: 10,
|
|
limit: 100,
|
|
plan: "demo",
|
|
month: "2026-03",
|
|
});
|
|
});
|
|
|
|
it("returns usage stats with X-API-Key auth", async () => {
|
|
const res = await request(app).get("/v1/usage/me").set("X-API-Key", TEST_KEY);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.plan).toBe("demo");
|
|
});
|
|
|
|
it("returns pro plan for pro key", async () => {
|
|
const res = await request(app).get("/v1/usage/me").set("X-API-Key", ADMIN_KEY);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toMatchObject({
|
|
plan: "pro",
|
|
limit: 5000,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("GET /v1/usage (admin)", () => {
|
|
it("returns 401 without auth", async () => {
|
|
const res = await request(app).get("/v1/usage");
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it("returns 403 with non-admin key", async () => {
|
|
const res = await request(app).get("/v1/usage").set("X-API-Key", TEST_KEY);
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.error).toBe("Admin access required");
|
|
});
|
|
|
|
it("returns usage stats with admin key", async () => {
|
|
const res = await request(app).get("/v1/usage").set("X-API-Key", ADMIN_KEY);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toHaveProperty("test-key");
|
|
});
|
|
|
|
it("returns 503 when ADMIN_API_KEY not set", async () => {
|
|
delete process.env.ADMIN_API_KEY;
|
|
const res = await request(app).get("/v1/usage").set("X-API-Key", ADMIN_KEY);
|
|
expect(res.status).toBe(503);
|
|
expect(res.body.error).toBe("Admin access not configured");
|
|
});
|
|
});
|
|
|
|
describe("GET /v1/concurrency (admin)", () => {
|
|
it("returns 403 with non-admin key", async () => {
|
|
const res = await request(app).get("/v1/concurrency").set("X-API-Key", TEST_KEY);
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it("returns concurrency stats with admin key", async () => {
|
|
const res = await request(app).get("/v1/concurrency").set("X-API-Key", ADMIN_KEY);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toMatchObject({ active: 2, queued: 0, maxConcurrent: 10 });
|
|
});
|
|
});
|
|
|
|
describe("POST /admin/cleanup (admin)", () => {
|
|
it("returns 403 with non-admin key", async () => {
|
|
const res = await request(app).post("/admin/cleanup").set("X-API-Key", TEST_KEY);
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it("returns cleanup results with admin key", async () => {
|
|
const { cleanupStaleData } = await import("../services/db.js");
|
|
vi.mocked(cleanupStaleData).mockResolvedValue({ deletedRows: 5 } as any);
|
|
|
|
const res = await request(app).post("/admin/cleanup").set("X-API-Key", ADMIN_KEY);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toMatchObject({ status: "ok", cleaned: { deletedRows: 5 } });
|
|
});
|
|
|
|
it("returns 500 when cleanup fails", async () => {
|
|
const { cleanupStaleData } = await import("../services/db.js");
|
|
vi.mocked(cleanupStaleData).mockRejectedValue(new Error("DB error"));
|
|
|
|
const res = await request(app).post("/admin/cleanup").set("X-API-Key", ADMIN_KEY);
|
|
expect(res.status).toBe(500);
|
|
expect(res.body.error).toBe("Cleanup failed");
|
|
});
|
|
});
|
|
});
|