forked from baron/baron-sso
권한부여 및 정합성 검사 추가
This commit is contained in:
@@ -3,14 +3,20 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import type React from "react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fetchAdminRPUsageDaily } from "../../lib/adminApi";
|
||||
import {
|
||||
fetchAdminRPUsageDaily,
|
||||
fetchDataIntegrityReport,
|
||||
} from "../../lib/adminApi";
|
||||
import AuthPage from "../auth/AuthPage";
|
||||
import GlobalOverviewPage from "./GlobalOverviewPage";
|
||||
|
||||
let currentRole = "super_admin";
|
||||
|
||||
vi.mock("../../lib/adminApi", () => ({
|
||||
fetchMe: vi.fn(async () => ({ role: "super_admin" })),
|
||||
fetchMe: vi.fn(async () => ({ role: currentRole })),
|
||||
fetchAdminOverviewStats: vi.fn(async () => ({
|
||||
totalTenants: 10,
|
||||
totalUsers: 152,
|
||||
oidcClients: 3,
|
||||
auditEvents24h: 18,
|
||||
})),
|
||||
@@ -93,6 +99,30 @@ vi.mock("../../lib/adminApi", () => ({
|
||||
},
|
||||
],
|
||||
})),
|
||||
fetchDataIntegrityReport: vi.fn(async () => ({
|
||||
status: "fail",
|
||||
checkedAt: "2026-05-14T00:00:00Z",
|
||||
summary: {
|
||||
totalChecks: 5,
|
||||
passed: 4,
|
||||
warnings: 0,
|
||||
failures: 1,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
key: "tenant_integrity",
|
||||
label: "테넌트 정합성",
|
||||
status: "pass",
|
||||
checks: [],
|
||||
},
|
||||
{
|
||||
key: "user_integrity",
|
||||
label: "사용자 정합성",
|
||||
status: "fail",
|
||||
checks: [],
|
||||
},
|
||||
],
|
||||
})),
|
||||
}));
|
||||
|
||||
function renderWithProviders(ui: React.ReactElement) {
|
||||
@@ -112,6 +142,7 @@ function renderWithProviders(ui: React.ReactElement) {
|
||||
|
||||
describe("admin overview and auth guard pages", () => {
|
||||
beforeEach(() => {
|
||||
currentRole = "super_admin";
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -132,15 +163,18 @@ describe("admin overview and auth guard pages", () => {
|
||||
expect(screen.queryByText("ReBAC 권한 검증 도구")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders overview summary metrics from the admin stats API", async () => {
|
||||
it("renders overview tenant count from the fully fetched tenant list", async () => {
|
||||
renderWithProviders(<GlobalOverviewPage />);
|
||||
|
||||
expect(
|
||||
(await screen.findByText("전체 테넌트 수")).parentElement,
|
||||
).toHaveTextContent("10");
|
||||
).toHaveTextContent("3");
|
||||
expect(screen.getByText("OIDC 클라이언트").parentElement).toHaveTextContent(
|
||||
"3",
|
||||
);
|
||||
expect(screen.getByText("전체 사용자 수").parentElement).toHaveTextContent(
|
||||
"152",
|
||||
);
|
||||
expect(screen.getByText("24시간 이벤트").parentElement).toHaveTextContent(
|
||||
"18",
|
||||
);
|
||||
@@ -172,6 +206,26 @@ describe("admin overview and auth guard pages", () => {
|
||||
expect(await screen.findAllByText("05월")).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it("shows the latest integrity summary at the bottom for super admins only", async () => {
|
||||
renderWithProviders(<GlobalOverviewPage />);
|
||||
|
||||
expect(await screen.findByText("정합성 최종 검증")).toBeInTheDocument();
|
||||
expect(screen.getByText("실패 1건")).toBeInTheDocument();
|
||||
expect(screen.getByText("테넌트 정합성")).toBeInTheDocument();
|
||||
expect(screen.getByText("사용자 정합성")).toBeInTheDocument();
|
||||
expect(fetchDataIntegrityReport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not fetch or show the integrity summary for non-super admins", async () => {
|
||||
currentRole = "tenant_admin";
|
||||
|
||||
renderWithProviders(<GlobalOverviewPage />);
|
||||
|
||||
await screen.findByText("회사별 앱별 로그인요청/기타 요청 현황");
|
||||
expect(screen.queryByText("정합성 최종 검증")).not.toBeInTheDocument();
|
||||
expect(fetchDataIntegrityReport).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("moves the permission checker to the auth guard page and removes mock guardrails", () => {
|
||||
renderWithProviders(<AuthPage />);
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
BarChart3,
|
||||
CheckCircle2,
|
||||
Database,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
@@ -9,12 +11,14 @@ import {
|
||||
import { type ReactNode, useMemo, useState } from "react";
|
||||
import { RoleGuard } from "../../components/auth/RoleGuard";
|
||||
import {
|
||||
type DataIntegrityStatus,
|
||||
type RPUsageDailyMetric,
|
||||
type RPUsagePeriod,
|
||||
type TenantSummary,
|
||||
fetchAdminOverviewStats,
|
||||
fetchAdminRPUsageDaily,
|
||||
fetchAllTenants,
|
||||
fetchDataIntegrityReport,
|
||||
} from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
@@ -151,6 +155,102 @@ function OverviewMetric({
|
||||
);
|
||||
}
|
||||
|
||||
function formatOverviewDateTime(value?: string) {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat("ko-KR", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function integrityStatusText(status: DataIntegrityStatus) {
|
||||
switch (status) {
|
||||
case "pass":
|
||||
return "정상";
|
||||
case "warning":
|
||||
return "주의";
|
||||
default:
|
||||
return "실패";
|
||||
}
|
||||
}
|
||||
|
||||
function integrityStatusClass(status: DataIntegrityStatus) {
|
||||
switch (status) {
|
||||
case "pass":
|
||||
return "text-emerald-700 dark:text-emerald-300";
|
||||
case "warning":
|
||||
return "text-amber-700 dark:text-amber-300";
|
||||
default:
|
||||
return "text-destructive";
|
||||
}
|
||||
}
|
||||
|
||||
function IntegrityOverviewSummary() {
|
||||
const { data, isError } = useQuery({
|
||||
queryKey: ["admin-overview-integrity"],
|
||||
queryFn: fetchDataIntegrityReport,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<section className="border-t border-border/60 pt-4">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<AlertTriangle size={16} />
|
||||
<span>정합성 최종 검증 결과를 불러오지 못했습니다.</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="border-t border-border/60 pt-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{data.status === "pass" ? (
|
||||
<CheckCircle2 size={18} className="text-emerald-600" />
|
||||
) : (
|
||||
<AlertTriangle size={18} className="text-amber-600" />
|
||||
)}
|
||||
<h3 className="text-base font-semibold">정합성 최종 검증</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||
<span
|
||||
className={`font-semibold ${integrityStatusClass(data.status)}`}
|
||||
>
|
||||
{integrityStatusText(data.status)}
|
||||
</span>
|
||||
<span className="tabular-nums">실패 {data.summary.failures}건</span>
|
||||
<span className="text-muted-foreground">
|
||||
{formatOverviewDateTime(data.checkedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 text-sm sm:grid-cols-2">
|
||||
{data.sections.map((section) => (
|
||||
<div
|
||||
key={section.key}
|
||||
className="flex items-center justify-between gap-3 rounded border border-border/60 px-3 py-2"
|
||||
>
|
||||
<span>{section.label}</span>
|
||||
<span
|
||||
className={`font-medium ${integrityStatusClass(section.status)}`}
|
||||
>
|
||||
{integrityStatusText(section.status)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function RPUsageMixedChart({
|
||||
rows,
|
||||
filters,
|
||||
@@ -371,6 +471,7 @@ function GlobalOverviewPage() {
|
||||
retry: false,
|
||||
});
|
||||
const stats = statsQuery.data;
|
||||
const visibleTenantCount = tenantsQuery.data?.items.length;
|
||||
const usageRows = usageQuery.data?.items ?? [];
|
||||
const metric = (value: number | undefined) =>
|
||||
value === undefined ? "-" : value.toLocaleString();
|
||||
@@ -444,7 +545,7 @@ function GlobalOverviewPage() {
|
||||
"ui.admin.overview.summary.total_tenants",
|
||||
"전체 테넌트 수",
|
||||
)}
|
||||
value={metric(stats?.totalTenants)}
|
||||
value={metric(visibleTenantCount ?? stats?.totalTenants)}
|
||||
/>
|
||||
<OverviewMetric
|
||||
icon={<ShieldCheck size={14} />}
|
||||
@@ -454,6 +555,11 @@ function GlobalOverviewPage() {
|
||||
)}
|
||||
value={metric(stats?.oidcClients)}
|
||||
/>
|
||||
<OverviewMetric
|
||||
icon={<Users size={14} />}
|
||||
label={t("ui.admin.overview.summary.total_users", "전체 사용자 수")}
|
||||
value={metric(stats?.totalUsers)}
|
||||
/>
|
||||
</RoleGuard>
|
||||
<OverviewMetric
|
||||
icon={<Activity size={14} />}
|
||||
@@ -491,6 +597,10 @@ function GlobalOverviewPage() {
|
||||
period={period}
|
||||
/>
|
||||
)}
|
||||
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<IntegrityOverviewSummary />
|
||||
</RoleGuard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user