c042adc51b
Each account row in the Account pool card has a pencil: tick the users whose sessions run on it (one moves off any other account, unticking sends one back to the balancer). PUT /api/accounts/:name/users saves the set in one transaction, audits each change and switches a Claude login into each added user's home at once. The card-level switch button and pinned list are gone.
210 lines
7.8 KiB
TypeScript
210 lines
7.8 KiB
TypeScript
/**
|
|
* @file AccountPoolCard.test.tsx
|
|
* @description The Users page's account pool card: lists the Claude and Codex
|
|
* accounts with their live panes and quota, says what an empty pool means, an
|
|
* admin adds an account only with their own password typed (the token goes
|
|
* in once and is not shown back) — a saved login moved in (the server's own
|
|
* included), or a setup-token — and removes one the same way, and an analyst
|
|
* sees no add or remove control. An account's Edit ticks the users who run on
|
|
* it, saved with the admin's own password; the users show on the account.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import type { AccountPool } from "../../../lib/api";
|
|
|
|
const accountPool = vi.hoisted(() => ({
|
|
list: vi.fn(),
|
|
add: vi.fn(),
|
|
remove: vi.fn(),
|
|
profiles: vi.fn(),
|
|
setUsers: vi.fn(),
|
|
}));
|
|
const users = vi.hoisted(() => ({ list: vi.fn() }));
|
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
return { ...actual, api: { accountPool, users } };
|
|
});
|
|
|
|
import { AccountPoolCard } from "../AccountPoolCard";
|
|
|
|
const POOL: AccountPool = {
|
|
accounts: [
|
|
{
|
|
name: "team-1",
|
|
kind: "login",
|
|
email: "team1@example.com",
|
|
refreshError: null,
|
|
addedAt: "2026-10-01T00:00:00Z",
|
|
expiresAt: null,
|
|
expired: false,
|
|
livePanes: 2,
|
|
spend5h: 1.5,
|
|
spend7d: 12,
|
|
tokens5h: 250_000,
|
|
tokens7d: 3_400_000,
|
|
quotaPercent: 42,
|
|
full: false,
|
|
},
|
|
],
|
|
codex: [
|
|
{
|
|
name: "cx-1",
|
|
kind: "apikey",
|
|
addedAt: "2026-10-02T00:00:00Z",
|
|
livePanes: 0,
|
|
spend5h: 0,
|
|
spend7d: 0,
|
|
tokens5h: 0,
|
|
tokens7d: 0,
|
|
quotaPercent: null,
|
|
full: false,
|
|
},
|
|
],
|
|
blockPercent: 90,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
for (const fn of Object.values(accountPool)) fn.mockReset();
|
|
accountPool.list.mockResolvedValue(POOL);
|
|
users.list.mockResolvedValue({
|
|
users: [
|
|
{ id: 7, username: "mia", disabled: false },
|
|
{ id: 8, username: "leo", disabled: false },
|
|
],
|
|
});
|
|
accountPool.profiles.mockResolvedValue({
|
|
profiles: [
|
|
{
|
|
name: "work",
|
|
label: "Work",
|
|
email: "w@example.com",
|
|
subscriptionType: "max",
|
|
active: false,
|
|
},
|
|
{ name: "me", label: "Me", email: "me@example.com", subscriptionType: "pro", active: true },
|
|
],
|
|
});
|
|
});
|
|
|
|
describe("AccountPoolCard", () => {
|
|
it("lists every account, and explains an empty pool", async () => {
|
|
const { unmount } = render(<AccountPoolCard isAdmin={false} />);
|
|
expect(await screen.findByText("team-1")).toBeInTheDocument();
|
|
expect(screen.getByText("cx-1")).toBeInTheDocument();
|
|
expect(screen.getByText("42%")).toBeInTheDocument();
|
|
expect(screen.getByText("250.0K tokens")).toBeInTheDocument();
|
|
expect(screen.getByText("$12.00 · 3.4M tokens in 7 days")).toBeInTheDocument();
|
|
expect(screen.getByText("team1@example.com")).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: /add account/i })).toBeNull();
|
|
expect(screen.queryByRole("button", { name: /remove/i })).toBeNull();
|
|
unmount();
|
|
accountPool.list.mockResolvedValue({ accounts: [], codex: [], blockPercent: 90 });
|
|
render(<AccountPoolCard isAdmin />);
|
|
expect(await screen.findByText(/no accounts yet/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("moves a saved login in, the server's own included", async () => {
|
|
accountPool.add.mockResolvedValue({ account: { name: "work" } });
|
|
const user = userEvent.setup();
|
|
render(<AccountPoolCard isAdmin />);
|
|
await screen.findByText("team-1");
|
|
await user.click(screen.getByRole("button", { name: /add account/i }));
|
|
const dialog = await screen.findByRole("dialog");
|
|
const select = await screen.findByLabelText("Profile");
|
|
await waitFor(() => expect(screen.getByRole("option", { name: /Work/ })).toBeInTheDocument());
|
|
expect(screen.getByRole("option", { name: /Me/ })).toBeEnabled();
|
|
await user.selectOptions(select, "work");
|
|
const confirm = Array.from(dialog.querySelectorAll("button")).find(
|
|
(b) => b.textContent === "Add account"
|
|
)!;
|
|
expect(confirm).toBeDisabled();
|
|
await user.type(screen.getByLabelText("Your password"), "boss-pass");
|
|
await user.click(confirm);
|
|
await waitFor(() =>
|
|
expect(accountPool.add).toHaveBeenCalledWith(
|
|
{ tool: "claude", name: "work", profile: "work" },
|
|
"boss-pass"
|
|
)
|
|
);
|
|
});
|
|
|
|
it("adds a setup-token only with the admin's own password", async () => {
|
|
accountPool.add.mockResolvedValue({ account: { name: "team-2" } });
|
|
const user = userEvent.setup();
|
|
render(<AccountPoolCard isAdmin />);
|
|
await screen.findByText("team-1");
|
|
await user.click(screen.getByRole("button", { name: /add account/i }));
|
|
const dialog = await screen.findByRole("dialog");
|
|
await user.selectOptions(screen.getByLabelText("From"), "token");
|
|
await user.type(screen.getByLabelText("Name"), "team-2");
|
|
await user.type(screen.getByLabelText("Token"), "sk-ant-oat01-xyz");
|
|
const confirm = Array.from(dialog.querySelectorAll("button")).find(
|
|
(b) => b.textContent === "Add account"
|
|
)!;
|
|
expect(confirm).toBeDisabled();
|
|
await user.type(screen.getByLabelText("Your password"), "boss-pass");
|
|
expect(confirm).toBeEnabled();
|
|
await user.click(confirm);
|
|
await waitFor(() =>
|
|
expect(accountPool.add).toHaveBeenCalledWith(
|
|
{ tool: "claude", name: "team-2", token: "sk-ant-oat01-xyz" },
|
|
"boss-pass"
|
|
)
|
|
);
|
|
});
|
|
|
|
it("removes an account with the admin's own password", async () => {
|
|
accountPool.remove.mockResolvedValue({ ok: true });
|
|
const user = userEvent.setup();
|
|
render(<AccountPoolCard isAdmin />);
|
|
await user.click(await screen.findByRole("button", { name: "Remove cx-1" }));
|
|
await user.type(await screen.findByLabelText("Your password"), "boss-pass");
|
|
await user.click(screen.getByRole("button", { name: "Remove" }));
|
|
await waitFor(() =>
|
|
expect(accountPool.remove).toHaveBeenCalledWith("codex", "cx-1", "boss-pass")
|
|
);
|
|
});
|
|
|
|
it("sets who runs on an account from its Edit, with the admin's own password", async () => {
|
|
accountPool.list.mockResolvedValue({
|
|
...POOL,
|
|
pins: {
|
|
claude: [
|
|
{ userId: 8, username: "leo", account: "team-1" },
|
|
{ userId: 7, username: "mia", account: "team-0" },
|
|
],
|
|
codex: [],
|
|
},
|
|
});
|
|
accountPool.setUsers.mockResolvedValue({
|
|
added: [{ userId: 7, username: "mia", switched: "switched" }],
|
|
removed: [{ userId: 8, username: "leo" }],
|
|
});
|
|
const user = userEvent.setup();
|
|
render(<AccountPoolCard isAdmin />);
|
|
expect(await screen.findByText("Used by: leo")).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: /switch a user/i })).toBeNull();
|
|
await user.click(screen.getByRole("button", { name: "Edit team-1" }));
|
|
const dialog = await screen.findByRole("dialog");
|
|
const leo = await screen.findByRole("checkbox", { name: /leo/ });
|
|
const mia = screen.getByRole("checkbox", { name: /mia/ });
|
|
expect(leo).toHaveAttribute("aria-checked", "true");
|
|
expect(mia).toHaveAttribute("aria-checked", "false");
|
|
expect(screen.getByText("now on team-0")).toBeInTheDocument();
|
|
await user.click(mia);
|
|
await user.click(leo);
|
|
const save = Array.from(dialog.querySelectorAll("button")).find(
|
|
(b) => b.textContent === "Save"
|
|
)!;
|
|
expect(save).toBeDisabled();
|
|
await user.type(screen.getByLabelText("Your password"), "boss-pass");
|
|
await user.click(save);
|
|
await waitFor(() =>
|
|
expect(accountPool.setUsers).toHaveBeenCalledWith("claude", "team-1", [7], "boss-pass")
|
|
);
|
|
});
|
|
});
|