1
0
forked from baron/baron-sso

test: raise frontend coverage baselines

This commit is contained in:
2026-05-29 14:31:10 +09:00
parent 592c1d1741
commit 3e31fdfa0c
50 changed files with 3482 additions and 214 deletions

View 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();
});
});

View 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();
});
});
});

View 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");
});
});

View File

@@ -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([
{

View 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" },
);
});
});