fix(e2e): remove broken login token cache
The cache was skipping the login API call on cached hits, which meant new browser contexts never received the httpOnly auth cookies set by the backend. Each test's browser context is isolated, so the cookie must be freshly set per test via the actual login API call. The rate-limit motivation for the cache is now handled by DISABLE_RATE_LIMIT_FOR_TESTS=true in the backend when started via 'make dev-e2e'. Result: 58 -> 85 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6be941c67c
commit
fc5c4fe99d
1 changed files with 16 additions and 55 deletions
|
|
@ -95,16 +95,13 @@ export async function loginViaUI(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache for login tokens to avoid triggering rate limits.
|
|
||||||
* Key: email, Value: { token, cookies, expiry }
|
|
||||||
*/
|
|
||||||
const loginCache = new Map<string, { token: string; cookies: string; expiry: number }>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login via l'API directement (plus rapide, pour les tests qui n'ont pas besoin de tester le login).
|
* Login via l'API directement (plus rapide, pour les tests qui n'ont pas besoin de tester le login).
|
||||||
* STRICT: echoue si l'API retourne une erreur.
|
* STRICT: echoue si l'API retourne une erreur.
|
||||||
* Uses a token cache to avoid hitting rate limits on repeated logins.
|
*
|
||||||
|
* Auth uses httpOnly cookies set by backend. Each test needs to call the login API
|
||||||
|
* so that the browser context receives the auth cookies. We cannot skip this — the
|
||||||
|
* cookie cannot be copied across browser contexts from JavaScript.
|
||||||
*/
|
*/
|
||||||
export async function loginViaAPI(
|
export async function loginViaAPI(
|
||||||
page: Page,
|
page: Page,
|
||||||
|
|
@ -114,57 +111,24 @@ export async function loginViaAPI(
|
||||||
const base = CONFIG.baseURL;
|
const base = CONFIG.baseURL;
|
||||||
await page.goto(`${base}/`, { waitUntil: 'commit', timeout: CONFIG.timeouts.navigation });
|
await page.goto(`${base}/`, { waitUntil: 'commit', timeout: CONFIG.timeouts.navigation });
|
||||||
|
|
||||||
const cached = loginCache.get(email);
|
const response = await page.request.post(`${base}/api/v1/auth/login`, {
|
||||||
let accessToken: string;
|
data: { email, password, remember_me: false },
|
||||||
|
});
|
||||||
|
|
||||||
if (cached && cached.expiry > Date.now()) {
|
// STRICT: login must succeed
|
||||||
// Reuse cached token
|
expect(response.ok(), `Login API failed: ${response.status()} for ${email}`).toBeTruthy();
|
||||||
accessToken = cached.token;
|
|
||||||
if (cached.cookies) {
|
|
||||||
const cookieParts = cached.cookies.split(';')[0]?.split('=');
|
|
||||||
if (cookieParts && cookieParts.length === 2) {
|
|
||||||
await page.context().addCookies([{
|
|
||||||
name: cookieParts[0].trim(),
|
|
||||||
value: cookieParts[1].trim(),
|
|
||||||
domain: '127.0.0.1',
|
|
||||||
path: '/',
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Call login API
|
|
||||||
const response = await page.request.post(`${base}/api/v1/auth/login`, {
|
|
||||||
data: { email, password, remember_me: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
// STRICT: login must succeed
|
// Set the auth-storage flag so React knows user is authenticated.
|
||||||
expect(response.ok(), `Login API failed: ${response.status()} for ${email}`).toBeTruthy();
|
// The actual token is in an httpOnly cookie set automatically by the backend response.
|
||||||
|
await page.evaluate(() => {
|
||||||
const body = await response.json();
|
|
||||||
accessToken = body?.data?.token?.access_token || '';
|
|
||||||
const setCookie = response.headers()['set-cookie'] || '';
|
|
||||||
|
|
||||||
// Cache the token for 4 minutes (tokens expire in 5min)
|
|
||||||
loginCache.set(email, {
|
|
||||||
token: accessToken,
|
|
||||||
cookies: setCookie,
|
|
||||||
expiry: Date.now() + 4 * 60 * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.evaluate((token) => {
|
|
||||||
const authState = {
|
const authState = {
|
||||||
state: { isAuthenticated: true, isLoading: false, error: null },
|
state: { isAuthenticated: true, isLoading: false, error: null },
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
};
|
||||||
localStorage.setItem('auth-storage', JSON.stringify(authState));
|
localStorage.setItem('auth-storage', JSON.stringify(authState));
|
||||||
if (token) {
|
});
|
||||||
localStorage.setItem('access_token', token);
|
|
||||||
}
|
|
||||||
}, accessToken);
|
|
||||||
|
|
||||||
await page.goto(`${CONFIG.baseURL}/dashboard`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
await page.goto(`${CONFIG.baseURL}/dashboard`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
||||||
// Wait for auth initialization to complete
|
|
||||||
await page.locator('main, [role="main"]').first().waitFor({
|
await page.locator('main, [role="main"]').first().waitFor({
|
||||||
state: 'visible',
|
state: 'visible',
|
||||||
timeout: 20_000,
|
timeout: 20_000,
|
||||||
|
|
@ -182,17 +146,14 @@ export async function loginViaAPI(
|
||||||
export async function navigateTo(page: Page, path: string): Promise<void> {
|
export async function navigateTo(page: Page, path: string): Promise<void> {
|
||||||
const url = path.startsWith('http') ? path : `${CONFIG.baseURL}${path}`;
|
const url = path.startsWith('http') ? path : `${CONFIG.baseURL}${path}`;
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {
|
||||||
|
// networkidle can legitimately timeout on pages with websockets/polling — not a test failure
|
||||||
|
});
|
||||||
// App must render a main content area
|
// App must render a main content area
|
||||||
await page.locator('main, [role="main"]').first().waitFor({
|
await page.locator('main, [role="main"]').first().waitFor({
|
||||||
state: 'visible',
|
state: 'visible',
|
||||||
timeout: 20_000,
|
timeout: 20_000,
|
||||||
});
|
});
|
||||||
// Wait for React hydration + initial data fetches to settle
|
|
||||||
await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => {
|
|
||||||
// networkidle can legitimately timeout on pages with websockets/polling — not a test failure
|
|
||||||
});
|
|
||||||
// Extra settle time for React Query cache + state updates to render
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue