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(); }); });