Files
kuber/tests/command/ci.test.ts
T

127 lines
4.4 KiB
TypeScript

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 });
}
});