import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { deleteOrphanUserLoginIDs, fetchDataIntegrityReport, fetchMe, fetchOrphanUserLoginIDs, fetchUserProjectionStatus, reconcileUserProjection, resetUserProjection, } from "../../lib/adminApi"; import { createI18nMock } from "../../test/i18nMock"; import DataIntegrityPage from "./DataIntegrityPage"; vi.mock("../../lib/i18n", () => createI18nMock()); let currentRole = "super_admin"; const integrityReport = { status: "fail", checkedAt: "2026-05-14T00:00:00Z", summary: { totalChecks: 2, passed: 1, warnings: 0, failures: 1, }, sections: [ { key: "tenant_integrity", label: "테넌트 정합성", status: "fail", checks: [ { key: "duplicate_tenant_slugs", label: "중복 테넌트 slug", description: "active tenant slug의 대소문자 무시 중복을 검사합니다.", status: "fail", severity: "error", count: 1, }, ], }, ], }; vi.mock("../../lib/adminApi", () => ({ fetchMe: vi.fn(async () => ({ role: currentRole })), fetchDataIntegrityReport: vi.fn(async () => integrityReport), fetchOrphanUserLoginIDs: vi.fn(async () => ({ items: [ { id: "login-id-1", userId: "user-1", userEmail: "missing@example.com", tenantId: "tenant-1", tenantSlug: "deleted-tenant", fieldKey: "emp_id", loginId: "EMP001", reasons: ["deleted_tenant"], }, ], total: 1, })), fetchUserProjectionStatus: vi.fn(async () => ({ name: "kratos_users", status: "ready", ready: true, lastSyncedAt: "2026-05-11T03:00:00Z", updatedAt: "2026-05-11T03:00:10Z", projectedUsers: 152, })), reconcileUserProjection: vi.fn(async () => ({ status: "success", syncedUsers: 152, updatedAt: "2026-05-11T03:01:00Z", })), resetUserProjection: vi.fn(async () => ({ status: "success", syncedUsers: 152, updatedAt: "2026-05-11T03:02:00Z", })), deleteOrphanUserLoginIDs: vi.fn(async () => ({ deletedCount: 1, deleted: [ { id: "login-id-1", userId: "user-1", tenantId: "tenant-1", fieldKey: "emp_id", loginId: "EMP001", reasons: ["deleted_tenant"], }, ], skippedIds: [], })), })); function renderPage() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return render( , ); } describe("DataIntegrityPage", () => { beforeEach(() => { currentRole = "super_admin"; vi.clearAllMocks(); vi.spyOn(window, "confirm").mockReturnValue(true); window.localStorage.setItem("locale", "ko"); }); it("renders integrity report for super_admin", async () => { renderPage(); expect(await screen.findByText("데이터 정합성 검증")).toBeInTheDocument(); expect( screen.getByRole("tab", { name: "정합성 검사" }), ).toBeInTheDocument(); expect( screen.getByRole("tab", { name: "사용자 동기화" }), ).toBeInTheDocument(); expect( await screen.findByText( "정합성 상태를 확인하고 데이터 모델 전반의 검증 결과를 살펴봅니다.", ), ).toBeInTheDocument(); expect(await screen.findByText("테넌트 정합성")).toBeInTheDocument(); expect(screen.getByText("중복 테넌트 slug")).toBeInTheDocument(); expect(screen.getAllByText("1").length).toBeGreaterThan(0); expect(fetchDataIntegrityReport).toHaveBeenCalledTimes(1); }); it("renders user projection sync inside data integrity", async () => { renderPage(); fireEvent.click(await screen.findByRole("tab", { name: "사용자 동기화" })); expect(await screen.findByText("사용자 동기화 관리")).toBeInTheDocument(); expect(await screen.findByText("Kratos 사용자 동기화")).toBeInTheDocument(); expect(screen.getByText("준비됨")).toBeInTheDocument(); expect(screen.getByText("152")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /재동기화/ })); await waitFor(() => { expect(reconcileUserProjection).toHaveBeenCalledTimes(1); }); fireEvent.click(screen.getByRole("button", { name: /초기화 후 재구축/ })); await waitFor(() => { expect(resetUserProjection).toHaveBeenCalledTimes(1); }); expect(fetchUserProjectionStatus).toHaveBeenCalled(); }); it("shows orphan login ID targets and deletes selected rows", async () => { vi.spyOn(window, "confirm").mockReturnValue(true); renderPage(); expect(await screen.findByText("유령 로그인 ID 정리")).toBeInTheDocument(); expect(await screen.findByText("EMP001")).toBeInTheDocument(); expect(screen.getByText("삭제된 테넌트")).toBeInTheDocument(); expect(fetchOrphanUserLoginIDs).toHaveBeenCalledTimes(1); fireEvent.click(screen.getByRole("checkbox", { name: "EMP001 선택" })); fireEvent.click(screen.getByRole("button", { name: "선택 삭제" })); await waitFor(() => { expect(deleteOrphanUserLoginIDs).toHaveBeenCalled(); }); expect(vi.mocked(deleteOrphanUserLoginIDs).mock.calls[0][0]).toEqual([ "login-id-1", ]); }); it("disables recheck button and shows manual recheck progress", async () => { let finishRecheck: (value: typeof integrityReport) => void = () => {}; const pendingRecheck = new Promise((resolve) => { finishRecheck = resolve; }); renderPage(); expect(await screen.findByText("중복 테넌트 slug")).toBeInTheDocument(); vi.mocked(fetchDataIntegrityReport).mockImplementationOnce( () => pendingRecheck, ); fireEvent.click(screen.getByRole("button", { name: "다시 검사" })); expect(screen.getByRole("button", { name: "검사 중" })).toBeDisabled(); expect( screen.getByText("정합성 검사를 실행 중입니다."), ).toBeInTheDocument(); finishRecheck(integrityReport); await waitFor(() => { expect(screen.getByRole("button", { name: "다시 검사" })).toBeEnabled(); }); expect(screen.getByText("검사가 완료되었습니다.")).toBeInTheDocument(); }); it("blocks non-super admins", async () => { currentRole = "tenant_admin"; renderPage(); expect(await screen.findByText("접근 권한이 없습니다")).toBeInTheDocument(); expect(fetchMe).toHaveBeenCalled(); expect(fetchDataIntegrityReport).not.toHaveBeenCalled(); }); it("renders localized integrity labels in English", async () => { window.localStorage.setItem("locale", "en"); renderPage(); expect(await screen.findByText("Data Integrity Check")).toBeInTheDocument(); expect( await screen.findByText( "Review integrity status and inspect checks across the admin data model.", ), ).toBeInTheDocument(); expect(await screen.findByText("Tenant integrity")).toBeInTheDocument(); expect( await screen.findByText("Duplicate tenant slug"), ).toBeInTheDocument(); expect( await screen.findByText( "Checks duplicate active tenant slugs using LOWER(TRIM(slug)).", ), ).toBeInTheDocument(); }); });