import { expect, spyOn, test } from "bun:test"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { ci, apiKeyRequest, runCi } from "../../command/ci"; import { provideContext } from "../../lib/context"; import { writeSession } from "../../lib/session"; import { resolveTrustIdentity } from "../../lib/trust"; test("CI requester supplies an API key without session authentication", async () => { const fetch = spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200 }), ); try { await apiKeyRequest("ci-secret")("/workspaces/shop/resources/plan", { headers: { "x-kuber-trust-project": "shop" }, }); const [, init] = fetch.mock.calls[0]!; const headers = new Headers(init?.headers); expect(headers.get("authorization")).toBe("Bearer ci-secret"); expect(headers.get("x-kuber-trust-project")).toBe("shop"); } finally { fetch.mockRestore(); } }); test("CI command requires an API key before loading project context", async () => { const previous = process.env.KUBER_API_KEY; delete process.env.KUBER_API_KEY; try { await expect( ci.run!({ args: { apiKey: undefined, build: false, trust: false }, } as never), ).rejects.toThrow("KUBER_API_KEY or --api-key is required"); } finally { if (previous === undefined) delete process.env.KUBER_API_KEY; else process.env.KUBER_API_KEY = previous; } }); test("CI trust grant and deployment pipeline use the API key requester", async () => { const root = await mkdtemp(join(tmpdir(), "kuber-ci-")); const previousCwd = process.cwd(); const previousConfigHome = process.env.XDG_CONFIG_HOME; const previousRuntimeDirectory = process.env.XDG_RUNTIME_DIR; const calls: Array<{ path: string; method: string | undefined; body: string | undefined; headers: Headers; }> = []; const fetch = spyOn(globalThis, "fetch").mockImplementation((async ( input, init, ) => { const url = new URL(input.toString()); const path = url.pathname.replace("/api/v2", ""); calls.push({ path, method: init?.method, body: typeof init?.body === "string" ? init.body : undefined, headers: new Headers(init?.headers), }); if (path === "/workspaces/shop") return new Response( JSON.stringify({ metadata: { name: "shop", uid: "workspace", resourceVersion: "1" }, }), ); if (path.endsWith("/plan")) return new Response(JSON.stringify({ desired: [], stale: [] })); if (path === "/snapshots/negotiate") return new Response( JSON.stringify({ workspace: "sha256:abc", missing: [], ready: true }), ); return new Response(JSON.stringify({ resourcesAdopted: 0 })); }) as typeof globalThis.fetch); try { process.env.XDG_CONFIG_HOME = join(root, "config"); process.env.XDG_RUNTIME_DIR = join(root, "runtime"); await writeSession( { token: "session-secret", expiresAt: "2030-01-01T00:00:00Z", user: { username: "ci", roles: [] }, }, true, ); await writeFile(join(root, "compose.yml"), "services: {}\n"); await writeFile( join(root, ".kuberrc.ts"), 'export default { project: "shop" };\n', ); const git = Bun.spawn(["git", "init", "-q", root]); expect(await git.exited).toBe(0); const fingerprint = (await resolveTrustIdentity("shop", root)).fingerprint; process.chdir(root); await provideContext(() => runCi(false, true, "ci-key")); expect(calls[0]?.path).toBe("/workspaces/shop/trust"); expect(calls[0]?.method).toBe("POST"); expect(JSON.parse(calls[0]?.body ?? "")).toEqual({ fingerprint }); expect(calls.some(({ path }) => path.endsWith("/resources/plan"))).toBe( true, ); expect(calls.some(({ path }) => path.endsWith("/resources/apply"))).toBe( true, ); expect( calls.every( ({ headers }) => headers.get("authorization") === "Bearer ci-key", ), ).toBe(true); } finally { fetch.mockRestore(); process.chdir(previousCwd); if (previousConfigHome === undefined) delete process.env.XDG_CONFIG_HOME; else process.env.XDG_CONFIG_HOME = previousConfigHome; if (previousRuntimeDirectory === undefined) delete process.env.XDG_RUNTIME_DIR; else process.env.XDG_RUNTIME_DIR = previousRuntimeDirectory; await rm(root, { recursive: true, force: true }); } });