f54a13fc8a
- Register GET /api/readyz after /api/health in src/index.ts (before authMiddleware)
- Runs `getDb().select({id: staff.id}).from(staff).limit(1)` inside try/catch
- Success → 200 {"status":"ready"}; failure → 503 {"status":"degraded","check":"db"}
- Raw driver error is console.error'd but never leaked to the response body
- /health and /api/health are unchanged (K8s probes remain DB-less per CTO decision)
- New unit tests: mock getDb to resolve → assert 200/ready; throw → assert 503/degraded
- Updated UAT_PLAYBOOK.md §4.0 with TC-API-0.2 and TC-API-0.3
Co-Authored-By: Paperclip <noreply@paperclip.ing>
88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { Hono } from "hono";
|
|
|
|
// ─── Mock db module ───────────────────────────────────────────────────────────
|
|
|
|
let selectImpl: () => Promise<unknown>;
|
|
|
|
vi.mock("@groombook/db", () => {
|
|
const staff = new Proxy(
|
|
{ _name: "staff" },
|
|
{
|
|
get(_target, prop) {
|
|
if (prop === "_name") return "staff";
|
|
return { table: "staff", column: prop };
|
|
},
|
|
}
|
|
);
|
|
|
|
return {
|
|
getDb: () => ({
|
|
select: (_fields: unknown) => ({
|
|
from: (_table: unknown) => ({
|
|
limit: (_n: number) => selectImpl(),
|
|
}),
|
|
}),
|
|
}),
|
|
staff,
|
|
};
|
|
});
|
|
|
|
// ─── Build test app ───────────────────────────────────────────────────────────
|
|
|
|
async function makeApp() {
|
|
// Import after mocks are in place
|
|
const { getDb, staff } = await import("@groombook/db");
|
|
|
|
const app = new Hono();
|
|
app.get("/api/readyz", async (c) => {
|
|
try {
|
|
await getDb().select({ id: staff.id }).from(staff).limit(1);
|
|
return c.json({ status: "ready" }, 200);
|
|
} catch (err) {
|
|
console.error("[readyz] DB check failed:", err);
|
|
return c.json({ status: "degraded", check: "db" }, 503);
|
|
}
|
|
});
|
|
return app;
|
|
}
|
|
|
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
describe("GET /api/readyz", () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("returns 200 {status:'ready'} when DB query succeeds", async () => {
|
|
selectImpl = () => Promise.resolve([{ id: "staff-1" }]);
|
|
|
|
const app = await makeApp();
|
|
const res = await app.request("/api/readyz", { method: "GET" });
|
|
const body = (await res.json()) as Record<string, unknown>;
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(body.status).toBe("ready");
|
|
});
|
|
|
|
it("returns 503 {status:'degraded',check:'db'} when DB query throws", async () => {
|
|
selectImpl = () => Promise.reject(new Error("42P01: relation staff does not exist"));
|
|
|
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
const app = await makeApp();
|
|
const res = await app.request("/api/readyz", { method: "GET" });
|
|
const body = (await res.json()) as Record<string, unknown>;
|
|
|
|
expect(res.status).toBe(503);
|
|
expect(body.status).toBe("degraded");
|
|
expect(body.check).toBe("db");
|
|
|
|
// Raw SQL / driver error must NOT appear in the response body
|
|
expect(JSON.stringify(body)).not.toContain("42P01");
|
|
expect(JSON.stringify(body)).not.toContain("relation");
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|