362 lines
15 KiB
JavaScript
362 lines
15 KiB
JavaScript
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
abilities: { canCreate: true, canManagePermissions: true, canResetPassword: true, canStatus: true, canView: true },
|
|
appCode: "fami",
|
|
catalogError: "",
|
|
catalogLoading: false,
|
|
catalogPage: null,
|
|
catalogQueryOptions: null,
|
|
confirm: vi.fn(),
|
|
createExternalAdminUser: vi.fn(),
|
|
getExternalAdminUserPermissions: vi.fn(),
|
|
invalidateQueries: vi.fn(),
|
|
listExternalAdminUsers: vi.fn(),
|
|
listExternalAdminPermissionCatalog: vi.fn(),
|
|
lookupExternalAdminUserTarget: vi.fn(),
|
|
queryOptions: null,
|
|
queryPage: { items: [], page: 1, pageSize: 50, total: 0 },
|
|
reload: vi.fn(),
|
|
resetExternalAdminUserPassword: vi.fn(),
|
|
showToast: vi.fn(),
|
|
updateExternalAdminUserStatus: vi.fn(),
|
|
updateExternalAdminUserPermissions: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@tanstack/react-query", () => ({
|
|
useQueryClient: () => ({ invalidateQueries: mocks.invalidateQueries }),
|
|
}));
|
|
|
|
vi.mock("@/app/app-scope/AppScopeProvider.jsx", () => ({
|
|
useAppScope: () => ({ appCode: mocks.appCode }),
|
|
}));
|
|
|
|
vi.mock("@/features/external-admin-users/api", () => ({
|
|
createExternalAdminUser: mocks.createExternalAdminUser,
|
|
getExternalAdminUserPermissions: mocks.getExternalAdminUserPermissions,
|
|
listExternalAdminUsers: mocks.listExternalAdminUsers,
|
|
listExternalAdminPermissionCatalog: mocks.listExternalAdminPermissionCatalog,
|
|
lookupExternalAdminUserTarget: mocks.lookupExternalAdminUserTarget,
|
|
resetExternalAdminUserPassword: mocks.resetExternalAdminUserPassword,
|
|
updateExternalAdminUserStatus: mocks.updateExternalAdminUserStatus,
|
|
updateExternalAdminUserPermissions: mocks.updateExternalAdminUserPermissions,
|
|
}));
|
|
|
|
vi.mock("@/features/external-admin-users/permissions.js", () => ({
|
|
useExternalAdminUserAbilities: () => mocks.abilities,
|
|
}));
|
|
|
|
vi.mock("@/shared/hooks/useAdminQuery.js", () => ({
|
|
useAdminQuery: (_queryFn, options) => {
|
|
mocks.queryOptions = options;
|
|
if (options.queryKey[0] === "external-admin-permission-catalog") {
|
|
mocks.catalogQueryOptions = options;
|
|
if (options.enabled) {
|
|
void _queryFn();
|
|
}
|
|
return {
|
|
data: mocks.catalogPage,
|
|
error: mocks.catalogError,
|
|
loading: mocks.catalogLoading,
|
|
reload: mocks.reload,
|
|
};
|
|
}
|
|
return { data: mocks.queryPage, error: "", loading: false, reload: mocks.reload };
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/shared/ui/ConfirmProvider.jsx", () => ({
|
|
useConfirm: () => mocks.confirm,
|
|
}));
|
|
|
|
vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
|
|
useToast: () => ({ showToast: mocks.showToast }),
|
|
}));
|
|
|
|
import { useExternalAdminUsersPage } from "./useExternalAdminUsersPage.js";
|
|
|
|
beforeEach(() => {
|
|
mocks.abilities = { canCreate: true, canManagePermissions: true, canResetPassword: true, canStatus: true, canView: true };
|
|
mocks.appCode = "fami";
|
|
mocks.catalogError = "";
|
|
mocks.catalogLoading = false;
|
|
mocks.catalogPage = permissionCatalogFixture();
|
|
mocks.queryPage = { items: [], page: 1, pageSize: 50, total: 0 };
|
|
mocks.confirm.mockResolvedValue(true);
|
|
mocks.createExternalAdminUser.mockResolvedValue(externalUserFixture());
|
|
mocks.listExternalAdminPermissionCatalog.mockResolvedValue(permissionCatalogFixture());
|
|
mocks.getExternalAdminUserPermissions.mockResolvedValue({
|
|
accountId: "71",
|
|
permissions: ["user:list"],
|
|
revision: 4,
|
|
});
|
|
mocks.invalidateQueries.mockResolvedValue(undefined);
|
|
mocks.lookupExternalAdminUserTarget.mockResolvedValue(targetFixture());
|
|
mocks.reload.mockResolvedValue(undefined);
|
|
mocks.resetExternalAdminUserPassword.mockResolvedValue(externalUserFixture());
|
|
mocks.updateExternalAdminUserStatus.mockResolvedValue({ ...externalUserFixture(), status: "disabled" });
|
|
mocks.updateExternalAdminUserPermissions.mockResolvedValue(externalUserFixture());
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
test("creates an App-scoped account with the canonical userId returned by lookup", async () => {
|
|
const { result } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
act(() => result.current.openCreate());
|
|
act(() => result.current.setDisplayUserId("VIP-1001"));
|
|
await act(async () => result.current.lookupTarget());
|
|
act(() =>
|
|
result.current.setCreateForm({
|
|
confirmPassword: "StrongPass123!",
|
|
password: "StrongPass123!",
|
|
username: "ops.fami",
|
|
}),
|
|
);
|
|
await act(async () => result.current.submitCreate({ preventDefault: vi.fn() }));
|
|
|
|
expect(mocks.lookupExternalAdminUserTarget).toHaveBeenCalledWith("fami", "VIP-1001");
|
|
expect(mocks.createExternalAdminUser).toHaveBeenCalledWith("fami", {
|
|
password: "StrongPass123!",
|
|
permissions: ["user:list", "user:update"],
|
|
targetUserId: "internal-user-1001",
|
|
username: "ops.fami",
|
|
});
|
|
expect(mocks.invalidateQueries).toHaveBeenCalledWith({ queryKey: ["external-admin-users", "fami"] });
|
|
expect(result.current.createOpen).toBe(false);
|
|
});
|
|
|
|
test("creates an explicit zero-permission account after the operator clears defaults", async () => {
|
|
const { result } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
act(() => result.current.openCreate());
|
|
act(() => result.current.setCreateForm({ permissions: [] }));
|
|
act(() => result.current.setDisplayUserId("VIP-1001"));
|
|
await act(async () => result.current.lookupTarget());
|
|
act(() =>
|
|
result.current.setCreateForm({
|
|
confirmPassword: "StrongPass123!",
|
|
password: "StrongPass123!",
|
|
username: "ops.zero",
|
|
}),
|
|
);
|
|
await act(async () => result.current.submitCreate({ preventDefault: vi.fn() }));
|
|
|
|
expect(mocks.createExternalAdminUser).toHaveBeenCalledWith("fami", {
|
|
password: "StrongPass123!",
|
|
permissions: [],
|
|
targetUserId: "internal-user-1001",
|
|
username: "ops.zero",
|
|
});
|
|
});
|
|
|
|
test("does not request the permission catalog without the configure-permissions grant", () => {
|
|
mocks.abilities = { ...mocks.abilities, canCreate: false, canManagePermissions: false };
|
|
|
|
renderHook(() => useExternalAdminUsersPage());
|
|
|
|
expect(mocks.catalogQueryOptions.enabled).toBe(false);
|
|
expect(mocks.listExternalAdminPermissionCatalog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("treats an empty successful catalog as unavailable and blocks create and update writes", async () => {
|
|
mocks.catalogPage = { items: [], total: 0 };
|
|
const item = externalUserFixture();
|
|
const { result } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
expect(result.current.catalogError).toBe("外管权限目录为空,请联系管理员");
|
|
act(() => result.current.openCreate());
|
|
act(() => result.current.setDisplayUserId("VIP-1001"));
|
|
await act(async () => result.current.lookupTarget());
|
|
act(() =>
|
|
result.current.setCreateForm({
|
|
confirmPassword: "StrongPass123!",
|
|
password: "StrongPass123!",
|
|
username: "ops.empty",
|
|
}),
|
|
);
|
|
await act(async () => result.current.submitCreate({ preventDefault: vi.fn() }));
|
|
expect(mocks.createExternalAdminUser).not.toHaveBeenCalled();
|
|
|
|
act(() => result.current.closeCreate());
|
|
act(() => result.current.openPermissions(item));
|
|
await waitFor(() => expect(result.current.permissionLoading).toBe(false));
|
|
await act(async () => result.current.submitPermissions());
|
|
|
|
expect(mocks.updateExternalAdminUserPermissions).not.toHaveBeenCalled();
|
|
expect(mocks.showToast).toHaveBeenCalledWith("外管权限目录为空,请联系管理员", "error");
|
|
});
|
|
|
|
test("clears list filters, lookup results and password form when App scope changes", async () => {
|
|
const { result, rerender } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
act(() => result.current.setKeyword("old account"));
|
|
act(() => result.current.setStatus("active"));
|
|
act(() => result.current.openCreate());
|
|
act(() => result.current.setDisplayUserId("VIP-1001"));
|
|
await act(async () => result.current.lookupTarget());
|
|
act(() => result.current.openPassword(externalUserFixture()));
|
|
act(() => result.current.setPasswordForm({ confirmPassword: "OldPass123!", password: "OldPass123!" }));
|
|
|
|
mocks.appCode = "lalu";
|
|
rerender();
|
|
|
|
await waitFor(() => expect(result.current.keyword).toBe(""));
|
|
expect(result.current.status).toBe("");
|
|
expect(result.current.createOpen).toBe(false);
|
|
expect(result.current.createForm).toEqual({ confirmPassword: "", displayUserId: "", password: "", permissions: [], username: "" });
|
|
expect(result.current.targetUser).toBeNull();
|
|
expect(result.current.passwordUser).toBeNull();
|
|
expect(result.current.passwordForm).toEqual({ confirmPassword: "", password: "" });
|
|
expect(mocks.queryOptions.queryKey[1]).toBe("lalu");
|
|
});
|
|
|
|
test("loads and replaces an account permission snapshot, including explicit zero permissions", async () => {
|
|
const item = externalUserFixture();
|
|
const { result } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
act(() => result.current.openPermissions(item));
|
|
await waitFor(() => expect(result.current.permissionForm.permissions).toEqual(["user:list"]));
|
|
expect(mocks.getExternalAdminUserPermissions).toHaveBeenCalledWith("fami", 71);
|
|
|
|
act(() => result.current.setPermissions([]));
|
|
await act(async () => result.current.submitPermissions());
|
|
|
|
expect(mocks.updateExternalAdminUserPermissions).toHaveBeenCalledWith("fami", 71, [], 4);
|
|
expect(mocks.showToast).toHaveBeenCalledWith("权限已更新,该账号需重新登录", "success");
|
|
expect(result.current.permissionUser).toBeNull();
|
|
});
|
|
|
|
test("keeps the permission drawer open and offers reload after a concurrent update", async () => {
|
|
const conflict = Object.assign(new Error("conflict"), { status: 409 });
|
|
mocks.updateExternalAdminUserPermissions.mockRejectedValueOnce(conflict);
|
|
const item = externalUserFixture();
|
|
const { result } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
act(() => result.current.openPermissions(item));
|
|
await waitFor(() => expect(result.current.permissionLoading).toBe(false));
|
|
await act(async () => result.current.submitPermissions());
|
|
|
|
expect(result.current.permissionUser).toEqual(item);
|
|
expect(result.current.permissionError).toBe("账号权限已被更新,请重新加载");
|
|
expect(mocks.showToast).toHaveBeenCalledWith("账号权限已被更新,请重新加载", "error");
|
|
|
|
mocks.getExternalAdminUserPermissions.mockResolvedValueOnce({
|
|
accountId: "71",
|
|
permissions: ["user:list", "user:update"],
|
|
revision: 5,
|
|
});
|
|
act(() => result.current.retryPermissions());
|
|
await waitFor(() => expect(result.current.permissionForm.expectedRevision).toBe(5));
|
|
await act(async () => result.current.submitPermissions());
|
|
|
|
expect(mocks.updateExternalAdminUserPermissions).toHaveBeenNthCalledWith(
|
|
2,
|
|
"fami",
|
|
71,
|
|
["user:list", "user:update"],
|
|
5,
|
|
);
|
|
expect(result.current.permissionUser).toBeNull();
|
|
});
|
|
|
|
test("does not expose backend permission codes when a non-conflict update fails", async () => {
|
|
mocks.updateExternalAdminUserPermissions.mockRejectedValueOnce(
|
|
new Error("privilege:grant requires user-title:grant"),
|
|
);
|
|
const { result } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
act(() => result.current.openPermissions(externalUserFixture()));
|
|
await waitFor(() => expect(result.current.permissionLoading).toBe(false));
|
|
await act(async () => result.current.submitPermissions());
|
|
|
|
expect(result.current.permissionError).toBe("更新外管权限失败");
|
|
expect(mocks.showToast).toHaveBeenCalledWith("更新外管权限失败", "error");
|
|
});
|
|
|
|
test("ignores a late user lookup response after switching App", async () => {
|
|
let resolveLookup;
|
|
mocks.lookupExternalAdminUserTarget.mockImplementationOnce(
|
|
() => new Promise((resolve) => {
|
|
resolveLookup = resolve;
|
|
}),
|
|
);
|
|
const { result, rerender } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
act(() => result.current.openCreate());
|
|
act(() => result.current.setDisplayUserId("VIP-1001"));
|
|
let lookupPromise;
|
|
act(() => {
|
|
lookupPromise = result.current.lookupTarget();
|
|
});
|
|
mocks.appCode = "lalu";
|
|
rerender();
|
|
await waitFor(() => expect(result.current.createOpen).toBe(false));
|
|
await act(async () => {
|
|
resolveLookup(targetFixture());
|
|
await lookupPromise;
|
|
});
|
|
|
|
expect(result.current.targetUser).toBeNull();
|
|
});
|
|
|
|
test("refreshes the App-scoped list after status and password changes", async () => {
|
|
const item = externalUserFixture();
|
|
const { result } = renderHook(() => useExternalAdminUsersPage());
|
|
|
|
await act(async () => result.current.toggleStatus(item));
|
|
expect(mocks.updateExternalAdminUserStatus).toHaveBeenCalledWith("fami", 71, "disabled");
|
|
|
|
act(() => result.current.openPassword(item));
|
|
act(() => result.current.setPasswordForm({ confirmPassword: "NextPass123!", password: "NextPass123!" }));
|
|
await act(async () => result.current.submitPassword({ preventDefault: vi.fn() }));
|
|
|
|
expect(mocks.resetExternalAdminUserPassword).toHaveBeenCalledWith("fami", 71, "NextPass123!");
|
|
expect(mocks.invalidateQueries).toHaveBeenCalledTimes(2);
|
|
expect(mocks.invalidateQueries).toHaveBeenNthCalledWith(1, { queryKey: ["external-admin-users", "fami"] });
|
|
expect(mocks.invalidateQueries).toHaveBeenNthCalledWith(2, { queryKey: ["external-admin-users", "fami"] });
|
|
});
|
|
|
|
function targetFixture() {
|
|
return {
|
|
avatar: "https://cdn.example.com/user.png",
|
|
defaultDisplayUserId: "1001",
|
|
displayUserId: "VIP-1001",
|
|
prettyDisplayUserId: "VIP-1001",
|
|
prettyId: "pretty-1",
|
|
status: "active",
|
|
userId: "internal-user-1001",
|
|
username: "Fami user",
|
|
};
|
|
}
|
|
|
|
function externalUserFixture() {
|
|
return {
|
|
appCode: "fami",
|
|
createdAtMs: 1784073600000,
|
|
createdByAdminId: 9,
|
|
id: 71,
|
|
lastLoginAtMs: 1784077200000,
|
|
linkedUser: targetFixture(),
|
|
permissions: [],
|
|
permissionRevision: 4,
|
|
status: "active",
|
|
updatedAtMs: 1784077200000,
|
|
username: "ops.fami",
|
|
};
|
|
}
|
|
|
|
function permissionCatalogFixture() {
|
|
return {
|
|
items: [
|
|
{ capabilities: ["user:list"], code: "user:list", defaultGranted: true, dependencies: [], group: "用户管理", label: "用户列表" },
|
|
{ capabilities: ["user:list", "user:update"], code: "user:update", defaultGranted: true, dependencies: ["user:list"], group: "用户管理", label: "编辑用户" },
|
|
{ capabilities: ["room:list"], code: "room:list", defaultGranted: false, dependencies: [], group: "房间管理", label: "房间列表" },
|
|
],
|
|
total: 3,
|
|
};
|
|
}
|