import { expect, type Page, test } from "@playwright/test"; import { type AuditLog, type Consent, type DevAssignableUser, installDevApiMock, makeClient, seedAuth, } from "./helpers/devfront-fixtures"; import { captureEvidence } from "./helpers/evidence"; test.afterEach(async ({ page }, testInfo) => { if (testInfo.status === "passed") { await captureEvidence(page, testInfo, testInfo.title); } }); async function mockAdminUserLookup(page: Page) { await page.route("**/api/v1/admin/users/*", async (route) => { const url = new URL(route.request().url()); const userId = url.pathname.split("/").pop(); const users: Record = { "creator-headless": { id: "creator-headless", name: "Headless Creator", email: "headless.creator@example.com", }, "creator-plain": { id: "creator-plain", name: "Plain Creator", email: "plain.creator@example.com", }, }; const found = userId ? users[userId] : undefined; await route.fulfill({ status: found ? 200 : 404, contentType: "application/json", body: JSON.stringify( found ? { ...found, role: "user", status: "active", createdAt: "2026-03-03T00:00:00.000Z", updatedAt: "2026-03-03T00:00:00.000Z", } : { error: "not found" }, ), }); }); } test("clients page loads correctly", async ({ page }) => { await seedAuth(page, "super_admin"); await installDevApiMock(page, { clients: [ makeClient("client-playwright", { name: "Playwright Client", createdAt: new Date().toISOString(), redirectUris: ["http://localhost:5174/callback"], }), ], consents: [] as Consent[], auditLogsByCursor: undefined, }); await page.goto("/clients"); await expect(page).toHaveURL(/\/clients$/); // 타이틀 확인 await expect(page).toHaveTitle(/바론 개발자 서비스/); // 페이지 내 주요 텍스트 확인 await expect(page.getByText("연동 앱 목록")).toBeVisible(); await expect( page.getByText("Total Applications", { exact: true }), ).toHaveCount(0); // 테이블 헤더 확인 await expect( page.locator("th").filter({ hasText: "애플리케이션" }), ).toBeVisible(); await expect( page.locator("th").filter({ hasText: /클라이언트 ID|Client ID/i }), ).toBeVisible(); }); test("clients page shows Tenant-limited only for tenant access restricted RP", async ({ page, }) => { await seedAuth(page, "super_admin"); await installDevApiMock(page, { clients: [ makeClient("client-limited", { name: "Limited RP", createdAt: "2026-05-02T00:00:00.000Z", metadata: { tenant_access_restricted: true, allowed_tenants: ["tenant-1"], }, }), makeClient("client-open", { name: "Open RP", createdAt: "2026-05-01T00:00:00.000Z", metadata: { tenant_access_restricted: false, allowed_tenants: [], }, }), ], consents: [] as Consent[], auditLogsByCursor: undefined, }); await page.goto("/clients"); const limitedRow = page.locator("tbody tr", { hasText: "Limited RP" }); await expect(limitedRow).toContainText("Tenant-limited"); const openRow = page.locator("tbody tr", { hasText: "Open RP" }); await expect(openRow).not.toContainText("Tenant-limited"); await expect(page.getByText("Tenant-scoped")).toHaveCount(0); }); test("clients page resolves creator and filters Headless Login clients", async ({ page, }) => { await seedAuth(page, "super_admin"); await installDevApiMock(page, { clients: [ makeClient("client-plain", { name: "Plain Server App", createdAt: "2026-05-01T00:00:00.000Z", creatorId: "creator-plain", }), makeClient("client-headless", { name: "Headless Login App", createdAt: "2026-05-02T00:00:00.000Z", creatorId: "creator-headless", metadata: { headless_login_enabled: true, }, }), ], users: [ { id: "creator-headless", name: "Headless Creator", email: "headless.creator@example.com", }, { id: "creator-plain", name: "Plain Creator", email: "plain.creator@example.com", }, ] as DevAssignableUser[], consents: [] as Consent[], auditLogsByCursor: undefined, }); await mockAdminUserLookup(page); await page.goto("/clients"); const headlessRow = page.locator("tbody tr", { hasText: "Headless Login App", }); await expect(headlessRow).toContainText("Headless Creator"); await expect(headlessRow).toContainText("headless.creator@example.com"); await page .getByRole("button", { name: /Advanced Filters|고급 필터/ }) .click(); await page .locator('select:has(option[value="headless"])') .selectOption("headless"); await expect( page.locator("tbody tr", { hasText: "Headless Login App" }), ).toBeVisible(); await expect( page.locator("tbody tr", { hasText: "Plain Server App" }), ).toHaveCount(0); }); test("overview page shows recent RP changes", async ({ page }) => { await seedAuth(page, "super_admin"); await installDevApiMock(page, { clients: [ makeClient("client-recent", { name: "Recent RP", }), ], consents: [] as Consent[], auditLogs: [ { event_id: "evt-1", timestamp: "2026-03-03T09:00:00.000Z", user_id: "actor-1", event_type: "CLIENT_RELATION_CREATE", status: "success", ip_address: "127.0.0.1", user_agent: "playwright", details: JSON.stringify({ action: "ADD_RELATION", target_id: "client-recent", relation: "config_editor", subject: "User:user-2", }), }, { event_id: "evt-2", timestamp: "2026-03-03T08:59:00.000Z", user_id: "actor-2", event_type: "CLIENT_ROTATE_SECRET", status: "success", ip_address: "127.0.0.1", user_agent: "playwright", details: JSON.stringify({ action: "ROTATE_SECRET", target_id: "client-recent", }), }, ] as AuditLog[], auditLogsByCursor: undefined, }); await page.goto("/"); await expect( page.getByRole("heading", { name: "최근 변경된 앱" }), ).toBeVisible(); await expect(page.getByText("클라이언트 시크릿 재발급")).toBeVisible(); await expect(page.getByText("관계 추가")).toBeVisible(); await expect( page.getByRole("link", { name: "Recent RP", exact: true }).first(), ).toBeVisible(); }); test("clients page shows only five apps by default and expands with more button", async ({ page, }) => { await seedAuth(page, "super_admin"); const clients = Array.from({ length: 6 }, (_, index) => makeClient(`client-${index + 1}`, { name: `Preview App ${index + 1}`, createdAt: new Date(Date.UTC(2026, 2, 3, 9, 10 - index, 0)).toISOString(), }), ); await installDevApiMock(page, { clients, consents: [] as Consent[], auditLogs: [] as AuditLog[], auditLogsByCursor: undefined, }); await page.goto("/clients"); await expect( page.getByRole("heading", { name: "연동 앱 목록" }), ).toBeVisible(); await expect( page .locator("table") .first() .locator("tbody tr") .filter({ hasText: /Preview App \d/, }), ).toHaveCount(5); await expect( page.getByText("Preview App 6", { exact: true }), ).not.toBeVisible(); const moreButton = page.getByRole("button", { name: "연동 앱 목록 더보기", }); await expect(moreButton).toBeVisible(); await expect(moreButton).toHaveCount(1); await moreButton.click(); await expect( page .locator("table") .first() .locator("tbody tr") .filter({ hasText: /Preview App \d/, }), ).toHaveCount(6); await expect(page.getByText("Preview App 6", { exact: true })).toBeVisible(); await expect( page.getByRole("button", { name: "연동 앱 목록 더보기" }), ).toHaveCount(0); }); test("overview page shows user-delete relation cleanup in recent changes", async ({ page, }) => { await seedAuth(page, "super_admin"); await installDevApiMock(page, { clients: [ makeClient("client-cleanup", { name: "Cleanup RP", }), ], consents: [] as Consent[], users: [ { id: "cleanup-actor", name: "Cleanup Actor", email: "cleanup.actor@example.com", } satisfies DevAssignableUser, ], auditLogs: [ { event_id: "evt-cleanup-1", timestamp: "2026-03-03T09:00:00.000Z", user_id: "cleanup-actor", event_type: "CLIENT_RELATION_DELETE", status: "success", ip_address: "127.0.0.1", user_agent: "playwright", details: JSON.stringify({ action: "REMOVE_RELATION", target_id: "client-cleanup", relation: "config_editor", subject: "User:deleted-user", before: { relation: "config_editor", subject: "User:deleted-user", }, }), }, ] as AuditLog[], auditLogsByCursor: undefined, }); await page.goto("/"); await expect( page.getByRole("heading", { name: "최근 변경된 앱" }), ).toBeVisible(); await expect( page.getByRole("link", { name: "Cleanup RP", exact: true }), ).toBeVisible(); await expect(page.getByText("관계 삭제", { exact: true })).toBeVisible(); await expect(page.getByText(/관계:\s*config_editor/)).toBeVisible(); await expect(page.getByText(/주체:\s*User:deleted-user/)).toBeVisible(); await expect( page.getByText("cleanup-actor", { exact: true }).first(), ).toBeVisible(); }); test("clients page no longer shows recent changes card", async ({ page }) => { await seedAuth(page, "super_admin"); const clients = Array.from({ length: 6 }, (_, index) => makeClient(`client-${index + 1}`, { name: `Recent App ${index + 1}`, }), ); const auditLogs = clients.map((client, index) => ({ event_id: `evt-recent-${index + 1}`, timestamp: `2026-03-03T09:${String(10 - index).padStart(2, "0")}:00.000Z`, user_id: `actor-${index + 1}`, event_type: "CLIENT_CREATE", status: "success" as const, ip_address: "127.0.0.1", user_agent: "playwright", details: JSON.stringify({ action: "CREATE_CLIENT", target_id: client.id, after: { name: client.name, }, }), })); await installDevApiMock(page, { clients, consents: [] as Consent[], auditLogs: auditLogs as AuditLog[], auditLogsByCursor: undefined, }); await page.goto("/clients"); await expect( page.getByRole("heading", { name: "최근 변경된 앱" }), ).toHaveCount(0); await expect( page.getByRole("heading", { name: "연동 앱 목록" }), ).toBeVisible(); });