From 2cf86605e22de559402e37e84ff9658538af6ca8 Mon Sep 17 00:00:00 2001 From: Pavel Sedlak Date: Wed, 22 Apr 2026 03:11:23 +0200 Subject: [PATCH] tests: Allow e2e to use injected token (cli) for UI access --- e2e/README.md | 15 ++-- e2e/tests/api/fixtures.ts | 4 + e2e/tests/common/constants.ts | 6 +- e2e/tests/ui/helpers/Auth.ts | 141 +++++++++++++++++++++++++++++++--- 4 files changed, 148 insertions(+), 18 deletions(-) diff --git a/e2e/README.md b/e2e/README.md index 73ee02339..799056631 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -38,12 +38,14 @@ General: For UI tests: -| Variable | Default Value | Description | -|--------------------------|-----------------------|------------------------------------------| -| TRUSTIFY_UI_URL | http://localhost:3000 | The UI URL | -| AUTH_REQUIRED | true | Whether or not auth is enabled in the UI | -| PLAYWRIGHT_AUTH_USER | admin | User name to be used when authenticating | -| PLAYWRIGHT_AUTH_PASSWORD | admin | Password to be used when authenticating | +| Variable | Default Value | Description | +|--------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------| +| TRUSTIFY_UI_URL | http://localhost:3000 | The UI URL | +| AUTH_REQUIRED | true | Whether or not auth is enabled in the UI | +| PLAYWRIGHT_AUTH_USER | admin | User name to be used when authenticating (form mode) | +| PLAYWRIGHT_AUTH_PASSWORD | admin | Password to be used when authenticating (form mode) | +| PLAYWRIGHT_UI_AUTH_MODE | form | Auth mode: `form` (fill login form) or `token_injection` (client credentials) | +| PLAYWRIGHT_AUTH_SCOPE | | OAuth2 scope for client_credentials token requests (required for token_injection, for EntraID e.g. `api:///.default`) | For API tests: @@ -54,6 +56,7 @@ For API tests: | PLAYWRIGHT_AUTH_URL | | OIDC Base URL, e.g. `http://localhost:9090/realms/trustd`. If not set, we will try to discover it from `index.html` | | PLAYWRIGHT_AUTH_CLIENT_ID | cli | OIDC Client ID | | PLAYWRIGHT_AUTH_CLIENT_SECRET | secret | OIDC Client Secret | +| PLAYWRIGHT_AUTH_SCOPE | | OAuth2 scope for client_credentials (required for Entra ID, e.g. `api:///.default`) | ## Available Commands diff --git a/e2e/tests/api/fixtures.ts b/e2e/tests/api/fixtures.ts index d18e95753..9a4cca5ae 100644 --- a/e2e/tests/api/fixtures.ts +++ b/e2e/tests/api/fixtures.ts @@ -6,6 +6,7 @@ import { AUTH_CLIENT_ID, AUTH_CLIENT_SECRET, AUTH_REQUIRED, + AUTH_SCOPE, AUTH_URL, logger, TRUSTIFY_API_URL, @@ -53,6 +54,9 @@ const getToken = async (baseURL?: string) => { data.append("grant_type", "client_credentials"); data.append("client_id", AUTH_CLIENT_ID); data.append("client_secret", AUTH_CLIENT_SECRET); + if (AUTH_SCOPE) { + data.append("scope", AUTH_SCOPE); + } return await axios.post(tokenServiceURL, data, { headers: { diff --git a/e2e/tests/common/constants.ts b/e2e/tests/common/constants.ts index ad7dfcf24..7c4023e9c 100644 --- a/e2e/tests/common/constants.ts +++ b/e2e/tests/common/constants.ts @@ -5,18 +5,22 @@ export const TRUSTIFY_API_URL = "http://localhost:8080/"; /** - * API only environment variables + * Shared auth environment variables (used by both API and UI tests) */ export const AUTH_URL = process.env.PLAYWRIGHT_AUTH_URL; export const AUTH_CLIENT_ID = process.env.PLAYWRIGHT_AUTH_CLIENT_ID ?? "cli"; export const AUTH_CLIENT_SECRET = process.env.PLAYWRIGHT_AUTH_CLIENT_SECRET ?? "secret"; +export const AUTH_SCOPE = process.env.PLAYWRIGHT_AUTH_SCOPE; /** * UI only environment variables */ export const AUTH_USER = process.env.PLAYWRIGHT_AUTH_USER ?? "admin"; export const AUTH_PASSWORD = process.env.PLAYWRIGHT_AUTH_PASSWORD ?? "admin"; +export const UI_AUTH_MODE = process.env.PLAYWRIGHT_UI_AUTH_MODE ?? "form"; +export const TRUSTIFY_UI_URL = + process.env.TRUSTIFY_UI_URL ?? "http://localhost:3000/"; /** * Log definition diff --git a/e2e/tests/ui/helpers/Auth.ts b/e2e/tests/ui/helpers/Auth.ts index 993a66b2c..b6235907e 100644 --- a/e2e/tests/ui/helpers/Auth.ts +++ b/e2e/tests/ui/helpers/Auth.ts @@ -1,23 +1,142 @@ import { expect, type Page } from "@playwright/test"; +import axios from "axios"; +import https from "node:https"; + import { + AUTH_CLIENT_ID, + AUTH_CLIENT_SECRET, AUTH_PASSWORD, AUTH_REQUIRED, + AUTH_SCOPE, + AUTH_URL, AUTH_USER, + TRUSTIFY_UI_URL, + UI_AUTH_MODE, + logger, } from "../../common/constants"; -export const login = async (page: Page) => { - if (AUTH_REQUIRED === "true") { - const userName = AUTH_USER; - const userPassword = AUTH_PASSWORD; +const httpsAgent = new https.Agent({ rejectUnauthorized: false }); + +const discoverFrontendOidcConfig = async (baseURL: string) => { + const response = await axios.get(baseURL, { + maxRedirects: 0, + httpsAgent, + timeout: 5000, + timeoutErrorMessage: `Could not fetch index.html from ${baseURL}`, + }); + + const matcher = response.data.match(/window._env\s*=\s*"([^"]+)"/); + const serverConfig = matcher?.[1]; + if (!serverConfig) { + throw new Error( + "Could not discover OIDC config from frontend index.html. " + + "Set PLAYWRIGHT_AUTH_URL explicitly.", + ); + } + + const envInfo: Record = JSON.parse(atob(serverConfig)); + return { + oidcServerUrl: envInfo.OIDC_SERVER_URL, + oidcClientId: envInfo.OIDC_CLIENT_ID || "frontend", + }; +}; + +const getClientCredentialsToken = async (authUrl: string) => { + const oidcConfigResponse = await axios.get( + `${authUrl}/.well-known/openid-configuration`, + { httpsAgent }, + ); + const tokenEndpoint = oidcConfigResponse.data.token_endpoint; + expect( + tokenEndpoint, + "Could not discover token_endpoint from OIDC configuration", + ).toBeTruthy(); - await page.goto("/importers"); + const data = new URLSearchParams(); + data.append("grant_type", "client_credentials"); + data.append("client_id", AUTH_CLIENT_ID); + data.append("client_secret", AUTH_CLIENT_SECRET); + if (AUTH_SCOPE) { + data.append("scope", AUTH_SCOPE); + } + + const response = await axios.post(tokenEndpoint, data, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + httpsAgent, + }); + + return response.data as { + access_token: string; + expires_in: number; + token_type: string; + }; +}; + +const loginWithTokenInjection = async (page: Page) => { + const { oidcServerUrl, oidcClientId } = + await discoverFrontendOidcConfig(TRUSTIFY_UI_URL); + logger.info( + `Discovered frontend OIDC config: server=${oidcServerUrl}, clientId=${oidcClientId}`, + ); + + const authUrl = AUTH_URL || oidcServerUrl; + if (!authUrl) { + throw new Error( + "Auth URL not available. Set PLAYWRIGHT_AUTH_URL or ensure the frontend exposes OIDC_SERVER_URL.", + ); + } - await page.fill('input[name="username"]:visible', userName); - await page.fill('input[name="password"]:visible', userPassword); - await page.keyboard.press("Enter"); + const tokenResponse = await getClientCredentialsToken(authUrl); + logger.info("Obtained access token via client_credentials"); + + const expiresAt = + Math.floor(Date.now() / 1000) + (tokenResponse.expires_in || 3600); + + const storageKey = `oidc.user:${oidcServerUrl}:${oidcClientId}`; + const userObject = JSON.stringify({ + access_token: tokenResponse.access_token, + token_type: "Bearer", + scope: AUTH_SCOPE || "openid", + expires_at: expiresAt, + profile: { + sub: AUTH_CLIENT_ID, + }, + }); + + logger.debug(`Injecting token into sessionStorage key: ${storageKey}`); + + await page.addInitScript( + ({ key, value }: { key: string; value: string }) => { + sessionStorage.setItem(key, value); + }, + { key: storageKey, value: userObject }, + ); + + await page.goto("/importers"); + await expect(page.getByRole("heading", { name: "Importers" })).toHaveCount(1); +}; + +const loginWithForm = async (page: Page) => { + const userName = AUTH_USER; + const userPassword = AUTH_PASSWORD; + + await page.goto("/importers"); + + await page.fill('input[name="username"]:visible', userName); + await page.fill('input[name="password"]:visible', userPassword); + await page.keyboard.press("Enter"); + + await expect(page.getByRole("heading", { name: "Importers" })).toHaveCount(1); +}; + +export const login = async (page: Page) => { + if (AUTH_REQUIRED !== "true") { + return; + } - await expect(page.getByRole("heading", { name: "Importers" })).toHaveCount( - 1, - ); // Ensure login was successful + if (UI_AUTH_MODE === "token_injection") { + await loginWithTokenInjection(page); + } else { + await loginWithForm(page); } };