Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 17m7s
- health.ts: Added tests for timeout race, version regex edge cases, non-Error catch blocks - browser.ts: Added comprehensive edge case test for buildPdfOptions with all fields - Branch coverage: health.ts improved from 50% to 83.33% - Function coverage: health.ts improved from 75% to 100% - Overall function coverage improved from 84.46% to 84.95% - Total tests: 772 → 776 (+4 new tests)
138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
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");
|
|
});
|
|
});
|