forked from baron/baron-sso
test: raise frontend coverage baselines
This commit is contained in:
166
orgfront/src/components/layout/AppLayout.test.tsx
Normal file
166
orgfront/src/components/layout/AppLayout.test.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import AppLayout from "./AppLayout";
|
||||
|
||||
const authState = {
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
activeNavigator: undefined as string | undefined,
|
||||
error: null as Error | null,
|
||||
user: {
|
||||
access_token: "access-token",
|
||||
expires_at: Math.floor(Date.now() / 1000) + 120,
|
||||
profile: {
|
||||
sub: "user-1",
|
||||
name: "Org Admin",
|
||||
email: "org@example.com",
|
||||
role: "super_admin",
|
||||
},
|
||||
},
|
||||
signinSilent: vi.fn(),
|
||||
removeUser: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("react-oidc-context", () => ({
|
||||
useAuth: () => authState,
|
||||
}));
|
||||
|
||||
vi.mock("../../features/auth/authApi", () => ({
|
||||
fetchMe: vi.fn(async () => ({
|
||||
id: "user-1",
|
||||
name: "Fetched Org Admin",
|
||||
email: "fetched@example.com",
|
||||
role: "super_admin",
|
||||
})),
|
||||
}));
|
||||
|
||||
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[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
authState.isAuthenticated = true;
|
||||
authState.isLoading = false;
|
||||
authState.activeNavigator = undefined;
|
||||
authState.error = null;
|
||||
authState.user.expires_at = Math.floor(Date.now() / 1000) + 120;
|
||||
authState.signinSilent.mockReset();
|
||||
authState.signinSilent.mockResolvedValue(undefined);
|
||||
authState.removeUser.mockReset();
|
||||
window.localStorage.clear();
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function renderLayout(initialEntry = "/clients") {
|
||||
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 initialEntries={[initialEntry]}>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppLayout />}>
|
||||
<Route path="clients" element={<div>Client outlet</div>} />
|
||||
<Route path="profile" element={<div>Profile outlet</div>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("orgfront AppLayout", () => {
|
||||
it("renders shell navigation, profile summary, and outlet content", async () => {
|
||||
const container = await renderLayout();
|
||||
|
||||
expect(container.textContent).toContain("Developer Console");
|
||||
expect(container.textContent).toContain("Clients");
|
||||
expect(container.textContent).toContain("Client outlet");
|
||||
expect(container.textContent).toContain("Fetched Org Admin");
|
||||
expect(document.documentElement.classList.contains("light")).toBe(true);
|
||||
});
|
||||
|
||||
it("toggles profile menu, navigates to profile, toggles theme, and logs out", async () => {
|
||||
const container = await renderLayout();
|
||||
|
||||
const themeButton = container.querySelector(
|
||||
'button[aria-label="테마 전환"]',
|
||||
) as HTMLButtonElement;
|
||||
await act(async () => {
|
||||
themeButton.click();
|
||||
});
|
||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
||||
|
||||
const profileButton = container.querySelector(
|
||||
'button[aria-label="계정 메뉴 열기"]',
|
||||
) as HTMLButtonElement;
|
||||
await act(async () => {
|
||||
profileButton.click();
|
||||
});
|
||||
expect(container.textContent).toContain("Account");
|
||||
|
||||
const profileMenuItem = Array.from(
|
||||
container.querySelectorAll('button[role="menuitem"]'),
|
||||
).find((button) => button.textContent?.includes("내 정보"));
|
||||
await act(async () => {
|
||||
(profileMenuItem as HTMLButtonElement).click();
|
||||
});
|
||||
expect(container.textContent).toContain("Profile outlet");
|
||||
|
||||
const logoutButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.includes("Logout"),
|
||||
);
|
||||
await act(async () => {
|
||||
(logoutButton as HTMLButtonElement).click();
|
||||
});
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
expect(authState.removeUser).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attempts silent renewal after user action when the session is expiring", async () => {
|
||||
authState.user.expires_at = Math.floor(Date.now() / 1000) + 60;
|
||||
await renderLayout();
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab" }));
|
||||
});
|
||||
|
||||
expect(authState.signinSilent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
95
orgfront/src/components/ui/basic.test.tsx
Normal file
95
orgfront/src/components/ui/basic.test.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
|
||||
import { Badge } from "./badge";
|
||||
import { Input } from "./input";
|
||||
import { Label } from "./label";
|
||||
import { Separator } from "./separator";
|
||||
import { Switch } from "./switch";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "./table";
|
||||
import { Textarea } from "./textarea";
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
const render = async (element: React.ReactElement) => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(element);
|
||||
});
|
||||
return root;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (container) {
|
||||
container.remove();
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe("orgfront UI wrappers", () => {
|
||||
it("renders form, badge, avatar, switch, separator, and table wrappers", async () => {
|
||||
const root = await render(
|
||||
<div>
|
||||
<Badge className="custom-badge" variant="secondary">
|
||||
Active
|
||||
</Badge>
|
||||
<Avatar className="custom-avatar">
|
||||
<AvatarImage alt="Org user" src="/avatar.png" />
|
||||
<AvatarFallback>OU</AvatarFallback>
|
||||
</Avatar>
|
||||
<Label className="custom-label" htmlFor="name">
|
||||
Name
|
||||
</Label>
|
||||
<Input id="name" className="custom-input" defaultValue="Org User" />
|
||||
<Textarea className="custom-textarea" defaultValue="Memo" />
|
||||
<Switch className="custom-switch" defaultChecked />
|
||||
<Separator className="custom-separator" />
|
||||
<Table className="custom-table">
|
||||
<TableCaption>Members</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Org User</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell>Total</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>,
|
||||
);
|
||||
|
||||
expect(container?.textContent).toContain("Active");
|
||||
expect(container?.textContent).toContain("OU");
|
||||
expect(container?.querySelector(".custom-input")).not.toBeNull();
|
||||
expect(container?.querySelector(".custom-switch")).not.toBeNull();
|
||||
expect(container?.querySelector(".custom-separator")).not.toBeNull();
|
||||
expect(container?.textContent).toContain("Members");
|
||||
expect(container?.textContent).toContain("Total");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
307
orgfront/src/features/coverage/pageSmoke.test.tsx
Normal file
307
orgfront/src/features/coverage/pageSmoke.test.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||
import AuditLogsPage from "../audit/AuditLogsPage";
|
||||
import AuthPage from "../auth/AuthPage";
|
||||
import ClientConsentsPage from "../clients/ClientConsentsPage";
|
||||
import ClientDetailsPage from "../clients/ClientDetailsPage";
|
||||
import ClientGeneralPage from "../clients/ClientGeneralPage";
|
||||
import ClientsPage from "../clients/ClientsPage";
|
||||
import { ClientFederationPage } from "../clients/routes/ClientFederationPage";
|
||||
import DashboardPage from "../dashboard/DashboardPage";
|
||||
import ProfilePage from "../profile/ProfilePage";
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
vi.mock("react-oidc-context", () => ({
|
||||
useAuth: () => ({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
user: {
|
||||
access_token: "access-token",
|
||||
profile: {
|
||||
sub: "user-1",
|
||||
role: "super_admin",
|
||||
tenant_id: "tenant-1",
|
||||
},
|
||||
},
|
||||
signinRedirect: vi.fn(),
|
||||
removeUser: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
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 clientSummary = {
|
||||
id: "client-a",
|
||||
name: "Console App",
|
||||
type: "private" as const,
|
||||
status: "active" as const,
|
||||
createdAt: "2026-05-01T00:00:00Z",
|
||||
redirectUris: ["https://app.example/callback"],
|
||||
scopes: ["openid", "profile"],
|
||||
tokenEndpointAuthMethod: "client_secret_basic",
|
||||
metadata: {
|
||||
headless_login_enabled: true,
|
||||
headless_login_jwks_uri: "https://app.example/jwks.json",
|
||||
},
|
||||
};
|
||||
|
||||
const clientDetail = {
|
||||
client: {
|
||||
...clientSummary,
|
||||
clientSecret: "secret-value",
|
||||
jwksUri: "https://app.example/jwks.json",
|
||||
grantTypes: ["authorization_code"],
|
||||
responseTypes: ["code"],
|
||||
},
|
||||
endpoints: {
|
||||
discovery: "https://sso.example/.well-known/openid-configuration",
|
||||
issuer: "https://sso.example",
|
||||
authorization: "https://sso.example/oauth2/auth",
|
||||
token: "https://sso.example/oauth2/token",
|
||||
userinfo: "https://sso.example/userinfo",
|
||||
},
|
||||
headlessJwksCache: {
|
||||
clientId: "client-a",
|
||||
jwksUri: "https://app.example/jwks.json",
|
||||
cachedAt: "2026-05-01T00:00:00Z",
|
||||
expiresAt: "2026-05-02T00:00:00Z",
|
||||
lastRefreshStatus: "success" as const,
|
||||
cachedKids: ["kid-1"],
|
||||
parsedKeys: [{ kid: "kid-1", kty: "RSA", use: "sig", alg: "RS256" }],
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("../../lib/devApi", () => ({
|
||||
fetchClients: vi.fn(async () => ({
|
||||
items: [clientSummary],
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
})),
|
||||
fetchDevStats: vi.fn(async () => ({
|
||||
total_clients: 1,
|
||||
active_sessions: 12,
|
||||
auth_failures_24h: 2,
|
||||
})),
|
||||
fetchClient: vi.fn(async () => clientDetail),
|
||||
updateClientStatus: vi.fn(async () => clientDetail),
|
||||
createClient: vi.fn(async () => clientDetail),
|
||||
updateClient: vi.fn(async () => clientDetail),
|
||||
rotateClientSecret: vi.fn(async () => clientDetail),
|
||||
refreshHeadlessJwksCache: vi.fn(async () => clientDetail),
|
||||
revokeHeadlessJwksCache: vi.fn(async () => undefined),
|
||||
deleteClient: vi.fn(async () => undefined),
|
||||
fetchConsents: vi.fn(async () => ({
|
||||
items: [
|
||||
{
|
||||
subject: "user-1",
|
||||
userName: "Consent User",
|
||||
clientId: "client-a",
|
||||
clientName: "Console App",
|
||||
grantedScopes: ["openid", "profile"],
|
||||
authenticatedAt: "2026-05-01T02:00:00Z",
|
||||
createdAt: "2026-05-01T00:00:00Z",
|
||||
status: "active",
|
||||
tenantId: "tenant-1",
|
||||
tenantName: "Hanmac",
|
||||
},
|
||||
],
|
||||
})),
|
||||
revokeConsent: vi.fn(async () => undefined),
|
||||
listIdpConfigsForClient: vi.fn(async () => [
|
||||
{
|
||||
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",
|
||||
},
|
||||
]),
|
||||
createIdpConfigForClient: vi.fn(async (payload) => ({
|
||||
id: "idp-1",
|
||||
...payload,
|
||||
status: "active",
|
||||
createdAt: "2026-05-01T00:00:00Z",
|
||||
updatedAt: "2026-05-01T00:00:00Z",
|
||||
})),
|
||||
updateIdpConfig: vi.fn(async (_clientId, idpId, payload) => ({
|
||||
id: idpId,
|
||||
client_id: "client-a",
|
||||
provider_type: "oidc",
|
||||
display_name: "Provider",
|
||||
status: "active",
|
||||
createdAt: "2026-05-01T00:00:00Z",
|
||||
updatedAt: "2026-05-01T00:00:00Z",
|
||||
...payload,
|
||||
})),
|
||||
deleteIdpConfig: vi.fn(async () => undefined),
|
||||
fetchDevAuditLogs: vi.fn(async () => ({
|
||||
items: [
|
||||
{
|
||||
event_id: "event-1",
|
||||
timestamp: "2026-05-01T00:00:00Z",
|
||||
user_id: "user-1",
|
||||
event_type: "client.update",
|
||||
status: "success",
|
||||
ip_address: "127.0.0.1",
|
||||
user_agent: "vitest",
|
||||
details: JSON.stringify({
|
||||
action: "client.update",
|
||||
target_id: "client-a",
|
||||
tenant_id: "tenant-1",
|
||||
request_id: "req-1",
|
||||
}),
|
||||
},
|
||||
],
|
||||
limit: 50,
|
||||
})),
|
||||
fetchMyTenants: vi.fn(async () => [
|
||||
{ id: "tenant-1", name: "Hanmac", slug: "hanmac" },
|
||||
]),
|
||||
}));
|
||||
|
||||
vi.mock("../auth/authApi", () => ({
|
||||
fetchMe: vi.fn(async () => ({
|
||||
id: "user-1",
|
||||
name: "Org User",
|
||||
email: "org@example.com",
|
||||
phone: "010-0000-0000",
|
||||
role: "super_admin",
|
||||
department: "Platform",
|
||||
tenantId: "tenant-1",
|
||||
tenant: { id: "tenant-1", name: "Hanmac", slug: "hanmac" },
|
||||
})),
|
||||
}));
|
||||
|
||||
const roots: Root[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function renderWithProviders(
|
||||
element: React.ReactElement,
|
||||
initialEntry = "/",
|
||||
) {
|
||||
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 initialEntries={[initialEntry]}>{element}</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("orgfront page smoke coverage", () => {
|
||||
it("renders the dashboard content", async () => {
|
||||
const container = await renderWithProviders(<DashboardPage />);
|
||||
|
||||
expect(container.textContent).toContain("RP 등록 현황");
|
||||
expect(container.textContent).toContain("Stack readiness");
|
||||
});
|
||||
|
||||
it("renders static auth guidance and forbidden messages", async () => {
|
||||
const auth = await renderWithProviders(<AuthPage />);
|
||||
expect(auth.textContent).toContain("Admin auth guardrails");
|
||||
expect(auth.textContent).toContain("TTL discipline");
|
||||
|
||||
const forbidden = await renderWithProviders(
|
||||
<ForbiddenMessage resourceToken="clients" />,
|
||||
);
|
||||
expect(forbidden.textContent).toContain("연동 앱 접근 권한 없음");
|
||||
});
|
||||
|
||||
it("renders client list, detail, edit, consent, and federation pages", async () => {
|
||||
const clients = await renderWithProviders(
|
||||
<Routes>
|
||||
<Route path="/clients" element={<ClientsPage />} />
|
||||
</Routes>,
|
||||
"/clients",
|
||||
);
|
||||
expect(clients.textContent).toContain("Console App");
|
||||
|
||||
const details = await renderWithProviders(
|
||||
<Routes>
|
||||
<Route path="/clients/:id" element={<ClientDetailsPage />} />
|
||||
</Routes>,
|
||||
"/clients/client-a",
|
||||
);
|
||||
expect(details.textContent).toContain("Client Secret");
|
||||
expect(details.textContent).toContain("https://sso.example/oauth2/token");
|
||||
|
||||
const general = await renderWithProviders(
|
||||
<Routes>
|
||||
<Route path="/clients/:id/edit" element={<ClientGeneralPage />} />
|
||||
</Routes>,
|
||||
"/clients/client-a/edit",
|
||||
);
|
||||
expect(general.textContent).toContain("Console App");
|
||||
|
||||
const consents = await renderWithProviders(
|
||||
<Routes>
|
||||
<Route path="/clients/:id/consents" element={<ClientConsentsPage />} />
|
||||
</Routes>,
|
||||
"/clients/client-a/consents",
|
||||
);
|
||||
expect(consents.textContent).toContain("Consent User");
|
||||
|
||||
const federation = await renderWithProviders(
|
||||
<Routes>
|
||||
<Route
|
||||
path="/clients/:id/federation"
|
||||
element={<ClientFederationPage />}
|
||||
/>
|
||||
</Routes>,
|
||||
"/clients/client-a/federation",
|
||||
);
|
||||
expect(federation.textContent).toContain("Workspace OIDC");
|
||||
});
|
||||
|
||||
it("renders audit logs and profile pages", async () => {
|
||||
const auditLogs = await renderWithProviders(<AuditLogsPage />);
|
||||
expect(auditLogs.textContent).toContain("client.update");
|
||||
expect(auditLogs.textContent).toContain("Loaded 1 rows");
|
||||
|
||||
const profile = await renderWithProviders(<ProfilePage />);
|
||||
expect(profile.textContent).toContain("Org User");
|
||||
expect(profile.textContent).toContain("Hanmac");
|
||||
});
|
||||
});
|
||||
@@ -1,23 +1,161 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const apiClient = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
};
|
||||
|
||||
const fetchAllCursorPages = vi.fn(async () => ({
|
||||
items: [{ id: "tenant-1", name: "Tenant", slug: "tenant" }],
|
||||
total: 1,
|
||||
}));
|
||||
|
||||
vi.mock("./apiClient", () => ({
|
||||
default: apiClient,
|
||||
}));
|
||||
|
||||
vi.mock("./auth", () => ({
|
||||
userManager: {
|
||||
getUser: vi.fn(async () => ({ access_token: "access-token" })),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../../common/core/pagination", () => ({
|
||||
fetchAllCursorPages,
|
||||
}));
|
||||
|
||||
describe("orgfront adminApi user tenant payloads", () => {
|
||||
beforeEach(() => {
|
||||
apiClient.get.mockReset();
|
||||
apiClient.post.mockReset();
|
||||
apiClient.put.mockReset();
|
||||
apiClient.delete.mockReset();
|
||||
|
||||
apiClient.get.mockResolvedValue({ data: { ok: true } });
|
||||
apiClient.post.mockResolvedValue({ data: { ok: true } });
|
||||
apiClient.put.mockResolvedValue({ data: { ok: true } });
|
||||
apiClient.delete.mockResolvedValue({ data: { ok: true } });
|
||||
fetchAllCursorPages.mockClear();
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
it("routes read APIs to their documented orgfront admin endpoints", async () => {
|
||||
const adminApi = await import("./adminApi");
|
||||
|
||||
await adminApi.fetchAuditLogs(10, "cursor-a");
|
||||
await adminApi.fetchTenants(25, 50, "parent-1", "cursor-b");
|
||||
await adminApi.fetchAllTenants({ pageSize: 200, parentId: "parent-1" });
|
||||
await adminApi.fetchTenant("tenant-1");
|
||||
await adminApi.fetchTenantAdmins("tenant-1");
|
||||
await adminApi.fetchTenantOwners("tenant-1");
|
||||
await adminApi.fetchGroups("tenant-1");
|
||||
await adminApi.fetchGroup("tenant-1", "group-1");
|
||||
await adminApi.fetchImportProgress("tenant-1", "progress-1");
|
||||
await adminApi.fetchGroupRoles("tenant-1", "group-1");
|
||||
await adminApi.fetchApiKeys(20, 40);
|
||||
await adminApi.fetchUsers(30, 60, "admin", "tenant");
|
||||
await adminApi.fetchUser("user-1");
|
||||
await adminApi.fetchPasswordPolicy();
|
||||
await adminApi.fetchUserRpHistory("user-1");
|
||||
await adminApi.fetchMe();
|
||||
await adminApi.fetchRelyingParties("tenant-1");
|
||||
await adminApi.fetchAllRelyingParties();
|
||||
await adminApi.fetchRelyingParty("client-1");
|
||||
await adminApi.fetchRPOwners("client-1");
|
||||
await adminApi.fetchPublicOrgChart("public-token");
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/v1/audit", {
|
||||
params: { limit: 10, cursor: "cursor-a" },
|
||||
});
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/v1/admin/tenants", {
|
||||
params: {
|
||||
limit: 25,
|
||||
offset: 50,
|
||||
parentId: "parent-1",
|
||||
cursor: "cursor-b",
|
||||
},
|
||||
});
|
||||
expect(fetchAllCursorPages).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: "/v1/admin/tenants",
|
||||
pageSize: 200,
|
||||
params: { parentId: "parent-1" },
|
||||
}),
|
||||
);
|
||||
expect(apiClient.get).toHaveBeenCalledWith(
|
||||
"/v1/admin/tenants/tenant-1/organization/group-1/roles",
|
||||
);
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/v1/public/orgchart", {
|
||||
params: { token: "public-token" },
|
||||
});
|
||||
});
|
||||
|
||||
it("routes mutation APIs to their documented orgfront admin endpoints", async () => {
|
||||
const adminApi = await import("./adminApi");
|
||||
|
||||
await adminApi.createTenant({ name: "Tenant", slug: "tenant" });
|
||||
await adminApi.updateTenant("tenant-1", { status: "inactive" });
|
||||
await adminApi.deleteTenant("tenant-1");
|
||||
await adminApi.deleteTenantsBulk(["tenant-1"]);
|
||||
await adminApi.approveTenant("tenant-1");
|
||||
await adminApi.addTenantAdmin("tenant-1", "user-1");
|
||||
await adminApi.removeTenantAdmin("tenant-1", "user-1");
|
||||
await adminApi.addTenantOwner("tenant-1", "user-1");
|
||||
await adminApi.removeTenantOwner("tenant-1", "user-1");
|
||||
await adminApi.createGroup("tenant-1", { name: "Group" });
|
||||
await adminApi.deleteGroup("tenant-1", "group-1");
|
||||
await adminApi.addGroupMember("tenant-1", "group-1", "user-1");
|
||||
await adminApi.removeGroupMember("tenant-1", "group-1", "user-1");
|
||||
await adminApi.importOrgChart(
|
||||
"tenant-1",
|
||||
new File(["name"], "org.csv"),
|
||||
"progress-1",
|
||||
);
|
||||
await adminApi.assignGroupRole("tenant-1", "group-1", "tenant-2", "owner");
|
||||
await adminApi.removeGroupRole("tenant-1", "group-1", "tenant-2", "owner");
|
||||
await adminApi.createApiKey({ name: "key", scopes: ["read"] });
|
||||
await adminApi.deleteApiKey("key-1");
|
||||
await adminApi.createUser({ email: "user@example.com", name: "User" });
|
||||
await adminApi.bulkCreateUsers([
|
||||
{ email: "user@example.com", name: "User", metadata: {} },
|
||||
]);
|
||||
await adminApi.bulkUpdateUsers({ userIds: ["user-1"], status: "inactive" });
|
||||
await adminApi.bulkDeleteUsers(["user-1"]);
|
||||
await adminApi.updateUser("user-1", { status: "active" });
|
||||
await adminApi.deleteUser("user-1");
|
||||
await adminApi.createRelyingParty("tenant-1", {
|
||||
client_name: "RP",
|
||||
redirect_uris: ["https://rp.example/callback"],
|
||||
});
|
||||
await adminApi.updateRelyingParty("client-1", {
|
||||
client_name: "RP",
|
||||
redirect_uris: ["https://rp.example/callback"],
|
||||
});
|
||||
await adminApi.deleteRelyingParty("client-1");
|
||||
await adminApi.addRPOwner("client-1", "User:user-1");
|
||||
await adminApi.removeRPOwner("client-1", "User:user-1");
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
"/v1/admin/tenants/tenant-1/organization/group-1/members",
|
||||
{ userId: "user-1" },
|
||||
);
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
"/v1/admin/tenants/tenant-1/organization/import?progressId=progress-1",
|
||||
expect.any(FormData),
|
||||
{ headers: { "Content-Type": "multipart/form-data" } },
|
||||
);
|
||||
expect(apiClient.put).toHaveBeenCalledWith("/v1/admin/users/user-1", {
|
||||
status: "active",
|
||||
});
|
||||
expect(apiClient.delete).toHaveBeenCalledWith(
|
||||
"/v1/admin/relying-parties/client-1/owners/User:user-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("sends tenantSlug without remapping it to companyCode when creating a user", async () => {
|
||||
const { createUser } = await import("./adminApi");
|
||||
apiClient.post.mockResolvedValue({ data: {} });
|
||||
|
||||
await createUser({
|
||||
email: "user@test.com",
|
||||
@@ -34,7 +172,6 @@ describe("orgfront adminApi user tenant payloads", () => {
|
||||
|
||||
it("sends tenantSlug without remapping it to companyCode when updating a user", async () => {
|
||||
const { updateUser } = await import("./adminApi");
|
||||
apiClient.put.mockResolvedValue({ data: {} });
|
||||
|
||||
await updateUser("user-id", { tenantSlug: "new-tenant" });
|
||||
|
||||
@@ -47,8 +184,6 @@ describe("orgfront adminApi user tenant payloads", () => {
|
||||
|
||||
it("keeps tenantSlug payloads unchanged for bulk user APIs", async () => {
|
||||
const { bulkCreateUsers, bulkUpdateUsers } = await import("./adminApi");
|
||||
apiClient.post.mockResolvedValue({ data: {} });
|
||||
apiClient.put.mockResolvedValue({ data: {} });
|
||||
|
||||
await bulkCreateUsers([
|
||||
{
|
||||
|
||||
139
orgfront/src/lib/devApi.test.ts
Normal file
139
orgfront/src/lib/devApi.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const apiClient = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("./apiClient", () => ({
|
||||
default: apiClient,
|
||||
}));
|
||||
|
||||
describe("orgfront devApi", () => {
|
||||
beforeEach(() => {
|
||||
apiClient.get.mockReset();
|
||||
apiClient.post.mockReset();
|
||||
apiClient.put.mockReset();
|
||||
apiClient.patch.mockReset();
|
||||
apiClient.delete.mockReset();
|
||||
|
||||
apiClient.get.mockResolvedValue({ data: { ok: true } });
|
||||
apiClient.post.mockResolvedValue({ data: { ok: true } });
|
||||
apiClient.put.mockResolvedValue({ data: { ok: true } });
|
||||
apiClient.patch.mockResolvedValue({ data: { ok: true } });
|
||||
apiClient.delete.mockResolvedValue({ data: { ok: true } });
|
||||
});
|
||||
|
||||
it("fetches dev resources with expected query parameters", async () => {
|
||||
const {
|
||||
fetchClients,
|
||||
fetchDevStats,
|
||||
fetchClient,
|
||||
fetchConsents,
|
||||
fetchDevAuditLogs,
|
||||
fetchMyTenants,
|
||||
listIdpConfigsForClient,
|
||||
} = await import("./devApi");
|
||||
|
||||
await fetchClients();
|
||||
await fetchDevStats();
|
||||
await fetchClient("client-a");
|
||||
await fetchConsents("user-a", "client-a", "active");
|
||||
await fetchDevAuditLogs(10, "cursor-a", {
|
||||
action: "client.update",
|
||||
client_id: "client-a",
|
||||
status: "success",
|
||||
tenant_id: "tenant-a",
|
||||
});
|
||||
await fetchMyTenants();
|
||||
await listIdpConfigsForClient("client-a");
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/dev/clients");
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/dev/stats");
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/dev/clients/client-a");
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/dev/consents", {
|
||||
params: { subject: "user-a", client_id: "client-a", status: "active" },
|
||||
});
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/dev/audit-logs", {
|
||||
params: {
|
||||
limit: 10,
|
||||
cursor: "cursor-a",
|
||||
action: "client.update",
|
||||
client_id: "client-a",
|
||||
status: "success",
|
||||
tenant_id: "tenant-a",
|
||||
},
|
||||
});
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/dev/my-tenants");
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/dev/clients/client-a/idps");
|
||||
});
|
||||
|
||||
it("omits optional consent filters when they are empty or all", async () => {
|
||||
const { fetchConsents, revokeConsent } = await import("./devApi");
|
||||
|
||||
await fetchConsents("user-a", undefined, "all");
|
||||
await revokeConsent("user-a");
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith("/dev/consents", {
|
||||
params: { subject: "user-a" },
|
||||
});
|
||||
expect(apiClient.delete).toHaveBeenCalledWith("/dev/consents", {
|
||||
params: { subject: "user-a" },
|
||||
});
|
||||
});
|
||||
|
||||
it("sends mutation requests to the documented dev endpoints", async () => {
|
||||
const {
|
||||
updateClientStatus,
|
||||
createClient,
|
||||
updateClient,
|
||||
rotateClientSecret,
|
||||
refreshHeadlessJwksCache,
|
||||
revokeHeadlessJwksCache,
|
||||
deleteClient,
|
||||
revokeConsent,
|
||||
createIdpConfigForClient,
|
||||
updateIdpConfig,
|
||||
deleteIdpConfig,
|
||||
} = await import("./devApi");
|
||||
|
||||
await updateClientStatus("client-a", "inactive");
|
||||
await createClient({ id: "client-a", name: "Console App" });
|
||||
await updateClient("client-a", { name: "Console App Updated" });
|
||||
await rotateClientSecret("client-a");
|
||||
await refreshHeadlessJwksCache("client-a");
|
||||
await revokeHeadlessJwksCache("client-a");
|
||||
await deleteClient("client-a");
|
||||
await revokeConsent("user-a", "client-a");
|
||||
await createIdpConfigForClient({
|
||||
client_id: "client-a",
|
||||
provider_type: "oidc",
|
||||
display_name: "OIDC Provider",
|
||||
status: "active",
|
||||
});
|
||||
await updateIdpConfig("client-a", "idp-a", { status: "inactive" });
|
||||
await deleteIdpConfig("client-a", "idp-a");
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith(
|
||||
"/dev/clients/client-a/status",
|
||||
{ status: "inactive" },
|
||||
);
|
||||
expect(apiClient.post).toHaveBeenCalledWith("/dev/clients", {
|
||||
id: "client-a",
|
||||
name: "Console App",
|
||||
});
|
||||
expect(apiClient.put).toHaveBeenCalledWith("/dev/clients/client-a", {
|
||||
name: "Console App Updated",
|
||||
});
|
||||
expect(apiClient.delete).toHaveBeenCalledWith("/dev/consents", {
|
||||
params: { subject: "user-a", client_id: "client-a" },
|
||||
});
|
||||
expect(apiClient.put).toHaveBeenCalledWith(
|
||||
"/dev/clients/client-a/idps/idp-a",
|
||||
{ status: "inactive" },
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user