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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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<{
|
||||
|
||||
Reference in New Issue
Block a user