forked from baron/baron-sso
Merge pull request 'feature/df-rp-e2e' (#1135) from feature/df-rp-e2e into dev
Reviewed-on: baron/baron-sso#1135
This commit is contained in:
@@ -1426,7 +1426,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p ../reports
|
mkdir -p ../reports
|
||||||
set +e
|
set +e
|
||||||
pnpm test 2>&1 | tee ../reports/devfront-test.log
|
pnpm run test:ci 2>&1 | tee ../reports/devfront-test.log
|
||||||
test_exit_code=${PIPESTATUS[0]}
|
test_exit_code=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -1442,7 +1442,7 @@ jobs:
|
|||||||
echo "1. \`cd devfront\`"
|
echo "1. \`cd devfront\`"
|
||||||
echo "2. \`pnpm install -C ../common --no-frozen-lockfile\`"
|
echo "2. \`pnpm install -C ../common --no-frozen-lockfile\`"
|
||||||
echo "3. \`pnpm exec playwright install --with-deps\`"
|
echo "3. \`pnpm exec playwright install --with-deps\`"
|
||||||
echo "4. \`pnpm test\`"
|
echo "4. \`pnpm run test:ci\`"
|
||||||
echo
|
echo
|
||||||
echo "## Log Tail (last 200 lines)"
|
echo "## Log Tail (last 200 lines)"
|
||||||
echo '```text'
|
echo '```text'
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
|
"test:ci": "pnpm test",
|
||||||
"test:coverage": "vitest run --coverage --bail 1",
|
"test:coverage": "vitest run --coverage --bail 1",
|
||||||
"test:unit": "vitest run --bail 1",
|
"test:unit": "vitest run --bail 1",
|
||||||
"test:roles": "playwright test tests/devfront-role-switch-report.spec.ts",
|
"test:roles": "playwright test tests/devfront-role-switch-report.spec.ts",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { act } from "react";
|
import { act, useEffect } from "react";
|
||||||
import { createRoot, type Root } from "react-dom/client";
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
import { MemoryRouter, Route, Routes, useNavigate } from "react-router-dom";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import AppLayout from "./AppLayout";
|
import AppLayout from "./AppLayout";
|
||||||
|
|
||||||
@@ -49,6 +49,24 @@ vi.mock("../../lib/i18n", () => ({
|
|||||||
|
|
||||||
const roots: Root[] = [];
|
const roots: Root[] = [];
|
||||||
|
|
||||||
|
type TestWindow = Window & {
|
||||||
|
__baronNavigate?: (to: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function RouteProbe() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(window as TestWindow).__baronNavigate = navigate;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
delete (window as TestWindow).__baronNavigate;
|
||||||
|
};
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
return <div>Client outlet</div>;
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authState.isAuthenticated = true;
|
authState.isAuthenticated = true;
|
||||||
authState.isLoading = false;
|
authState.isLoading = false;
|
||||||
@@ -89,7 +107,7 @@ async function renderLayout(initialEntry = "/clients") {
|
|||||||
<MemoryRouter initialEntries={[initialEntry]}>
|
<MemoryRouter initialEntries={[initialEntry]}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<AppLayout />}>
|
<Route path="/" element={<AppLayout />}>
|
||||||
<Route path="clients" element={<div>Client outlet</div>} />
|
<Route path="clients" element={<RouteProbe />} />
|
||||||
<Route path="profile" element={<div>Profile outlet</div>} />
|
<Route path="profile" element={<div>Profile outlet</div>} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
@@ -181,4 +199,15 @@ describe("devfront AppLayout", () => {
|
|||||||
|
|
||||||
expect(authState.signinSilent).toHaveBeenCalled();
|
expect(authState.signinSilent).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("attempts silent renewal when route changes and the session is expiring", async () => {
|
||||||
|
authState.user.expires_at = Math.floor(Date.now() / 1000) + 60;
|
||||||
|
await renderLayout();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
(window as TestWindow).__baronNavigate?.("/profile");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(authState.signinSilent).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -361,13 +361,7 @@ test.describe("DevFront RP claim cache", () => {
|
|||||||
const defaultValueInput = page
|
const defaultValueInput = page
|
||||||
.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i)
|
.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i)
|
||||||
.first();
|
.first();
|
||||||
await expect(defaultValueInput).toHaveAttribute("inputmode", "numeric");
|
|
||||||
await expect(defaultValueInput).toHaveAttribute("pattern", "-?[0-9]*");
|
|
||||||
await defaultValueInput.fill("3.14");
|
await defaultValueInput.fill("3.14");
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByText(/Claim 기본값이 타입과 맞지 않습니다|does not match/i),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("button", { name: /^저장$|^Save$/i }),
|
page.getByRole("button", { name: /^저장$|^Save$/i }),
|
||||||
).toBeDisabled();
|
).toBeDisabled();
|
||||||
|
|||||||
239
devfront/tests/devfront-login-claims.spec.ts
Normal file
239
devfront/tests/devfront-login-claims.spec.ts
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import {
|
||||||
|
getPersistedOidcUser,
|
||||||
|
installDevApiMock,
|
||||||
|
seedAuth,
|
||||||
|
} from "./helpers/devfront-fixtures";
|
||||||
|
import { captureEvidence } from "./helpers/evidence";
|
||||||
|
|
||||||
|
type ClaimScenario = {
|
||||||
|
title: string;
|
||||||
|
role: "super_admin" | "user";
|
||||||
|
tenantName: string;
|
||||||
|
userMeTenantId: string;
|
||||||
|
userMeCompanyCode: string;
|
||||||
|
profileClaims: Record<string, unknown>;
|
||||||
|
expectedProfileAssertions: Record<string, unknown>;
|
||||||
|
expectTenantsToBeAbsent?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const claimScenarios: ClaimScenario[] = [
|
||||||
|
{
|
||||||
|
title: "Server Side App preserves tenant and rp claims",
|
||||||
|
role: "super_admin",
|
||||||
|
tenantName: "Server Side Tenant",
|
||||||
|
userMeTenantId: "tenant-server",
|
||||||
|
userMeCompanyCode: "server-hq",
|
||||||
|
profileClaims: {
|
||||||
|
tenant_id: "tenant-server",
|
||||||
|
companyCode: "server-hq",
|
||||||
|
profile: {
|
||||||
|
names: {
|
||||||
|
name: "서버 앱 사용자",
|
||||||
|
},
|
||||||
|
emails: ["server@example.com"],
|
||||||
|
},
|
||||||
|
joined_tenants: ["tenant-server", "tenant-ops"],
|
||||||
|
tenants: {
|
||||||
|
"tenant-server": {
|
||||||
|
department: "Platform",
|
||||||
|
grade: "Lead",
|
||||||
|
},
|
||||||
|
"tenant-ops": {
|
||||||
|
department: "Operations",
|
||||||
|
grade: "Member",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rp_claims: {
|
||||||
|
approvalLevel: "A",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
rp_custom_claims: {
|
||||||
|
"server-app": {
|
||||||
|
approvalLevel: "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedProfileAssertions: {
|
||||||
|
tenant_id: "tenant-server",
|
||||||
|
companyCode: "server-hq",
|
||||||
|
joined_tenants: ["tenant-server", "tenant-ops"],
|
||||||
|
rp_claims: {
|
||||||
|
approvalLevel: "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "PKCE preserves nested profile claims without tenant map expansion",
|
||||||
|
role: "user",
|
||||||
|
tenantName: "PKCE Tenant",
|
||||||
|
userMeTenantId: "tenant-pkce",
|
||||||
|
userMeCompanyCode: "pkce-hq",
|
||||||
|
profileClaims: {
|
||||||
|
tenant_id: "tenant-pkce",
|
||||||
|
companyCode: "pkce-hq",
|
||||||
|
profile: {
|
||||||
|
names: {
|
||||||
|
name: "PKCE 사용자",
|
||||||
|
},
|
||||||
|
emails: ["pkce@example.com"],
|
||||||
|
},
|
||||||
|
joined_tenants: ["tenant-pkce"],
|
||||||
|
rp_claims: {
|
||||||
|
features: ["sso", "claims"],
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
rp_custom_claims: {
|
||||||
|
"pkce-app": {
|
||||||
|
features: ["sso", "claims"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedProfileAssertions: {
|
||||||
|
tenant_id: "tenant-pkce",
|
||||||
|
companyCode: "pkce-hq",
|
||||||
|
joined_tenants: ["tenant-pkce"],
|
||||||
|
rp_claims: {
|
||||||
|
features: ["sso", "claims"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectTenantsToBeAbsent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Headless login keeps session claims together with rp claims",
|
||||||
|
role: "super_admin",
|
||||||
|
tenantName: "Headless Tenant",
|
||||||
|
userMeTenantId: "tenant-headless",
|
||||||
|
userMeCompanyCode: "headless-hq",
|
||||||
|
profileClaims: {
|
||||||
|
tenant_id: "tenant-headless",
|
||||||
|
companyCode: "headless-hq",
|
||||||
|
profile: {
|
||||||
|
names: {
|
||||||
|
name: "헤드리스 사용자",
|
||||||
|
},
|
||||||
|
emails: ["headless@example.com"],
|
||||||
|
},
|
||||||
|
joined_tenants: ["tenant-headless", "tenant-support"],
|
||||||
|
tenants: {
|
||||||
|
"tenant-headless": {
|
||||||
|
department: "Automation",
|
||||||
|
grade: "Manager",
|
||||||
|
},
|
||||||
|
"tenant-support": {
|
||||||
|
department: "Support",
|
||||||
|
grade: "Agent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rp_claims: {
|
||||||
|
approvalLevel: "B",
|
||||||
|
loginMode: "headless",
|
||||||
|
},
|
||||||
|
sid: "session-headless-1",
|
||||||
|
session_id: "session-headless-1",
|
||||||
|
metadata: {
|
||||||
|
rp_custom_claims: {
|
||||||
|
"headless-app": {
|
||||||
|
approvalLevel: "B",
|
||||||
|
loginMode: "headless",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedProfileAssertions: {
|
||||||
|
tenant_id: "tenant-headless",
|
||||||
|
companyCode: "headless-hq",
|
||||||
|
joined_tenants: ["tenant-headless", "tenant-support"],
|
||||||
|
rp_claims: {
|
||||||
|
approvalLevel: "B",
|
||||||
|
loginMode: "headless",
|
||||||
|
},
|
||||||
|
sid: "session-headless-1",
|
||||||
|
session_id: "session-headless-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe("DevFront login claims", () => {
|
||||||
|
test.afterEach(async ({ page }, testInfo) => {
|
||||||
|
if (testInfo.status === "passed") {
|
||||||
|
await captureEvidence(page, testInfo, testInfo.title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const scenario of claimScenarios) {
|
||||||
|
test(scenario.title, async ({ page }) => {
|
||||||
|
await seedAuth(page, {
|
||||||
|
role: scenario.role,
|
||||||
|
profile: scenario.profileClaims,
|
||||||
|
});
|
||||||
|
|
||||||
|
await installDevApiMock(page, {
|
||||||
|
clients: [],
|
||||||
|
consents: [],
|
||||||
|
auditLogsByCursor: undefined,
|
||||||
|
users: [],
|
||||||
|
tenants: [
|
||||||
|
{
|
||||||
|
id: scenario.userMeTenantId,
|
||||||
|
name: scenario.tenantName,
|
||||||
|
slug: scenario.userMeCompanyCode,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/v1/user/me", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: "playwright-user",
|
||||||
|
loginId: "playwright@example.com",
|
||||||
|
email: "playwright@example.com",
|
||||||
|
name: "Playwright User",
|
||||||
|
phoneNumber: "",
|
||||||
|
department: "QA",
|
||||||
|
tenantId: "",
|
||||||
|
tenantName: "",
|
||||||
|
role: scenario.role,
|
||||||
|
createdAt: "2026-06-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-06-01T00:00:00.000Z",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/profile");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "내 정보" }),
|
||||||
|
).toBeVisible();
|
||||||
|
const storedUser = await getPersistedOidcUser(page);
|
||||||
|
expect(storedUser).not.toBeNull();
|
||||||
|
expect(storedUser?.profile).toMatchObject(
|
||||||
|
scenario.expectedProfileAssertions,
|
||||||
|
);
|
||||||
|
if (scenario.expectTenantsToBeAbsent) {
|
||||||
|
expect(storedUser?.profile).not.toHaveProperty("tenants");
|
||||||
|
} else {
|
||||||
|
expect(storedUser?.profile).toHaveProperty("tenants");
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
page.getByText(String(scenario.profileClaims.tenant_id)),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByText(scenario.userMeCompanyCode)).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "권한 및 역할" }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "시스템 역할" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByText(
|
||||||
|
scenario.role === "super_admin"
|
||||||
|
? /^(시스템 관리자|Super Admin|SUPER_ADMIN)$/i
|
||||||
|
: /^(일반 사용자|General User|USER)$/i,
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -73,6 +73,22 @@ export type DeveloperRequest = {
|
|||||||
adminNotes?: string; // 추가
|
adminNotes?: string; // 추가
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SeedAuthOptions = {
|
||||||
|
role?: string;
|
||||||
|
accessToken?: string;
|
||||||
|
idToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
sessionState?: string;
|
||||||
|
expiresInSeconds?: number;
|
||||||
|
state?: Record<string, unknown>;
|
||||||
|
profile?: Record<string, unknown>;
|
||||||
|
tenantId?: string;
|
||||||
|
companyCode?: string;
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ClientRelation = {
|
export type ClientRelation = {
|
||||||
relation: string;
|
relation: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
@@ -148,30 +164,100 @@ export function makeClient(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function seedAuth(page: Page, role?: string) {
|
function resolveSeedAuthOptions(
|
||||||
|
roleOrOptions?: string | SeedAuthOptions,
|
||||||
|
): Required<Pick<SeedAuthOptions, "role">> & SeedAuthOptions {
|
||||||
|
if (typeof roleOrOptions === "string") {
|
||||||
|
return { role: roleOrOptions };
|
||||||
|
}
|
||||||
|
return { role: roleOrOptions?.role ?? "super_admin", ...roleOrOptions };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPersistedOidcUser(page: Page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const storage = window.localStorage;
|
||||||
|
for (let index = 0; index < storage.length; index += 1) {
|
||||||
|
const key = storage.key(index);
|
||||||
|
if (
|
||||||
|
key === null ||
|
||||||
|
!key.startsWith("oidc.user:") ||
|
||||||
|
!key.endsWith(":devfront")
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawValue = storage.getItem(key);
|
||||||
|
if (!rawValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawValue) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedAuth(
|
||||||
|
page: Page,
|
||||||
|
roleOrOptions?: string | SeedAuthOptions,
|
||||||
|
) {
|
||||||
|
const options = resolveSeedAuthOptions(roleOrOptions);
|
||||||
const nowInSeconds = Math.floor(Date.now() / 1000);
|
const nowInSeconds = Math.floor(Date.now() / 1000);
|
||||||
seededRoles.set(page, role || "super_admin");
|
const profile = {
|
||||||
|
sub: "playwright-user",
|
||||||
|
email: options.email ?? "playwright@example.com",
|
||||||
|
name: options.name ?? "Playwright User",
|
||||||
|
phone: options.phone ?? "",
|
||||||
|
role: options.profile?.role ?? options.role,
|
||||||
|
tenant_id: options.tenantId ?? "tenant-a",
|
||||||
|
companyCode: options.companyCode ?? "tenant-a",
|
||||||
|
...options.profile,
|
||||||
|
};
|
||||||
|
seededRoles.set(
|
||||||
|
page,
|
||||||
|
typeof profile.role === "string" ? profile.role : options.role,
|
||||||
|
);
|
||||||
|
|
||||||
await page.addInitScript(
|
await page.addInitScript(
|
||||||
({ issuedAt, injectedRole }) => {
|
({
|
||||||
|
issuedAt,
|
||||||
|
injectedRole,
|
||||||
|
injectedProfile,
|
||||||
|
injectedState,
|
||||||
|
injectedIdToken,
|
||||||
|
injectedAccessToken,
|
||||||
|
injectedRefreshToken,
|
||||||
|
injectedSessionState,
|
||||||
|
injectedExpiresInSeconds,
|
||||||
|
}) => {
|
||||||
(
|
(
|
||||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||||
)._IS_TEST_MODE = true;
|
)._IS_TEST_MODE = true;
|
||||||
|
|
||||||
const mockOidcUser = {
|
const mockOidcUser = {
|
||||||
id_token: "playwright-id-token",
|
id_token: injectedIdToken,
|
||||||
session_state: "playwright-session",
|
session_state: injectedSessionState,
|
||||||
access_token: "playwright-access-token",
|
access_token: injectedAccessToken,
|
||||||
refresh_token: "playwright-refresh-token",
|
refresh_token: injectedRefreshToken,
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
scope: "openid profile email",
|
scope: "openid profile email",
|
||||||
profile: {
|
profile: {
|
||||||
sub: "playwright-user",
|
sub: "playwright-user",
|
||||||
email: "playwright@example.com",
|
email: "playwright@example.com",
|
||||||
name: "Playwright User",
|
name: "Playwright User",
|
||||||
...(injectedRole ? { role: injectedRole } : {}),
|
phone: "",
|
||||||
|
role: injectedRole || "super_admin",
|
||||||
|
tenant_id: "tenant-a",
|
||||||
|
companyCode: "tenant-a",
|
||||||
|
...(injectedProfile || {}),
|
||||||
},
|
},
|
||||||
expires_at: issuedAt + 3600,
|
state: injectedState,
|
||||||
|
expires_at: issuedAt + injectedExpiresInSeconds,
|
||||||
};
|
};
|
||||||
|
|
||||||
const storageKeys = [
|
const storageKeys = [
|
||||||
@@ -191,9 +277,25 @@ export async function seedAuth(page: Page, role?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.localStorage.setItem("dev_role", injectedRole || "super_admin");
|
window.localStorage.setItem("dev_role", injectedRole || "super_admin");
|
||||||
window.localStorage.setItem("dev_tenant_id", "tenant-a");
|
window.localStorage.setItem(
|
||||||
|
"dev_tenant_id",
|
||||||
|
typeof injectedProfile.tenant_id === "string"
|
||||||
|
? injectedProfile.tenant_id
|
||||||
|
: "tenant-a",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
issuedAt: nowInSeconds,
|
||||||
|
injectedRole:
|
||||||
|
typeof profile.role === "string" ? profile.role : options.role,
|
||||||
|
injectedProfile: profile,
|
||||||
|
injectedState: options.state ?? { returnTo: "/clients" },
|
||||||
|
injectedIdToken: options.idToken ?? "playwright-id-token",
|
||||||
|
injectedAccessToken: options.accessToken ?? "playwright-access-token",
|
||||||
|
injectedRefreshToken: options.refreshToken ?? "playwright-refresh-token",
|
||||||
|
injectedSessionState: options.sessionState ?? "playwright-session",
|
||||||
|
injectedExpiresInSeconds: options.expiresInSeconds ?? 3600,
|
||||||
},
|
},
|
||||||
{ issuedAt: nowInSeconds, injectedRole: role ?? "" },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.route("**/oidc/**", async (route) => {
|
await page.route("**/oidc/**", async (route) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'providers/linked_rps_provider.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
String? resolveLinkedRpLaunchUrl(LinkedRp rp) {
|
String? resolveLinkedRpLaunchUrl(LinkedRp rp) {
|
||||||
final normalizedStatus = rp.status.trim().toLowerCase();
|
final normalizedStatus = rp.status.trim().toLowerCase();
|
||||||
|
|||||||
@@ -4,57 +4,7 @@ import 'package:userfront/core/services/auth_proxy_service.dart';
|
|||||||
import 'package:userfront/core/services/auth_token_store.dart';
|
import 'package:userfront/core/services/auth_token_store.dart';
|
||||||
import 'package:userfront/core/services/http_client.dart';
|
import 'package:userfront/core/services/http_client.dart';
|
||||||
import 'package:userfront/core/services/runtime_env.dart';
|
import 'package:userfront/core/services/runtime_env.dart';
|
||||||
|
import '../models.dart';
|
||||||
class LinkedRp {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String logo;
|
|
||||||
final String url;
|
|
||||||
final String initUrl;
|
|
||||||
final bool autoLoginSupported;
|
|
||||||
final String autoLoginUrl;
|
|
||||||
final String status;
|
|
||||||
final List<String> scopes;
|
|
||||||
final DateTime? lastAuthenticatedAt;
|
|
||||||
|
|
||||||
LinkedRp({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.logo,
|
|
||||||
required this.url,
|
|
||||||
required this.initUrl,
|
|
||||||
required this.autoLoginSupported,
|
|
||||||
required this.autoLoginUrl,
|
|
||||||
required this.status,
|
|
||||||
required this.scopes,
|
|
||||||
required this.lastAuthenticatedAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory LinkedRp.fromJson(Map<String, dynamic> json) {
|
|
||||||
final rawLastAuth = json['lastAuthenticatedAt']?.toString() ?? '';
|
|
||||||
DateTime? parsedLastAuth;
|
|
||||||
if (rawLastAuth.isNotEmpty) {
|
|
||||||
try {
|
|
||||||
parsedLastAuth = DateTime.parse(rawLastAuth).toLocal();
|
|
||||||
} catch (_) {
|
|
||||||
parsedLastAuth = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LinkedRp(
|
|
||||||
id: json['id']?.toString() ?? '',
|
|
||||||
name: json['name']?.toString() ?? '',
|
|
||||||
logo: json['logo']?.toString() ?? '',
|
|
||||||
url: json['url']?.toString() ?? '',
|
|
||||||
initUrl: json['init_url']?.toString() ?? '',
|
|
||||||
autoLoginSupported: json['auto_login_supported'] == true,
|
|
||||||
autoLoginUrl: json['auto_login_url']?.toString() ?? '',
|
|
||||||
status: json['status']?.toString() ?? 'unknown',
|
|
||||||
scopes: (json['scopes'] as List?)?.whereType<String>().toList() ?? [],
|
|
||||||
lastAuthenticatedAt: parsedLastAuth,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LinkedRpsNotifier extends AsyncNotifier<List<LinkedRp>> {
|
class LinkedRpsNotifier extends AsyncNotifier<List<LinkedRp>> {
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import '../../../../core/ui/layout_breakpoints.dart';
|
|||||||
import '../../../../core/ui/toast_service.dart';
|
import '../../../../core/ui/toast_service.dart';
|
||||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||||
import '../domain/dashboard_providers.dart';
|
import '../domain/dashboard_providers.dart';
|
||||||
import '../domain/models.dart' hide LinkedRp;
|
import '../domain/models.dart';
|
||||||
import 'audit_device_utils.dart';
|
import 'audit_device_utils.dart';
|
||||||
import 'package:userfront/i18n.dart';
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:userfront/features/dashboard/domain/linked_rp_launch.dart';
|
import 'package:userfront/features/dashboard/domain/linked_rp_launch.dart';
|
||||||
import 'package:userfront/features/dashboard/domain/providers/linked_rps_provider.dart';
|
import 'package:userfront/features/dashboard/domain/models.dart';
|
||||||
|
|
||||||
LinkedRp _linkedRp({
|
LinkedRp _linkedRp({
|
||||||
required String status,
|
required String status,
|
||||||
|
|||||||
Reference in New Issue
Block a user