1
0
forked from baron/baron-sso

역할 전환 E2E 및 권한 안내 검증 테스트 추가

This commit is contained in:
2026-03-04 13:09:41 +09:00
parent 9946108313
commit e2d3e389f3
5 changed files with 207 additions and 27 deletions

View File

@@ -9,6 +9,7 @@
"lint": "biome check .", "lint": "biome check .",
"preview": "vite preview", "preview": "vite preview",
"test": "playwright test", "test": "playwright test",
"test:roles": "playwright test tests/devfront-role-switch-report.spec.ts",
"test:ui": "playwright test --ui" "test:ui": "playwright test --ui"
}, },
"dependencies": { "dependencies": {

View File

@@ -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");
});
});

View File

@@ -1,9 +1,9 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { import {
type Consent,
installDevApiMock, installDevApiMock,
makeClient, makeClient,
seedAuth, seedAuth,
type Consent,
} from "./helpers/devfront-fixtures"; } from "./helpers/devfront-fixtures";
test.describe("DevFront security and isolation", () => { 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("PKCE only app")).toBeVisible();
await expect(page.getByText("Server side App")).not.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$/);
});
}); });

View File

@@ -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); const nowInSeconds = Math.floor(Date.now() / 1000);
await page.addInitScript((issuedAt) => { await page.addInitScript(
const mockOidcUser = { ({ issuedAt, injectedRole }) => {
id_token: "playwright-id-token", const mockOidcUser = {
session_state: "playwright-session", id_token: "playwright-id-token",
access_token: "playwright-access-token", session_state: "playwright-session",
refresh_token: "playwright-refresh-token", access_token: "playwright-access-token",
token_type: "Bearer", refresh_token: "playwright-refresh-token",
scope: "openid profile email", token_type: "Bearer",
profile: { scope: "openid profile email",
sub: "playwright-user", profile: {
email: "playwright@example.com", sub: "playwright-user",
name: "Playwright User", email: "playwright@example.com",
}, name: "Playwright User",
expires_at: issuedAt + 3600, ...(injectedRole ? { role: injectedRole } : {}),
}; },
expires_at: issuedAt + 3600,
};
window.localStorage.setItem( window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc:devfront", "oidc.user:http://localhost:5000/oidc:devfront",
JSON.stringify(mockOidcUser), JSON.stringify(mockOidcUser),
); );
window.localStorage.setItem( window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc/:devfront", "oidc.user:http://localhost:5000/oidc/:devfront",
JSON.stringify(mockOidcUser), JSON.stringify(mockOidcUser),
); );
window.localStorage.setItem("dev_tenant_id", "tenant-a"); window.localStorage.setItem("dev_tenant_id", "tenant-a");
}, nowInSeconds); },
{ issuedAt: nowInSeconds, injectedRole: role ?? "" },
);
} }
function json(route: Route, payload: unknown, status = 200) { function json(route: Route, payload: unknown, status = 200) {

View File

@@ -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",
});
}