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

256 lines
8.3 KiB
TypeScript

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<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 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<unknown> };
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]);
});
});