Files
kuber/tests/command/auth.test.ts
T
2026-10-07 05:10:59 +00:00

181 lines
5.4 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { EventEmitter } from "node:events";
import { Writable } from "node:stream";
import {
createLoginPrompt,
interactiveLogin,
type LoginPrompt,
} from "../../command/auth";
import { KuberApiError } from "../../lib/api";
import type { KuberSession } from "../../lib/session";
const session: KuberSession = {
token: "test-token",
expiresAt: "2099-01-01T00:00:00Z",
user: { username: "alice", roles: ["user"] },
};
async function start(prompt: LoginPrompt) {
const ready = new Promise<void>((resolve) => {
(prompt as unknown as EventEmitter).once("run", resolve);
});
const result = prompt.run();
await ready;
return { result };
}
async function credentials(
prompt: LoginPrompt,
username: string,
password: string,
) {
for (const choice of prompt.choices) {
choice.input = choice.value =
choice.name === "username" ? username : password;
}
await prompt.render();
}
describe("BasicAuth login", () => {
test("authenticates once against the supplied API and preserves persistence", async () => {
const calls: unknown[][] = [];
const prompt = createLoginPrompt(
"alice",
true,
async (...args) => {
calls.push(args);
return session;
},
{ show: false },
);
const { result } = await start(prompt);
expect(prompt.values.username).toBe("alice");
await credentials(prompt, " alice ", "sensitive-password");
await Promise.all([prompt.submit(), prompt.submit()]);
expect(await result).toEqual(session);
expect(calls).toEqual([["alice", "sensitive-password", true]]);
expect(prompt.values.password).toBe("");
});
test("incorrect authentication closes the prompt and retains the CLI API error", async () => {
const error = new KuberApiError("Incorrect credentials", 401);
const prompt = createLoginPrompt(
"",
false,
async () => {
throw error;
},
{ show: false },
);
const { result } = await start(prompt);
const rejected = result.catch((error: unknown) => error);
await credentials(prompt, "alice", "secret");
await prompt.submit();
expect(await rejected).toBe(error);
expect(prompt.state.closed).toBe(true);
expect(prompt.values.password).toBe("");
});
test("cancellation rejects without calling authentication", async () => {
let calls = 0;
const prompt = createLoginPrompt(
"",
false,
async () => {
calls++;
return session;
},
{ show: false },
);
const { result } = await start(prompt);
const rejected = result.catch((error: unknown) => error);
await credentials(prompt, "alice", "secret");
await prompt.cancel();
expect(await rejected).toMatchObject({ message: "Login cancelled" });
expect(calls).toBe(0);
expect(prompt.values.password).toBe("");
});
test.each(["submit", "cancel"] as const)(
"masks password while editing and never prints it on %s",
async (action) => {
let output = "";
const stdout = new Writable({
write(chunk, _encoding, callback) {
output += chunk.toString();
callback();
},
}) as unknown as NodeJS.WriteStream;
const prompt = createLoginPrompt("", false, async () => session, {
show: false,
stdout,
});
const { result } = await start(prompt);
const settled = result.catch(() => undefined);
await credentials(prompt, "alice", "never-print-this");
// show:false disables terminal listeners; enable writes only after initialization.
(prompt as unknown as { state: { show: boolean } }).state.show = true;
await prompt.render();
expect(output).toContain("password");
expect(output).toContain("*".repeat("never-print-this".length));
expect(output).not.toContain("never-print-this");
await prompt[action]();
await settled;
expect(output).not.toContain("never-print-this");
},
);
test("empty username fails before contacting authentication", async () => {
let calls = 0;
const prompt = createLoginPrompt(
"",
false,
async () => {
calls++;
return session;
},
{ show: false },
);
const { result } = await start(prompt);
const rejected = result.catch((error: unknown) => error);
await prompt.submit();
expect(await rejected).toMatchObject({ message: "Username is required" });
expect(calls).toBe(0);
});
test("noninteractive onboarding and login fail before creating a prompt", async () => {
let calls = 0;
await expect(
interactiveLogin("alice", true, {
isTTY: false,
prompt: () => {
calls++;
throw new Error("Unexpected prompt");
},
}),
).rejects.toThrow("Login requires an interactive terminal");
expect(calls).toBe(0);
});
test("onboarding returns the session using the shared prompt", async () => {
const result = await interactiveLogin(" alice ", true, {
isTTY: true,
prompt: (username, persistent) => {
expect(username).toBe("alice");
expect(persistent).toBe(true);
const prompt = createLoginPrompt(
username,
persistent,
async () => session,
{ show: false },
);
(prompt as unknown as EventEmitter).once("run", () => {
void prompt.submit();
});
return prompt;
},
});
expect(result).toEqual(session);
});
});