import { describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; import { spawnSync } from "node:child_process"; 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 Promise.all([prompt.cancel(), prompt.cancel()]); expect(await rejected).toMatchObject({ message: "Login cancelled" }); expect(calls).toBe(0); expect(prompt.values.password).toBe(""); }); test("preserves non-cancellation prompt initialization errors", async () => { const error = new Error("terminal initialization failed"); const prompt = createLoginPrompt("", false, async () => session, { show: false, }); const enquirerPrototype = Object.getPrototypeOf( Object.getPrototypeOf(prompt), ) as { run: () => Promise }; const originalRun = enquirerPrototype.run; enquirerPrototype.run = () => Promise.reject(error); try { await expect(prompt.run()).rejects.toBe(error); } finally { enquirerPrototype.run = originalRun; } }); test.skipIf(!Bun.which("python3"))( "Ctrl+C in a real terminal cancels cleanly without authenticating", () => { const script = `import os, pty, select, subprocess, sys, time master, slave = pty.openpty() code = 'import { createLoginPrompt } from "./command/auth.ts"; const p = createLoginPrompt("alice", true, async () => { throw Error("API must not be called") }); p.run().catch(error => console.error(error.message))' child = subprocess.Popen([sys.argv[1], "--eval", code], stdin=slave, stdout=slave, stderr=slave) os.close(slave) time.sleep(0.6) os.write(master, b"\\x03") output = b"" deadline = time.time() + 5 while time.time() < deadline and child.poll() is None: ready, _, _ = select.select([master], [], [], 0.1) if ready: try: output += os.read(master, 4096) except OSError: break if child.poll() is None: child.kill() child.wait() sys.stdout.buffer.write(output) sys.exit(0 if b"Login cancelled" in output and b"ERR_USE_AFTER_CLOSE" not in output and b"API must not be called" not in output else 1)`; const result = spawnSync("python3", ["-c", script, process.execPath], { cwd: process.cwd(), encoding: "utf8", timeout: 10_000, }); expect(result.error).toBeUndefined(); expect(result.status).toBe(0); expect(result.stdout).toContain("Login cancelled"); expect(result.stdout).not.toContain("ERR_USE_AFTER_CLOSE"); expect(result.stdout).not.toContain("API must not be called"); }, ); 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); }); test("interactive login persists by default and accepts temporary-session override", async () => { const persistence: boolean[] = []; for (const override of [undefined, false] as const) { await interactiveLogin("alice", override, { isTTY: true, prompt: (_username, persistent) => { persistence.push(persistent ?? true); const prompt = createLoginPrompt( "alice", persistent ?? true, async () => session, { show: false }, ); (prompt as unknown as EventEmitter).once("run", () => { void prompt.submit(); }); return prompt; }, }); } expect(persistence).toEqual([true, false]); }); });