1
0
forked from baron/baron-sso
Files
baron-sso/adminfront/src/features/coverage/adminLargePages.test.tsx
2026-06-10 09:36:57 +09:00

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");
});
});