forked from baron/baron-sso
245 lines
7.4 KiB
TypeScript
245 lines
7.4 KiB
TypeScript
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(
|
|
<QueryClientProvider client={queryClient}>
|
|
<DataIntegrityPage />
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
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<typeof integrityReport>((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();
|
|
});
|
|
});
|