import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; import type { ComposeSpecification, Service } from "../../schema/docker.d"; import { DatabaseReconciliationError, buildDatabaseUrl, buildPostgresEnvironment, getComposePostgresClaims, getServicePostgresClaim, isPostgresVolumeEntry, reconcilePostgresClaim, reconcilePostgresClaims, } from "../../lib/database"; import { objectApi } from "../../lib/k8s"; afterEach(() => mock.restore()); describe("managed PostgreSQL claims", () => { test("supports short and explicit syntax", () => { expect( getServicePostgresClaim("app", { volumes: ["postgresql:app"], } as Service), ).toEqual({ service: "app", username: "app", database: "app", secretName: "postgres-app", }); expect( getServicePostgresClaim("app", { volumes: ["postgresql:user/database"], } as Service), ).toEqual({ service: "app", username: "user", database: "database", secretName: "postgres-user", }); }); test.each([ "postgresql:", "postgresql:user/", "postgresql:/database", "postgresql:user/database/extra", "postgresql:user:database", ])("rejects malformed declaration %s", (entry) => { expect(() => getServicePostgresClaim("app", { volumes: [entry] } as Service), ).toThrow("Use postgresql: or postgresql:/"); }); test("ignores unrelated entries and rejects duplicate declarations", () => { expect( getServicePostgresClaim("app", { volumes: ["cache:/cache"], } as Service), ).toBeUndefined(); expect(isPostgresVolumeEntry("cache:/cache")).toBe(false); expect(() => getServicePostgresClaim("app", { volumes: ["postgresql:one", "postgresql:two"], } as Service), ).toThrow("declares multiple postgres volumes"); }); test("allows one owner to share a database", () => { const compose = { services: { app: { volumes: ["postgresql:user/database"] }, worker: { volumes: ["postgresql:user/database"] }, }, } as ComposeSpecification; expect(getComposePostgresClaims(compose)).toHaveLength(2); }); test("rejects conflicting database owners", () => { const compose = { services: { app: { volumes: ["postgresql:one/shared"] }, worker: { volumes: ["postgresql:two/shared"] }, }, } as ComposeSpecification; expect(() => getComposePostgresClaims(compose)).toThrow( "Database shared is claimed by both one and two", ); }); test("escapes every connection URL component", () => { expect( buildDatabaseUrl( { service: "app", username: "user@host", database: "my/database", secretName: "postgres-user", }, { username: "user@host", password: "p:a/ss?#" }, ), ).toBe( "postgresql://user%40host:p%3Aa%2Fss%3F%23@c.database.svc.cluster.local:5432/my%2Fdatabase", ); }); test("injects Redis into services with managed PostgreSQL", () => { expect( buildPostgresEnvironment( { service: "app", username: "app", database: "app", secretName: "postgres-app", }, { username: "app", password: "secret" }, ), ).toEqual({ DATABASE_URL: "postgresql://app:secret@c.database.svc.cluster.local:5432/app", REDIS_URL: "redis://redis.database.svc.cluster.local", }); }); test("reconciles a selected claim before returning credentials", async () => { const claim = { service: "app", username: "app", database: "app", secretName: "postgres-app", }; spyOn(objectApi, "read").mockImplementation(async (resource) => { if (resource.kind === "Database") throw { code: 404 }; if (resource.kind === "Secret") { return { ...resource, data: { username: Buffer.from("app").toString("base64"), password: Buffer.from("secret").toString("base64"), }, } as never; } return { ...resource, spec: { managed: { roles: [] } } } as never; }); const patch = spyOn(objectApi, "patch").mockImplementation( async (resource) => resource as never, ); const create = spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never); expect(await reconcilePostgresClaim("project", claim)).toEqual({ username: "app", password: "secret", }); expect(patch.mock.calls.map(([resource]) => resource.kind)).toEqual([ "Secret", "Cluster", ]); expect(create.mock.calls.map(([resource]) => resource.kind)).toEqual(["Database"]); }); test("reports database apply phase and claim without exposing provider credentials", async () => { const claim = { service: "app", username: "app_role", database: "app_db", secretName: "postgres-app_role", }; spyOn(objectApi, "read").mockImplementation(async (resource) => { if (resource.kind === "Database") throw { code: 404 }; if (resource.kind === "Secret") { return { ...resource, data: { password: Buffer.from("private-value").toString("base64") } } as never; } return { ...resource, spec: { managed: { roles: [] } } } as never; }); spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never); spyOn(objectApi, "create").mockImplementation(async (resource) => { if (resource.kind === "Database") { throw Object.assign(new Error("Forbidden: password=private-value"), { code: 403 }); } return resource as never; }); let failure: unknown; try { await reconcilePostgresClaim("project", claim); } catch (error) { failure = error; } expect(failure).toBeInstanceOf(DatabaseReconciliationError); expect((failure as Error).message).toBe( "Database reconciliation failed during database apply for requested database claim: Forbidden (HTTP 403)", ); expect((failure as Error).message).not.toContain("private-value"); }); test.each(["read", "patch"])("distinguishes role secret %s failure", async (method) => { const secretLike = "DB_PASSWORD_private123"; spyOn(objectApi, "read").mockImplementation(async (resource) => { if (resource.kind === "Database") throw { code: 404 }; if (method === "read") throw new Error(`token=${secretLike}`); return undefined as never; }); spyOn(objectApi, "patch").mockImplementation(async () => { throw new Error(`token=${secretLike}`); }); let failure: unknown; try { await reconcilePostgresClaim("project", { service: secretLike, username: secretLike, database: secretLike, secretName: `postgres-${secretLike}`, }); } catch (error) { failure = error; } expect(failure).toBeInstanceOf(DatabaseReconciliationError); expect((failure as DatabaseReconciliationError).phase).toBe(`role secret ${method === "read" ? "lookup" : "apply"}`); expect((failure as Error).message).not.toContain(secretLike); }); test("uses safe provider status and reason without exposing request bodies or unsafe claim identifiers", () => { const cause = Object.assign(new Error("request body DATABASE_URL=postgresql://admin:private@db.example/app"), { statusCode: 422, body: { reason: "Invalid", code: 422, message: "token=private" }, }); const failure = new DatabaseReconciliationError("managed role update", cause, { service: "web", database: "postgresql://admin:private@db.example/app", username: "web_role", secretName: "secret", }); expect(failure.message).toBe( "Database reconciliation failed during managed role update for requested database claim: Invalid (HTTP 422)", ); expect(failure.cause).toBe(cause); expect(failure.message).not.toContain("private"); }); test("rejects an unrecognized phase containing a syntactically valid secret-like name", () => { const failure = new DatabaseReconciliationError("DB_PASSWORD_private123", new Error("private")); expect(failure.phase).toBe("operation execution"); expect(failure.message).not.toContain("DB_PASSWORD_private123"); }); test("identifies failure while preparing malformed managed roles before a Cluster write", async () => { spyOn(objectApi, "read").mockImplementation(async (resource) => resource.kind === "Secret" ? { ...resource, data: { password: Buffer.from("private").toString("base64") } } as never : resource.kind === "Database" ? Promise.reject({ code: 404 }) : { ...resource, spec: { managed: { roles: {} } } } as never); const patch = spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never); let failure: unknown; try { await reconcilePostgresClaim("project", { service: "DB_PASSWORD_private123", username: "role", database: "db", secretName: "postgres-role", }); } catch (error) { failure = error; } expect(failure).toBeInstanceOf(DatabaseReconciliationError); expect((failure as DatabaseReconciliationError).phase).toBe("managed role preparation"); expect((failure as Error).message).not.toContain("DB_PASSWORD_private123"); expect(patch.mock.calls.map(([resource]) => resource.kind)).toEqual(["Secret"]); }); test("reports malformed claims as claim discovery failures without echoing compose input", async () => { await expect(reconcilePostgresClaims("project", { services: { web: { volumes: ["postgresql:user:password=private"] } }, } as ComposeSpecification)).rejects.toThrow( "Database reconciliation failed during claim discovery: Check PostgreSQL claim declarations", ); }); test("reconciles a CNPG cluster returned with managed fields without sending them back", async () => { const claim = { service: "app", username: "app", database: "app", secretName: "postgres-app", }; spyOn(objectApi, "read").mockImplementation(async (resource) => { if (resource.kind === "Database") throw { code: 404 }; if (resource.kind === "Secret") { return { ...resource, data: { username: Buffer.from("app").toString("base64"), password: Buffer.from("secret").toString("base64"), }, } as never; } return { ...resource, metadata: { ...resource.metadata, managedFields: [{ manager: "cloudnative-pg" }], resourceVersion: "42", uid: "cluster-uid", creationTimestamp: "2026-09-08T00:00:00Z", generation: 3, }, spec: { managed: { roles: [{ name: "existing" }] } }, status: { phase: "Ready" }, } as never; }); const patch = spyOn(objectApi, "patch").mockImplementation( async (resource) => { if (resource.metadata?.managedFields) throw new Error("metadata.managedFields must be nil"); return resource as never; }, ); spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never); await reconcilePostgresClaim("project", claim); const cluster = patch.mock.calls.find( ([resource]) => resource.kind === "Cluster", )?.[0]; expect(cluster).toMatchObject({ metadata: { name: "postgres", namespace: "database" }, spec: { managed: { roles: [{ name: "existing" }, { name: "app" }] }, }, }); expect(cluster?.metadata).not.toHaveProperty("managedFields"); expect(cluster?.metadata).not.toHaveProperty("resourceVersion"); expect(cluster).not.toHaveProperty("status"); }); test("two projects reuse a foreign database without applying its metadata", async () => { const claim = { service: "web", username: "sastify", database: "sastify-store", secretName: "postgres-sastify" }; const live = { apiVersion: "postgresql.cnpg.io/v1", kind: "Database", metadata: { name: claim.database, namespace: "database", labels: { "kuber.dev/project": "sastify-api", "kuber.dev/workspace-uid": "foreign-uid", } }, spec: { owner: claim.username, cluster: { name: "postgres" } }, }; const original = structuredClone(live); spyOn(objectApi, "read").mockImplementation(async (resource) => resource.kind === "Database" ? live as never : resource.kind === "Secret" ? { ...resource, data: { password: Buffer.from("shared-password").toString("base64") } } as never : { ...resource, spec: { managed: { roles: [] } } } as never); const patch = spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never); const create = spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never); const compose = { services: { web: { volumes: ["postgresql:sastify/sastify-store"] } } } as ComposeSpecification; for (const project of ["sastify-api", "another-project"]) { const env = await reconcilePostgresClaims(project, compose); expect(env.web?.DATABASE_URL).toBe("postgresql://sastify:shared-password@c.database.svc.cluster.local:5432/sastify-store"); } expect(live).toEqual(original); expect(patch.mock.calls.map(([resource]) => resource.kind)).toEqual(["Secret", "Cluster", "Secret", "Cluster"]); expect(create).not.toHaveBeenCalled(); }); test("validates all databases in a compose file before writing the first role", async () => { const read = spyOn(objectApi, "read").mockImplementation(async (resource) => { if (resource.kind === "Database" && resource.metadata?.name === "first") throw { code: 404 }; if (resource.kind === "Database") return { ...resource, spec: { owner: "another-role", cluster: { name: "postgres" }, } } as never; throw new Error("Role lookup must not run"); }); const patch = spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never); const create = spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never); await expect(reconcilePostgresClaims("project", { services: { first: { volumes: ["postgresql:first"] }, second: { volumes: ["postgresql:second"] }, } } as ComposeSpecification)).rejects.toMatchObject({ phase: "database ownership" }); expect(read.mock.calls.map(([resource]) => resource.kind)).toEqual(["Database", "Database"]); expect(patch).not.toHaveBeenCalled(); expect(create).not.toHaveBeenCalled(); }); test.each([ ["wrong role", "other", "postgres"], ["wrong cluster", "sastify", "other"], ])("rejects existing database with %s before any writes", async (_case, owner, cluster) => { const claim = { service: "web", username: "sastify", database: "sastify-store", secretName: "postgres-sastify" }; spyOn(objectApi, "read").mockImplementation(async (resource) => resource.kind === "Database" ? { ...resource, spec: { owner, cluster: { name: cluster } } } as never : undefined as never); const patch = spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never); const create = spyOn(objectApi, "create").mockImplementation(async (resource) => resource as never); await expect(reconcilePostgresClaim("project", claim)).rejects.toMatchObject({ phase: "database ownership" }); expect(patch).not.toHaveBeenCalled(); expect(create).not.toHaveBeenCalled(); }); test("creates missing databases only once and fails closed on create races", async () => { spyOn(objectApi, "read").mockImplementation(async (resource) => { if (resource.kind === "Database") throw { code: 404 }; return { ...resource, spec: { managed: { roles: [] } } } as never; }); spyOn(objectApi, "patch").mockImplementation(async (resource) => resource as never); const create = spyOn(objectApi, "create").mockImplementation(async () => { throw Object.assign(new Error("Conflict"), { code: 409 }); }); await expect(reconcilePostgresClaims("project", { services: { web: { volumes: ["postgresql:sastify/sastify-store"] }, worker: { volumes: ["postgresql:sastify/sastify-store"] }, } } as ComposeSpecification)).rejects.toMatchObject({ phase: "database apply" }); expect(create).toHaveBeenCalledTimes(1); expect(create.mock.calls[0]?.[0]).toMatchObject({ metadata: { name: "sastify-store", labels: { "kuber.dev/project": "project" } }, spec: { owner: "sastify", cluster: { name: "postgres" } }, }); }); });