231 lines
6.2 KiB
TypeScript
231 lines
6.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
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,
|
|
} from "../../command/users";
|
|
import type { ApiRequestInit } 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"] },
|
|
requestReturning(key, calls),
|
|
);
|
|
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/keys",
|
|
init: { method: "POST", json: { capabilities: ["kubernetes:write"] } },
|
|
},
|
|
{
|
|
path: "/users/alice/keys/key-identifier-123",
|
|
init: { method: "DELETE" },
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
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"]),
|
|
);
|
|
});
|
|
});
|