import { expect, test } from "@playwright/test"; import { type Consent, installDevApiMock, makeClient, seedAuth, } from "./helpers/devfront-fixtures"; import { captureEvidence } from "./helpers/evidence"; test.describe("DevFront security and isolation", () => { test.afterEach(async ({ page }, testInfo) => { if (testInfo.status === "passed") { await captureEvidence(page, testInfo, testInfo.title); } }); test.beforeEach(async ({ page }) => { page.on("dialog", async (dialog) => { await dialog.accept(); }); await seedAuth(page); }); test("tenant isolation: forbidden client shows blocked error", async ({ page, }) => { const state = { clients: [makeClient("tenant-a-client", { name: "Tenant A app" })], consents: [] as Consent[], auditLogsByCursor: undefined, }; await installDevApiMock(page, state); await page.goto("/clients/tenant-b-client"); await expect( page.getByText( /Error loading (app|client)|앱 정보를 불러오지 못했습니다|클라이언트 정보를 불러오지 못했습니다/i, ), ).toBeVisible(); }); test("RBAC: user without manage_all permission should not see private apps", async ({ page, }) => { const state = { clients: [ makeClient("pkce-client", { name: "PKCE only app", type: "pkce", }), ], consents: [] as Consent[], auditLogsByCursor: undefined, }; await installDevApiMock(page, state); await page.goto("/clients"); await expect(page.getByText("PKCE only app")).toBeVisible(); await expect(page.getByText("Server side App")).not.toBeVisible(); }); test("tenant_member user can enter DevFront and sees empty RP list", async ({ page, }) => { await seedAuth(page, "tenant_member"); const state = { clients: [] as ReturnType[], consents: [] as Consent[], auditLogsByCursor: undefined, }; await installDevApiMock(page, state); await page.goto("/clients"); await expect(page).toHaveURL(/\/clients$/); await expect( page.getByText(/조회 가능한 RP가 없습니다|No RPs are available/i), ).toBeVisible(); await expect( page.getByRole("button", { name: /연동 앱 추가|새 클라이언트|Create/i }), ).not.toBeVisible(); }); test("rp_admin receives 403 on clients list and sees ForbiddenMessage", async ({ page, }) => { await seedAuth(page, "rp_admin"); const state = { clients: [] as ReturnType[], consents: [] as Consent[], auditLogsByCursor: undefined, }; await installDevApiMock(page, state); await page.route("**/api/v1/dev/clients", async (route) => { if (route.request().method() === "GET") { return route.fulfill({ status: 403, contentType: "application/json", body: '{"error": "forbidden"}', }); } return route.fallback(); }); await page.goto("/clients"); await expect( page.getByText(/RP 관리자는|RP administrators can only access/i), ).toBeVisible(); }); test("tenant_admin receives 403 on audit logs and sees ForbiddenMessage", async ({ page, }) => { await seedAuth(page, "tenant_admin"); const state = { clients: [] as ReturnType[], consents: [] as Consent[], auditLogsByCursor: undefined, }; await installDevApiMock(page, state); await page.route("**/api/v1/dev/audit-logs*", async (route) => { if (route.request().method() === "GET") { return route.fulfill({ status: 403, contentType: "application/json", body: '{"error": "forbidden"}', }); } return route.fallback(); }); await page.goto("/audit-logs"); await expect( page.getByText(/테넌트 관리자 권한|Tenant administrator permissions/i), ).toBeVisible(); }); test("user sees audit log access CTA when access is blocked", async ({ page, }, testInfo) => { await seedAuth(page, "user"); const state = { clients: [] as ReturnType[], consents: [] as Consent[], auditLogsByCursor: undefined, developerRequests: [], }; await installDevApiMock(page, state); await page.goto("/audit-logs"); await expect( page.getByRole("heading", { name: /감사 로그|Audit Logs/ }), ).toBeVisible(); await expect( page.getByText( /감사 로그는 개발자 권한이 있어야 볼 수 있습니다|Audit logs are available only to users with developer access/i, ), ).toBeVisible(); const requestBtn = page.getByRole("button", { name: /개발자 권한 신청/, }); await expect(requestBtn).toBeVisible(); await requestBtn.click(); await expect(page).toHaveURL(/\/developer-requests$/); await captureEvidence(page, testInfo, "security-user-audit-request-entry"); }); test("user with approved developer request can enter audit logs without CTA", async ({ page, }, testInfo) => { await seedAuth(page, "user"); const state = { clients: [] as ReturnType[], consents: [] as Consent[], auditLogs: [ { event_id: "evt-audit-1", timestamp: "2026-05-29T00:00:00.000Z", user_id: "playwright-user", event_type: "CLIENT_UPDATE", status: "success" as const, ip_address: "127.0.0.1", user_agent: "playwright", details: JSON.stringify({ action: "UPDATE_CLIENT", target_id: "tenant-a-client", tenant_id: "tenant-a", }), }, ], auditLogsByCursor: undefined, developerRequests: [ { id: "req-approved", userId: "playwright-user", userName: "Playwright User", name: "Playwright User", userEmail: "playwright@example.com", organization: "Tenant A", reason: "Need access", status: "approved", createdAt: "2026-05-29T00:00:00.000Z", updatedAt: "2026-05-29T00:10:00.000Z", approvedAt: "2026-05-29T00:10:00.000Z", }, ], }; await installDevApiMock(page, state); await page.goto("/audit-logs"); await expect(page.getByText("UPDATE_CLIENT")).toBeVisible(); await expect( page.getByRole("button", { name: /개발자 권한 신청/ }), ).toHaveCount(0); await captureEvidence(page, testInfo, "security-user-audit-approved"); }); });