import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; import type { ComposeSpecification, Service } from "../../schema/docker.d"; import { buildDatabaseUrl, buildPostgresEnvironment, getComposePostgresClaims, getServicePostgresClaim, isPostgresVolumeEntry, reconcilePostgresClaim, } 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 === "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, ); expect(await reconcilePostgresClaim("project", claim)).toEqual({ username: "app", password: "secret", }); expect(patch.mock.calls.map(([resource]) => resource.kind)).toEqual([ "Secret", "Cluster", "Database", ]); }); });