diff --git a/devfront/src/features/clients/ClientConsentsPage.test.tsx b/devfront/src/features/clients/ClientConsentsPage.test.tsx new file mode 100644 index 00000000..c8d9cede --- /dev/null +++ b/devfront/src/features/clients/ClientConsentsPage.test.tsx @@ -0,0 +1,204 @@ +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 ClientConsentsPage from "./ClientConsentsPage"; + +const fetchClientMock = vi.fn(); +const fetchConsentsMock = vi.fn(); +const fetchRPUserMetadataMock = vi.fn(); +const updateRPUserMetadataMock = vi.fn(); +const revokeConsentMock = vi.fn(); + +vi.mock("../../lib/devApi", () => ({ + fetchClient: (...args: unknown[]) => fetchClientMock(...args), + fetchConsents: (...args: unknown[]) => fetchConsentsMock(...args), + fetchRPUserMetadata: (...args: unknown[]) => fetchRPUserMetadataMock(...args), + updateRPUserMetadata: (...args: unknown[]) => + updateRPUserMetadataMock(...args), + revokeConsent: (...args: unknown[]) => revokeConsentMock(...args), +})); + +vi.mock("../../lib/i18n", () => ({ + t: (key: string, fallback?: string, vars?: Record) => { + let text = fallback ?? key; + for (const [name, value] of Object.entries(vars ?? {})) { + text = text.replaceAll(`{{${name}}}`, String(value)); + } + return text; + }, +})); + +const roots: Root[] = []; + +const clientDetail = { + client: { + id: "client-a", + name: "Claims App", + type: "private" as const, + status: "active" as const, + redirectUris: ["https://rp.example.com/callback"], + scopes: ["openid", "profile"], + tokenEndpointAuthMethod: "client_secret_basic", + metadata: { + id_token_claims: [ + { + namespace: "rp_claims", + key: "license", + value: "12345678", + valueType: "text", + readPermission: "admin_only", + writePermission: "admin_only", + }, + ], + }, + }, + endpoints: { + discovery: "https://issuer/.well-known/openid-configuration", + issuer: "https://issuer", + authorization: "https://issuer/oauth2/auth", + token: "https://issuer/oauth2/token", + userinfo: "https://issuer/userinfo", + }, +}; + +function buildMetadata() { + return { + license: "abcd", + license_permissions: { + readPermission: "user_and_admin", + writePermission: "admin_only", + }, + }; +} + +async function flush() { + await act(async () => { + 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( + + + + } + /> + + + , + ); + }); + await flush(); + + return { container }; +} + +describe("ClientConsentsPage RP custom claims", () => { + beforeEach(() => { + fetchClientMock.mockResolvedValue(clientDetail); + fetchConsentsMock.mockResolvedValue({ + items: [ + { + subject: "user-1", + userName: "Consent User", + clientId: "client-a", + clientName: "Claims App", + grantedScopes: ["openid", "profile"], + authenticatedAt: "2026-06-11T09:00:00Z", + createdAt: "2026-06-10T09:00:00Z", + status: "active", + tenantId: "tenant-1", + tenantName: "Hanmac", + rpMetadata: buildMetadata(), + }, + ], + }); + fetchRPUserMetadataMock.mockResolvedValue({ + clientId: "client-a", + userId: "user-1", + metadata: buildMetadata(), + }); + updateRPUserMetadataMock.mockResolvedValue({ + clientId: "client-a", + userId: "user-1", + metadata: buildMetadata(), + }); + revokeConsentMock.mockResolvedValue(undefined); + }); + + afterEach(() => { + for (const root of roots.splice(0)) { + act(() => { + root.unmount(); + }); + } + vi.clearAllMocks(); + document.body.innerHTML = ""; + }); + + it("removes the RP custom claim permission selectors while keeping claim data editable", async () => { + const { container } = await renderPage(); + + const editButton = Array.from(container.querySelectorAll("button")).find( + (button) => + button.textContent?.includes("사용자 Claim 설정") || + button.textContent?.includes("User Claim Settings"), + ); + expect(editButton).toBeDefined(); + + await act(async () => { + editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect( + container.querySelectorAll('select[aria-label="읽기 권한"]'), + ).toHaveLength(0); + expect( + container.querySelectorAll('select[aria-label="쓰기 권한"]'), + ).toHaveLength(0); + expect(container.textContent).toContain("license"); + expect(container.textContent).toContain("abcd"); + + const saveButton = Array.from(container.querySelectorAll("button")).find( + (button) => + button.textContent?.includes("Claim 저장") || + button.textContent?.includes("Save"), + ); + expect(saveButton).toBeDefined(); + + await act(async () => { + saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect(updateRPUserMetadataMock).toHaveBeenCalledWith( + "client-a", + "user-1", + expect.objectContaining({ + license: "abcd", + license_permissions: { + readPermission: "user_and_admin", + writePermission: "admin_only", + }, + }), + ); + }); +}); diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index a3dd2824..1e6f1fa9 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -1023,7 +1023,7 @@ function ClientConsentsPage() { metadataDraftRows.map((row) => (
{row.key} @@ -1036,7 +1036,7 @@ function ClientConsentsPage() { value: event.target.value, }) } - className="h-10 rounded-md border border-input bg-background px-3 font-mono text-xs shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + className="h-10 w-full max-w-[180px] rounded-md border border-input bg-background px-3 font-mono text-xs shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" aria-label={`${row.key} boolean`} > @@ -1051,12 +1051,12 @@ function ClientConsentsPage() { value: event.target.value, }) } - className="min-h-10 font-mono text-xs" placeholder={ row.valueType === "array" ? `["value"]` : `{"key": "value"}` } + className="min-h-10 w-full max-w-[320px] font-mono text-xs" aria-label={`${row.key} ${row.valueType}`} /> ) : ( @@ -1071,7 +1071,7 @@ function ClientConsentsPage() { value: event.target.value, }) } - className="font-mono text-xs" + className="w-full max-w-[320px] font-mono text-xs" placeholder={t( "ui.dev.clients.consents.rp_claims.value_placeholder", "claim value", @@ -1099,63 +1099,9 @@ function ClientConsentsPage() { )}
)} - - {row.valueType}