import { expect, test } from "@playwright/test"; import { type ClientRelation, type Consent, installDevApiMock, makeClient, seedAuth, } from "./helpers/devfront-fixtures"; import { installDevFrontStaticRoutes } from "./helpers/static-devfront"; const editRelations = [ { relation: "config_editor", subject: "User:playwright-user", subjectType: "User", subjectId: "playwright-user", }, { relation: "admins", subject: "User:playwright-user", subjectType: "User", subjectId: "playwright-user", }, { relation: "config_editor", subject: "User:admin-user", subjectType: "User", subjectId: "admin-user", }, { relation: "config_editor", subject: "User:undefined", subjectType: "User", subjectId: "undefined", }, { relation: "config_editor", subject: "User:", subjectType: "User", subjectId: "", }, ] satisfies ClientRelation[]; test.describe("DevFront RP claim cache", () => { test.beforeEach(async ({ page }) => { await installDevFrontStaticRoutes(page); await seedAuth(page, "super_admin"); }); test("keeps saved RP claim value visible after saving", async ({ page }) => { const state = { clients: [ makeClient("client-claims", { name: "Claims app", metadata: { id_token_claims: [ { namespace: "rp_claims", key: "old_claim", value: "A", valueType: "text", readPermission: "admin_only", writePermission: "admin_only", }, ], }, }), ], consents: [] as Consent[], relations: { "client-claims": editRelations, }, auditLogsByCursor: undefined, mockRole: "super_admin", }; await installDevApiMock(page, state); await page.goto("http://devfront.test/clients/client-claims/settings"); const claimKeyInput = page .getByPlaceholder(/e\.g\. locale|예: locale/i) .first(); await expect(claimKeyInput).toHaveValue("old_claim"); await expect(claimKeyInput).toBeEnabled(); await claimKeyInput.fill("new_claim"); await page.getByRole("button", { name: /^저장$|^Save$/i }).click(); await expect .poll( () => ( state.clients[0]?.metadata?.id_token_claims as | Array<{ key?: string }> | undefined )?.[0]?.key, ) .toBe("new_claim"); await expect(claimKeyInput).toHaveValue("new_claim"); }); test("adds supported scopes and custom claim keys from the scope picker without offline_access", async ({ page, }) => { const state = { clients: [ makeClient("client-claims", { name: "Claims app", metadata: { structured_scopes: [ { id: "scope-openid", name: "openid", description: "OIDC", mandatory: true, }, ], id_token_claims: [ { namespace: "rp_claims", key: "employee_code", value: "E001", valueType: "text", readPermission: "admin_only", writePermission: "admin_only", }, ], }, }), ], consents: [] as Consent[], relations: { "client-claims": editRelations, }, auditLogsByCursor: undefined, mockRole: "super_admin", }; await installDevApiMock(page, state); await page.goto("http://devfront.test/clients/client-claims/settings"); await page .getByRole("button", { name: /스코프 추가|Scope 추가|Add Scope/i }) .click(); await expect(page.getByText("offline_access", { exact: true })).toHaveCount( 0, ); await expect( page.getByRole("button", { name: /employee_code/ }), ).toBeVisible(); await page.getByRole("button", { name: /employee_code/ }).click(); await page.getByRole("button", { name: /^저장$|^Save$/i }).click(); await expect .poll(() => ( state.clients[0]?.metadata?.structured_scopes as | Array<{ name?: string }> | undefined )?.some((scope) => scope.name === "employee_code"), ) .toBe(true); }); test("forces read permission on when write permission is enabled", async ({ page, }) => { const state = { clients: [ makeClient("client-claims", { name: "Claims app", metadata: { id_token_claims: [ { namespace: "rp_claims", key: "locale", value: "ko", valueType: "text", readPermission: "admin_only", writePermission: "admin_only", }, ], }, }), ], consents: [] as Consent[], relations: { "client-claims": editRelations, }, auditLogsByCursor: undefined, mockRole: "super_admin", }; await installDevApiMock(page, state); await page.goto("http://devfront.test/clients/client-claims/settings"); const readSwitch = page .getByRole("switch", { name: /사용자 읽기|Allow user read/i }) .first(); const writeSwitch = page .getByRole("switch", { name: /사용자 쓰기|Allow user write/i }) .first(); await expect(readSwitch).toHaveAttribute("aria-checked", "false"); await expect(writeSwitch).toHaveAttribute("aria-checked", "false"); await expect(readSwitch).toBeEnabled(); await expect(writeSwitch).toBeEnabled(); await writeSwitch.click(); await expect(readSwitch).toHaveAttribute("aria-checked", "true"); await expect(writeSwitch).toHaveAttribute("aria-checked", "true"); await page.getByRole("button", { name: /^저장$|^Save$/i }).click(); await expect .poll( () => ( state.clients[0]?.metadata?.id_token_claims as | Array<{ readPermission?: string; writePermission?: string; }> | undefined )?.[0], ) .toMatchObject({ readPermission: "user_and_admin", writePermission: "user_and_admin", }); }); test("blocks saving an RP claim default value that does not match the selected value type", async ({ page, }) => { const state = { clients: [ makeClient("client-claims", { name: "Claims app", metadata: { id_token_claims: [ { namespace: "rp_claims", key: "profile", value: "{}", valueType: "text", readPermission: "admin_only", writePermission: "admin_only", }, ], }, }), ], consents: [] as Consent[], relations: { "client-claims": editRelations, }, auditLogsByCursor: undefined, mockRole: "super_admin", }; await installDevApiMock(page, state); await page.goto("http://devfront.test/clients/client-claims/settings"); await page .getByLabel(/Claim 값 타입|Claim value type/i) .first() .selectOption("object"); await page .locator('textarea[placeholder="{\\"key\\": \\"value\\"}"]') .fill("not-json"); await expect( page.getByRole("button", { name: /^저장$|^Save$/i }), ).toBeDisabled(); expect( ( state.clients[0]?.metadata?.id_token_claims as | Array<{ valueType?: string; value?: string }> | undefined )?.[0], ).toMatchObject({ value: "{}", valueType: "text", }); }); test("saves a float RP claim default value and blocks decimal values for integer number claims", async ({ page, }) => { const state = { clients: [ makeClient("client-claims", { name: "Claims app", metadata: { id_token_claims: [ { namespace: "rp_claims", key: "ratio", value: "0", valueType: "text", readPermission: "admin_only", writePermission: "admin_only", }, ], }, }), ], consents: [] as Consent[], relations: { "client-claims": editRelations, }, auditLogsByCursor: undefined, mockRole: "super_admin", }; await installDevApiMock(page, state); await page.goto("http://devfront.test/clients/client-claims/settings"); await page .getByLabel(/Claim 값 타입|Claim value type/i) .first() .selectOption("float"); await page .getByPlaceholder(/기본값을 입력하세요|Enter the default value/i) .first() .fill("3.14"); await page.getByRole("button", { name: /^저장$|^Save$/i }).click(); await expect .poll( () => ( state.clients[0]?.metadata?.id_token_claims as | Array<{ valueType?: string; value?: string }> | undefined )?.[0], ) .toMatchObject({ value: "3.14", valueType: "float", }); const valueTypeSelect = page .getByLabel(/Claim 값 타입|Claim value type/i) .first(); await expect(valueTypeSelect).toHaveValue("float"); await expect( page.getByRole("button", { name: /^저장$|^Save$/i }), ).toBeEnabled(); await valueTypeSelect.selectOption("number"); await expect(valueTypeSelect).toHaveValue("number"); const defaultValueInput = page .getByPlaceholder(/기본값을 입력하세요|Enter the default value/i) .first(); await expect(defaultValueInput).toHaveAttribute("inputmode", "numeric"); await expect(defaultValueInput).toHaveAttribute("pattern", "-?[0-9]*"); await defaultValueInput.fill("3.14"); await expect( page.getByText(/Claim 기본값이 타입과 맞지 않습니다|does not match/i), ).toBeVisible(); await expect( page.getByRole("button", { name: /^저장$|^Save$/i }), ).toBeDisabled(); }); });