feat: release 2.7.0-rc3
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user