f1a90e120e
System Health's per-user CPU/RAM card asked UserAvatar for the viewer's own picture with no user id, and the host stats never carried avatarAt, so every card fell back to letters. The groups now carry avatarAt and the card passes userId. The user editor's header, the pool board's member chips and the pool member picker drew hand-made initials; they use UserAvatar too.
159 lines
5.8 KiB
TypeScript
159 lines
5.8 KiB
TypeScript
/**
|
|
* @file UserQuota.test.tsx
|
|
* @description Spend quotas on the Users page: a row's "spent of limit" line
|
|
* (red once used up, "(default)" when it is the default), the default quota
|
|
* card (an admin edits it with their own password, an analyst only reads),
|
|
* and the Edit user form sending a user's own quota as part of its ONE PATCH.
|
|
* @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 { ServerUserUsage } from "../../../lib/api";
|
|
|
|
const users = vi.hoisted(() => ({
|
|
setQuotaDefault: vi.fn(),
|
|
update: vi.fn(),
|
|
get: vi.fn(),
|
|
avatarUrl: () => null,
|
|
}));
|
|
const lanes = vi.hoisted(() => ({ list: vi.fn() }));
|
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
return { ...actual, api: { users, lanes } };
|
|
});
|
|
|
|
import { QuotaDefaultCard, QuotaSummary } from "../UserQuota";
|
|
import { UserEditDialog } from "../UserEditDialog";
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
users.setQuotaDefault.mockResolvedValue({ user: {}, quotaDefault: null });
|
|
users.update.mockResolvedValue({ user: {} });
|
|
users.get.mockResolvedValue({ user: {}, workspaces: [] });
|
|
lanes.list.mockResolvedValue({ lanes: [] });
|
|
});
|
|
|
|
describe("QuotaSummary", () => {
|
|
it("shows spent of the limit, red and marked default once used up", () => {
|
|
render(
|
|
<QuotaSummary
|
|
quota={{
|
|
limit_usd: 10,
|
|
period: "day",
|
|
source: "default",
|
|
spend: 12.5,
|
|
pct: 125,
|
|
exceeded: true,
|
|
resetsAt: "2026-10-29T17:00:00.000Z",
|
|
since: "2026-10-28T17:00:00.000Z",
|
|
}}
|
|
/>
|
|
);
|
|
const line = screen.getByText(/of \$10\.00 \/ day/);
|
|
expect(line.textContent).toContain("$12.50");
|
|
expect(line.textContent).toContain("(default)");
|
|
expect(line.className).toContain("text-status-danger");
|
|
expect(screen.getByRole("meter")).toHaveAttribute("aria-valuenow", "100");
|
|
});
|
|
|
|
it("says No quota without one, and nothing for an older server", () => {
|
|
const { container, rerender } = render(<QuotaSummary quota={null} />);
|
|
expect(screen.getByText("No quota")).toBeInTheDocument();
|
|
rerender(<QuotaSummary quota={undefined} />);
|
|
expect(container.textContent).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("QuotaDefaultCard", () => {
|
|
it("an admin sets the default with their own password", async () => {
|
|
const user = userEvent.setup();
|
|
const onSaved = vi.fn();
|
|
render(<QuotaDefaultCard quotaDefault={null} isAdmin onSaved={onSaved} />);
|
|
expect(screen.getByTestId("quota-default-value").textContent).toBe("No default — no limit");
|
|
await user.click(screen.getByRole("button", { name: /Edit/ }));
|
|
await user.click(screen.getByRole("checkbox", { name: "Limit what each user may spend" }));
|
|
await user.type(screen.getByLabelText("Limit in dollars"), "25");
|
|
await user.selectOptions(screen.getByLabelText("Period"), "5h");
|
|
const save = screen.getByRole("button", { name: "Save" });
|
|
expect(save).toBeDisabled(); // no password yet
|
|
await user.type(screen.getByLabelText("Your password"), "pw");
|
|
await user.click(save);
|
|
await waitFor(() => expect(onSaved).toHaveBeenCalled());
|
|
expect(users.setQuotaDefault).toHaveBeenCalledWith(
|
|
expect.objectContaining({ limit_usd: 25, period: "5h" }),
|
|
"pw"
|
|
);
|
|
});
|
|
|
|
it("an analyst only reads it", () => {
|
|
render(
|
|
<QuotaDefaultCard
|
|
quotaDefault={{ limit_usd: 5, period: "day" }}
|
|
isAdmin={false}
|
|
onSaved={() => {}}
|
|
/>
|
|
);
|
|
expect(screen.getByTestId("quota-default-value").textContent).toBe("$5.00 / day");
|
|
expect(screen.queryByRole("button", { name: /Edit/ })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("UserEditDialog quota", () => {
|
|
const mia: ServerUserUsage = {
|
|
id: 7,
|
|
username: "mia",
|
|
role: "developer",
|
|
disabled: false,
|
|
workspaces: 0,
|
|
sessions: 0,
|
|
last7d: { cost: 0, tokens: 0 },
|
|
last30d: { cost: 0, tokens: 0 },
|
|
quotaSetting: { mode: "default" },
|
|
quota: null,
|
|
} as ServerUserUsage;
|
|
|
|
it("sends the user's own limit in the one PATCH", async () => {
|
|
const user = userEvent.setup();
|
|
const onDone = vi.fn();
|
|
render(
|
|
<UserEditDialog
|
|
user={mia}
|
|
self={false}
|
|
users={[mia]}
|
|
quotaDefault={{ limit_usd: 10, period: "day" }}
|
|
onClose={() => {}}
|
|
onDone={onDone}
|
|
/>
|
|
);
|
|
const select = screen.getByLabelText("Spend quota");
|
|
expect(screen.getByRole("option", { name: "Default ($10.00 / day)" })).toBeInTheDocument();
|
|
await user.selectOptions(select, "custom");
|
|
await user.type(screen.getByLabelText("Limit in dollars"), "40");
|
|
await user.type(screen.getByLabelText("Your password"), "pw");
|
|
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
await waitFor(() => expect(onDone).toHaveBeenCalled());
|
|
expect(users.update).toHaveBeenCalledWith(
|
|
7,
|
|
{ quota: expect.objectContaining({ mode: "custom", limit_usd: 40, period: "day" }) },
|
|
"pw"
|
|
);
|
|
});
|
|
|
|
it("No limit is a change; an empty custom limit keeps Save off", async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<UserEditDialog user={mia} self={false} users={[mia]} onClose={() => {}} onDone={() => {}} />
|
|
);
|
|
await user.type(screen.getByLabelText("Your password"), "pw");
|
|
await user.selectOptions(screen.getByLabelText("Spend quota"), "custom");
|
|
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
|
|
await user.selectOptions(screen.getByLabelText("Spend quota"), "unlimited");
|
|
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
await waitFor(() =>
|
|
expect(users.update).toHaveBeenCalledWith(7, { quota: { mode: "unlimited" } }, "pw")
|
|
);
|
|
});
|
|
});
|