feat: improve API keys and build workflows
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
revokeUserSessions,
|
||||
setUserDisabled,
|
||||
updateUser,
|
||||
users,
|
||||
} from "../../command/users";
|
||||
import type { ApiRequestInit } from "../../lib/api";
|
||||
|
||||
@@ -130,7 +131,10 @@ describe("user administration commands", () => {
|
||||
await createApiKey(
|
||||
"alice",
|
||||
{ capabilities: ["kubernetes:write"] },
|
||||
requestReturning(key, calls),
|
||||
async <T>(path: string, init?: ApiRequestInit): Promise<T> => {
|
||||
calls.push({ path, init });
|
||||
return (init ? key : user) as T;
|
||||
},
|
||||
);
|
||||
expect(
|
||||
await revokeApiKey(
|
||||
@@ -148,6 +152,7 @@ describe("user administration commands", () => {
|
||||
);
|
||||
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"] } },
|
||||
@@ -158,6 +163,157 @@ describe("user administration commands", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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(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");
|
||||
});
|
||||
});
|
||||
|
||||
const operation = {
|
||||
|
||||
Reference in New Issue
Block a user