import { ANONYMOUS_PRINCIPAL, type ModuleDatabase, type ModuleRouteHandler, type ModuleRuntimeServices, parsePermissionId, type RegisteredModuleHttpRoute, } from "@ekklesia/contracts"; import { type AuthorizationEvaluator, type AuthorizationStore, createPermissionRegistry, } from "@ekklesia/core/authorization"; import type { EkklesiaIdentityRuntime } from "@ekklesia/core/identity"; import { createModuleUiPublicationRegistry, EMPTY_MODULE_REALTIME_REGISTRY, } from "@ekklesia/core/modules"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createApiApp } from "../src/app.js"; import { type ApiHealthOptions, HEALTH_LIVE_PATH, HEALTH_READY_PATH, READINESS_DATABASE_TIMEOUT_MS, } from "../src/health-routes.js"; import { createInMemoryDashboardStore } from "./support/in-memory-dashboard-store.js"; import { testDashboardHostAssets } from "./support/dashboard-host-assets.js"; import { emptyModuleConfiguration } from "./support/in-memory-module-configuration.js"; import { emptyModuleLifecycle } from "./support/in-memory-module-lifecycle.js"; import { emptyModuleStorage } from "./support/in-memory-module-storage.js"; const trustedOrigin = "https://dashboard.example.test"; const modulePermission = parsePermissionId("health.status.read"); /** What a real driver failure can carry, and what must therefore never reach a response. */ const DRIVER_FAILURE_DETAIL = "connect ECONNREFUSED postgresql://ekklesia:hunter2@db.internal:5432/ekklesia SELECT 1"; function fakeRuntime(): EkklesiaIdentityRuntime { return { handleAuthRequest: vi.fn(), resolvePrincipal: vi.fn().mockResolvedValue(ANONYMOUS_PRINCIPAL), }; } function fakeStore(): AuthorizationStore { return { createGrant: vi.fn(), grantPlatformOwner: vi.fn(), hasActiveGrant: vi.fn().mockResolvedValue(false), isPlatformOwner: vi.fn().mockResolvedValue(false), listGrantsForUser: vi.fn().mockResolvedValue({ grants: [] }), revokeGrant: vi.fn(), revokePlatformOwner: vi.fn(), }; } function fakeModuleServices(): ModuleRuntimeServices { return Object.freeze({ database: Object.freeze({}) as unknown as ModuleDatabase }); } function healthOptions(overrides: Partial = {}) { return { checkDatabase: vi.fn<() => Promise>().mockResolvedValue(undefined), isModuleStartupComplete: vi.fn<() => boolean>().mockReturnValue(true), ...overrides, }; } function buildApp( health: ApiHealthOptions, moduleRoutes: readonly RegisteredModuleHttpRoute[] = Object.freeze([]), ) { const evaluator: AuthorizationEvaluator = { authorize: vi.fn().mockResolvedValue({ allowed: true }), }; const runtime = fakeRuntime(); const app = createApiApp({ authorization: { evaluator, registry: createPermissionRegistry([ { baseline: "public", description: "Test permission.", id: modulePermission }, ]), store: fakeStore(), }, moduleRoutes, moduleRealtime: EMPTY_MODULE_REALTIME_REGISTRY, moduleServices: fakeModuleServices(), moduleUi: createModuleUiPublicationRegistry([]), moduleLifecycle: emptyModuleLifecycle(), moduleConfiguration: emptyModuleConfiguration(), moduleStorage: emptyModuleStorage(), dashboard: createInMemoryDashboardStore(), dashboardHostAssets: testDashboardHostAssets(), health, runtime, trustedOrigins: [trustedOrigin], }); return { app, evaluator, runtime }; } /** Resolves once every already-queued microtask has run, using the real `setImmediate` — the only * timer functions these tests fake are `setTimeout` and `clearTimeout`. */ function drainMicrotasks(): Promise { return new Promise((resolve) => setImmediate(resolve)); } /** Whether `promise` settled with no fake time advanced at all: "promptly", made exact. */ async function settlesWithoutWaiting(promise: Response | Promise): Promise { let settled = false; void Promise.resolve(promise).then( () => { settled = true; }, () => { settled = true; }, ); await drainMicrotasks(); return settled; } async function expectNotReady(response: Response): Promise { expect(response.status).toBe(503); expect(response.headers.get("cache-control")).toBe("no-store"); expect(await response.json()).toEqual({ code: "NOT_READY" }); } afterEach(() => { vi.useRealTimers(); }); describe("GET /api/v1/health/live", () => { it("returns 200 with a fixed body and Cache-Control: no-store", async () => { const health = healthOptions(); const { app } = buildApp(health); const response = await app.request(HEALTH_LIVE_PATH); expect(response.status).toBe(200); expect(response.headers.get("cache-control")).toBe("no-store"); expect(response.headers.get("content-type")).toMatch(/^application\/json/u); expect(await response.json()).toEqual({ status: "live" }); }); it("does not consult the database or module startup, so it stays 200 while readiness is 503", async () => { const health = healthOptions({ checkDatabase: vi.fn().mockRejectedValue(new Error(DRIVER_FAILURE_DETAIL)), isModuleStartupComplete: vi.fn().mockReturnValue(false), }); const { app } = buildApp(health); const response = await app.request(HEALTH_LIVE_PATH); expect(response.status).toBe(200); expect(health.checkDatabase).not.toHaveBeenCalled(); expect(health.isModuleStartupComplete).not.toHaveBeenCalled(); }); }); describe("GET /api/v1/health/ready", () => { it("returns 200 with a fixed body when the database answers and module startup completed", async () => { const health = healthOptions(); const { app } = buildApp(health); const response = await app.request(HEALTH_READY_PATH); expect(response.status).toBe(200); expect(response.headers.get("cache-control")).toBe("no-store"); expect(await response.json()).toEqual({ status: "ready" }); expect(health.checkDatabase).toHaveBeenCalledTimes(1); }); it("returns a fixed 503 when the database query fails, carrying nothing from the failure", async () => { const health = healthOptions({ checkDatabase: vi.fn().mockRejectedValue(new Error(DRIVER_FAILURE_DETAIL)), }); const { app } = buildApp(health); const response = await app.request(HEALTH_READY_PATH); const text = await response.clone().text(); await expectNotReady(response); for (const fragment of ["ECONNREFUSED", "hunter2", "db.internal", "SELECT", "Error"]) { expect(text).not.toContain(fragment); } }); it("treats a check that throws synchronously exactly like one that rejects", async () => { const health = healthOptions({ checkDatabase: vi.fn(() => { throw new Error(DRIVER_FAILURE_DETAIL); }), }); const { app } = buildApp(health); await expectNotReady(await app.request(HEALTH_READY_PATH)); }); it("returns 503 without touching the database before module startup completed", async () => { const health = healthOptions({ isModuleStartupComplete: vi.fn().mockReturnValue(false) }); const { app } = buildApp(health); await expectNotReady(await app.request(HEALTH_READY_PATH)); expect(health.checkDatabase).not.toHaveBeenCalled(); }); it("returns 503 when shutdown began while the database query was in flight", async () => { let startupComplete = true; const health = healthOptions({ checkDatabase: vi.fn(async () => { startupComplete = false; }), isModuleStartupComplete: () => startupComplete, }); const { app } = buildApp(health); await expectNotReady(await app.request(HEALTH_READY_PATH)); }); it("starts a fresh check for each sequential probe rather than caching a success", async () => { const health = healthOptions(); const { app } = buildApp(health); await app.request(HEALTH_READY_PATH); await app.request(HEALTH_READY_PATH); expect(health.checkDatabase).toHaveBeenCalledTimes(2); }); it("reports ready for a slow query that still answers inside the bound", async () => { vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); let answer: () => void = () => undefined; const health = healthOptions({ checkDatabase: vi.fn( () => new Promise((resolve) => { answer = resolve; }), ), }); const { app } = buildApp(health); const pending = app.request(HEALTH_READY_PATH); await vi.advanceTimersByTimeAsync(READINESS_DATABASE_TIMEOUT_MS - 1); answer(); expect((await pending).status).toBe(200); }); }); describe("readiness never piles up database checks", () => { it("keeps exactly one check in flight for a query that never settles, while every probe answers 503 promptly", async () => { vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); const health = healthOptions({ checkDatabase: vi.fn(() => new Promise(() => undefined)), }); const { app } = buildApp(health); // Probes arriving inside the first check's window share it and its single deadline. const first = app.request(HEALTH_READY_PATH); const concurrent = app.request(HEALTH_READY_PATH); await drainMicrotasks(); expect(health.checkDatabase).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(READINESS_DATABASE_TIMEOUT_MS); await expectNotReady(await first); await expectNotReady(await concurrent); // After the deadline the query is still unsettled: no new check starts, and each probe answers // without waiting out another bound. for (let probe = 0; probe < 20; probe += 1) { const pending = app.request(HEALTH_READY_PATH); expect(await settlesWithoutWaiting(pending)).toBe(true); await expectNotReady(await pending); } expect(health.checkDatabase).toHaveBeenCalledTimes(1); }); it("starts a new check only once the timed-out query has actually settled", async () => { vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); let settleHungQuery: (error: Error) => void = () => undefined; const checkDatabase = vi .fn<() => Promise>() .mockImplementationOnce( () => new Promise((_resolve, reject) => { settleHungQuery = reject; }), ) .mockResolvedValue(undefined); const { app } = buildApp(healthOptions({ checkDatabase })); const first = app.request(HEALTH_READY_PATH); await vi.advanceTimersByTimeAsync(READINESS_DATABASE_TIMEOUT_MS); await expectNotReady(await first); await expectNotReady(await app.request(HEALTH_READY_PATH)); expect(checkDatabase).toHaveBeenCalledTimes(1); // The database cancels the abandoned statement; the next probe may finally ask again. settleHungQuery(new Error(DRIVER_FAILURE_DETAIL)); await drainMicrotasks(); const recovered = await app.request(HEALTH_READY_PATH); expect(recovered.status).toBe(200); expect(checkDatabase).toHaveBeenCalledTimes(2); }); }); describe("health route boundaries", () => { it.each([ HEALTH_LIVE_PATH, HEALTH_READY_PATH, ])("%s accepts GET only, answering every other method with a fixed 405 and never checking readiness", async (path) => { const health = healthOptions(); const { app } = buildApp(health); for (const method of ["HEAD", "POST", "PUT", "PATCH", "DELETE"]) { const response = await app.request(path, { method }); expect(response.status, method).toBe(405); expect(response.headers.get("allow")).toBe("GET"); expect(response.headers.get("cache-control")).toBe("no-store"); expect(await response.text()).toBe(""); } expect(health.checkDatabase).not.toHaveBeenCalled(); expect(health.isModuleStartupComplete).not.toHaveBeenCalled(); }); it.each([ HEALTH_LIVE_PATH, HEALTH_READY_PATH, ])("%s is unauthenticated: it never resolves a principal or evaluates authorization, even with a cookie", async (path) => { const { app, evaluator, runtime } = buildApp(healthOptions()); const response = await app.request(path, { headers: { Cookie: "better-auth.session_token=anything", Origin: trustedOrigin }, }); expect(response.status).toBe(200); expect(runtime.resolvePrincipal).not.toHaveBeenCalled(); expect(runtime.handleAuthRequest).not.toHaveBeenCalled(); expect(evaluator.authorize).not.toHaveBeenCalled(); }); it('cannot be shadowed by a module, even one whose id is "health"', async () => { const moduleHandler = vi.fn( async () => new Response("module", { status: 200 }), ); const moduleRoutes = ["/live", "/ready"].map( (path) => Object.freeze({ bodyMode: "buffered", handler: moduleHandler, maxBodyBytes: 1024, method: "GET", moduleId: "health", path, permission: modulePermission, }) as RegisteredModuleHttpRoute, ); const { app } = buildApp(healthOptions(), moduleRoutes); expect(await (await app.request(HEALTH_LIVE_PATH)).json()).toEqual({ status: "live" }); expect(await (await app.request(HEALTH_READY_PATH)).json()).toEqual({ status: "ready" }); expect(moduleHandler).not.toHaveBeenCalled(); // The module's own routes still live where every module route lives, and only there. const moduleResponse = await app.request("/api/v1/modules/health/ready"); expect(await moduleResponse.text()).toBe("module"); expect(moduleHandler).toHaveBeenCalledTimes(1); }); });