From e2d3e389f3a1c18df08043a6ec1b6fdc8b897e75 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 4 Mar 2026 13:09:41 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=AD=ED=95=A0=20=EC=A0=84=ED=99=98=20E2E?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B6=8C=ED=95=9C=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/package.json | 1 + .../tests/devfront-role-switch-report.spec.ts | 141 ++++++++++++++++++ devfront/tests/devfront-security.spec.ts | 12 +- devfront/tests/helpers/devfront-fixtures.ts | 56 +++---- devfront/tests/helpers/evidence.ts | 24 +++ 5 files changed, 207 insertions(+), 27 deletions(-) create mode 100644 devfront/tests/devfront-role-switch-report.spec.ts create mode 100644 devfront/tests/helpers/evidence.ts diff --git a/devfront/package.json b/devfront/package.json index a8c53649..7db0ed87 100644 --- a/devfront/package.json +++ b/devfront/package.json @@ -9,6 +9,7 @@ "lint": "biome check .", "preview": "vite preview", "test": "playwright test", + "test:roles": "playwright test tests/devfront-role-switch-report.spec.ts", "test:ui": "playwright test --ui" }, "dependencies": { diff --git a/devfront/tests/devfront-role-switch-report.spec.ts b/devfront/tests/devfront-role-switch-report.spec.ts new file mode 100644 index 00000000..794564d7 --- /dev/null +++ b/devfront/tests/devfront-role-switch-report.spec.ts @@ -0,0 +1,141 @@ +import { expect, test } from "@playwright/test"; +import { + type AuditLog, + type Consent, + installDevApiMock, + makeClient, + seedAuth, +} from "./helpers/devfront-fixtures"; +import { captureEvidence } from "./helpers/evidence"; + +test.describe("DevFront role report", () => { + test.beforeEach(async ({ page }) => { + page.on("dialog", async (dialog) => { + await dialog.accept(); + }); + }); + + test("user(tenant_member) is blocked with 안내 문구", async ({ + page, + }, testInfo) => { + await seedAuth(page, "user"); + await installDevApiMock(page, { + clients: [], + consents: [] as Consent[], + auditLogs: [] as AuditLog[], + }); + + await page.goto("/clients"); + await expect( + page.getByText(/관리자 전용 화면|administrator only/i), + ).toBeVisible(); + await captureEvidence(page, testInfo, "role-user-blocked"); + }); + + test("rp_admin sees only assigned Gitea app and its logs", async ({ + page, + }, testInfo) => { + await seedAuth(page, "rp_admin"); + const state = { + clients: [makeClient("gitea-client", { name: "Gitea" })], + consents: [] as Consent[], + auditLogs: [ + { + event_id: "evt-rp-1", + timestamp: "2026-03-04T01:00:00.000Z", + user_id: "rp-admin-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: "gitea-client", + tenant_id: "tenant-a", + }), + }, + ] as AuditLog[], + }; + await installDevApiMock(page, state); + + await page.goto("/clients"); + await expect(page.getByRole("link", { name: /Gitea/ })).toBeVisible(); + await expect(page.getByText("gitea-client")).toBeVisible(); + await captureEvidence(page, testInfo, "role-rp-admin-clients"); + + await page.goto("/audit-logs"); + await expect(page.getByText("UPDATE_CLIENT")).toBeVisible(); + await expect(page.getByText("gitea-client")).toBeVisible(); + await captureEvidence(page, testInfo, "role-rp-admin-audit"); + }); + + test("tenant_admin can manage tenant apps and see tenant logs", async ({ + page, + }, testInfo) => { + await seedAuth(page, "tenant_admin"); + const state = { + clients: [ + makeClient("tenant-a-app-1", { name: "Tenant A CRM" }), + makeClient("tenant-a-app-2", { name: "Tenant A ERP" }), + ], + consents: [] as Consent[], + auditLogs: [] as AuditLog[], + auditLogsByCursor: undefined, + }; + await installDevApiMock(page, state); + + await page.goto("/clients"); + await expect(page.getByText("Tenant A CRM")).toBeVisible(); + await expect(page.getByText("Tenant A ERP")).toBeVisible(); + await captureEvidence(page, testInfo, "role-tenant-admin-clients"); + + await page.goto("/clients/tenant-a-app-1/settings"); + await page + .getByPlaceholder("My Awesome Application") + .fill("Tenant A CRM Updated"); + await page.getByRole("button", { name: /^저장$|^Save$/i }).click(); + + await page.goto("/audit-logs"); + await expect(page.getByText("UPDATE_CLIENT")).toBeVisible({ + timeout: 30000, + }); + await expect(page.getByText("tenant-a-app-1")).toBeVisible(); + await captureEvidence(page, testInfo, "role-tenant-admin-audit"); + }); + + test("super_admin sees all and can generate log entries", async ({ + page, + }, testInfo) => { + await seedAuth(page, "super_admin"); + const state = { + clients: [ + makeClient("tenant-a-app", { name: "Tenant A App" }), + makeClient("tenant-b-app", { name: "Tenant B App" }), + ], + consents: [] as Consent[], + auditLogs: [] as AuditLog[], + auditLogsByCursor: undefined, + }; + await installDevApiMock(page, state); + + await page.goto("/clients"); + await expect(page.getByText("Tenant A App")).toBeVisible(); + await expect(page.getByText("Tenant B App")).toBeVisible(); + await captureEvidence(page, testInfo, "role-super-admin-clients"); + + await page.goto("/clients/new"); + await page + .getByPlaceholder("My Awesome Application") + .fill("Super Admin Created App"); + await page + .getByPlaceholder(/https:\/\/app\.example\.com\/callback/i) + .fill("https://super-admin.example.com/callback"); + await page.getByRole("button", { name: /앱 생성|Create/i }).click(); + + await page.goto("/audit-logs"); + await expect(page.getByText("CREATE_CLIENT")).toBeVisible({ + timeout: 30000, + }); + await captureEvidence(page, testInfo, "role-super-admin-audit"); + }); +}); diff --git a/devfront/tests/devfront-security.spec.ts b/devfront/tests/devfront-security.spec.ts index 75e684df..f6c7cd1a 100644 --- a/devfront/tests/devfront-security.spec.ts +++ b/devfront/tests/devfront-security.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from "@playwright/test"; import { + type Consent, installDevApiMock, makeClient, seedAuth, - type Consent, } from "./helpers/devfront-fixtures"; test.describe("DevFront security and isolation", () => { @@ -47,4 +47,14 @@ test.describe("DevFront security and isolation", () => { await expect(page.getByText("PKCE only app")).toBeVisible(); await expect(page.getByText("Server side App")).not.toBeVisible(); }); + + test("tenant_member user is blocked at AuthGuard", async ({ page }) => { + await seedAuth(page, "tenant_member"); + + await page.goto("/clients"); + await expect( + page.getByText(/DevFront는 관리자 전용 화면입니다|administrator access/i), + ).toBeVisible(); + await expect(page).toHaveURL(/\/clients$/); + }); }); diff --git a/devfront/tests/helpers/devfront-fixtures.ts b/devfront/tests/helpers/devfront-fixtures.ts index e2f092cc..fd154b43 100644 --- a/devfront/tests/helpers/devfront-fixtures.ts +++ b/devfront/tests/helpers/devfront-fixtures.ts @@ -70,35 +70,39 @@ export function makeClient( }; } -export async function seedAuth(page: Page) { +export async function seedAuth(page: Page, role?: string) { const nowInSeconds = Math.floor(Date.now() / 1000); - await page.addInitScript((issuedAt) => { - const mockOidcUser = { - id_token: "playwright-id-token", - session_state: "playwright-session", - access_token: "playwright-access-token", - refresh_token: "playwright-refresh-token", - token_type: "Bearer", - scope: "openid profile email", - profile: { - sub: "playwright-user", - email: "playwright@example.com", - name: "Playwright User", - }, - expires_at: issuedAt + 3600, - }; + await page.addInitScript( + ({ issuedAt, injectedRole }) => { + const mockOidcUser = { + id_token: "playwright-id-token", + session_state: "playwright-session", + access_token: "playwright-access-token", + refresh_token: "playwright-refresh-token", + token_type: "Bearer", + scope: "openid profile email", + profile: { + sub: "playwright-user", + email: "playwright@example.com", + name: "Playwright User", + ...(injectedRole ? { role: injectedRole } : {}), + }, + expires_at: issuedAt + 3600, + }; - window.localStorage.setItem( - "oidc.user:http://localhost:5000/oidc:devfront", - JSON.stringify(mockOidcUser), - ); - window.localStorage.setItem( - "oidc.user:http://localhost:5000/oidc/:devfront", - JSON.stringify(mockOidcUser), - ); - window.localStorage.setItem("dev_tenant_id", "tenant-a"); - }, nowInSeconds); + window.localStorage.setItem( + "oidc.user:http://localhost:5000/oidc:devfront", + JSON.stringify(mockOidcUser), + ); + window.localStorage.setItem( + "oidc.user:http://localhost:5000/oidc/:devfront", + JSON.stringify(mockOidcUser), + ); + window.localStorage.setItem("dev_tenant_id", "tenant-a"); + }, + { issuedAt: nowInSeconds, injectedRole: role ?? "" }, + ); } function json(route: Route, payload: unknown, status = 200) { diff --git a/devfront/tests/helpers/evidence.ts b/devfront/tests/helpers/evidence.ts new file mode 100644 index 00000000..a2507776 --- /dev/null +++ b/devfront/tests/helpers/evidence.ts @@ -0,0 +1,24 @@ +import type { Page, TestInfo } from "@playwright/test"; + +function safeName(name: string): string { + return name + .trim() + .toLowerCase() + .replace(/[^a-z0-9-_]+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} + +export async function captureEvidence( + page: Page, + testInfo: TestInfo, + name: string, +) { + const filename = `${safeName(name)}.png`; + const fullPath = testInfo.outputPath(filename); + await page.screenshot({ path: fullPath, fullPage: true }); + await testInfo.attach(name, { + path: fullPath, + contentType: "image/png", + }); +}