1
0
forked from baron/baron-sso

devfront 테스트 커버리지 추가 보강

This commit is contained in:
2026-06-01 17:37:13 +09:00
parent a4d457073a
commit 38605ac8a3
13 changed files with 1513 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
import { act } from "react-dom/test-utils";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import ClientsPage from "./ClientsPage";
const navigateMock = vi.fn();
const fetchClientsMock = vi.fn();
const fetchMeMock = vi.fn();
const fetchDeveloperRequestStatusMock = vi.fn();
const fetchMyTenantsMock = vi.fn();
const requestDeveloperAccessMock = vi.fn();
let authState = {
user: {
access_token: "access-token",
profile: {
role: "super_admin",
tenant_id: "tenant-1",
companyCode: "HANMAC",
name: "Dev Admin",
email: "dev@example.com",
phone: "010-0000-0000",
},
},
};
vi.mock("react-oidc-context", () => ({
useAuth: () => authState,
}));
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual<typeof import("react-router-dom")>(
"react-router-dom",
);
return {
...actual,
useNavigate: () => navigateMock,
};
});
vi.mock("../../lib/devApi", () => ({
fetchClients: () => fetchClientsMock(),
fetchMe: () => fetchMeMock(),
fetchDeveloperRequestStatus: () => fetchDeveloperRequestStatusMock(),
fetchMyTenants: () => fetchMyTenantsMock(),
requestDeveloperAccess: (...args: unknown[]) =>
requestDeveloperAccessMock(...args),
}));
vi.mock("../../lib/i18n", () => ({
t: (key: string, fallback?: string, vars?: Record<string, unknown>) => {
let text = fallback ?? key;
for (const [name, value] of Object.entries(vars ?? {})) {
text = text.replaceAll(`{{${name}}}`, String(value));
}
return text;
},
}));
const roots: Root[] = [];
afterEach(() => {
for (const root of roots.splice(0)) {
act(() => {
root.unmount();
});
}
vi.clearAllMocks();
document.body.innerHTML = "";
});
beforeEach(() => {
authState = {
user: {
access_token: "access-token",
profile: {
role: "super_admin",
tenant_id: "tenant-1",
companyCode: "HANMAC",
name: "Dev Admin",
email: "dev@example.com",
phone: "010-0000-0000",
},
},
};
fetchClientsMock.mockResolvedValue({
items: [],
limit: 100,
offset: 0,
});
fetchMeMock.mockResolvedValue({
role: "super_admin",
name: "Dev Admin",
email: "dev@example.com",
phone: "010-0000-0000",
});
fetchDeveloperRequestStatusMock.mockResolvedValue({ status: "none" });
fetchMyTenantsMock.mockResolvedValue([
{
id: "tenant-1",
name: "Hanmac",
slug: "hanmac",
type: "COMPANY",
parentId: null,
description: "",
status: "active",
memberCount: 10,
createdAt: "2026-05-01T00:00:00Z",
updatedAt: "2026-05-01T00:00:00Z",
},
]);
});
function makeClients(count: number) {
return Array.from({ length: count }, (_, index) => ({
id: `client-${index + 1}`,
name: `App ${index + 1}`,
type: index % 2 === 0 ? "private" : "pkce",
status: index % 2 === 0 ? "active" : "inactive",
createdAt: `2026-05-${String(index + 1).padStart(2, "0")}T00:00:00Z`,
redirectUris: [],
scopes: [],
metadata: {},
}));
}
async function setInputValue(input: HTMLInputElement, value: string) {
const descriptor = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
);
descriptor?.set?.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 0));
}
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 },
mutations: { retry: false },
},
});
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<ClientsPage />
</MemoryRouter>
</QueryClientProvider>,
);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
return container;
}
describe("ClientsPage", () => {
it("expands the list and applies search filters", async () => {
fetchClientsMock.mockResolvedValue({
items: makeClients(6),
limit: 100,
offset: 0,
});
const container = await renderPage();
expect(container.textContent).toContain("총 6개의 애플리케이션이 등록되어 있습니다.");
expect(container.textContent).toContain("App 6");
expect(container.textContent).toContain("App 2");
expect(container.textContent).not.toContain("App 1");
const moreButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "더보기",
);
expect(moreButton).toBeTruthy();
await act(async () => {
moreButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("App 6");
expect(container.textContent).toContain("접기");
const advancedButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "Advanced Filters",
);
expect(advancedButton).toBeTruthy();
await act(async () => {
advancedButton?.dispatchEvent(
new MouseEvent("click", { bubbles: true }),
);
});
const searchInput = Array.from(
container.querySelectorAll("input"),
).find((input) =>
input.getAttribute("placeholder")?.includes("클라이언트 이름/ID로 검색"),
) as HTMLInputElement | undefined;
expect(searchInput).toBeTruthy();
await act(async () => {
await setInputValue(searchInput!, "missing-client");
});
expect(container.textContent).toContain("조건에 맞는 연동 앱이 없습니다.");
const resetButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "초기화",
);
expect(resetButton).toBeTruthy();
await act(async () => {
resetButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await act(async () => {
await setInputValue(searchInput!, "");
});
expect(container.textContent).toContain("App 1");
});
it("navigates to the developer request page from empty states", async () => {
authState = {
user: {
access_token: "access-token",
profile: {
role: "user",
tenant_id: "tenant-1",
companyCode: "HANMAC",
name: "Requester",
email: "requester@example.com",
phone: "010-1234-5678",
},
},
};
fetchClientsMock.mockResolvedValue({
items: [],
limit: 100,
offset: 0,
});
fetchDeveloperRequestStatusMock.mockResolvedValue({ status: "none" });
fetchMeMock.mockResolvedValue({
role: "user",
name: "Requester",
email: "requester@example.com",
phone: "010-1234-5678",
});
const container = await renderPage();
expect(container.textContent).toContain("개발자 등록 신청하기");
const requestButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "개발자 등록 신청하기",
);
expect(requestButton).toBeTruthy();
await act(async () => {
requestButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(navigateMock).toHaveBeenCalledWith("/developer-requests");
});
});

View File

@@ -0,0 +1,65 @@
import { act } from "react-dom/test-utils";
import { createRoot, type Root } from "react-dom/client";
import type { ReactNode, ComponentProps } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ClientLogo } from "./ClientLogo";
vi.mock("../../../components/ui/avatar", () => ({
Avatar: ({ children }: { children: ReactNode }) => (
<div data-testid="avatar">{children}</div>
),
AvatarImage: (props: ComponentProps<"img">) => <img {...props} />,
AvatarFallback: ({ children }: { children: ReactNode }) => (
<div data-testid="fallback">{children}</div>
),
}));
const roots: Root[] = [];
afterEach(() => {
for (const root of roots.splice(0)) {
act(() => {
root.unmount();
});
}
document.body.innerHTML = "";
});
function renderLogo(client: Parameters<typeof ClientLogo>[0]["client"]) {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(<ClientLogo client={client} />);
});
return container;
}
describe("ClientLogo", () => {
it("renders the fallback icon when no logo url exists", () => {
const container = renderLogo({
name: "",
type: "private",
metadata: {},
});
expect(container.querySelectorAll("svg").length).toBeGreaterThan(0);
});
it("uses the logo image when a trimmed url is provided", () => {
const container = renderLogo({
name: "Gitea",
type: "pkce",
metadata: { logo_url: " https://example.com/logo.png " },
});
const image = container.querySelector("img");
expect(image).not.toBeNull();
expect(container.querySelector("[data-testid='fallback']")).not.toBeNull();
expect(image?.getAttribute("alt")).toContain("Gitea");
expect(image?.getAttribute("src")).toBe("https://example.com/logo.png");
});
});

View File

@@ -0,0 +1,181 @@
import { act } from "react-dom/test-utils";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ClientFederationPage } from "./ClientFederationPage";
let params: { id?: string } = { id: "client-a" };
const listIdpConfigsMock = vi.fn();
const createIdpConfigMock = vi.fn();
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual<typeof import("react-router-dom")>(
"react-router-dom",
);
return {
...actual,
useParams: () => params,
};
});
vi.mock("../../../lib/devApi", () => ({
listIdpConfigsForClient: (clientId: string) =>
listIdpConfigsMock(clientId),
createIdpConfigForClient: (payload: unknown) =>
createIdpConfigMock(payload),
}));
vi.mock("../../../lib/i18n", () => ({
t: (key: string, fallback?: string, vars?: Record<string, unknown>) => {
let text = fallback ?? key;
for (const [name, value] of Object.entries(vars ?? {})) {
text = text.replaceAll(`{{${name}}}`, String(value));
}
return text;
},
}));
const roots: Root[] = [];
afterEach(() => {
for (const root of roots.splice(0)) {
act(() => {
root.unmount();
});
}
vi.clearAllMocks();
document.body.innerHTML = "";
});
beforeEach(() => {
params = { id: "client-a" };
listIdpConfigsMock.mockResolvedValue([
{
id: "idp-1",
client_id: "client-a",
provider_type: "oidc",
display_name: "Workspace OIDC",
status: "active",
issuer_url: "https://accounts.example",
oidc_client_id: "oidc-client",
scopes: "openid email profile",
createdAt: "2026-05-01T00:00:00Z",
updatedAt: "2026-05-01T00:00:00Z",
},
]);
createIdpConfigMock.mockResolvedValue({
id: "idp-2",
client_id: "client-a",
provider_type: "oidc",
display_name: "New Provider",
status: "active",
createdAt: "2026-05-02T00:00:00Z",
updatedAt: "2026-05-02T00:00:00Z",
});
});
async function setInputValue(input: HTMLInputElement, value: string) {
const descriptor = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
);
descriptor?.set?.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 0));
}
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 },
mutations: { retry: false },
},
});
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<ClientFederationPage />
</QueryClientProvider>,
);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
return container;
}
describe("ClientFederationPage", () => {
it("shows a missing client id message when no route param exists", async () => {
params = {};
const container = await renderPage();
expect(container.textContent).toContain("Client ID is missing");
});
it("opens the create modal and submits a new IdP config", async () => {
const container = await renderPage();
expect(container.textContent).toContain("Workspace OIDC");
const addButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "Add Provider",
);
expect(addButton).toBeTruthy();
await act(async () => {
addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("Add Identity Provider");
const displayName = container.querySelector(
'input[name="display_name"]',
) as HTMLInputElement | null;
const issuerUrl = container.querySelector(
'input[name="issuer_url"]',
) as HTMLInputElement | null;
const clientId = container.querySelector(
'input[name="oidc_client_id"]',
) as HTMLInputElement | null;
const clientSecret = container.querySelector(
'input[name="oidc_client_secret"]',
) as HTMLInputElement | null;
expect(displayName).toBeTruthy();
expect(issuerUrl).toBeTruthy();
expect(clientId).toBeTruthy();
expect(clientSecret).toBeTruthy();
await act(async () => {
await setInputValue(displayName!, "New Provider");
await setInputValue(issuerUrl!, "https://login.example");
await setInputValue(clientId!, "client-oidc");
await setInputValue(clientSecret!, "secret-value");
});
const submitButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "Save Configuration",
);
expect(submitButton).toBeTruthy();
await act(async () => {
submitButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 0));
});
expect(createIdpConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
client_id: "client-a",
display_name: "New Provider",
issuer_url: "https://login.example",
oidc_client_id: "client-oidc",
oidc_client_secret: "secret-value",
}),
);
});
});