forked from baron/baron-sso
custom claim 타입보정 UI. 대표테넌트 노출 보정
This commit is contained in:
47
test/code_check_biome_dedup_test.sh
Normal file
47
test/code_check_biome_dedup_test.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORKFLOW_FILE="$ROOT_DIR/.gitea/workflows/code_check.yml"
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
job_block() {
|
||||
local job="$1"
|
||||
awk -v job=" ${job}:" '
|
||||
$0 == job { in_job = 1; print; next }
|
||||
in_job && /^ [a-zA-Z0-9_-]+:/ { exit }
|
||||
in_job { print }
|
||||
' "$WORKFLOW_FILE"
|
||||
}
|
||||
|
||||
lint_block="$(job_block lint)"
|
||||
biome_block="$(job_block biome-check)"
|
||||
|
||||
if printf '%s\n' "$lint_block" | grep -Eq 'Biome check (adminfront|devfront|orgfront)|npx biome check'; then
|
||||
fail "lint job must not duplicate frontend Biome checks; keep them in biome-check"
|
||||
fi
|
||||
|
||||
for app in adminfront devfront orgfront; do
|
||||
printf '%s\n' "$biome_block" | grep -Fq "Install ${app} dependencies" ||
|
||||
fail "biome-check job must install ${app} dependencies"
|
||||
printf '%s\n' "$biome_block" | grep -Fq "Biome check ${app}" ||
|
||||
fail "biome-check job must check ${app}"
|
||||
done
|
||||
|
||||
for job in \
|
||||
adminfront-vitest-coverage \
|
||||
devfront-vitest-coverage \
|
||||
orgfront-vitest-coverage \
|
||||
adminfront-tests \
|
||||
devfront-tests \
|
||||
orgfront-tests; do
|
||||
block="$(job_block "$job")"
|
||||
printf '%s\n' "$block" | grep -Fq " - biome-check" ||
|
||||
fail "${job} must depend on biome-check instead of duplicating/depending on lint for frontend quality gate"
|
||||
done
|
||||
|
||||
echo "OK: Code Check runs frontend Biome only in the biome-check job"
|
||||
604
test/rp_claims_live_e2e.mjs
Normal file
604
test/rp_claims_live_e2e.mjs
Normal file
@@ -0,0 +1,604 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import playwright from "../devfront/node_modules/playwright/index.js";
|
||||
|
||||
const { chromium } = playwright;
|
||||
|
||||
const gatewayUrl = process.env.RP_CLAIMS_E2E_GATEWAY_URL ?? "http://localhost:5000";
|
||||
const devfrontUrl = process.env.RP_CLAIMS_E2E_DEVFRONT_URL ?? "http://localhost:5174";
|
||||
const reportDir = process.env.RP_CLAIMS_E2E_REPORT_DIR ?? "reports/rp-claims-live-e2e";
|
||||
const tenantSlug = process.env.RP_CLAIMS_E2E_TENANT_SLUG ?? "public-org";
|
||||
const runId = process.env.RP_CLAIMS_E2E_RUN_ID ?? `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
const email = `rp-claims-${runId}@example.test`;
|
||||
const loginId = `rp${runId.replaceAll("-", "").slice(0, 18)}`;
|
||||
const loginIdentifier = email;
|
||||
const password = `RpClaims${runId.slice(0, 6)}!1aA`;
|
||||
const redirectUri = "http://127.0.0.1:18080/callback";
|
||||
const subjectByLoginId = new Map();
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
name: "rp-a",
|
||||
clientId: `rp-claims-a-${runId}`,
|
||||
defaults: {
|
||||
approvalLevel: "A",
|
||||
activeMember: true,
|
||||
score: 1,
|
||||
featureList: ["default"],
|
||||
preferences: { theme: "light", density: "comfortable" },
|
||||
contractDate: "2026-06-09",
|
||||
approvedAt: "2026-06-09T09:30",
|
||||
adminManagedNote: "admin-default",
|
||||
},
|
||||
updates: [
|
||||
{
|
||||
label: "admin-update",
|
||||
metadata: {
|
||||
approvalLevel: "B",
|
||||
activeMember: false,
|
||||
score: 42,
|
||||
featureList: ["sso", "claims"],
|
||||
preferences: { theme: "dark", density: "compact" },
|
||||
contractDate: "2026-06-10",
|
||||
approvedAt: "2026-06-09T10:30",
|
||||
adminManagedNote: "admin-updated",
|
||||
approvalLevel_permissions: {
|
||||
readPermission: "admin_only",
|
||||
writePermission: "user_and_admin",
|
||||
},
|
||||
},
|
||||
expected: {
|
||||
approvalLevel: "B",
|
||||
activeMember: false,
|
||||
score: 42,
|
||||
featureList: ["sso", "claims"],
|
||||
preferences: { theme: "dark", density: "compact" },
|
||||
contractDate: "2026-06-10",
|
||||
approvedAt: "2026-06-09T10:30",
|
||||
adminManagedNote: "admin-updated",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "second-update",
|
||||
metadata: {
|
||||
approvalLevel: "C",
|
||||
activeMember: true,
|
||||
score: 7.5,
|
||||
featureList: ["delta", "omega", "42"],
|
||||
preferences: { theme: "contrast", density: "spacious", nested: { level: 2 } },
|
||||
contractDate: "2026-06-11",
|
||||
approvedAt: "2026-06-11T14:45:30Z",
|
||||
adminManagedNote: "admin-updated-again",
|
||||
approvalLevel_permissions: {
|
||||
readPermission: "user_and_admin",
|
||||
writePermission: "user_and_admin",
|
||||
},
|
||||
},
|
||||
expected: {
|
||||
approvalLevel: "C",
|
||||
activeMember: true,
|
||||
score: 7.5,
|
||||
featureList: ["delta", "omega", "42"],
|
||||
preferences: { theme: "contrast", density: "spacious", nested: { level: 2 } },
|
||||
contractDate: "2026-06-11",
|
||||
approvedAt: "2026-06-11T14:45:30Z",
|
||||
adminManagedNote: "admin-updated-again",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "rp-b",
|
||||
clientId: `rp-claims-b-${runId}`,
|
||||
defaults: {
|
||||
approvalLevel: "B-default",
|
||||
activeMember: false,
|
||||
score: 100,
|
||||
featureList: ["rp-b-default"],
|
||||
preferences: { theme: "blue", density: "wide" },
|
||||
contractDate: "2026-07-01",
|
||||
approvedAt: "2026-07-01T08:00",
|
||||
},
|
||||
updates: [
|
||||
{
|
||||
label: "rp-b-isolated-update",
|
||||
metadata: {
|
||||
approvalLevel: "B-only",
|
||||
activeMember: true,
|
||||
score: 314,
|
||||
featureList: ["only", "rp-b"],
|
||||
preferences: { theme: "green", density: "narrow" },
|
||||
contractDate: "2026-07-02",
|
||||
approvedAt: "2026-07-02T09:15",
|
||||
},
|
||||
expected: {
|
||||
approvalLevel: "B-only",
|
||||
activeMember: true,
|
||||
score: 314,
|
||||
featureList: ["only", "rp-b"],
|
||||
preferences: { theme: "green", density: "narrow" },
|
||||
contractDate: "2026-07-02",
|
||||
approvedAt: "2026-07-02T09:15",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const report = {
|
||||
runId,
|
||||
gatewayUrl,
|
||||
devfrontUrl,
|
||||
loginId,
|
||||
loginIdentifier,
|
||||
email,
|
||||
createdAt: new Date().toISOString(),
|
||||
steps: [],
|
||||
scenarios: [],
|
||||
passed: false,
|
||||
};
|
||||
|
||||
function assertDeepEqual(actual, expected, label) {
|
||||
const actualJson = JSON.stringify(canonicalize(actual));
|
||||
const expectedJson = JSON.stringify(canonicalize(expected));
|
||||
if (actualJson !== expectedJson) {
|
||||
throw new Error(`${label}: expected ${expectedJson}, got ${actualJson}`);
|
||||
}
|
||||
}
|
||||
|
||||
function canonicalize(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(canonicalize);
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, item]) => [key, canonicalize(item)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertAbsent(record, key, label) {
|
||||
if (Object.hasOwn(record ?? {}, key)) {
|
||||
throw new Error(`${label}: ${key} must not be present`);
|
||||
}
|
||||
}
|
||||
|
||||
function base64urlDecode(input) {
|
||||
const padded = input.padEnd(input.length + ((4 - (input.length % 4)) % 4), "=");
|
||||
return Buffer.from(padded.replaceAll("-", "+").replaceAll("_", "/"), "base64").toString("utf8");
|
||||
}
|
||||
|
||||
function decodeJwtPayload(token) {
|
||||
const parts = String(token).split(".");
|
||||
if (parts.length < 2) {
|
||||
throw new Error("id_token is not a JWT");
|
||||
}
|
||||
return JSON.parse(base64urlDecode(parts[1]));
|
||||
}
|
||||
|
||||
function base64url(buffer) {
|
||||
return Buffer.from(buffer)
|
||||
.toString("base64")
|
||||
.replaceAll("+", "-")
|
||||
.replaceAll("/", "_")
|
||||
.replaceAll("=", "");
|
||||
}
|
||||
|
||||
function pkceChallenge(verifier) {
|
||||
return base64url(createHash("sha256").update(verifier).digest());
|
||||
}
|
||||
|
||||
function claimDefinitions(defaults) {
|
||||
const claims = [
|
||||
{ key: "approvalLevel", valueType: "text", value: defaults.approvalLevel },
|
||||
{ key: "activeMember", valueType: "boolean", value: String(defaults.activeMember) },
|
||||
{ key: "score", valueType: "number", value: String(defaults.score) },
|
||||
{ key: "featureList", valueType: "array", value: JSON.stringify(defaults.featureList) },
|
||||
{ key: "preferences", valueType: "object", value: JSON.stringify(defaults.preferences) },
|
||||
{ key: "contractDate", valueType: "date", value: defaults.contractDate },
|
||||
{ key: "approvedAt", valueType: "datetime", value: defaults.approvedAt },
|
||||
].filter((claim) => claim.value !== undefined);
|
||||
if (defaults.adminManagedNote !== undefined) {
|
||||
claims.push({
|
||||
key: "adminManagedNote",
|
||||
valueType: "text",
|
||||
value: defaults.adminManagedNote,
|
||||
writePermission: "admin_only",
|
||||
});
|
||||
}
|
||||
return claims.map((claim) => ({
|
||||
namespace: "rp_claims",
|
||||
readPermission: "user_and_admin",
|
||||
writePermission: claim.writePermission ?? "user_and_admin",
|
||||
...claim,
|
||||
}));
|
||||
}
|
||||
|
||||
async function requestJson(url, options = {}) {
|
||||
const { cookieJar, ...fetchOptions } = options;
|
||||
const cookieHeader = cookieJar ? cookieHeaderFromJar(cookieJar) : "";
|
||||
const response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Test-Role": "super_admin",
|
||||
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
||||
...(options.headers ?? {}),
|
||||
},
|
||||
});
|
||||
rememberCookies(cookieJar, response);
|
||||
const text = await response.text();
|
||||
let body = null;
|
||||
if (text) {
|
||||
try {
|
||||
body = JSON.parse(text);
|
||||
} catch {
|
||||
body = text;
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`${options.method ?? "GET"} ${url} failed: ${response.status} ${text}`);
|
||||
}
|
||||
return { response, body };
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
const body = {
|
||||
email,
|
||||
loginId,
|
||||
password,
|
||||
name: `RP Claims ${runId}`,
|
||||
role: "user",
|
||||
tenantSlug,
|
||||
};
|
||||
const result = await requestJson(`${gatewayUrl}/api/v1/admin/users`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const id = result.body?.id ?? result.body?.user?.id ?? result.body?.identityId;
|
||||
if (!id) {
|
||||
throw new Error(`created user response has no id: ${JSON.stringify(result.body)}`);
|
||||
}
|
||||
subjectByLoginId.set(loginIdentifier, id);
|
||||
report.steps.push({ step: "create-user", status: "ok", subject: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
async function createClient(scenario) {
|
||||
const body = {
|
||||
id: scenario.clientId,
|
||||
name: `RP Claims E2E ${scenario.name} ${runId}`,
|
||||
type: "pkce",
|
||||
status: "active",
|
||||
redirectUris: [redirectUri],
|
||||
scopes: ["openid", "profile", "email"],
|
||||
grantTypes: ["authorization_code", "refresh_token"],
|
||||
responseTypes: ["code"],
|
||||
tokenEndpointAuthMethod: "none",
|
||||
skipConsent: false,
|
||||
metadata: {
|
||||
id_token_claims: claimDefinitions(scenario.defaults),
|
||||
tenant_id: "tenant-rp-claims-live-e2e",
|
||||
},
|
||||
};
|
||||
await requestJson(`${devfrontUrl}/api/v1/dev/clients`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
report.steps.push({ step: "create-client", status: "ok", clientId: scenario.clientId });
|
||||
}
|
||||
|
||||
function cookieHeaderFromJar(cookieJar) {
|
||||
return Object.entries(cookieJar ?? {})
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
function rememberCookies(cookieJar, response) {
|
||||
if (!cookieJar || !response?.headers) {
|
||||
return;
|
||||
}
|
||||
const setCookies =
|
||||
typeof response.headers.getSetCookie === "function"
|
||||
? response.headers.getSetCookie()
|
||||
: response.headers.get("set-cookie")
|
||||
? [response.headers.get("set-cookie")]
|
||||
: [];
|
||||
for (const header of setCookies) {
|
||||
const firstPart = String(header ?? "").split(";")[0];
|
||||
const separator = firstPart.indexOf("=");
|
||||
if (separator <= 0) {
|
||||
continue;
|
||||
}
|
||||
cookieJar[firstPart.slice(0, separator)] = firstPart.slice(separator + 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function manualFetch(url, options = {}) {
|
||||
try {
|
||||
const { cookieJar, ...fetchOptions } = options;
|
||||
const cookieHeader = cookieJar ? cookieHeaderFromJar(cookieJar) : "";
|
||||
const response = await fetch(url, {
|
||||
redirect: "manual",
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
||||
...(options.headers ?? {}),
|
||||
},
|
||||
});
|
||||
rememberCookies(cookieJar, response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`manual fetch failed for ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function locationFrom(response, label) {
|
||||
const location = response.headers.get("location");
|
||||
if (!location) {
|
||||
throw new Error(`${label}: redirect location missing, status=${response.status}`);
|
||||
}
|
||||
return new URL(location, gatewayUrl).toString();
|
||||
}
|
||||
|
||||
function searchParam(url, name, label) {
|
||||
const value = new URL(url).searchParams.get(name);
|
||||
if (!value) {
|
||||
throw new Error(`${label}: ${name} missing from ${url}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function authorizeAndDecodeClaims(clientId, label) {
|
||||
const state = `${label}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
const nonce = `${label}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
const verifier = base64url(randomBytes(32));
|
||||
const cookieJar = {};
|
||||
const authorize = new URL(`${gatewayUrl}/oidc/oauth2/auth`);
|
||||
authorize.searchParams.set("client_id", clientId);
|
||||
authorize.searchParams.set("redirect_uri", redirectUri);
|
||||
authorize.searchParams.set("response_type", "code");
|
||||
authorize.searchParams.set("scope", "openid profile email");
|
||||
authorize.searchParams.set("state", state);
|
||||
authorize.searchParams.set("nonce", nonce);
|
||||
authorize.searchParams.set("code_challenge", pkceChallenge(verifier));
|
||||
authorize.searchParams.set("code_challenge_method", "S256");
|
||||
const trace = { label, clientId, authorize: authorize.toString(), redirects: [] };
|
||||
report.steps.push({ step: "authorize-token", status: "started", trace });
|
||||
|
||||
const first = await manualFetch(authorize, { cookieJar });
|
||||
let next = locationFrom(first, `${label}: authorize`);
|
||||
trace.redirects.push({ from: "authorize", status: first.status, location: next });
|
||||
const loginChallenge = searchParam(next, "login_challenge", `${label}: login redirect`);
|
||||
|
||||
const login = await requestJson(`${gatewayUrl}/api/v1/auth/password/login`, {
|
||||
method: "POST",
|
||||
cookieJar,
|
||||
body: JSON.stringify({ loginId: loginIdentifier, password, login_challenge: loginChallenge }),
|
||||
});
|
||||
next = login.body.redirectTo;
|
||||
if (!next) {
|
||||
throw new Error(`${label}: password login response missing redirectTo`);
|
||||
}
|
||||
trace.redirects.push({ from: "password-login", location: next });
|
||||
|
||||
let afterLogin = await manualFetch(next, { cookieJar });
|
||||
next = locationFrom(afterLogin, `${label}: after login`);
|
||||
trace.redirects.push({ from: "after-login", status: afterLogin.status, location: next });
|
||||
if (new URL(next).searchParams.has("consent_challenge")) {
|
||||
const consentChallenge = searchParam(next, "consent_challenge", `${label}: consent redirect`);
|
||||
const consent = await requestJson(`${gatewayUrl}/api/v1/auth/consent/accept`, {
|
||||
method: "POST",
|
||||
cookieJar,
|
||||
body: JSON.stringify({
|
||||
consent_challenge: consentChallenge,
|
||||
grant_scope: ["openid", "profile", "email"],
|
||||
}),
|
||||
});
|
||||
next = consent.body.redirectTo;
|
||||
if (!next) {
|
||||
throw new Error(`${label}: consent response missing redirectTo`);
|
||||
}
|
||||
trace.redirects.push({ from: "consent-accept", location: next });
|
||||
afterLogin = await manualFetch(next, { cookieJar });
|
||||
next = locationFrom(afterLogin, `${label}: after consent`);
|
||||
trace.redirects.push({ from: "after-consent", status: afterLogin.status, location: next });
|
||||
}
|
||||
|
||||
if (new URL(next).searchParams.has("error")) {
|
||||
const callback = new URL(next);
|
||||
throw new Error(`${label}: callback error ${callback.searchParams.get("error")} ${callback.searchParams.get("error_description") ?? ""}`.trim());
|
||||
}
|
||||
|
||||
if (!new URL(next).searchParams.has("code")) {
|
||||
if (new URL(next).hostname === "127.0.0.1") {
|
||||
throw new Error(`${label}: callback has no code: ${next}`);
|
||||
}
|
||||
const final = await manualFetch(next, { cookieJar });
|
||||
next = locationFrom(final, `${label}: final code redirect`);
|
||||
trace.redirects.push({ from: "final", status: final.status, location: next });
|
||||
}
|
||||
|
||||
const code = searchParam(next, "code", `${label}: callback`);
|
||||
const returnedState = searchParam(next, "state", `${label}: callback`);
|
||||
if (returnedState !== state) {
|
||||
throw new Error(`${label}: state mismatch`);
|
||||
}
|
||||
|
||||
const tokenResponse = await fetch(`${gatewayUrl}/oidc/oauth2/token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
code,
|
||||
code_verifier: verifier,
|
||||
}),
|
||||
});
|
||||
const tokenText = await tokenResponse.text();
|
||||
let tokenBody;
|
||||
try {
|
||||
tokenBody = JSON.parse(tokenText);
|
||||
} catch {
|
||||
tokenBody = tokenText;
|
||||
}
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error(`${label}: token failed ${tokenResponse.status} ${tokenText}`);
|
||||
}
|
||||
const payload = decodeJwtPayload(tokenBody.id_token);
|
||||
report.steps.push({ step: "authorize-token", status: "ok", trace });
|
||||
return {
|
||||
idTokenPayload: payload,
|
||||
rpClaims: payload.rp_claims,
|
||||
tokenResponse: {
|
||||
token_type: tokenBody.token_type,
|
||||
expires_in: tokenBody.expires_in,
|
||||
scope: tokenBody.scope,
|
||||
has_access_token: Boolean(tokenBody.access_token),
|
||||
has_id_token: Boolean(tokenBody.id_token),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function updateClaimsFromDevfrontBrowser(page, scenario, metadata, label) {
|
||||
const result = await requestClaimsUpdateFromDevfrontBrowser(page, scenario, metadata);
|
||||
if (!result.ok) {
|
||||
throw new Error(`${label}: devfront metadata update failed ${result.status} ${JSON.stringify(result.body)}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function requestClaimsUpdateFromDevfrontBrowser(page, scenario, metadata) {
|
||||
const subject = subjectByLoginId.get(loginIdentifier);
|
||||
return page.evaluate(
|
||||
async ({ clientId, subject, metadata }) => {
|
||||
const response = await fetch(`/api/v1/dev/clients/${clientId}/users/${subject}/metadata`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Test-Role": "super_admin",
|
||||
"X-Tenant-ID": "tenant-rp-claims-live-e2e",
|
||||
},
|
||||
body: JSON.stringify({ metadata }),
|
||||
});
|
||||
const text = await response.text();
|
||||
let body;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
body = text;
|
||||
}
|
||||
return { ok: response.ok, status: response.status, body };
|
||||
},
|
||||
{ clientId: scenario.clientId, subject, metadata },
|
||||
);
|
||||
}
|
||||
|
||||
let browser;
|
||||
try {
|
||||
await mkdir(reportDir, { recursive: true });
|
||||
const subject = await createUser();
|
||||
report.subject = subject;
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
await createClient(scenario);
|
||||
}
|
||||
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({
|
||||
extraHTTPHeaders: {
|
||||
"X-Test-Role": "super_admin",
|
||||
"X-Tenant-ID": "tenant-rp-claims-live-e2e",
|
||||
},
|
||||
});
|
||||
await page.goto(devfrontUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
const scenarioReport = {
|
||||
name: scenario.name,
|
||||
clientId: scenario.clientId,
|
||||
defaultCheck: null,
|
||||
negativeChecks: [],
|
||||
updates: [],
|
||||
};
|
||||
const defaults = await authorizeAndDecodeClaims(scenario.clientId, `${scenario.name}-default`);
|
||||
assertDeepEqual(defaults.rpClaims, scenario.defaults, `${scenario.name}: default rp_claims`);
|
||||
scenarioReport.defaultCheck = {
|
||||
expected: scenario.defaults,
|
||||
actual: defaults.rpClaims,
|
||||
token: defaults.tokenResponse,
|
||||
};
|
||||
|
||||
const undefinedClaimResult = await requestClaimsUpdateFromDevfrontBrowser(page, scenario, {
|
||||
internalMemo: "must-be-rejected",
|
||||
});
|
||||
if (undefinedClaimResult.status !== 400) {
|
||||
throw new Error(`${scenario.name}: undefined claim update should be rejected with 400, got ${undefinedClaimResult.status}`);
|
||||
}
|
||||
scenarioReport.negativeChecks.push({
|
||||
label: "undefined-claim-rejected",
|
||||
status: undefinedClaimResult.status,
|
||||
body: undefinedClaimResult.body,
|
||||
});
|
||||
|
||||
if (Object.hasOwn(scenario.defaults, "score")) {
|
||||
const invalidTypeResult = await requestClaimsUpdateFromDevfrontBrowser(page, scenario, {
|
||||
score: "not-a-number",
|
||||
});
|
||||
if (invalidTypeResult.status !== 400) {
|
||||
throw new Error(`${scenario.name}: invalid number update should be rejected with 400, got ${invalidTypeResult.status}`);
|
||||
}
|
||||
scenarioReport.negativeChecks.push({
|
||||
label: "invalid-number-rejected",
|
||||
status: invalidTypeResult.status,
|
||||
body: invalidTypeResult.body,
|
||||
});
|
||||
}
|
||||
|
||||
for (const update of scenario.updates) {
|
||||
const updateResult = await updateClaimsFromDevfrontBrowser(page, scenario, update.metadata, `${scenario.name}-${update.label}`);
|
||||
const decoded = await authorizeAndDecodeClaims(scenario.clientId, `${scenario.name}-${update.label}`);
|
||||
assertDeepEqual(decoded.rpClaims, update.expected, `${scenario.name}.${update.label}: updated rp_claims`);
|
||||
assertAbsent(decoded.rpClaims, "internalMemo", `${scenario.name}.${update.label}`);
|
||||
assertAbsent(decoded.rpClaims, "approvalLevel_permissions", `${scenario.name}.${update.label}`);
|
||||
assertAbsent(decoded.rpClaims, "adminManagedNote_permissions", `${scenario.name}.${update.label}`);
|
||||
scenarioReport.updates.push({
|
||||
label: update.label,
|
||||
devfrontUpdateStatus: updateResult.status,
|
||||
expected: update.expected,
|
||||
actual: decoded.rpClaims,
|
||||
token: decoded.tokenResponse,
|
||||
});
|
||||
}
|
||||
report.scenarios.push(scenarioReport);
|
||||
}
|
||||
|
||||
const rpAAfter = report.scenarios.find((item) => item.name === "rp-a")?.updates.at(-1)?.actual;
|
||||
const rpBAfter = report.scenarios.find((item) => item.name === "rp-b")?.updates.at(-1)?.actual;
|
||||
if (rpAAfter?.approvalLevel === rpBAfter?.approvalLevel) {
|
||||
throw new Error("rp isolation failed: rp-a and rp-b approvalLevel unexpectedly match");
|
||||
}
|
||||
assertAbsent(rpBAfter, "internalMemo", "rp-b isolation");
|
||||
assertAbsent(rpBAfter, "adminManagedNote", "rp-b isolation");
|
||||
|
||||
report.passed = true;
|
||||
} catch (error) {
|
||||
report.passed = false;
|
||||
report.error = {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
};
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
report.finishedAt = new Date().toISOString();
|
||||
const reportPath = `${reportDir}/rp-claims-live-e2e-${runId}.json`;
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(JSON.stringify({ passed: report.passed, reportPath, runId }, null, 2));
|
||||
}
|
||||
Reference in New Issue
Block a user