forked from baron/baron-sso
507 lines
15 KiB
TypeScript
507 lines
15 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import {
|
|
cleanup,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
} from "@testing-library/react";
|
|
import type React from "react";
|
|
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createI18nMock } from "../../test/i18nMock";
|
|
import * as adminApi from "../../lib/adminApi";
|
|
import { TenantWorksmobilePage } from "../tenants/routes/TenantWorksmobilePage";
|
|
import TenantListPage from "../tenants/routes/TenantListPage";
|
|
import UserCreatePage from "../users/UserCreatePage";
|
|
import UserDetailPage from "../users/UserDetailPage";
|
|
|
|
const tenantItems = [
|
|
{
|
|
id: "tenant-root",
|
|
type: "COMPANY_GROUP",
|
|
name: "한맥 가족",
|
|
slug: "hanmac-family",
|
|
description: "root",
|
|
status: "active",
|
|
memberCount: 0,
|
|
createdAt: "2026-05-01T00:00:00Z",
|
|
updatedAt: "2026-05-01T00:00:00Z",
|
|
},
|
|
{
|
|
id: "tenant-company",
|
|
type: "COMPANY",
|
|
parentId: "tenant-root",
|
|
name: "GPDTDC",
|
|
slug: "gpdtdc",
|
|
description: "company",
|
|
status: "active",
|
|
memberCount: 2,
|
|
config: {
|
|
userSchema: [
|
|
{
|
|
key: "employee_id",
|
|
label: "사번",
|
|
type: "text",
|
|
required: false,
|
|
},
|
|
],
|
|
},
|
|
createdAt: "2026-05-01T00:00:00Z",
|
|
updatedAt: "2026-05-01T00:00:00Z",
|
|
},
|
|
{
|
|
id: "tenant-leaf",
|
|
type: "ORGANIZATION",
|
|
parentId: "tenant-company",
|
|
name: "기술연구팀",
|
|
slug: "gpdtdc-rnd",
|
|
description: "leaf",
|
|
status: "active",
|
|
memberCount: 1,
|
|
createdAt: "2026-05-01T00:00:00Z",
|
|
updatedAt: "2026-05-01T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
const userDetail = {
|
|
id: "user-1",
|
|
email: "engineer@example.com",
|
|
name: "Engineer User",
|
|
phone: "010-0000-0000",
|
|
role: "user",
|
|
status: "active",
|
|
tenantSlug: "gpdtdc-rnd",
|
|
tenantId: "tenant-leaf",
|
|
department: "기술연구팀",
|
|
grade: "책임",
|
|
position: "팀장",
|
|
jobTitle: "Backend",
|
|
metadata: {
|
|
employee_id: "EMP001",
|
|
sub_email: ["engineer.sub@example.com"],
|
|
},
|
|
tenant: tenantItems[2],
|
|
appointments: [
|
|
{
|
|
tenantId: "tenant-leaf",
|
|
tenantSlug: "gpdtdc-rnd",
|
|
tenantName: "기술연구팀",
|
|
isPrimary: true,
|
|
isOwner: false,
|
|
isAdmin: false,
|
|
isManager: true,
|
|
department: "기술연구팀",
|
|
grade: "책임",
|
|
position: "팀장",
|
|
jobTitle: "Backend",
|
|
metadata: { employee_id: "EMP001" },
|
|
},
|
|
],
|
|
createdAt: "2026-05-01T00:00:00Z",
|
|
updatedAt: "2026-05-02T00:00:00Z",
|
|
};
|
|
|
|
vi.mock("../../lib/i18n", () => createI18nMock());
|
|
|
|
vi.mock("../../components/auth/RoleGuard", () => ({
|
|
RoleGuard: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
}));
|
|
|
|
vi.mock("../../lib/adminApi", () => ({
|
|
fetchMe: vi.fn(async () => ({
|
|
id: "admin-1",
|
|
role: "super_admin",
|
|
name: "Admin User",
|
|
email: "admin@example.com",
|
|
})),
|
|
fetchAllTenants: vi.fn(async () => ({
|
|
items: tenantItems,
|
|
total: tenantItems.length,
|
|
})),
|
|
fetchTenants: vi.fn(async () => ({
|
|
items: tenantItems,
|
|
limit: 500,
|
|
offset: 0,
|
|
total: tenantItems.length,
|
|
nextCursor: null,
|
|
})),
|
|
fetchTenant: vi.fn(async (id: string) => {
|
|
return tenantItems.find((tenant) => tenant.id === id) ?? tenantItems[1];
|
|
}),
|
|
createUser: vi.fn(async () => ({
|
|
id: "created-user",
|
|
email: "created@example.com",
|
|
generatedPassword: "GeneratedPassword!1",
|
|
})),
|
|
fetchUser: vi.fn(async () => userDetail),
|
|
fetchUserRpHistory: vi.fn(async () => [
|
|
{
|
|
client_id: "orgfront",
|
|
client_name: "OrgFront",
|
|
last_login_at: "2026-05-01T00:00:00Z",
|
|
login_count: 3,
|
|
},
|
|
]),
|
|
fetchGlobalCustomClaimDefinitions: vi.fn(async () => ({ items: [] })),
|
|
fetchPasswordPolicy: vi.fn(async () => ({
|
|
minLength: 12,
|
|
lowercase: true,
|
|
uppercase: true,
|
|
number: true,
|
|
nonAlphanumeric: true,
|
|
minCharacterTypes: 3,
|
|
})),
|
|
updateUser: vi.fn(async () => userDetail),
|
|
deleteUser: vi.fn(async () => undefined),
|
|
updateTenant: vi.fn(async () => tenantItems[1]),
|
|
deleteTenantsBulk: vi.fn(async () => ({ deleted: 1 })),
|
|
exportTenantsCSV: vi.fn(async () => new Blob(["name,slug\nGPDTDC,gpdtdc"])),
|
|
importTenantsCSV: vi.fn(async () => ({
|
|
created: 1,
|
|
updated: 0,
|
|
failed: 0,
|
|
errors: [],
|
|
})),
|
|
fetchWorksmobileOverview: vi.fn(async () => ({
|
|
tenant: tenantItems[1],
|
|
config: {
|
|
enabled: true,
|
|
tokenConfigured: true,
|
|
adminTenantId: "works-admin",
|
|
domainMappings: { "example.com": 1001 },
|
|
},
|
|
recentJobs: [
|
|
{
|
|
id: "job-1",
|
|
resourceType: "USER",
|
|
resourceId: "user-1",
|
|
action: "SYNC",
|
|
status: "failed",
|
|
retryCount: 1,
|
|
lastError: "temporary failure",
|
|
createdAt: "2026-05-01T00:00:00Z",
|
|
updatedAt: "2026-05-01T00:10:00Z",
|
|
},
|
|
],
|
|
})),
|
|
fetchWorksmobileComparison: vi.fn(async () => ({
|
|
users: [
|
|
{
|
|
resourceType: "USER",
|
|
baronId: "user-1",
|
|
baronName: "Engineer User",
|
|
baronEmail: "engineer@example.com",
|
|
baronPrimaryOrgId: "tenant-leaf",
|
|
baronPrimaryOrgName: "기술연구팀",
|
|
worksmobileId: "works-user-1",
|
|
worksmobileName: "Engineer User",
|
|
worksmobileEmail: "engineer@example.com",
|
|
worksmobileDomainId: 1001,
|
|
worksmobilePrimaryOrgId: "works-org-1",
|
|
worksmobilePrimaryOrgName: "기술연구팀",
|
|
status: "matched",
|
|
},
|
|
{
|
|
resourceType: "USER",
|
|
baronId: "user-2",
|
|
baronName: "New User",
|
|
baronEmail: "new@example.com",
|
|
worksmobileJobStatus: "failed",
|
|
worksmobileJobRetryCount: 2,
|
|
worksmobileLastError: "worksmobile api failed",
|
|
status: "missing_in_worksmobile",
|
|
},
|
|
{
|
|
resourceType: "USER",
|
|
baronId: "user-3",
|
|
baronName: "Next User",
|
|
baronEmail: "next@example.com",
|
|
status: "missing_in_worksmobile",
|
|
},
|
|
],
|
|
groups: [
|
|
{
|
|
resourceType: "ORG_UNIT",
|
|
baronId: "tenant-leaf",
|
|
baronSlug: "gpdtdc-rnd",
|
|
baronName: "기술연구팀",
|
|
worksmobileId: "works-org-1",
|
|
worksmobileName: "기술연구팀",
|
|
status: "needs_update",
|
|
},
|
|
],
|
|
})),
|
|
fetchWorksmobileCredentialBatches: vi.fn(async () => [
|
|
{
|
|
batchId: "credential-batch-1",
|
|
operation: "worksmobile_user_sync",
|
|
userCount: 1,
|
|
processedCount: 1,
|
|
failedCount: 1,
|
|
hasPasswords: true,
|
|
failures: [
|
|
{
|
|
userId: "failed-user",
|
|
email: "failed-user@samaneng.com",
|
|
status: "failed",
|
|
retryCount: 2,
|
|
lastError: "worksmobile api failed",
|
|
updatedAt: "2026-06-01T04:05:00Z",
|
|
},
|
|
],
|
|
createdAt: "2026-06-01T04:00:00Z",
|
|
updatedAt: "2026-06-01T04:00:00Z",
|
|
},
|
|
{
|
|
batchId: "credential-batch-pending",
|
|
operation: "worksmobile_user_sync",
|
|
userCount: 2,
|
|
pendingCount: 1,
|
|
processingCount: 1,
|
|
processedCount: 0,
|
|
failedCount: 0,
|
|
hasPasswords: true,
|
|
createdAt: "2026-06-01T04:10:00Z",
|
|
updatedAt: "2026-06-01T04:10:00Z",
|
|
},
|
|
]),
|
|
enqueueWorksmobileBackfillDryRun: vi.fn(async () => ({ id: "job-dry" })),
|
|
retryWorksmobileJob: vi.fn(async () => ({ id: "job-retry" })),
|
|
downloadWorksmobileInitialPasswordsCSV: vi.fn(async () => ({
|
|
blob: new Blob(["id"]),
|
|
filename: "worksmobile_initial_passwords.csv",
|
|
})),
|
|
enqueueWorksmobileOrgUnitSync: vi.fn(async () => ({ id: "job-org" })),
|
|
enqueueWorksmobileOrgUnitDelete: vi.fn(async () => ({ id: "job-delete" })),
|
|
enqueueWorksmobileUserSync: vi.fn(async () => ({ id: "job-user" })),
|
|
resetWorksmobileUserPassword: vi.fn(async () => ({ id: "job-reset" })),
|
|
deleteWorksmobileCredentialBatchPasswords: vi.fn(async () => ({
|
|
batchId: "credential-batch-1",
|
|
userCount: 1,
|
|
hasPasswords: false,
|
|
})),
|
|
}));
|
|
|
|
function renderWithProviders(ui: React.ReactElement, entry = "/") {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<MemoryRouter initialEntries={[entry]}>{ui}</MemoryRouter>
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
describe("adminfront large page coverage smoke", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
if (typeof window !== "undefined") {
|
|
(window as any)._IS_TEST_MODE = true;
|
|
}
|
|
});
|
|
|
|
it("renders user creation form with tenant context", async () => {
|
|
renderWithProviders(
|
|
<Routes>
|
|
<Route path="/users/new" element={<UserCreatePage />} />
|
|
</Routes>,
|
|
"/users/new?tenantSlug=gpdtdc-rnd",
|
|
);
|
|
|
|
expect(await screen.findByText("사용자 추가")).toBeInTheDocument();
|
|
expect(screen.getByLabelText("이메일")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders user detail form and RP history", async () => {
|
|
renderWithProviders(
|
|
<Routes>
|
|
<Route path="/users/:id" element={<UserDetailPage />} />
|
|
</Routes>,
|
|
"/users/user-1",
|
|
);
|
|
|
|
expect(await screen.findByDisplayValue("Engineer User")).toBeInTheDocument();
|
|
expect(screen.getAllByText("기술연구팀").length).toBeGreaterThan(0);
|
|
expect(screen.getByDisplayValue("engineer@example.com")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders tenant list hierarchy", async () => {
|
|
renderWithProviders(
|
|
<Routes>
|
|
<Route path="/tenants" element={<TenantListPage />} />
|
|
</Routes>,
|
|
"/tenants",
|
|
);
|
|
|
|
expect(await screen.findByText("GPDTDC")).toBeInTheDocument();
|
|
expect(screen.getByText("기술연구팀")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders worksmobile comparison screens", async () => {
|
|
cleanup();
|
|
renderWithProviders(
|
|
<Routes>
|
|
<Route
|
|
path="/tenants/:tenantId/worksmobile"
|
|
element={<TenantWorksmobilePage />}
|
|
/>
|
|
</Routes>,
|
|
"/tenants/tenant-company/worksmobile",
|
|
);
|
|
|
|
expect(await screen.findByText("Worksmobile 연동")).toBeInTheDocument();
|
|
expect(await screen.findByText("Baron / Works 비교")).toBeInTheDocument();
|
|
expect(
|
|
await screen.findByText("최근 실패: worksmobile api failed"),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText("Backfill Dry-run")).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "초기 비밀번호 CSV" })).toBeNull();
|
|
});
|
|
|
|
it("does not automatically download the selected Worksmobile user credential batch after create enqueue", async () => {
|
|
vi.spyOn(window.URL, "createObjectURL").mockReturnValue("blob:test");
|
|
vi.spyOn(window.URL, "revokeObjectURL").mockImplementation(() => {});
|
|
renderWithProviders(
|
|
<Routes>
|
|
<Route
|
|
path="/tenants/:tenantId/worksmobile"
|
|
element={<TenantWorksmobilePage />}
|
|
/>
|
|
</Routes>,
|
|
"/tenants/tenant-company/worksmobile",
|
|
);
|
|
|
|
await screen.findByText("New User");
|
|
fireEvent.click(screen.getByRole("checkbox", { name: "New User 선택" }));
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: "선택 구성원 WORKS에 생성" }),
|
|
);
|
|
fireEvent.change(screen.getByLabelText("초기 비밀번호"), {
|
|
target: { value: "InitialPassword!1" },
|
|
});
|
|
fireEvent.click(screen.getByRole("button", { name: "생성 작업 등록" }));
|
|
|
|
await waitFor(() =>
|
|
expect(adminApi.enqueueWorksmobileUserSync).toHaveBeenCalledWith(
|
|
"tenant-company",
|
|
"user-2",
|
|
undefined,
|
|
"InitialPassword!1",
|
|
),
|
|
);
|
|
expect(adminApi.downloadWorksmobileInitialPasswordsCSV).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("continues selected Worksmobile user create enqueue after one row fails", async () => {
|
|
vi.mocked(adminApi.enqueueWorksmobileUserSync)
|
|
.mockRejectedValueOnce(new Error("sync failed"))
|
|
.mockResolvedValueOnce({ id: "job-user-3" } as never);
|
|
vi.spyOn(window.URL, "createObjectURL").mockReturnValue("blob:test");
|
|
vi.spyOn(window.URL, "revokeObjectURL").mockImplementation(() => {});
|
|
renderWithProviders(
|
|
<Routes>
|
|
<Route
|
|
path="/tenants/:tenantId/worksmobile"
|
|
element={<TenantWorksmobilePage />}
|
|
/>
|
|
</Routes>,
|
|
"/tenants/tenant-company/worksmobile",
|
|
);
|
|
|
|
await screen.findByText("New User");
|
|
fireEvent.click(screen.getByRole("checkbox", { name: "New User 선택" }));
|
|
fireEvent.click(screen.getByRole("checkbox", { name: "Next User 선택" }));
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: "선택 구성원 WORKS에 생성" }),
|
|
);
|
|
fireEvent.change(screen.getByLabelText("초기 비밀번호"), {
|
|
target: { value: "InitialPassword!1" },
|
|
});
|
|
fireEvent.click(screen.getByRole("button", { name: "생성 작업 등록" }));
|
|
|
|
await waitFor(() =>
|
|
expect(adminApi.enqueueWorksmobileUserSync).toHaveBeenCalledTimes(2),
|
|
);
|
|
expect(adminApi.enqueueWorksmobileUserSync).toHaveBeenNthCalledWith(
|
|
1,
|
|
"tenant-company",
|
|
"user-2",
|
|
undefined,
|
|
"InitialPassword!1",
|
|
);
|
|
expect(adminApi.enqueueWorksmobileUserSync).toHaveBeenNthCalledWith(
|
|
2,
|
|
"tenant-company",
|
|
"user-3",
|
|
undefined,
|
|
"InitialPassword!1",
|
|
);
|
|
expect(adminApi.downloadWorksmobileInitialPasswordsCSV).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders and retries Worksmobile jobs from history", async () => {
|
|
renderWithProviders(
|
|
<Routes>
|
|
<Route
|
|
path="/tenants/:tenantId/worksmobile"
|
|
element={<TenantWorksmobilePage />}
|
|
/>
|
|
</Routes>,
|
|
"/tenants/tenant-company/worksmobile",
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("tab", { name: "이력" }));
|
|
expect((await screen.findAllByText("user-1")).length).toBeGreaterThan(0);
|
|
expect(screen.getByText("failed")).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getAllByRole("button", { name: "" })[0]);
|
|
await waitFor(() =>
|
|
expect(adminApi.retryWorksmobileJob).toHaveBeenCalledWith(
|
|
"tenant-company",
|
|
"job-1",
|
|
),
|
|
);
|
|
});
|
|
|
|
it("opens Worksmobile password management for matched users", async () => {
|
|
const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
|
|
renderWithProviders(
|
|
<Routes>
|
|
<Route
|
|
path="/tenants/:tenantId/worksmobile"
|
|
element={<TenantWorksmobilePage />}
|
|
/>
|
|
</Routes>,
|
|
"/tenants/tenant-company/worksmobile",
|
|
);
|
|
|
|
await screen.findByText("Worksmobile 연동");
|
|
fireEvent.click(screen.getAllByRole("button", { name: "양쪽 다 있음" })[0]);
|
|
await screen.findAllByText("Engineer User");
|
|
fireEvent.click(
|
|
screen.getByRole("button", {
|
|
name: "Engineer User 비밀번호 관리",
|
|
}),
|
|
);
|
|
|
|
expect(openSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining(
|
|
"https://auth.worksmobile.com/integrate/password/manage",
|
|
),
|
|
"_blank",
|
|
"noopener,noreferrer",
|
|
);
|
|
const [url] = openSpy.mock.calls[0] ?? [];
|
|
const parsed = new URL(String(url));
|
|
expect(parsed.searchParams.get("targetUserTenantId")).toBe("works-admin");
|
|
expect(parsed.searchParams.get("targetUserDomainId")).toBe("1001");
|
|
expect(parsed.searchParams.get("targetUserIdNo")).toBe("works-user-1");
|
|
});
|
|
});
|