import { expect, test } from "@playwright/test"; import { type ClientStatus, type Consent, installDevApiMock, makeClient, seedAuth, } from "./helpers/devfront-fixtures"; const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i; const sshRsaPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAABwECAwQFBgc= test@example"; test.describe("DevFront clients lifecycle", () => { test.beforeEach(async ({ page }) => { page.on("dialog", async (dialog) => { await dialog.accept(); }); await seedAuth(page); }); test("create, update status, and delete", async ({ page }) => { const state = { clients: [makeClient("existing-client", { name: "Existing app" })], consents: [] as Consent[], updatedStatus: "active" as ClientStatus, auditLogsByCursor: undefined, onUpdateStatus(status: ClientStatus) { this.updatedStatus = status; }, }; await installDevApiMock(page, state); await page.goto("/clients"); await expect(page.getByText("Existing app")).toBeVisible(); await page .getByRole("button", { name: /연동 앱 추가|새 클라이언트|Create/i }) .click(); await expect(page).toHaveURL(/\/clients\/new$/); await page .getByPlaceholder(appNamePlaceholder) .fill("Playwright Created App"); await page .getByPlaceholder(/https:\/\/app\.example\.com\/callback/i) .fill("https://playwright.example.com/callback"); await page .getByRole("button", { name: /앱 생성|클라이언트 생성|Create/i }) .click(); await expect(page).toHaveURL(/\/clients\/client-\d+\/settings$/); await expect( page.getByRole("heading", { name: /연동 앱 설정|클라이언트 설정|Client Settings/i, }), ).toBeVisible(); await page.getByRole("button", { name: /비활성|Inactive/i }).click(); await page.getByRole("button", { name: /^저장$|^Save$/i }).click(); await expect.poll(() => state.updatedStatus).toBe("inactive"); await page.getByRole("button", { name: /삭제|Delete/i }).click(); await expect(page).toHaveURL(/\/clients$/); await expect(page.getByText("Playwright Created App")).not.toBeVisible(); }); test("rotate secret shows new value", async ({ page }) => { let rotatedSecret = ""; const state = { clients: [makeClient("client-rotate", { name: "Rotate app" })], consents: [] as Consent[], auditLogsByCursor: undefined, onRotateSecret(newSecret: string) { rotatedSecret = newSecret; }, }; await installDevApiMock(page, state); await page.goto("/clients/client-rotate"); await expect( page.getByRole("heading", { name: "Rotate app", exact: true }), ).toBeVisible(); await page.getByTitle(/비밀키 재발급|Rotate/i).click(); await expect.poll(() => rotatedSecret).toBe("client-rotate-rotated-secret"); await expect(page.getByText("client-rotate-rotated-secret")).toBeVisible(); }); test("update name and redirect URI should be persisted", async ({ page }) => { const state = { clients: [ makeClient("client-edit", { name: "Before Name", redirectUris: ["https://before.example.com/callback"], }), ], consents: [] as Consent[], auditLogsByCursor: undefined, }; await installDevApiMock(page, state); await page.goto("/clients/client-edit/settings"); await page.getByPlaceholder(appNamePlaceholder).fill("After Name"); await page.getByRole("button", { name: /^저장$|^Save$/i }).click(); await expect.poll(() => state.clients[0]?.name).toBe("After Name"); await page.goto("/clients/client-edit"); await page .getByRole("textbox", { name: /인증 콜백 URL|Callback/i }) .fill("https://after.example.com/callback"); await page .getByRole("button", { name: /Redirect URIs 저장|Save/i }) .click(); await expect .poll(() => state.clients[0]?.redirectUris[0]) .toBe("https://after.example.com/callback"); await page.reload(); await expect( page.getByRole("textbox", { name: /인증 콜백 URL|Callback/i }), ).toHaveValue(/https:\/\/after\.example\.com\/callback/); }); test("pkce trusted rp with inline ssh-rsa key should persist mapped payload", async ({ page, }) => { const state = { clients: [makeClient("client-trusted", { name: "Trusted App", type: "pkce" })], consents: [] as Consent[], auditLogsByCursor: undefined, }; await installDevApiMock(page, state); await page.goto("/clients/client-trusted/settings"); await page .getByRole("switch", { name: /Trusted RP \(자체 로그인 UI 사용\)|Trusted RP \(Custom Login UI\)/i, }) .click(); await expect( page.getByRole("heading", { name: /공개키 등록|Public Key Registration/i, }), ).toBeVisible(); await page .getByPlaceholder( /ssh-rsa AAA\.\.\.|Paste an 'ssh-rsa AAA\.\.\.' public key first/i, ) .fill(sshRsaPublicKey); await page.getByRole("button", { name: /^저장$|^Save$/i }).click(); await expect.poll(() => state.clients[0]?.tokenEndpointAuthMethod).toBe( "private_key_jwt", ); await expect .poll(() => state.clients[0]?.metadata?.headless_login_enabled) .toBe(true); await expect .poll( () => (state.clients[0]?.jwks as { keys?: Array<{ kty?: string; alg?: string }> }) ?.keys?.[0]?.kty, ) .toBe("RSA"); await expect .poll( () => (state.clients[0]?.jwks as { keys?: Array<{ kty?: string; alg?: string }> }) ?.keys?.[0]?.alg, ) .toBe("RS256"); await page.reload(); await expect( page.getByRole("heading", { name: /공개키 등록|Public Key Registration/i, }), ).toBeVisible(); await expect( page.getByPlaceholder( /ssh-rsa AAA\.\.\.|Paste an 'ssh-rsa AAA\.\.\.' public key first/i, ), ).toHaveValue(/"kty": "RSA"/); }); });