import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { AdminUserManagePanel } from "../../sidebar/AdminUserManagePanel"; import type { AdminUser } from "../../sidebar/AdminManagementSections"; const api = vi.hoisted(() => ({ adminGetUserHosts: vi.fn(async () => [] as unknown[]), adminDeleteUserHost: vi.fn(async () => ({})), adminGetUserCredentials: vi.fn(async () => [] as unknown[]), adminDeleteUserCredential: vi.fn(async () => ({})), adminGetUserSnippets: vi.fn(async () => [] as unknown[]), adminCreateUserSnippet: vi.fn(async () => ({})), adminUpdateUserSnippet: vi.fn(async () => ({})), adminDeleteUserSnippet: vi.fn(async () => ({})), adminResetUserPassword: vi.fn(async () => ({ dataWiped: false })), adminDisableUserTotp: vi.fn(async () => ({})), adminExportUserData: vi.fn(async () => ({})), getSessions: vi.fn(async () => ({ sessions: [] as unknown[] })), revokeSession: vi.fn(async () => ({})), revokeAllUserSessions: vi.fn(async () => ({})), getApiKeys: vi.fn(async () => ({ apiKeys: [] as unknown[] })), createApiKey: vi.fn(async () => ({})), deleteApiKey: vi.fn(async () => ({})), deleteUser: vi.fn(async () => ({})), getUserRoles: vi.fn(async () => ({ roles: [] as unknown[] })), assignRoleToUser: vi.fn(async () => ({})), removeRoleFromUser: vi.fn(async () => ({})), })); vi.mock("@/main-axios", () => api); vi.mock("../../sidebar/HostEditor", () => ({ HostEditor: () =>
, })); vi.mock("../../sidebar/CredentialEditorView", () => ({ CredentialEditorView: () =>
, })); vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }), })); vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() }, })); function makeUser(overrides: Partial = {}): AdminUser { return { id: "u2", username: "bob", isAdmin: false, isOidc: false, passwordHash: "hash", dataUnlocked: true, totpEnabled: false, ...overrides, }; } function renderPanel( user: AdminUser, callbacks: Partial void>> = {}, ) { return render( , ); } beforeEach(() => { for (const fn of Object.values(api)) fn.mockClear(); }); describe("AdminUserManagePanel", () => { it("renders all manage tabs and loads the target user's data", async () => { renderPanel(makeUser()); for (const tab of [ "admin.manageTabAccount", "admin.manageTabHosts", "admin.manageTabCredentials", "admin.manageTabSnippets", "admin.manageTabSessions", "admin.manageTabDanger", ]) { expect(screen.getByText(tab)).toBeTruthy(); } await waitFor(() => { expect(api.adminGetUserHosts).toHaveBeenCalledWith("u2"); expect(api.adminGetUserCredentials).toHaveBeenCalledWith("u2"); expect(api.adminGetUserSnippets).toHaveBeenCalledWith("u2"); expect(api.getUserRoles).toHaveBeenCalledWith("u2"); }); }); it("skips data fetches and shows the locked notice when data is locked", async () => { renderPanel(makeUser({ dataUnlocked: false })); expect(screen.getByText("admin.dataLockedBadge")).toBeTruthy(); await userEvent.click(screen.getByText("admin.manageTabHosts")); expect(screen.getByText("admin.dataLockedNotice")).toBeTruthy(); expect(screen.queryByText("admin.addHostForUser")).toBeNull(); expect(api.adminGetUserHosts).not.toHaveBeenCalled(); expect(api.adminGetUserCredentials).not.toHaveBeenCalled(); expect(api.adminGetUserSnippets).not.toHaveBeenCalled(); }); it("lists the user's hosts and opens a tab via the connect button", async () => { api.adminGetUserHosts.mockResolvedValueOnce([ { id: 7, name: "web-01", username: "root", ip: "10.0.0.5", port: 22, authType: "password", connectionType: "ssh", }, ]); const onOpenHostTab = vi.fn(); renderPanel(makeUser(), { onOpenHostTab }); await userEvent.click(screen.getByText("admin.manageTabHosts")); await screen.findByText("web-01"); await userEvent.click(screen.getByTitle("admin.connectToHost")); expect(onOpenHostTab).toHaveBeenCalledTimes(1); expect(onOpenHostTab.mock.calls[0][0]).toMatchObject({ id: "7", ip: "10.0.0.5", }); }); it("disables TOTP through the confirm dialog", async () => { const onTotpDisabled = vi.fn(); renderPanel(makeUser({ totpEnabled: true }), { onTotpDisabled }); expect(screen.getByText("admin.totpStatusEnabled")).toBeTruthy(); await userEvent.click(screen.getByText("admin.disableTotp")); await userEvent.click(screen.getByText("common.confirm")); await waitFor(() => { expect(api.adminDisableUserTotp).toHaveBeenCalledWith("u2"); expect(onTotpDisabled).toHaveBeenCalled(); }); expect(screen.getByText("admin.totpStatusDisabled")).toBeTruthy(); }); it("creates a snippet for the target user", async () => { renderPanel(makeUser()); await userEvent.click(screen.getByText("admin.manageTabSnippets")); await userEvent.click(screen.getByText("admin.addSnippetForUser")); await userEvent.type( screen.getByPlaceholderText("admin.snippetNamePlaceholder"), "restart", ); await userEvent.type( screen.getByPlaceholderText("admin.snippetContentPlaceholder"), "systemctl restart nginx", ); await userEvent.click(screen.getByText("common.save")); await waitFor(() => { expect(api.adminCreateUserSnippet).toHaveBeenCalledWith("u2", { name: "restart", content: "systemctl restart nginx", folder: null, }); }); }); it("deletes the user from the danger tab after confirmation", async () => { const onUserDeleted = vi.fn(); renderPanel(makeUser(), { onUserDeleted }); await userEvent.click(screen.getByText("admin.manageTabDanger")); await userEvent.click(screen.getByText("admin.deleteUser")); await userEvent.click(screen.getByText("common.confirm")); await waitFor(() => { expect(api.deleteUser).toHaveBeenCalledWith("bob"); expect(onUserDeleted).toHaveBeenCalled(); }); }); it("blocks deleting admin accounts", async () => { renderPanel(makeUser({ isAdmin: true })); await userEvent.click(screen.getByText("admin.manageTabDanger")); const deleteBtn = screen .getByText("admin.deleteUser") .closest("button") as HTMLButtonElement; expect(deleteBtn.disabled).toBe(true); expect(screen.getByText("admin.deleteUserAdminBlocked")).toBeTruthy(); }); });