/** * @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ĩ */ 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>(); 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( ); 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(); expect(screen.getByText("No quota")).toBeInTheDocument(); rerender(); 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(); 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( {}} /> ); 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( {}} 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( {}} 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") ); }); });