This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// `/api/ready` (spec §13): the verdict, the migration level it compares against, and what the answer
|
||||
// does not say. The probes are functions, so the verdict is tested without a database; the migration
|
||||
// list is the real one, read from this repository's folders.
|
||||
|
||||
import { readdirSync } from "node:fs"
|
||||
import { appRoot, expectedMigrations, GRANT_MIGRATIONS, missingMigrations } from "@app/config"
|
||||
import { appModules } from "@app/modules"
|
||||
import { buildRegistry } from "@conusai-kit/core"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { checkReadiness, type ReadinessProbes } from "../apps/web/src/server/readiness.js"
|
||||
|
||||
const ALL = [
|
||||
"0001_kernel",
|
||||
"0004_files",
|
||||
"0005_proposals",
|
||||
"0006_app-user-identity",
|
||||
"notes/0001_notes",
|
||||
...GRANT_MIGRATIONS,
|
||||
]
|
||||
|
||||
const probes = (overrides: Partial<ReadinessProbes> = {}, warnings: string[] = []): ReadinessProbes => ({
|
||||
store: async () => 1,
|
||||
reader: async () => 1,
|
||||
expected: async () => ALL,
|
||||
applied: async () => new Set(ALL),
|
||||
platform: async () => true,
|
||||
warn: (message) => warnings.push(message),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe("the readiness verdict", () => {
|
||||
it("is ready when the store, the reader and the migration level are there", async () => {
|
||||
expect(await checkReadiness(probes())).toEqual({
|
||||
ready: true,
|
||||
checks: { store: true, reader: true, migrations: true, platform: true },
|
||||
})
|
||||
})
|
||||
|
||||
it("is not ready when the runtime role cannot query, or the reader cannot", async () => {
|
||||
const down = async () => {
|
||||
throw new Error("connect ECONNREFUSED postgresql://app_app:secret@db:5432/app")
|
||||
}
|
||||
expect((await checkReadiness(probes({ store: down }))).checks).toMatchObject({ store: false, reader: true })
|
||||
expect((await checkReadiness(probes({ reader: down }))).checks).toMatchObject({ store: true, reader: false })
|
||||
expect((await checkReadiness(probes({ store: down }))).ready).toBe(false)
|
||||
expect((await checkReadiness(probes({ reader: down }))).ready).toBe(false)
|
||||
})
|
||||
|
||||
it("is not ready while a migration this build expects has not run", async () => {
|
||||
const behind = new Set(ALL.slice(0, -1))
|
||||
const report = await checkReadiness(probes({ applied: async () => behind }))
|
||||
expect(report.ready).toBe(false)
|
||||
expect(report.checks.migrations).toBe(false)
|
||||
})
|
||||
|
||||
it("is ready when the database is ahead of this build: a newer release has already migrated it", async () => {
|
||||
const ahead = new Set([...ALL, "9999_from_the_future"])
|
||||
expect((await checkReadiness(probes({ applied: async () => ahead }))).ready).toBe(true)
|
||||
})
|
||||
|
||||
it("reports the platform without requiring it", async () => {
|
||||
const report = await checkReadiness(probes({ platform: async () => false }))
|
||||
expect(report.ready).toBe(true)
|
||||
expect(report.checks.platform).toBe(false)
|
||||
const thrown = await checkReadiness(
|
||||
probes({
|
||||
platform: async () => {
|
||||
throw new Error("boom")
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(thrown.checks.platform).toBe(false)
|
||||
})
|
||||
|
||||
it("answers with booleans and nothing else, and logs the reason where an operator reads it", async () => {
|
||||
const warnings: string[] = []
|
||||
const report = await checkReadiness(
|
||||
probes(
|
||||
{
|
||||
store: async () => {
|
||||
throw new Error("password authentication failed for user app_app\nHINT: pg_hba.conf")
|
||||
},
|
||||
applied: async () => new Set(["0001_kernel"]),
|
||||
},
|
||||
warnings,
|
||||
),
|
||||
)
|
||||
const answer = JSON.stringify(report)
|
||||
expect(answer).not.toContain("password")
|
||||
expect(answer).not.toContain("0004_files")
|
||||
expect(Object.values(report.checks).every((value) => typeof value === "boolean")).toBe(true)
|
||||
expect(warnings).toEqual([
|
||||
"ready: store failed: password authentication failed for user app_app",
|
||||
expect.stringMatching(/^ready: migrations failed: the migrate job has not applied 0004_files/),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("the migration level", () => {
|
||||
it("lists the app's files, then each module's, then the grants the migrate job adds", async () => {
|
||||
const expected = await expectedMigrations(buildRegistry([...appModules]))
|
||||
const files = readdirSync(`${appRoot()}/migrations/postgres`)
|
||||
.filter((name) => name.endsWith(".sql"))
|
||||
.map((name) => name.replace(/\.sql$/, ""))
|
||||
.sort()
|
||||
expect(expected.slice(0, files.length)).toEqual(files)
|
||||
expect(expected).toContain("notes/0001_notes")
|
||||
expect(expected.slice(-2)).toEqual([...GRANT_MIGRATIONS])
|
||||
})
|
||||
|
||||
it("names what a database lacks", () => {
|
||||
expect(missingMigrations(["a", "b", "c"], new Set(["a", "c", "z"]))).toEqual(["b"])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user