Files
kuber/tests/command/administration.test.ts
2026-10-07 10:33:21 +00:00

516 lines
15 KiB
TypeScript

import { describe, expect, spyOn, test } from "bun:test";
import { runCommand } from "citty";
import { listAuditEvents } from "../../command/audit";
import { main } from "../../command/main";
import { getOperation, listOperations } from "../../command/operations";
import {
addUser,
createApiKey,
deleteUser,
listApiKeys,
listUsers,
revokeApiKey,
revokeUserSessions,
setUserDisabled,
updateUser,
users,
} from "../../command/users";
import type { ApiRequestInit } from "../../lib/api";
import * as api from "../../lib/api";
type Call = { path: string; init?: ApiRequestInit };
function requestReturning<T>(result: T, calls: Call[]) {
return async <R>(path: string, init?: ApiRequestInit): Promise<R> => {
calls.push({ path, init });
return result as unknown as R;
};
}
const user = {
username: "alice",
roles: ["admin"],
disabled: false,
updatedAt: "2026-09-02T10:00:00.000Z",
};
describe("user administration commands", () => {
test("lists and creates users through authenticated API routes", async () => {
const calls: Call[] = [];
expect(
await listUsers(requestReturning({ items: [user] }, calls)),
).toContain("alice");
expect(
await addUser(
"alice",
"secret",
["admin"],
requestReturning(user, calls),
),
).toContain("admin");
expect(calls).toEqual([
{ path: "/users", init: undefined },
{
path: "/users",
init: {
method: "POST",
json: { username: "alice", password: "secret", roles: ["admin"] },
},
},
]);
});
test("updates roles, passwords, and enabled state with PATCH", async () => {
const calls: Call[] = [];
const request = requestReturning(user, calls);
await updateUser(
"alice/example",
{ roles: ["operator"], password: "new" },
request,
);
await setUserDisabled("alice", true, request);
await setUserDisabled("alice", false, request);
expect(calls).toEqual([
{
path: "/users/alice%2Fexample",
init: {
method: "PATCH",
json: { roles: ["operator"], password: "new" },
},
},
{
path: "/users/alice",
init: { method: "PATCH", json: { disabled: true } },
},
{
path: "/users/alice",
init: { method: "PATCH", json: { disabled: false } },
},
]);
});
test("requires confirmation for deletion and supports session revocation", async () => {
const calls: Call[] = [];
const request = requestReturning(undefined, calls);
expect(await deleteUser("alice", false, request)).toBe(
"Deletion cancelled",
);
expect(await deleteUser("alice", true, request)).toBe("Deleted user alice");
expect(
await revokeUserSessions(
"alice",
requestReturning({ username: "alice", revoked: 2 }, calls),
),
).toBe("Revoked 2 sessions for alice");
expect(calls).toEqual([
{ path: "/users/alice", init: { method: "DELETE" } },
{
path: "/users/alice/sessions/revoke",
init: { method: "POST" },
},
]);
});
test("manages API keys below the user route without leaking token in lists", async () => {
const calls: Call[] = [];
const key = {
id: "key-identifier-123",
username: "alice",
capabilities: ["kubernetes:write"] as const,
expiresAt: "2026-12-01T00:00:00.000Z",
disabled: false,
token: "shown-once-token",
};
expect(
await listApiKeys(
"alice/example",
requestReturning({ items: [{ ...key, token: undefined }] }, calls),
),
).not.toContain(key.token);
await createApiKey(
"alice",
{ capabilities: ["kubernetes:write"] },
async <T>(path: string, init?: ApiRequestInit): Promise<T> => {
calls.push({ path, init });
return (init ? key : user) as T;
},
);
expect(
await revokeApiKey(
"alice",
key.id,
false,
requestReturning(undefined, calls),
),
).toContain("cancelled");
await revokeApiKey(
"alice",
key.id,
true,
requestReturning(undefined, calls),
);
expect(calls).toEqual([
{ path: "/users/alice%2Fexample/keys", init: undefined },
{ path: "/users/alice", init: undefined },
{
path: "/users/alice/keys",
init: { method: "POST", json: { capabilities: ["kubernetes:write"] } },
},
{
path: "/users/alice/keys/key-identifier-123",
init: { method: "DELETE" },
},
]);
});
test("lists all users' keys without a username and filters with one", async () => {
const calls: Call[] = [];
const request = async <T>(
path: string,
init?: ApiRequestInit,
): Promise<T> => {
calls.push({ path, init });
if (path === "/users")
return { items: [user, { ...user, username: "bob" }] } as T;
return {
items: [
{
id: `${path.includes("bob") ? "bob" : "alice"}-key`,
username: path.includes("bob") ? "bob" : "alice",
capabilities: ["kubernetes:read"],
disabled: false,
},
],
} as T;
};
const all = await listApiKeys(undefined, request);
expect(all).toContain("alice-key");
expect(all).toContain("bob-key");
expect(all).toContain("never");
expect(calls.map((call) => call.path)).toEqual([
"/users",
"/users/alice/keys",
"/users/bob/keys",
]);
calls.length = 0;
expect(await listApiKeys("alice", request)).not.toContain("bob-key");
expect(calls.map((call) => call.path)).toEqual(["/users/alice/keys"]);
});
test("key creation requires an existing active user", async () => {
const calls: Call[] = [];
const body = { capabilities: ["kubernetes:read" as const] };
await expect(
createApiKey("missing", body, async (path) => {
calls.push({ path });
throw new Error("User 'missing' not found");
}),
).rejects.toThrow("User 'missing' not found");
expect(calls).toEqual([{ path: "/users/missing" }]);
await expect(
createApiKey(
"alice",
body,
requestReturning({ ...user, disabled: true }, calls),
),
).rejects.toThrow("user 'alice' is inactive");
expect(calls.at(-1)?.path).toBe("/users/alice");
});
test("creates non-expiring and finite API keys with the requested POST bodies", async () => {
const calls: Call[] = [];
const key = {
id: "key-identifier-123",
username: "alice",
capabilities: ["kubernetes:read"] as const,
disabled: false,
token: "shown-once-token",
};
const request = async <T>(
path: string,
init?: ApiRequestInit,
): Promise<T> => {
calls.push({ path, init });
return (init ? key : user) as T;
};
await createApiKey(
"alice",
{ capabilities: ["kubernetes:read"], workspace: "team/shop" },
request,
);
const expiresAt = "2026-12-01T00:00:00.000Z";
await createApiKey(
"alice",
{
capabilities: ["kubernetes:read"],
workspace: "team/shop",
expiresAt,
},
request,
);
expect(calls).toEqual([
{ path: "/users/alice", init: undefined },
{
path: "/users/alice/keys",
init: {
method: "POST",
json: { capabilities: ["kubernetes:read"], workspace: "team/shop" },
},
},
{ path: "/users/alice", init: undefined },
{
path: "/users/alice/keys",
init: {
method: "POST",
json: {
capabilities: ["kubernetes:read"],
workspace: "team/shop",
expiresAt,
},
},
},
]);
expect(calls[1]?.init?.json).not.toHaveProperty("expiresAt");
});
test("key command help declares username, capability examples, and non-expiry", async () => {
const commands = (users.subCommands as Record<string, any>)?.keys;
if (!commands || typeof commands === "function")
throw new Error("Missing keys command");
const subCommands = await Promise.resolve(commands.subCommands);
const create = subCommands?.create;
const ls = subCommands?.ls;
if (
!create ||
typeof create === "function" ||
!ls ||
typeof ls === "function"
)
throw new Error("Missing key subcommands");
expect(create.args?.username).toMatchObject({
type: "positional",
required: true,
});
expect(create.args?.capabilities?.description).toContain(
"kubernetes:read,kubernetes:write",
);
expect(create.args?.["expires-days"]?.description).toContain("none");
expect(create.args?.workspace?.alias).toBe("w");
expect(create.args?.["expires-days"]?.alias).toBe("e");
expect(create.meta?.description).toContain("--expireDays=none");
expect(ls.meta?.description).toContain("optional positional username");
await expect(
create.run?.({
args: {
_: [],
username: "alice",
capabilities: "invalid",
"expires-days": "90",
},
} as never),
).rejects.toThrow("kubernetes:read,kubernetes:write");
await expect(
create.run?.({
args: {
_: [],
username: "alice",
capabilities: "kubernetes:read",
"expires-days": "NaN",
},
} as never),
).rejects.toThrow("1 to 365, or none");
});
test("parses both API key creation forms and retains username position", async () => {
const calls: Call[] = [];
const request = spyOn(api, "apiRequest").mockImplementation((async <T>(
path: string,
init?: ApiRequestInit,
) => {
calls.push({ path, init });
return (
init
? {
id: "fake-key-id-123456",
username: "dmgnr",
capabilities:
init.json &&
(init.json as { capabilities: string[] }).capabilities,
workspace: "kuber-server",
token: "fake-token-never-real",
disabled: false,
}
: { username: "dmgnr", roles: ["admin"], disabled: false }
) as T;
}) as typeof api.apiRequest);
const log = spyOn(console, "log").mockImplementation(() => {});
const keyCommand = (users.subCommands as Record<string, any>).keys;
const create = keyCommand.subCommands.create;
try {
await runCommand(users, {
rawArgs: [
"keys",
"create",
"--capabilities",
"kubernetes:write",
"--workspace",
"kuber-server",
"--expires-days",
"none",
"dmgnr",
],
});
expect(calls[1]?.path).toBe("/users/dmgnr/keys");
expect(calls[1]?.init?.json).toEqual({
capabilities: ["kubernetes:write"],
workspace: "kuber-server",
});
calls.length = 0;
await runCommand(users, {
rawArgs: [
"keys",
"create",
"--capabilities",
"kubernetes:read",
"--expires-days",
"30",
"dmgnr",
],
});
expect(
(calls[1]?.init?.json as { expiresAt: string }).expiresAt,
).toBeString();
calls.length = 0;
await runCommand(users, {
rawArgs: [
"keys",
"create",
"-w",
"kuber-server",
"-e",
"none",
"dmgnr",
"kubernetes:write,kubernetes:read",
],
});
expect(calls[1]?.path).toBe("/users/dmgnr/keys");
expect(calls[1]?.init?.json).toEqual({
capabilities: ["kubernetes:write", "kubernetes:read"],
workspace: "kuber-server",
});
await expect(
create.run?.({ args: { username: "dmgnr" } } as never),
).rejects.toThrow("Provide capabilities");
await expect(
create.run?.({
args: {
username: "dmgnr",
capabilities: "kubernetes:read",
_: ["dmgnr", "kubernetes:write"],
},
} as never),
).rejects.toThrow("not both");
await expect(
create.run?.({
args: {
username: "dmgnr",
capabilities: "kubernetes:read",
"expires-days": "0",
},
} as never),
).rejects.toThrow("1 to 365, or none");
calls.length = 0;
await runCommand(users, {
rawArgs: [
"keys",
"create",
"dmgnr",
"--capabilities",
"kubernetes:read",
],
});
expect(
(calls[1]?.init?.json as { expiresAt: string }).expiresAt,
).toBeString();
} finally {
request.mockRestore();
log.mockRestore();
}
});
});
const operation = {
metadata: {
name: "operation-1",
creationTimestamp: "2026-09-02T10:00:00.000Z",
},
spec: { workspaceId: "team/shop", action: "restart" },
status: { state: "succeeded", result: { deployments: ["web"] } },
};
describe("operations and audit commands", () => {
test("lists filtered operations and gets operation details", async () => {
const calls: Call[] = [];
const listing = await listOperations(
"team/shop",
requestReturning({ items: [operation] }, calls),
);
const detail = await getOperation(
"operation/1",
requestReturning(operation, calls),
);
expect(listing).toContain("restart");
expect(detail).toContain('Result: {"deployments":["web"]}');
expect(calls).toEqual([
{ path: "/operations?workspaceId=team%2Fshop", init: undefined },
{ path: "/operations/operation%2F1", init: undefined },
]);
});
test("lists audit events with an optional workspace filter", async () => {
const calls: Call[] = [];
const output = await listAuditEvents(
"team/shop",
requestReturning(
{
items: [
{
metadata: {
name: "audit-1",
creationTimestamp: "2026-09-02T10:00:00.000Z",
},
spec: {
actor: { username: "alice" },
action: "workspace.restart",
workspaceId: "team/shop",
outcome: "success",
},
},
],
},
calls,
),
);
expect(output).toContain("alice");
expect(output).toContain("workspace.restart");
expect(calls).toEqual([
{ path: "/audit?workspaceId=team%2Fshop", init: undefined },
]);
});
test("registers administration command groups", async () => {
const subCommands = await Promise.resolve(main.subCommands);
expect(Object.keys(subCommands ?? {})).toEqual(
expect.arrayContaining(["users", "operations", "audit"]),
);
});
});