212 lines
6.2 KiB
TypeScript
212 lines
6.2 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createI18nMock } from "../../test/i18nMock";
|
|
import UserDetailPage from "./UserDetailPage";
|
|
|
|
const updateUserMock = vi.hoisted(() => vi.fn());
|
|
const profileRoleMock = vi.hoisted(() => ({ role: "super_admin" }));
|
|
|
|
vi.mock("../../lib/i18n", () => createI18nMock());
|
|
|
|
vi.mock("../../lib/adminApi", () => ({
|
|
deleteUser: vi.fn(),
|
|
fetchAllTenants: vi.fn(async () => ({
|
|
items: [
|
|
{
|
|
id: "tenant-hanmac",
|
|
type: "COMPANY",
|
|
name: "한맥기술",
|
|
slug: "hanmac",
|
|
description: "",
|
|
status: "active",
|
|
memberCount: 1,
|
|
createdAt: "2026-06-01T00:00:00Z",
|
|
updatedAt: "2026-06-01T00:00:00Z",
|
|
},
|
|
],
|
|
total: 1,
|
|
})),
|
|
fetchMe: vi.fn(async () => ({
|
|
id: "admin-user",
|
|
role: profileRoleMock.role,
|
|
name: "Admin",
|
|
email: "admin@example.com",
|
|
})),
|
|
fetchGlobalCustomClaimDefinitions: vi.fn(async () => ({
|
|
items: [
|
|
{
|
|
key: "contract_date",
|
|
label: "계약일",
|
|
valueType: "date",
|
|
readPermission: "admin_only",
|
|
writePermission: "admin_only",
|
|
description: "",
|
|
},
|
|
],
|
|
})),
|
|
fetchPasswordPolicy: vi.fn(async () => ({ minLength: 12 })),
|
|
fetchTenant: vi.fn(),
|
|
fetchUser: vi.fn(async () => ({
|
|
id: "user-1",
|
|
email: "user@example.com",
|
|
name: "사용자",
|
|
phone: "01012345678",
|
|
role: "user",
|
|
status: "active",
|
|
tenantSlug: "hanmac",
|
|
tenant: {
|
|
id: "tenant-hanmac",
|
|
type: "COMPANY",
|
|
name: "한맥기술",
|
|
slug: "hanmac",
|
|
description: "",
|
|
status: "active",
|
|
memberCount: 1,
|
|
createdAt: "2026-06-01T00:00:00Z",
|
|
updatedAt: "2026-06-01T00:00:00Z",
|
|
},
|
|
joinedTenants: [],
|
|
metadata: {
|
|
employee_id: {
|
|
"0": "h",
|
|
"1": "j",
|
|
"2": "k",
|
|
"3": "w",
|
|
"4": "o",
|
|
"5": "n",
|
|
},
|
|
global_custom_claims: {
|
|
contract_date: "2026-06-09",
|
|
},
|
|
},
|
|
createdAt: "2026-06-01T00:00:00Z",
|
|
updatedAt: "2026-06-01T00:00:00Z",
|
|
})),
|
|
fetchUserRpHistory: vi.fn(async () => []),
|
|
updateUser: updateUserMock,
|
|
}));
|
|
|
|
function renderUserDetailPage() {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<MemoryRouter initialEntries={["/users/user-1"]}>
|
|
<Routes>
|
|
<Route path="/users/:id" element={<UserDetailPage />} />
|
|
</Routes>
|
|
</MemoryRouter>
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
describe("UserDetailPage Worksmobile employee number", () => {
|
|
beforeEach(() => {
|
|
updateUserMock.mockReset();
|
|
updateUserMock.mockResolvedValue({});
|
|
profileRoleMock.role = "super_admin";
|
|
});
|
|
|
|
it("shows and saves metadata employee_id from the user edit form", async () => {
|
|
renderUserDetailPage();
|
|
|
|
const employeeInput = await screen.findByLabelText("사번");
|
|
|
|
expect(employeeInput).toHaveValue("hjkwon");
|
|
|
|
fireEvent.change(employeeInput, { target: { value: "EMP001" } });
|
|
fireEvent.click(screen.getByRole("button", { name: /저장하기/ }));
|
|
|
|
await waitFor(() => expect(updateUserMock).toHaveBeenCalled());
|
|
expect(updateUserMock).toHaveBeenCalledWith(
|
|
"user-1",
|
|
expect.objectContaining({
|
|
metadata: expect.objectContaining({ employee_id: "EMP001" }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("allows super admin to save a changed email", async () => {
|
|
renderUserDetailPage();
|
|
|
|
const emailInput = await screen.findByLabelText("이메일");
|
|
fireEvent.change(emailInput, { target: { value: "changed@example.com" } });
|
|
fireEvent.click(screen.getByRole("button", { name: /저장하기/ }));
|
|
|
|
await waitFor(() => expect(updateUserMock).toHaveBeenCalled());
|
|
expect(updateUserMock).toHaveBeenCalledWith(
|
|
"user-1",
|
|
expect.objectContaining({
|
|
email: "changed@example.com",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("shows forbidden message for non-super admin", async () => {
|
|
profileRoleMock.role = "tenant_admin";
|
|
renderUserDetailPage();
|
|
|
|
expect(
|
|
await screen.findByText("이 작업을 수행할 권한이 없습니다."),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("removes metadata employee_id when the field is cleared", async () => {
|
|
renderUserDetailPage();
|
|
|
|
const employeeInput = await screen.findByLabelText("사번");
|
|
|
|
fireEvent.change(employeeInput, { target: { value: "" } });
|
|
fireEvent.click(screen.getByRole("button", { name: /저장하기/ }));
|
|
|
|
await waitFor(() => expect(updateUserMock).toHaveBeenCalled());
|
|
const payload = updateUserMock.mock.calls[0][1];
|
|
expect(payload.metadata).not.toHaveProperty("employee_id");
|
|
});
|
|
|
|
it("only allows editing per-user values for globally defined custom claims", async () => {
|
|
renderUserDetailPage();
|
|
|
|
const tab = await screen.findByTestId("global-custom-claim-tab");
|
|
fireEvent.click(tab);
|
|
|
|
expect(
|
|
screen.queryByRole("button", { name: "추가" }),
|
|
).not.toBeInTheDocument();
|
|
const valueInput = await screen.findByTestId(
|
|
"global-custom-claim-value-contract_date",
|
|
);
|
|
|
|
expect(screen.getByText("contract_date")).toBeInTheDocument();
|
|
expect(valueInput).toHaveValue("2026-06-09");
|
|
expect(valueInput).toHaveAttribute("type", "date");
|
|
|
|
fireEvent.change(valueInput, { target: { value: "2026-07-01" } });
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: /사용자 Claim 값 저장/ }),
|
|
);
|
|
|
|
await waitFor(() => expect(updateUserMock).toHaveBeenCalled());
|
|
expect(updateUserMock).toHaveBeenCalledWith(
|
|
"user-1",
|
|
expect.objectContaining({
|
|
metadata: expect.objectContaining({
|
|
global_custom_claims: expect.objectContaining({
|
|
contract_date: "2026-07-01",
|
|
}),
|
|
global_custom_claim_permissions: expect.objectContaining({
|
|
contract_date: {
|
|
readPermission: "admin_only",
|
|
writePermission: "admin_only",
|
|
},
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|