From d480a018573193e651f05997842a60b56b69800c Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 11 Jun 2026 14:25:03 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=84=EB=A6=AC=EB=90=9C=20tenant=20?= =?UTF-8?q?=EC=8A=A4=EC=BD=94=ED=94=84=20=EC=A0=9C=EC=96=B4=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clients/ClientGeneralPage.claims.test.tsx | 113 ++++++++++++++++-- .../features/clients/ClientGeneralPage.tsx | 11 +- 2 files changed, 112 insertions(+), 12 deletions(-) diff --git a/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx b/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx index 547993c6..439ffe13 100644 --- a/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx @@ -68,7 +68,36 @@ vi.mock("../../lib/i18n", () => ({ const roots: Root[] = []; -function makeClientDetail(claimKey: string): ClientDetailResponse { +function makeClientDetail( + claimKey: string, + options?: { + includeTenantScope?: boolean; + tenantAccessRestricted?: boolean; + tenantScopeMandatory?: boolean; + }, +): ClientDetailResponse { + const includeTenantScope = options?.includeTenantScope ?? false; + const tenantAccessRestricted = options?.tenantAccessRestricted ?? false; + const tenantScopeMandatory = options?.tenantScopeMandatory ?? false; + const structuredScopes = [ + { + id: "1", + name: "openid", + description: "", + mandatory: true, + }, + ]; + + if (includeTenantScope) { + structuredScopes.push({ + id: "2", + name: "tenant", + description: "Tenant access", + mandatory: tenantScopeMandatory, + locked: tenantAccessRestricted, + }); + } + return { client: { id: "client-claims", @@ -76,18 +105,14 @@ function makeClientDetail(claimKey: string): ClientDetailResponse { type: "private", status: "active", redirectUris: ["https://rp.example.com/callback"], - scopes: ["openid", "profile"], + scopes: includeTenantScope + ? ["openid", "tenant", "profile"] + : ["openid", "profile"], tokenEndpointAuthMethod: "client_secret_basic", metadata: { description: "Claims app", - structured_scopes: [ - { - id: "1", - name: "openid", - description: "", - mandatory: true, - }, - ], + tenant_access_restricted: tenantAccessRestricted, + structured_scopes: structuredScopes, id_token_claims: [ { namespace: "rp_claims", @@ -304,6 +329,74 @@ describe("ClientGeneralPage RP claims", () => { ); }); + it("preserves tenant scope mandatory state when tenant access restriction is off", async () => { + fetchClientMock.mockResolvedValue( + makeClientDetail("old_claim", { + includeTenantScope: true, + tenantAccessRestricted: false, + tenantScopeMandatory: true, + }), + ); + updateClientMock.mockResolvedValue( + makeClientDetail("old_claim", { + includeTenantScope: true, + tenantAccessRestricted: false, + tenantScopeMandatory: false, + }), + ); + + const { container } = await renderPage(); + + const tenantScopeRow = Array.from( + container.querySelectorAll("tr"), + ).find((row) => + Array.from(row.querySelectorAll("input")).some( + (input) => (input as HTMLInputElement).value === "tenant", + ), + ); + + expect(tenantScopeRow).toBeDefined(); + const mandatorySwitch = + tenantScopeRow?.querySelector('[role="switch"]'); + expect(mandatorySwitch).toBeDefined(); + expect(mandatorySwitch?.getAttribute("aria-checked")).toBe("true"); + + await act(async () => { + mandatorySwitch?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect(mandatorySwitch?.getAttribute("aria-checked")).toBe("false"); + + const saveButton = Array.from(container.querySelectorAll("button")).find( + (button) => + button.textContent?.includes("저장") || + button.textContent?.includes("Save"), + ); + expect(saveButton).toBeDefined(); + + await act(async () => { + saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect(updateClientMock).toHaveBeenCalledWith( + "client-claims", + expect.objectContaining({ + metadata: expect.objectContaining({ + tenant_access_restricted: false, + structured_scopes: expect.arrayContaining([ + expect.objectContaining({ + name: "tenant", + mandatory: false, + locked: false, + }), + ]), + }), + }), + ); + }); + it("keeps nullable and default value as separate RP claim settings", async () => { const { container } = await renderPage(); diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 98f320c6..af826426 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -689,11 +689,18 @@ function ClientGeneralPage() { if (scope.name.trim() !== "tenant") { return scope; } + if (restricted) { + return { + ...scope, + description: scope.description || tenantScopeDescription, + mandatory: true, + locked: true, + }; + } return { ...scope, description: scope.description || tenantScopeDescription, - mandatory: restricted, - locked: restricted, + locked: false, }; });