import { afterEach, describe, expect, spyOn, test } from "bun:test"; import { rm } from "node:fs/promises"; import { runCommand } from "citty"; import { KuberApiError, type ApiRequestInit } from "../../lib/api"; import { getTrustPath, readTrust, updateTrust } from "../../lib/trust"; import { grantTrust, revokeTrust, statusTrust, trust, } from "../../command/trust"; const originalConfig = process.env.XDG_CONFIG_HOME; const identity = { project: "demo", fingerprint: "a".repeat(64) }; afterEach(async () => { const path = getTrustPath(); if (originalConfig === undefined) delete process.env.XDG_CONFIG_HOME; else process.env.XDG_CONFIG_HOME = originalConfig; await rm(path.slice(0, path.lastIndexOf("/")), { recursive: true, force: true, }); }); function requester( calls: Array<{ path: string; init?: ApiRequestInit }>, response: unknown = undefined, ) { return async (path: string, init?: ApiRequestInit): Promise => { calls.push({ path, init }); return response as T; }; } describe("trust command", () => { test("citty dispatch of bare trust runs the parent grant handler", async () => { await expect(runCommand(trust, { rawArgs: [] })).rejects.toThrow(); }); test.each(["status", "revoke"])( "citty dispatch of %s does not run the parent grant handler", async (subcommand) => { const command = { ...trust, subCommands: Object.fromEntries( Object.entries(trust.subCommands!).map(([name, child]) => [ name, { ...child, run: async () => {} }, ]), ), }; // Without a provided app context, a parent grant would reject in // current(). Successful dispatch proves citty did not grant afterward. await expect( runCommand(command, { rawArgs: [subcommand] }), ).resolves.toBeDefined(); }, ); test("grants, reports, and revokes the current namespace fingerprint", async () => { process.env.XDG_CONFIG_HOME = `/tmp/kuber-trust-command-${crypto.randomUUID()}`; const calls: Array<{ path: string; init?: ApiRequestInit }> = []; const output = spyOn(console, "log").mockImplementation(() => {}); await grantTrust(identity, requester(calls)); expect(calls).toEqual([ { path: "/workspaces/demo/trust", init: { method: "POST", json: { fingerprint: identity.fingerprint } }, }, ]); expect(await readTrust()).toEqual([identity]); calls.length = 0; await statusTrust( identity, requester(calls, { fingerprints: [identity.fingerprint] }), ); expect(calls).toEqual([ { path: "/workspaces/demo/trust", init: undefined }, ]); expect(output.mock.calls.map(([line]) => line)).toEqual([ "Trusted this directory for namespace demo", "Namespace: demo", "Local: trusted", "Server: registered", ]); calls.length = 0; await revokeTrust(identity, requester(calls)); expect(calls).toEqual([ { path: `/workspaces/demo/trust?fingerprint=${identity.fingerprint}`, init: { method: "DELETE" }, }, ]); expect(await readTrust()).toEqual([]); output.mockRestore(); }); test("removes local trust when the server registration is already absent", async () => { process.env.XDG_CONFIG_HOME = `/tmp/kuber-trust-command-${crypto.randomUUID()}`; await updateTrust(() => [identity]); const output = spyOn(console, "log").mockImplementation(() => {}); await revokeTrust(identity, async () => { throw new KuberApiError("not found", 404); }); expect(await readTrust()).toEqual([]); expect(output).toHaveBeenCalledWith("Revoked trust for namespace demo"); output.mockRestore(); }); test("removes local trust before reporting a remote revoke failure", async () => { process.env.XDG_CONFIG_HOME = `/tmp/kuber-trust-command-${crypto.randomUUID()}`; await updateTrust(() => [identity]); await expect( revokeTrust(identity, async () => { throw new KuberApiError("unavailable", 503); }), ).rejects.toThrow("Removed local trust for namespace demo"); expect(await readTrust()).toEqual([]); }); });