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:
senke 2026-04-05 16:15:11 +02:00
parent 6be941c67c
commit fc5c4fe99d

View file

@ -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).
* 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(
page: Page,
@ -114,57 +111,24 @@ export async function loginViaAPI(
const base = CONFIG.baseURL;
await page.goto(`${base}/`, { waitUntil: 'commit', timeout: CONFIG.timeouts.navigation });
const cached = loginCache.get(email);
let accessToken: string;
const response = await page.request.post(`${base}/api/v1/auth/login`, {
data: { email, password, remember_me: false },
});
if (cached && cached.expiry > Date.now()) {
// Reuse cached token
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
expect(response.ok(), `Login API failed: ${response.status()} for ${email}`).toBeTruthy();
// STRICT: login must succeed
expect(response.ok(), `Login API failed: ${response.status()} for ${email}`).toBeTruthy();
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) => {
// Set the auth-storage flag so React knows user is authenticated.
// The actual token is in an httpOnly cookie set automatically by the backend response.
await page.evaluate(() => {
const authState = {
state: { isAuthenticated: true, isLoading: false, error: null },
version: 1,
};
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 });
// Wait for auth initialization to complete
await page.locator('main, [role="main"]').first().waitFor({
state: 'visible',
timeout: 20_000,
@ -182,17 +146,14 @@ export async function loginViaAPI(
export async function navigateTo(page: Page, path: string): Promise<void> {
const url = path.startsWith('http') ? path : `${CONFIG.baseURL}${path}`;
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
await page.locator('main, [role="main"]').first().waitFor({
state: 'visible',
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);
}
/**