import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createRoot, type Root } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import AuditLogsPage from "./AuditLogsPage"; const navigateMock = vi.fn(); const fetchMeMock = vi.fn(); const fetchDevAuditLogsMock = vi.fn(); let gateState = { hasDeveloperAccess: true, isDeveloperRequestPending: false, canRequestDeveloperAccess: false, isLoadingDeveloperAccessGate: false, }; vi.mock("react-oidc-context", () => ({ useAuth: () => ({ isAuthenticated: true, isLoading: false, user: { access_token: "access-token", profile: { role: "super_admin", tenant_id: "tenant-1", }, }, }), })); vi.mock("react-router-dom", () => ({ useNavigate: () => navigateMock, })); vi.mock("../developer-access/developerAccessGate", () => ({ useDeveloperAccessGate: () => gateState, })); vi.mock("../../lib/devApi", () => ({ fetchDevAuditLogs: (...args: unknown[]) => fetchDevAuditLogsMock(...args), })); vi.mock("../auth/authApi", () => ({ fetchMe: (...args: unknown[]) => fetchMeMock(...args), })); vi.mock("../../../../common/core/components/audit", () => ({ AuditLogTable: ({ logs, onLoadMore, }: { logs: Array<{ event_id: string }>; onLoadMore: () => void; }) => (
table:{logs.length}
), })); vi.mock("../../components/common/ForbiddenMessage", () => ({ ForbiddenMessage: ({ resourceToken }: { resourceToken: string }) => (
Forbidden:{resourceToken}
), })); const roots: Root[] = []; afterEach(() => { for (const root of roots.splice(0)) { act(() => { root.unmount(); }); } vi.clearAllMocks(); document.body.innerHTML = ""; }); beforeEach(() => { gateState = { hasDeveloperAccess: true, isDeveloperRequestPending: false, canRequestDeveloperAccess: false, isLoadingDeveloperAccessGate: false, }; fetchMeMock.mockResolvedValue({ id: "user-1", role: "super_admin", }); fetchDevAuditLogsMock.mockResolvedValue({ items: [ { event_id: "evt-1", timestamp: "2026-05-28T06:07:18.000Z", user_id: "user-1", event_type: "CLIENT_UPDATE", status: "success", ip_address: "127.0.0.1", user_agent: "Vitest", details: JSON.stringify({ action: "업데이트", target_id: "client-a", }), }, ], limit: 50, }); }); async function renderPage() { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); roots.push(root); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); await act(async () => { root.render( , ); }); await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); return container; } describe("AuditLogsPage", () => { it("shows the loading gate state", async () => { gateState = { hasDeveloperAccess: false, isDeveloperRequestPending: false, canRequestDeveloperAccess: false, isLoadingDeveloperAccessGate: true, }; const container = await renderPage(); expect(container.textContent).toContain("로딩 중..."); }); it("renders the access request card when access is denied", async () => { gateState = { hasDeveloperAccess: false, isDeveloperRequestPending: false, canRequestDeveloperAccess: true, isLoadingDeveloperAccessGate: false, }; const container = await renderPage(); expect(container.textContent).toContain( "감사 로그는 개발자 권한이 있어야 볼 수 있습니다.", ); const button = Array.from(container.querySelectorAll("button")).find( (item) => item.textContent?.includes("개발자 권한 신청"), ); expect(button).toBeTruthy(); await act(async () => { button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(navigateMock).toHaveBeenCalledWith("/developer-requests"); }); it("exports the fetched logs as CSV", async () => { const createObjectURL = vi .spyOn(URL, "createObjectURL") .mockReturnValue("blob:csv"); const revokeObjectURL = vi.spyOn(URL, "revokeObjectURL").mockReturnValue(); const clickSpy = vi .spyOn(HTMLAnchorElement.prototype, "click") .mockImplementation(() => {}); const container = await renderPage(); expect(container.textContent).toContain("table:1"); const button = Array.from(container.querySelectorAll("button")).find( (item) => item.textContent === "CSV 내보내기", ); expect(button).toBeTruthy(); await act(async () => { button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(createObjectURL).toHaveBeenCalled(); expect(clickSpy).toHaveBeenCalled(); expect(revokeObjectURL).toHaveBeenCalledWith("blob:csv"); }); it("renders the forbidden state on 403 errors", async () => { fetchDevAuditLogsMock.mockRejectedValueOnce({ response: { status: 403 }, message: "Forbidden", }); const container = await renderPage(); expect(container.textContent).toContain("Forbidden:audit"); }); });