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, }, ]), 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", 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에 생성" }), ); await waitFor(() => expect(adminApi.enqueueWorksmobileUserSync).toHaveBeenCalledWith( "tenant-company", "user-2", expect.any(String), ), ); const credentialBatchId = vi.mocked( adminApi.enqueueWorksmobileUserSync, ).mock.calls[0][2]; 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에 생성" }), ); await waitFor(() => expect(adminApi.enqueueWorksmobileUserSync).toHaveBeenCalledTimes(2), ); expect(adminApi.enqueueWorksmobileUserSync).toHaveBeenNthCalledWith( 1, "tenant-company", "user-2", expect.any(String), ); expect(adminApi.enqueueWorksmobileUserSync).toHaveBeenNthCalledWith( 2, "tenant-company", "user-3", expect.any(String), ); expect(adminApi.downloadWorksmobileInitialPasswordsCSV).not.toHaveBeenCalled(); }); it("downloads or deletes Worksmobile credential batches from history", async () => { vi.spyOn(window.URL, "createObjectURL").mockReturnValue("blob:test"); vi.spyOn(window.URL, "revokeObjectURL").mockImplementation(() => {}); vi.spyOn(window, "confirm").mockReturnValue(true); renderWithProviders( } /> , "/tenants/tenant-company/worksmobile", ); fireEvent.click(screen.getByRole("tab", { name: "이력" })); await screen.findByText("credential-batch-1"); expect( screen.getByRole("button", { name: "credential-batch-pending 비밀번호 CSV 다운로드", }), ).toBeDisabled(); fireEvent.click( screen.getByRole("button", { name: "credential-batch-1 비밀번호 CSV 다운로드", }), ); await waitFor(() => expect( adminApi.downloadWorksmobileInitialPasswordsCSV, ).toHaveBeenCalledWith("tenant-company", "credential-batch-1"), ); fireEvent.click( screen.getByRole("button", { name: "credential-batch-1 비밀번호 값 삭제", }), ); await waitFor(() => expect( adminApi.deleteWorksmobileCredentialBatchPasswords, ).toHaveBeenCalledWith("tenant-company", "credential-batch-1"), ); fireEvent.click( screen.getByRole("button", { name: "credential-batch-1 실패 사유 보기", }), ); expect(await screen.findByText("failed-user@samaneng.com")).toBeInTheDocument(); expect(screen.getByText("worksmobile api failed")).toBeInTheDocument(); }); it("enqueues Worksmobile password reset as a credential batch", async () => { vi.spyOn(window, "confirm").mockReturnValue(true); 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 비밀번호 재설정", }), ); await waitFor(() => expect(adminApi.resetWorksmobileUserPassword).toHaveBeenCalledWith( "tenant-company", "user-1", expect.any(String), ), ); expect(adminApi.downloadWorksmobileInitialPasswordsCSV).not.toHaveBeenCalled(); }); });