/** * @file UserEditDialog.test.tsx * @description The admin's account editor: Save stays off until something * changed AND the admin typed their own password; one PATCH carries only the * changed fields (profile, role, disable, workspace add/remove, sign-out); an * admin editing their own row cannot touch role or state; Generate fills a * 16-character password. * @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(() => ({ get: vi.fn(), update: vi.fn() })); const lanes = vi.hoisted(() => ({ list: vi.fn() })); vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { users, lanes } }; }); import { UserEditDialog, generatePassword } from "../UserEditDialog"; const usage = { cost: 0, tokens: 0 }; const mia: ServerUserUsage = { id: 2, username: "mia", role: "developer", disabled: false, displayName: null, email: null, createdAt: "2026-10-01T00:00:00.000Z", workspaces: 1, sessions: 3, last7d: usage, last30d: usage, logins: 2, lastLoginAt: null, }; function open(user = mia, self = false) { const onDone = vi.fn(); const onClose = vi.fn(); render( ); return { onDone, onClose }; } const save = () => screen.getByRole("button", { name: "Save" }); beforeEach(() => { users.get.mockReset().mockResolvedValue({ user: mia, workspaces: [{ id: 5, title: "api", cwd: "/w/api" }], }); users.update.mockReset().mockResolvedValue({ user: mia }); lanes.list.mockReset().mockResolvedValue({ lanes: [ { id: 5, title: "api", cwd: "/w/api", owner_id: 2 }, { id: 7, title: "web", cwd: "/w/web", owner_id: null }, ], }); }); describe("UserEditDialog", () => { it("needs a change and the admin's own password before Save", async () => { const ue = userEvent.setup(); open(); await screen.findByText("api"); expect(save().hasAttribute("disabled")).toBe(true); await ue.type(screen.getByLabelText("Your password"), "boss-pass"); expect(save().hasAttribute("disabled")).toBe(true); // nothing changed yet await ue.type(screen.getByLabelText("Display name"), "Mia"); expect(save().hasAttribute("disabled")).toBe(false); }); it("sends one PATCH with only what changed", async () => { const ue = userEvent.setup(); const { onDone } = open(); await screen.findByText("api"); await ue.type(screen.getByLabelText("Email"), "mia@x.vn"); await ue.selectOptions(screen.getByLabelText("Role"), "analyst"); await ue.click(screen.getByRole("switch", { name: "Active" })); await ue.click(screen.getByRole("button", { name: "Remove api" })); await ue.selectOptions(screen.getByLabelText("Add workspace"), "7"); await ue.click(screen.getByRole("button", { name: "Add workspace" })); await ue.click(screen.getByRole("checkbox", { name: "Sign out everywhere" })); await ue.type(screen.getByLabelText("Your password"), "boss-pass"); await ue.click(save()); await waitFor(() => expect(users.update).toHaveBeenCalledWith( 2, { email: "mia@x.vn", role: "analyst", disabled: true, addWorkspaces: [7], removeWorkspaces: [5], signOut: true, }, "boss-pass" ) ); expect(onDone).toHaveBeenCalledWith("mia updated"); }); it("never lets an admin change their own role or disable themselves", async () => { open({ ...mia, role: "admin" }, true); await screen.findByText("api"); expect(screen.getByLabelText("Role").hasAttribute("disabled")).toBe(true); expect(screen.getByRole("switch", { name: "Active" }).hasAttribute("disabled")).toBe(true); }); it("accepts a short new password: there is no minimum length", async () => { const ue = userEvent.setup(); open(); await screen.findByText("api"); await ue.type(screen.getByLabelText("New password"), "abc"); await ue.type(screen.getByLabelText("Your password"), "boss-pass"); expect(save().hasAttribute("disabled")).toBe(false); }); it("generates a 16-character password", async () => { expect(generatePassword()).toHaveLength(16); const ue = userEvent.setup(); open(); await screen.findByText("api"); await ue.click(screen.getByRole("button", { name: /Generate/ })); expect((screen.getByLabelText("New password") as HTMLInputElement).value).toHaveLength(16); }); });