Skip to content

Commit 6962b66

Browse files
committed
add retries to api data token setup and xsrf token fallback
1 parent be80912 commit 6962b66

1 file changed

Lines changed: 44 additions & 28 deletions

File tree

e2e/utils/api.ts

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,44 +21,56 @@ export interface TestContext {
2121
*
2222
* The browser's native fetch includes the laravel_token cookie (set by
2323
* CreateFreshApiToken during the dashboard page load), so authentication
24-
* is handled by the browser's own cookie jar — no Playwright cookie sync
25-
* issues. The returned Bearer token is then used for all subsequent API
26-
* calls, making them completely independent of cookie state.
24+
* is handled by the browser's own cookie jar. The returned Bearer token is
25+
* then used for all subsequent API calls, making them independent of cookie state.
26+
*
27+
* If the first attempt returns 401 (Octane hasn't fully committed the session yet),
28+
* we reload the page to trigger a fresh CreateFreshApiToken and retry once.
2729
*/
2830
async function createApiToken(page: Page): Promise<string> {
29-
const result = await page.evaluate(async (baseUrl) => {
30-
const xsrfCookie = document.cookie
31-
.split('; ')
32-
.find((c) => c.startsWith('XSRF-TOKEN='));
33-
const xsrfToken = xsrfCookie
34-
? decodeURIComponent(xsrfCookie.split('=').slice(1).join('='))
35-
: '';
36-
37-
const res = await fetch(`${baseUrl}/api/v1/users/me/api-tokens`, {
38-
method: 'POST',
39-
headers: {
40-
'Content-Type': 'application/json',
41-
Accept: 'application/json',
42-
'X-XSRF-TOKEN': xsrfToken,
43-
},
44-
body: JSON.stringify({ name: 'playwright-test' }),
45-
});
31+
for (let attempt = 0; attempt < 2; attempt++) {
32+
const result = await page.evaluate(async (baseUrl) => {
33+
const xsrfCookie = document.cookie.split('; ').find((c) => c.startsWith('XSRF-TOKEN='));
34+
const xsrfToken = xsrfCookie
35+
? decodeURIComponent(xsrfCookie.split('=').slice(1).join('='))
36+
: '';
37+
38+
const res = await fetch(`${baseUrl}/api/v1/users/me/api-tokens`, {
39+
method: 'POST',
40+
headers: {
41+
'Content-Type': 'application/json',
42+
Accept: 'application/json',
43+
'X-XSRF-TOKEN': xsrfToken,
44+
},
45+
body: JSON.stringify({ name: 'playwright-test' }),
46+
});
47+
48+
if (!res.ok) {
49+
return null;
50+
}
4651

47-
if (!res.ok) {
48-
throw new Error(`Failed to create API token: ${res.status} ${await res.text()}`);
52+
const body = await res.json();
53+
return body.data.access_token as string;
54+
}, PLAYWRIGHT_BASE_URL);
55+
56+
if (result) {
57+
return result;
4958
}
5059

51-
const body = await res.json();
52-
return body.data.access_token as string;
53-
}, PLAYWRIGHT_BASE_URL);
60+
// Reload to get a fresh laravel_token cookie and retry
61+
await page.reload({ waitUntil: 'domcontentloaded' });
62+
}
5463

55-
return result;
64+
throw new Error('Failed to create API token after retry');
5665
}
5766

58-
function bearerHeaders(token: string): Record<string, string> {
67+
function buildAuthHeaders(token: string, xsrfToken: string): Record<string, string> {
5968
return {
6069
Accept: 'application/json',
6170
Authorization: `Bearer ${token}`,
71+
// XSRF header is needed for web routes (e.g. PUT /teams) that go through
72+
// VerifyCsrfToken middleware. API routes ignore it but it doesn't hurt.
73+
...(xsrfToken ? { 'X-XSRF-TOKEN': xsrfToken } : {}),
6274
};
6375
}
6476

@@ -69,7 +81,11 @@ function bearerHeaders(token: string): Record<string, string> {
6981
export async function setupTestContext(page: Page): Promise<TestContext> {
7082
const token = await createApiToken(page);
7183
const request = page.request;
72-
const headers = bearerHeaders(token);
84+
85+
const cookies = await page.context().cookies();
86+
const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN');
87+
const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';
88+
const headers = buildAuthHeaders(token, xsrfToken);
7389

7490
const orgId = await getOrganizationId(request, headers);
7591
const memberId = await getCurrentMemberId(request, orgId, headers);

0 commit comments

Comments
 (0)