add retries to api data token setup and xsrf token fallback

This commit is contained in:
Gregor Vostrak
2026-03-02 16:44:51 +01:00
parent b775aaf1df
commit 5b053bc2c1

View File

@@ -21,44 +21,56 @@ export interface TestContext {
* *
* The browser's native fetch includes the laravel_token cookie (set by * The browser's native fetch includes the laravel_token cookie (set by
* CreateFreshApiToken during the dashboard page load), so authentication * CreateFreshApiToken during the dashboard page load), so authentication
* is handled by the browser's own cookie jar — no Playwright cookie sync * is handled by the browser's own cookie jar. The returned Bearer token is
* issues. The returned Bearer token is then used for all subsequent API * then used for all subsequent API calls, making them independent of cookie state.
* calls, making them completely independent of cookie state. *
* If the first attempt returns 401 (Octane hasn't fully committed the session yet),
* we reload the page to trigger a fresh CreateFreshApiToken and retry once.
*/ */
async function createApiToken(page: Page): Promise<string> { async function createApiToken(page: Page): Promise<string> {
const result = await page.evaluate(async (baseUrl) => { for (let attempt = 0; attempt < 2; attempt++) {
const xsrfCookie = document.cookie const result = await page.evaluate(async (baseUrl) => {
.split('; ') const xsrfCookie = document.cookie.split('; ').find((c) => c.startsWith('XSRF-TOKEN='));
.find((c) => c.startsWith('XSRF-TOKEN=')); const xsrfToken = xsrfCookie
const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.split('=').slice(1).join('='))
? decodeURIComponent(xsrfCookie.split('=').slice(1).join('=')) : '';
: '';
const res = await fetch(`${baseUrl}/api/v1/users/me/api-tokens`, { const res = await fetch(`${baseUrl}/api/v1/users/me/api-tokens`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Accept: 'application/json', Accept: 'application/json',
'X-XSRF-TOKEN': xsrfToken, 'X-XSRF-TOKEN': xsrfToken,
}, },
body: JSON.stringify({ name: 'playwright-test' }), body: JSON.stringify({ name: 'playwright-test' }),
}); });
if (!res.ok) { if (!res.ok) {
throw new Error(`Failed to create API token: ${res.status} ${await res.text()}`); return null;
}
const body = await res.json();
return body.data.access_token as string;
}, PLAYWRIGHT_BASE_URL);
if (result) {
return result;
} }
const body = await res.json(); // Reload to get a fresh laravel_token cookie and retry
return body.data.access_token as string; await page.reload({ waitUntil: 'domcontentloaded' });
}, PLAYWRIGHT_BASE_URL); }
return result; throw new Error('Failed to create API token after retry');
} }
function bearerHeaders(token: string): Record<string, string> { function buildAuthHeaders(token: string, xsrfToken: string): Record<string, string> {
return { return {
Accept: 'application/json', Accept: 'application/json',
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
// XSRF header is needed for web routes (e.g. PUT /teams) that go through
// VerifyCsrfToken middleware. API routes ignore it but it doesn't hurt.
...(xsrfToken ? { 'X-XSRF-TOKEN': xsrfToken } : {}),
}; };
} }
@@ -69,7 +81,11 @@ function bearerHeaders(token: string): Record<string, string> {
export async function setupTestContext(page: Page): Promise<TestContext> { export async function setupTestContext(page: Page): Promise<TestContext> {
const token = await createApiToken(page); const token = await createApiToken(page);
const request = page.request; const request = page.request;
const headers = bearerHeaders(token);
const cookies = await page.context().cookies();
const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN');
const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';
const headers = buildAuthHeaders(token, xsrfToken);
const orgId = await getOrganizationId(request, headers); const orgId = await getOrganizationId(request, headers);
const memberId = await getCurrentMemberId(request, orgId, headers); const memberId = await getCurrentMemberId(request, orgId, headers);