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((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); }); });