import { describe, it, expect, vi, beforeEach } from "vitest"; import express from "express"; import request from "supertest"; import { getPoolStats } from "../services/browser.js"; import { pool } from "../services/db.js"; let app: express.Express; beforeEach(async () => { vi.clearAllMocks(); // Default: healthy DB const mockClient = { query: vi.fn() .mockResolvedValueOnce({ rows: [{ 1: 1 }] }) // SELECT 1 .mockResolvedValueOnce({ rows: [{ version: "PostgreSQL 17.4 on x86_64" }] }), // SELECT version() release: vi.fn(), }; vi.mocked(pool.connect).mockResolvedValue(mockClient as any); vi.mocked(getPoolStats).mockReturnValue({ poolSize: 16, totalPages: 16, availablePages: 14, queueDepth: 0, pdfCount: 5, restarting: false, uptimeMs: 60000, browsers: [], }); const { healthRouter } = await import("../routes/health.js"); app = express(); app.use("/health", healthRouter); }); describe("GET /health", () => { it("returns 200 with status ok when DB is healthy", async () => { const res = await request(app).get("/health"); expect(res.status).toBe(200); expect(res.body.status).toBe("ok"); expect(res.body.database.status).toBe("ok"); }); it("returns 503 with status degraded on DB error", async () => { vi.mocked(pool.connect).mockRejectedValue(new Error("Connection refused")); const res = await request(app).get("/health"); expect(res.status).toBe(503); expect(res.body.status).toBe("degraded"); expect(res.body.database.status).toBe("error"); }); it("includes pool stats", async () => { const res = await request(app).get("/health"); expect(res.body.pool).toMatchObject({ size: 16, available: 14, queueDepth: 0, pdfCount: 5, }); }); it("includes version", async () => { const res = await request(app).get("/health"); expect(res.body.version).toBeDefined(); expect(typeof res.body.version).toBe("string"); }); it("returns 503 when client.query() throws and releases client with destroy flag", async () => { const mockRelease = vi.fn(); const mockClient = { query: vi.fn().mockRejectedValue(new Error("Query failed")), release: mockRelease, }; vi.mocked(pool.connect).mockResolvedValue(mockClient as any); const res = await request(app).get("/health"); expect(res.status).toBe(503); expect(res.body.status).toBe("degraded"); expect(res.body.database.status).toBe("error"); expect(res.body.database.message).toContain("Query failed"); // Verify client.release(true) was called to destroy the bad connection expect(mockRelease).toHaveBeenCalledWith(true); }); it("returns 503 when database health check times out (timeout race wins)", async () => { // Make pool.connect() hang longer than HEALTH_CHECK_TIMEOUT_MS (3000ms) const mockClient = { query: vi.fn(), release: vi.fn(), }; vi.mocked(pool.connect).mockImplementation(() => new Promise((resolve) => { // Resolve after 5000ms, which is longer than the 3000ms timeout setTimeout(() => resolve(mockClient as any), 5000); }) ); const res = await request(app).get("/health"); expect(res.status).toBe(503); expect(res.body.status).toBe("degraded"); expect(res.body.database.status).toBe("error"); expect(res.body.database.message).toContain("Database health check timed out"); }); it("returns PostgreSQL for version string without PostgreSQL match", async () => { const mockClient = { query: vi.fn() .mockResolvedValueOnce({ rows: [{ 1: 1 }] }) // SELECT 1 .mockResolvedValueOnce({ rows: [{ version: "MySQL 8.0.33" }] }), // No PostgreSQL in version string release: vi.fn(), }; vi.mocked(pool.connect).mockResolvedValue(mockClient as any); const res = await request(app).get("/health"); expect(res.status).toBe(200); expect(res.body.status).toBe("ok"); expect(res.body.database.status).toBe("ok"); expect(res.body.database.version).toBe("PostgreSQL"); // fallback when no regex match }); it("returns 503 when non-Error is thrown in catch block", async () => { // Make pool.connect() throw a non-Error object vi.mocked(pool.connect).mockRejectedValue("String error message"); const res = await request(app).get("/health"); expect(res.status).toBe(503); expect(res.body.status).toBe("degraded"); expect(res.body.database.status).toBe("error"); expect(res.body.database.message).toBe("Database connection failed"); }); });