forked from baron/baron-sso
chore: consolidate local integration changes
This commit is contained in:
124
devfront/tests/devfront-client-tenant-access.spec.ts
Normal file
124
devfront/tests/devfront-client-tenant-access.spec.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, type Page, type TestInfo, test } from "@playwright/test";
|
||||
import {
|
||||
type Consent,
|
||||
installDevApiMock,
|
||||
makeClient,
|
||||
seedAuth,
|
||||
} from "./helpers/devfront-fixtures";
|
||||
import { captureEvidence } from "./helpers/evidence";
|
||||
|
||||
const existingTenantId = "11111111-1111-4111-8111-111111111111";
|
||||
const addedTenantId = "22222222-2222-4222-8222-222222222222";
|
||||
|
||||
async function captureTenantAccessEvidence(
|
||||
page: Page,
|
||||
testInfo: TestInfo,
|
||||
name: string,
|
||||
) {
|
||||
await captureEvidence(page, testInfo, name);
|
||||
const evidenceDir = path.join(process.cwd(), "e2e-evidence");
|
||||
await mkdir(evidenceDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(evidenceDir, `${name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("DevFront client tenant access settings", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on("dialog", async (dialog) => {
|
||||
await dialog.accept();
|
||||
});
|
||||
await seedAuth(page);
|
||||
});
|
||||
|
||||
test("adds and removes allowed tenants with UUID copy evidence", async ({
|
||||
context,
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
|
||||
const state = {
|
||||
clients: [
|
||||
makeClient("client-tenant-access", {
|
||||
name: "Tenant Access App",
|
||||
scopes: ["openid", "profile", "email"],
|
||||
metadata: {
|
||||
tenant_access_restricted: true,
|
||||
allowed_tenants: [existingTenantId],
|
||||
},
|
||||
}),
|
||||
],
|
||||
consents: [] as Consent[],
|
||||
auditLogsByCursor: undefined,
|
||||
tenants: [
|
||||
{
|
||||
id: existingTenantId,
|
||||
name: "Alpha Tenant",
|
||||
slug: "alpha",
|
||||
description: "Existing allowed tenant",
|
||||
type: "organization",
|
||||
},
|
||||
{
|
||||
id: addedTenantId,
|
||||
name: "Beta Tenant",
|
||||
slug: "beta",
|
||||
description: "Tenant added during E2E",
|
||||
type: "organization",
|
||||
},
|
||||
],
|
||||
};
|
||||
await installDevApiMock(page, state);
|
||||
|
||||
await page.goto("/clients/client-tenant-access/settings");
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /테넌트 접근 제한|Tenant access/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId(`allowed-tenant-${existingTenantId}`),
|
||||
).toContainText(existingTenantId);
|
||||
await page.getByTestId(`allowed-tenant-copy-${existingTenantId}`).click();
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => navigator.clipboard.readText()))
|
||||
.toBe(existingTenantId);
|
||||
|
||||
await page
|
||||
.getByPlaceholder(/테넌트 이름 또는 슬러그로 검색|tenant name or slug/i)
|
||||
.fill("beta");
|
||||
await page.getByRole("button", { name: /Beta Tenant/i }).click();
|
||||
await expect(
|
||||
page.getByTestId(`allowed-tenant-${addedTenantId}`),
|
||||
).toContainText(addedTenantId);
|
||||
await captureTenantAccessEvidence(
|
||||
page,
|
||||
testInfo,
|
||||
"tenant-access-allowed-tenant-added",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
|
||||
await expect
|
||||
.poll(() => state.clients[0]?.metadata?.allowed_tenants)
|
||||
.toEqual([existingTenantId, addedTenantId]);
|
||||
|
||||
await page.getByTestId(`allowed-tenant-remove-${addedTenantId}`).click();
|
||||
await expect(
|
||||
page.getByTestId(`allowed-tenant-${addedTenantId}`),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId(`allowed-tenant-${existingTenantId}`),
|
||||
).toContainText(existingTenantId);
|
||||
await captureTenantAccessEvidence(
|
||||
page,
|
||||
testInfo,
|
||||
"tenant-access-allowed-tenant-deleted",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
|
||||
await expect
|
||||
.poll(() => state.clients[0]?.metadata?.allowed_tenants)
|
||||
.toEqual([existingTenantId]);
|
||||
});
|
||||
});
|
||||
@@ -144,29 +144,31 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
|
||||
await page.goto("/clients/client-claims/settings");
|
||||
await page.getByRole("button", { name: /Claim 추가|Add Claim/i }).click();
|
||||
await page.getByPlaceholder(/e\.g\. locale|예: locale/i).fill("locale");
|
||||
await expect(page.getByText("rp_claims").first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByLabel(/Claim namespace|Claim 네임스페이스/i),
|
||||
).toHaveCount(0);
|
||||
await page
|
||||
.getByLabel(/Claim namespace|Claim 네임스페이스/i)
|
||||
.first()
|
||||
.selectOption("top_level");
|
||||
.getByPlaceholder(/e\.g\. locale|예: locale/i)
|
||||
.fill("contract_date");
|
||||
await page
|
||||
.getByLabel(/Claim value type|Claim 값 타입/i)
|
||||
.first()
|
||||
.selectOption("text");
|
||||
.selectOption("date");
|
||||
await page
|
||||
.getByPlaceholder(/Claim 값을 입력하세요|Enter the claim value/i)
|
||||
.first()
|
||||
.fill("ko-KR");
|
||||
.fill("2026-06-09");
|
||||
await page
|
||||
.getByLabel(/읽기 권한|Read permission/i)
|
||||
.first()
|
||||
.selectOption("user_and_admin");
|
||||
|
||||
await page.getByRole("button", { name: /Claim 추가|Add Claim/i }).click();
|
||||
await page
|
||||
.getByPlaceholder(/e\.g\. locale|예: locale/i)
|
||||
.nth(1)
|
||||
.fill("tier");
|
||||
await page
|
||||
.getByLabel(/Claim namespace|Claim 네임스페이스/i)
|
||||
.nth(1)
|
||||
.selectOption("rp_claims");
|
||||
await page
|
||||
.getByLabel(/Claim value type|Claim 값 타입/i)
|
||||
.nth(1)
|
||||
@@ -210,7 +212,7 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
| undefined
|
||||
)?.[0]?.namespace,
|
||||
)
|
||||
.toBe("top_level");
|
||||
.toBe("rp_claims");
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
@@ -225,7 +227,50 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
| undefined
|
||||
)?.[0]?.key,
|
||||
)
|
||||
.toBe("locale");
|
||||
.toBe("contract_date");
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
(
|
||||
state.clients[0]?.metadata?.id_token_claims as
|
||||
| Array<{
|
||||
namespace?: string;
|
||||
key?: string;
|
||||
value?: string;
|
||||
valueType?: string;
|
||||
readPermission?: string;
|
||||
writePermission?: string;
|
||||
}>
|
||||
| undefined
|
||||
)?.[0]?.valueType,
|
||||
)
|
||||
.toBe("date");
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
(
|
||||
state.clients[0]?.metadata?.id_token_claims as
|
||||
| Array<{
|
||||
readPermission?: string;
|
||||
writePermission?: string;
|
||||
}>
|
||||
| undefined
|
||||
)?.[0]?.readPermission,
|
||||
)
|
||||
.toBe("user_and_admin");
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
(
|
||||
state.clients[0]?.metadata?.id_token_claims as
|
||||
| Array<{
|
||||
readPermission?: string;
|
||||
writePermission?: string;
|
||||
}>
|
||||
| undefined
|
||||
)?.[0]?.writePermission,
|
||||
)
|
||||
.toBe("admin_only");
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
@@ -263,7 +308,7 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
).toHaveCount(2);
|
||||
await expect(
|
||||
page.getByPlaceholder(/e\.g\. locale|예: locale/i).first(),
|
||||
).toHaveValue("locale");
|
||||
).toHaveValue("contract_date");
|
||||
await expect(
|
||||
page.getByPlaceholder(/e\.g\. locale|예: locale/i).nth(1),
|
||||
).toHaveValue("tier");
|
||||
@@ -274,7 +319,7 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
page
|
||||
.getByPlaceholder(/Claim 값을 입력하세요|Enter the claim value/i)
|
||||
.first(),
|
||||
).toHaveValue("ko-KR");
|
||||
).toHaveValue("2026-06-09");
|
||||
await expect(
|
||||
page
|
||||
.getByPlaceholder(/Claim 값을 입력하세요|Enter the claim value/i)
|
||||
|
||||
@@ -23,7 +23,27 @@ test.describe("DevFront consents", () => {
|
||||
|
||||
test("consent list and revoke flow", async ({ page }) => {
|
||||
const state = {
|
||||
clients: [makeClient("client-consent", { name: "Consent app" })],
|
||||
clients: [
|
||||
makeClient("client-consent", {
|
||||
name: "Consent app",
|
||||
metadata: {
|
||||
id_token_claims: [
|
||||
{
|
||||
namespace: "rp_claims",
|
||||
key: "contract_date",
|
||||
valueType: "date",
|
||||
value: "2026-06-09",
|
||||
},
|
||||
{
|
||||
namespace: "rp_claims",
|
||||
key: "approved_at",
|
||||
valueType: "datetime",
|
||||
value: "2026-06-09T09:30",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
consents: [
|
||||
{
|
||||
subject: "user-1",
|
||||
@@ -36,6 +56,13 @@ test.describe("DevFront consents", () => {
|
||||
status: "active",
|
||||
tenantId: "tenant-a",
|
||||
tenantName: "Tenant A",
|
||||
rpMetadata: {
|
||||
approvalLevel: "A",
|
||||
approvalLevel_permissions: {
|
||||
readPermission: "admin_only",
|
||||
writePermission: "admin_only",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as Consent[],
|
||||
auditLogsByCursor: undefined,
|
||||
@@ -45,6 +72,36 @@ test.describe("DevFront consents", () => {
|
||||
await page.goto("/clients/client-consent/consents");
|
||||
await expect(page.getByText("Alice")).toBeVisible();
|
||||
await expect(page.getByText("Tenant A")).toBeVisible();
|
||||
await expect(page.getByText(/approvalLevel:\s*A/)).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: /Claims|Claim/i }).click();
|
||||
await expect(page.getByText("RP Custom Claims")).toBeVisible();
|
||||
await expect(page.getByText("contract_date")).toBeVisible();
|
||||
await expect(page.getByText("approved_at")).toBeVisible();
|
||||
await expect(page.locator('input[type="date"]')).toHaveValue("2026-06-09");
|
||||
await page.locator('input[type="date"]').fill("2026-06-10");
|
||||
await page.locator('input[type="datetime-local"]').fill("2026-06-09T10:30");
|
||||
await page
|
||||
.getByLabel(/쓰기 권한|Write permission/i)
|
||||
.first()
|
||||
.selectOption("user_and_admin");
|
||||
await page.getByRole("button", { name: /Claim 저장|Save Claim/i }).click();
|
||||
await expect
|
||||
.poll(() => state.consents[0]?.rpMetadata?.contract_date)
|
||||
.toBe("2026-06-10");
|
||||
await expect
|
||||
.poll(() => state.consents[0]?.rpMetadata?.approved_at)
|
||||
.toBe("2026-06-09T10:30");
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
(
|
||||
state.consents[0]?.rpMetadata?.contract_date_permissions as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
)?.writePermission,
|
||||
)
|
||||
.toBe("user_and_admin");
|
||||
|
||||
await page.getByRole("button", { name: /권한 철회|철회|Revoke/i }).click();
|
||||
await expect(page.getByText(/Revoked|철회/i).first()).toBeVisible();
|
||||
|
||||
@@ -51,6 +51,7 @@ export type Consent = {
|
||||
status: "active" | "revoked";
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
rpMetadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DeveloperRequestStatus = "pending" | "approved" | "rejected";
|
||||
@@ -89,6 +90,14 @@ export type DevAssignableUser = {
|
||||
loginId?: string;
|
||||
};
|
||||
|
||||
export type DevTenantSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type AuditLog = {
|
||||
event_id: string;
|
||||
timestamp: string;
|
||||
@@ -106,6 +115,7 @@ export type DevApiMockState = {
|
||||
developerRequests?: DeveloperRequest[];
|
||||
relations?: Record<string, ClientRelation[]>;
|
||||
users?: DevAssignableUser[];
|
||||
tenants?: DevTenantSummary[];
|
||||
auditLogsByCursor?: Record<
|
||||
string,
|
||||
{ items: AuditLog[]; next_cursor?: string }
|
||||
@@ -397,9 +407,12 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
||||
}
|
||||
|
||||
if (pathname === "/api/v1/dev/my-tenants" && method === "GET") {
|
||||
return json(route, [
|
||||
{ id: "tenant-a", name: "Tenant A", slug: "tenant-a" },
|
||||
]);
|
||||
return json(
|
||||
route,
|
||||
state.tenants ?? [
|
||||
{ id: "tenant-a", name: "Tenant A", slug: "tenant-a" },
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (pathname === "/api/v1/dev/stats" && method === "GET") {
|
||||
@@ -602,6 +615,50 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
pathname.startsWith("/api/v1/dev/clients/") &&
|
||||
pathname.includes("/users/") &&
|
||||
pathname.endsWith("/metadata") &&
|
||||
method === "GET"
|
||||
) {
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const clientId = parts[4] ?? "";
|
||||
const userId = parts[6] ?? "";
|
||||
const target = state.consents.find(
|
||||
(row) => row.clientId === clientId && row.subject === userId,
|
||||
);
|
||||
return json(route, {
|
||||
clientId,
|
||||
userId,
|
||||
metadata: target?.rpMetadata ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
pathname.startsWith("/api/v1/dev/clients/") &&
|
||||
pathname.includes("/users/") &&
|
||||
pathname.endsWith("/metadata") &&
|
||||
method === "PUT"
|
||||
) {
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const clientId = parts[4] ?? "";
|
||||
const userId = parts[6] ?? "";
|
||||
const payload = (request.postDataJSON() as {
|
||||
metadata?: Record<string, unknown>;
|
||||
}) || { metadata: {} };
|
||||
const target = state.consents.find(
|
||||
(row) => row.clientId === clientId && row.subject === userId,
|
||||
);
|
||||
if (target) {
|
||||
target.rpMetadata = payload.metadata ?? {};
|
||||
}
|
||||
return json(route, {
|
||||
clientId,
|
||||
userId,
|
||||
metadata: payload.metadata ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/api/v1/dev/clients/") && method === "PUT") {
|
||||
const clientId = parseClientId(pathname);
|
||||
const payload = (request.postDataJSON() as {
|
||||
|
||||
Reference in New Issue
Block a user