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) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-31 20:07:26 +03:00
parent 3f6fb501dd
commit 513b0f7470
2 changed files with 68 additions and 33 deletions

View File

@@ -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);

View File

@@ -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<string> {
// 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<string | null> {
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<void> {
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<string> {
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<string> {
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<{