From 513b0f7470204a9a2c0eba7d57796cde016038cd Mon Sep 17 00:00:00 2001 From: master <> Date: Tue, 31 Mar 2026 20:07:26 +0300 Subject: [PATCH] Fix flaky auth fixture and advisory-sync test timeouts Root cause: after 20+ minutes of serial test execution, the OIDC login flow becomes slower and the 30s token acquisition timeout in live-auth.fixture.ts gets exceeded, causing cascading failures in the last few test files. Fixes: - live-auth.fixture.ts: increase token waitForFunction timeout from 30s to 60s, add retry loop (2 attempts with backoff), increase initial navigation timeout to 45s, extract helper functions for clarity - advisory-sync.e2e.spec.ts: increase page.goto timeout from 30s to 45s for UI tests, add explicit toBeVisible wait on tab before clicking, add explicit timeout on connectivity check API call Co-Authored-By: Claude Opus 4.6 (1M context) --- .../integrations/advisory-sync.e2e.spec.ts | 11 ++- .../e2e/integrations/live-auth.fixture.ts | 90 +++++++++++++------ 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/advisory-sync.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/advisory-sync.e2e.spec.ts index 3cb7211bb..071b711a3 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/advisory-sync.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/advisory-sync.e2e.spec.ts @@ -162,7 +162,9 @@ test.describe('Advisory Sync — Source Management', () => { test('connectivity check returns result with details', async ({ apiRequest }) => { const sourceId = 'osv'; - const resp = await apiRequest.post(`/api/v1/advisory-sources/${sourceId}/check`); + const resp = await apiRequest.post(`/api/v1/advisory-sources/${sourceId}/check`, { + timeout: 30_000, + }); expect(resp.status()).toBe(200); const body = await resp.json(); @@ -179,9 +181,9 @@ test.describe('Advisory Sync — UI Verification', () => { test('Advisory & VEX Sources tab loads catalog', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations/advisory-vex-sources`, { waitUntil: 'networkidle', - timeout: 30_000, + timeout: 45_000, }); - await page.waitForTimeout(3_000); + await page.waitForTimeout(2_000); // Verify the page loaded — should show source catalog content const pageContent = await page.textContent('body'); @@ -200,10 +202,11 @@ test.describe('Advisory Sync — UI Verification', () => { }); test('tab switching to Advisory & VEX works from shell', async ({ liveAuthPage: page }) => { - await page.goto(`${BASE}/setup/integrations`, { waitUntil: 'networkidle', timeout: 30_000 }); + await page.goto(`${BASE}/setup/integrations`, { waitUntil: 'networkidle', timeout: 45_000 }); await page.waitForTimeout(2_000); const tab = page.getByRole('tab', { name: /advisory/i }); + await expect(tab).toBeVisible({ timeout: 10_000 }); await tab.click(); await page.waitForTimeout(1_500); diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/live-auth.fixture.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/live-auth.fixture.ts index cafc7945f..fa089049f 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/live-auth.fixture.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/live-auth.fixture.ts @@ -5,33 +5,39 @@ import { test as base, expect, Page, APIRequestContext } from '@playwright/test' * * Unlike the mocked auth.fixture.ts, this performs a real OIDC login against * the live Authority service and extracts a Bearer token for API calls. + * + * Resilience features: + * - 60s token acquisition timeout (up from 30s) to handle slow OIDC flows + * - Retry loop: up to 2 login attempts before failing + * - Handles all landing states: /welcome, /connect/authorize, direct dashboard */ const BASE_URL = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; const ADMIN_USER = process.env['STELLAOPS_ADMIN_USER'] || 'admin'; const ADMIN_PASS = process.env['STELLAOPS_ADMIN_PASS'] || 'Admin@Stella2026!'; -async function loginAndGetToken(page: Page): Promise { - // Navigate to the app - await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 30_000 }); - await page.waitForTimeout(2_000); - - // Check if already authenticated (session exists) - const existingToken = await page.evaluate(() => { +async function extractToken(page: Page): Promise { + return page.evaluate(() => { const s = sessionStorage.getItem('stellaops.auth.session.full'); - return s ? JSON.parse(s)?.tokens?.accessToken : null; + if (!s) return null; + try { + return JSON.parse(s)?.tokens?.accessToken || null; + } catch { return null; } }); - if (existingToken) return existingToken; +} - // If we land on /welcome, click Sign In - if (page.url().includes('/welcome')) { +async function performLogin(page: Page): Promise { + const url = page.url(); + + // State 1: On /welcome page — click Sign In to start OIDC flow + if (url.includes('/welcome')) { const signInBtn = page.getByRole('button', { name: /sign in/i }); await signInBtn.waitFor({ state: 'visible', timeout: 10_000 }); await signInBtn.click(); await page.waitForTimeout(3_000); } - // If already on /connect/authorize, fill the login form + // State 2: On OIDC /connect/ page — fill credentials if (page.url().includes('/connect/')) { const usernameField = page.getByRole('textbox', { name: /username/i }); await usernameField.waitFor({ state: 'visible', timeout: 15_000 }); @@ -40,26 +46,52 @@ async function loginAndGetToken(page: Page): Promise { await page.getByRole('button', { name: /sign in/i }).click(); } - // Wait for the session token to appear in sessionStorage (polls every 500ms) - const token = await page.waitForFunction( - () => { - const s = sessionStorage.getItem('stellaops.auth.session.full'); - if (!s) return null; - try { - const parsed = JSON.parse(s); - return parsed?.tokens?.accessToken || null; - } catch { return null; } - }, - null, - { timeout: 30_000, polling: 500 }, - ); + // State 3: Already on dashboard (session cookie still valid) — token should be in sessionStorage +} - const tokenValue = await token.jsonValue() as string; - if (!tokenValue) { - throw new Error('Login succeeded but failed to extract auth token from sessionStorage'); +async function loginAndGetToken(page: Page): Promise { + const maxAttempts = 2; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + // Navigate to the app + await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 45_000 }); + await page.waitForTimeout(2_000); + + // Check if already authenticated + const existingToken = await extractToken(page); + if (existingToken) return existingToken; + + // Perform the login flow + await performLogin(page); + + // Wait for token to appear in sessionStorage (60s timeout, poll every 500ms) + const tokenHandle = await page.waitForFunction( + () => { + const s = sessionStorage.getItem('stellaops.auth.session.full'); + if (!s) return null; + try { + return JSON.parse(s)?.tokens?.accessToken || null; + } catch { return null; } + }, + null, + { timeout: 60_000, polling: 500 }, + ); + + const tokenValue = await tokenHandle.jsonValue() as string; + if (tokenValue) return tokenValue; + } catch (err) { + if (attempt === maxAttempts) { + throw new Error( + `Login failed after ${maxAttempts} attempts: ${err instanceof Error ? err.message : String(err)}`, + ); + } + // Brief pause before retry + await page.waitForTimeout(2_000); + } } - return tokenValue; + throw new Error('Login failed: could not extract auth token'); } export const test = base.extend<{