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( {ui} , ); } 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( } /> , "/users/new?tenantSlug=gpdtdc-rnd", ); expect(await screen.findByText("사용자 추가")).toBeInTheDocument(); expect(screen.getByLabelText("이메일")).toBeInTheDocument(); }); it("renders user detail form and RP history", async () => { renderWithProviders( } /> , "/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( } /> , "/tenants", ); expect(await screen.findByText("GPDTDC")).toBeInTheDocument(); expect(screen.getByText("기술연구팀")).toBeInTheDocument(); }); it("renders worksmobile comparison screens", async () => { cleanup(); renderWithProviders( } /> , "/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( } /> , "/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( } /> , "/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( } /> , "/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( } /> , "/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"); }); });