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,77 @@
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ForbiddenMessage } from "./ForbiddenMessage";
const authState = {
user: {
profile: {
role: "user",
},
},
};
vi.mock("react-oidc-context", () => ({
useAuth: () => authState,
}));
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();
});
}
});
async function renderMessage(resourceToken: "audit" | "clients" | "consents") {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
roots.push(root);
await act(async () => {
root.render(<ForbiddenMessage resourceToken={resourceToken} />);
});
return container;
}
describe("ForbiddenMessage", () => {
it("renders resource-specific user guidance", async () => {
authState.user.profile.role = "user";
const audit = await renderMessage("audit");
expect(audit.textContent).toContain("Audit Logs");
expect(audit.textContent).toContain("audit read relationship");
const consents = await renderMessage("consents");
expect(consents.textContent).toContain("User Consent Grants");
expect(consents.textContent).toContain("consent read");
const clients = await renderMessage("clients");
expect(clients.textContent).toContain("Connected Applications");
expect(clients.textContent).toContain("target RP");
});
it("renders role-specific administrator guidance", async () => {
authState.user.profile.role = "rp_admin";
const rpAdmin = await renderMessage("clients");
expect(rpAdmin.textContent).toContain("RP administrators");
authState.user.profile.role = "tenant_admin";
const tenantAdmin = await renderMessage("clients");
expect(tenantAdmin.textContent).toContain("tenant administrator");
});
});

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: "Dev Admin",
email: "dev@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 Dev 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("devfront 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 Dev 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="Toggle theme"]',
) as HTMLButtonElement;
await act(async () => {
themeButton.click();
});
expect(document.documentElement.classList.contains("dark")).toBe(true);
const profileButton = container.querySelector(
'button[aria-label="Open account menu"]',
) as HTMLButtonElement;
await act(async () => {
profileButton.click();
});
expect(container.textContent).toContain("My Profile");
const profileMenuItem = Array.from(
container.querySelectorAll('button[role="menuitem"]'),
).find((button) => button.textContent?.includes("My Profile"));
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,161 @@
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 AuthCallbackPage from "./AuthCallbackPage";
import AuthGuard from "./AuthGuard";
import AuthPage from "./AuthPage";
import LoginPage from "./LoginPage";
const authState = {
isAuthenticated: false,
isLoading: false,
activeNavigator: undefined as string | undefined,
error: null as Error | null,
user: undefined as
| {
state?: unknown;
}
| undefined,
signinRedirect: vi.fn(),
};
vi.mock("react-oidc-context", () => ({
useAuth: () => authState,
}));
vi.mock("../../lib/auth", () => ({
userManager: {
signinPopupCallback: vi.fn(async () => undefined),
},
}));
const roots: Root[] = [];
beforeEach(() => {
authState.isAuthenticated = false;
authState.isLoading = false;
authState.activeNavigator = undefined;
authState.error = null;
authState.user = undefined;
authState.signinRedirect.mockReset();
authState.signinRedirect.mockResolvedValue(undefined);
});
afterEach(() => {
for (const root of roots.splice(0)) {
act(() => {
root.unmount();
});
}
});
async function renderWithRouter(
element: React.ReactElement,
{
entry = "/",
path = "*",
}: {
entry?: string;
path?: string;
} = {},
) {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
roots.push(root);
await act(async () => {
root.render(
<MemoryRouter initialEntries={[entry]}>
<Routes>
<Route path={path} element={element}>
<Route index element={<div>Protected outlet</div>} />
</Route>
<Route path="/login" element={<div>Login route</div>} />
<Route path="/clients" element={<div>Clients route</div>} />
<Route path="/profile" element={<div>Profile route</div>} />
</Routes>
</MemoryRouter>,
);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
return container;
}
describe("devfront auth pages", () => {
it("renders the static auth planning page", async () => {
const container = await renderWithRouter(<AuthPage />);
expect(container.textContent).toContain("Admin auth guardrails");
expect(container.textContent).toContain("Device approval");
});
it("renders login page and starts SSO redirect from the action button", async () => {
const container = await renderWithRouter(<LoginPage />, {
entry: "/login?returnTo=/profile",
path: "/login",
});
expect(container.textContent).toContain("개발자 포털 로그인");
const loginButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent?.includes("SSO 계정으로 로그인"),
);
await act(async () => {
(loginButton as HTMLButtonElement).click();
});
expect(authState.signinRedirect).toHaveBeenCalledWith({
state: { returnTo: "/clients" },
});
});
it("shows AuthGuard loading, error, redirect, and protected outlet states", async () => {
authState.isLoading = true;
const loading = await renderWithRouter(<AuthGuard />);
expect(loading.textContent).toContain("Loading...");
authState.isLoading = false;
authState.error = new Error("OIDC failed");
const error = await renderWithRouter(<AuthGuard />);
expect(error.textContent).toContain("Authentication Error");
const retryButton = error.querySelector("button") as HTMLButtonElement;
await act(async () => {
retryButton.click();
});
expect(authState.signinRedirect).toHaveBeenCalled();
authState.error = null;
const redirected = await renderWithRouter(<AuthGuard />);
expect(redirected.textContent).toContain("Login route");
authState.isAuthenticated = true;
const protectedPage = await renderWithRouter(<AuthGuard />);
expect(protectedPage.textContent).toContain("Protected outlet");
});
it("navigates from callback by auth result and stored return target", async () => {
authState.isAuthenticated = true;
authState.user = { state: { returnTo: "/profile" } };
const authenticated = await renderWithRouter(<AuthCallbackPage />, {
entry: "/auth/callback",
path: "/auth/callback",
});
expect(authenticated.textContent).toContain("Profile route");
authState.isAuthenticated = false;
authState.error = new Error("callback failed");
const failed = await renderWithRouter(<AuthCallbackPage />, {
entry: "/auth/callback",
path: "/auth/callback",
});
expect(failed.textContent).toContain("Login route");
});
});

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import {
compareNullableValues,
sortItems,
toggleSort,
} from "../../../../common/core/utils/sort";
describe("common sort utilities in devfront coverage", () => {
it("keeps nullish values last and compares normalized primitive values", () => {
expect(compareNullableValues(null, "alpha", "asc")).toBe(1);
expect(compareNullableValues("alpha", undefined, "asc")).toBe(-1);
expect(compareNullableValues("Beta", "alpha", "asc")).toBe(1);
expect(compareNullableValues("Beta", "alpha", "desc")).toBe(-1);
expect(compareNullableValues(true, false, "asc")).toBe(1);
expect(
compareNullableValues(
new Date("2026-05-02T00:00:00Z"),
new Date("2026-05-01T00:00:00Z"),
"asc",
),
).toBe(1);
});
it("toggles sort direction and sorts with default and custom resolvers", () => {
const firstSort = toggleSort(null, "name");
expect(firstSort).toEqual({ key: "name", direction: "asc" });
expect(toggleSort(firstSort, "name")).toEqual({
key: "name",
direction: "desc",
});
expect(toggleSort(firstSort, "createdAt")).toEqual({
key: "createdAt",
direction: "asc",
});
const rows = [
{ name: "charlie", rank: 3, nested: { score: 20 } },
{ name: "Alpha", rank: 1, nested: { score: 30 } },
{ name: "bravo", rank: 2, nested: { score: 10 } },
];
expect(
sortItems(rows, { key: "name", direction: "asc" }).map(
(row) => row.name,
),
).toEqual(["Alpha", "bravo", "charlie"]);
expect(
sortItems(rows, { key: "score", direction: "desc" }, {
score: (row) => row.nested.score,
}).map((row) => row.name),
).toEqual(["Alpha", "charlie", "bravo"]);
expect(sortItems(rows, null)).not.toBe(rows);
});
});

View File

@@ -0,0 +1,383 @@
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 AuditLogsPage from "../audit/AuditLogsPage";
import ClientConsentsPage from "../clients/ClientConsentsPage";
import ClientDetailsPage from "../clients/ClientDetailsPage";
import ClientGeneralPage from "../clients/ClientGeneralPage";
import ClientRelationsPage from "../clients/ClientRelationsPage";
import ClientsPage from "../clients/ClientsPage";
import { ClientFederationPage } from "../clients/routes/ClientFederationPage";
import DeveloperRequestPage from "../developer-request/DeveloperRequestPage";
import GlobalOverviewPage from "../overview/GlobalOverviewPage";
import ProfilePage from "../profile/ProfilePage";
const authProfile = {
sub: "user-1",
role: "super_admin",
tenant_id: "tenant-1",
companyCode: "HANMAC",
};
vi.mock("react-oidc-context", () => ({
useAuth: () => ({
isAuthenticated: true,
isLoading: false,
user: {
access_token: "access-token",
profile: authProfile,
},
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",
id_token_claims: [
{
namespace: "rp_claims",
key: "employee_id",
value: "E001",
valueType: "text",
},
],
},
};
const clientDetail = {
client: {
...clientSummary,
clientSecret: "secret-value",
jwksUri: "https://app.example/jwks.json",
backchannelLogoutUri: "https://app.example/logout",
backchannelLogoutSessionRequired: true,
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",
lastCheckedAt: "2026-05-01T01:00:00Z",
lastSuccessfulVerificationAt: "2026-05-01T01: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,
})),
fetchDevRPUsageDaily: vi.fn(async () => ({
days: 14,
period: "day",
items: [
{
date: "2026-05-01",
tenantId: "tenant-1",
tenantType: "COMPANY",
tenantName: "Hanmac",
clientId: "client-a",
clientName: "Console App",
loginRequests: 10,
otherRequests: 4,
uniqueSubjects: 3,
},
{
date: "2026-05-08",
tenantId: "tenant-1",
tenantType: "COMPANY",
tenantName: "Hanmac",
clientId: "client-a",
clientName: "Console App",
loginRequests: 8,
otherRequests: 5,
uniqueSubjects: 4,
},
],
})),
fetchClient: vi.fn(async () => clientDetail),
fetchClientRelations: vi.fn(async () => ({
items: [
{
relation: "admins",
subject: "User:user-1",
subjectType: "User",
subjectId: "user-1",
userName: "Dev Admin",
userEmail: "dev@example.com",
},
],
})),
fetchDevUsers: vi.fn(async () => ({
items: [
{
id: "user-2",
name: "Editor User",
email: "editor@example.com",
loginId: "editor",
},
],
})),
addClientRelation: vi.fn(async () => ({
relation: "admins",
subject: "User:user-2",
subjectType: "User",
subjectId: "user-2",
})),
removeClientRelation: vi.fn(async () => undefined),
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: "{\"client_id\":\"client-a\"}",
},
],
limit: 50,
})),
fetchMyTenants: vi.fn(async () => [
{
id: "tenant-1",
name: "Hanmac",
slug: "hanmac",
type: "COMPANY",
status: "active",
description: "",
memberCount: 10,
createdAt: "2026-05-01T00:00:00Z",
updatedAt: "2026-05-01T00:00:00Z",
},
]),
fetchDeveloperRequestStatus: vi.fn(async () => ({ status: "approved" })),
requestDeveloperAccess: vi.fn(async () => ({ status: "pending" })),
fetchDeveloperRequests: vi.fn(async () => [
{
id: 1,
userId: "user-3",
tenantId: "tenant-1",
name: "Requester",
organization: "Hanmac",
email: "requester@example.com",
reason: "Need RP access",
status: "pending",
createdAt: "2026-05-01T00:00:00Z",
updatedAt: "2026-05-01T00:00:00Z",
},
]),
approveDeveloperRequest: vi.fn(async () => ({ status: "approved" })),
rejectDeveloperRequest: vi.fn(async () => ({ status: "rejected" })),
cancelDeveloperRequestApproval: vi.fn(async () => ({ status: "cancelled" })),
}));
vi.mock("../auth/authApi", () => ({
fetchMe: vi.fn(async () => ({
id: "user-1",
email: "dev@example.com",
name: "Dev Admin",
role: "super_admin",
tenantId: "tenant-1",
})),
}));
const roots: Root[] = [];
afterEach(() => {
for (const root of roots.splice(0)) {
act(() => {
root.unmount();
});
}
});
async function renderPage(
element: React.ReactElement,
{
path = "/",
entry = path,
}: {
path?: string;
entry?: string;
} = {},
) {
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={[entry]}>
<Routes>
<Route path={path} element={element} />
</Routes>
</MemoryRouter>
</QueryClientProvider>,
);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
return container;
}
describe("devfront coverage smoke pages", () => {
it("renders overview, client list, audit, developer request, and profile pages", async () => {
const overview = await renderPage(<GlobalOverviewPage />);
expect(overview.textContent).toContain("Console App");
const clients = await renderPage(<ClientsPage />);
expect(clients.textContent).toContain("Console App");
const audit = await renderPage(<AuditLogsPage />);
expect(audit.textContent).toContain("client.update");
const requests = await renderPage(<DeveloperRequestPage />);
expect(requests.textContent).toContain("Requester");
const profile = await renderPage(<ProfilePage />);
expect(profile.textContent).toContain("Dev Admin");
});
it("renders client detail, settings, consent, federation, and relationship pages", async () => {
const details = await renderPage(<ClientDetailsPage />, {
path: "/clients/:id",
entry: "/clients/client-a",
});
expect(details.textContent).toContain("Console App");
const settings = await renderPage(<ClientGeneralPage />, {
path: "/clients/:id/settings",
entry: "/clients/client-a/settings",
});
expect(settings.textContent).toContain("Console App");
const consents = await renderPage(<ClientConsentsPage />, {
path: "/clients/:id/consents",
entry: "/clients/client-a/consents",
});
expect(consents.textContent).toContain("Consent User");
const federation = await renderPage(<ClientFederationPage />, {
path: "/clients/:id/federation",
entry: "/clients/client-a/federation",
});
expect(federation.textContent).toContain("Workspace OIDC");
const relations = await renderPage(<ClientRelationsPage />, {
path: "/clients/:id/relationships",
entry: "/clients/client-a/relationships",
});
expect(relations.textContent).toContain("Dev Admin");
});
});

View File

@@ -0,0 +1,250 @@
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("devApi", () => {
beforeEach(() => {
apiClient.get.mockReset();
apiClient.post.mockReset();
apiClient.put.mockReset();
apiClient.patch.mockReset();
apiClient.delete.mockReset();
});
it("fetches list and detail resources with expected query parameters", async () => {
const {
fetchClients,
fetchDevStats,
fetchDevRPUsageDaily,
fetchTenants,
fetchClient,
fetchClientRelations,
fetchDevUsers,
fetchConsents,
fetchDevAuditLogs,
fetchMyTenants,
fetchDeveloperRequestStatus,
fetchDeveloperRequests,
listIdpConfigsForClient,
} = await import("./devApi");
apiClient.get.mockResolvedValue({ data: { ok: true } });
await fetchClients();
await fetchDevStats();
await fetchDevRPUsageDaily({ days: 30, period: "week" });
await fetchTenants(25, 50, "tenant-parent");
await fetchClient("client-a");
await fetchClientRelations("client-a");
await fetchDevUsers("admin", 5, "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 fetchDeveloperRequestStatus("tenant-a");
await fetchDeveloperRequests("pending");
await listIdpConfigsForClient("client-a");
expect(apiClient.get).toHaveBeenCalledWith("/dev/clients");
expect(apiClient.get).toHaveBeenCalledWith("/dev/stats");
expect(apiClient.get).toHaveBeenCalledWith("/dev/rp-usage/daily", {
params: { days: 30, period: "week" },
});
expect(apiClient.get).toHaveBeenCalledWith("/tenants", {
params: { limit: 25, offset: 50, parentId: "tenant-parent" },
});
expect(apiClient.get).toHaveBeenCalledWith("/dev/clients/client-a");
expect(apiClient.get).toHaveBeenCalledWith(
"/dev/clients/client-a/relations",
);
expect(apiClient.get).toHaveBeenCalledWith("/dev/users", {
params: { search: "admin", limit: 5, clientId: "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/developer-request/status",
{
params: { tenantId: "tenant-a" },
},
);
expect(apiClient.get).toHaveBeenCalledWith("/dev/developer-request/list", {
params: { status: "pending" },
});
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");
apiClient.get.mockResolvedValue({ data: { items: [] } });
apiClient.delete.mockResolvedValue({ data: {} });
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 {
addClientRelation,
removeClientRelation,
updateClientStatus,
createClient,
updateClient,
rotateClientSecret,
refreshHeadlessJwksCache,
revokeHeadlessJwksCache,
deleteClient,
revokeConsent,
createIdpConfigForClient,
updateIdpConfig,
deleteIdpConfig,
requestDeveloperAccess,
approveDeveloperRequest,
rejectDeveloperRequest,
cancelDeveloperRequestApproval,
} = await import("./devApi");
apiClient.post.mockResolvedValue({ data: { ok: true } });
apiClient.put.mockResolvedValue({ data: { ok: true } });
apiClient.patch.mockResolvedValue({ data: { ok: true } });
apiClient.delete.mockResolvedValue({ data: {} });
await addClientRelation("client-a", {
relation: "admins",
userId: "user-a",
});
await removeClientRelation("client-a", "admins", "User:user-a");
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");
await requestDeveloperAccess({
name: "Dev User",
organization: "Hanmac",
reason: "Need RP access",
tenantId: "tenant-a",
});
await approveDeveloperRequest(1, "approved");
await rejectDeveloperRequest(2, "rejected");
await cancelDeveloperRequestApproval(3, "cancelled");
expect(apiClient.post).toHaveBeenCalledWith(
"/dev/clients/client-a/relations",
{ relation: "admins", userId: "user-a" },
);
expect(apiClient.delete).toHaveBeenCalledWith(
"/dev/clients/client-a/relations",
{
params: { relation: "admins", subject: "User:user-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.post).toHaveBeenCalledWith(
"/dev/clients/client-a/secret/rotate",
);
expect(apiClient.post).toHaveBeenCalledWith(
"/dev/clients/client-a/headless-jwks/refresh",
);
expect(apiClient.delete).toHaveBeenCalledWith(
"/dev/clients/client-a/headless-jwks/cache",
);
expect(apiClient.delete).toHaveBeenCalledWith("/dev/clients/client-a");
expect(apiClient.delete).toHaveBeenCalledWith("/dev/consents", {
params: { subject: "user-a", client_id: "client-a" },
});
expect(apiClient.post).toHaveBeenCalledWith("/dev/clients/client-a/idps", {
client_id: "client-a",
provider_type: "oidc",
display_name: "OIDC Provider",
status: "active",
});
expect(apiClient.put).toHaveBeenCalledWith(
"/dev/clients/client-a/idps/idp-a",
{
status: "inactive",
},
);
expect(apiClient.delete).toHaveBeenCalledWith(
"/dev/clients/client-a/idps/idp-a",
);
expect(apiClient.post).toHaveBeenCalledWith("/dev/developer-request", {
name: "Dev User",
organization: "Hanmac",
reason: "Need RP access",
tenantId: "tenant-a",
});
expect(apiClient.post).toHaveBeenCalledWith(
"/dev/developer-request/1/approve",
{
adminNotes: "approved",
},
);
expect(apiClient.post).toHaveBeenCalledWith(
"/dev/developer-request/2/reject",
{
adminNotes: "rejected",
},
);
expect(apiClient.post).toHaveBeenCalledWith(
"/dev/developer-request/3/cancel-approval",
{
adminNotes: "cancelled",
},
);
});
});