Compare commits

..

5 Commits

Author SHA1 Message Date
Gregor Vostrak
785c8b939f only use xsrf token for organization requests 2026-03-02 17:08:08 +01:00
Gregor Vostrak
b2fa07b38b bump retries and wait for networkidle in retry 2026-03-02 16:57:15 +01:00
Gregor Vostrak
5b053bc2c1 add retries to api data token setup and xsrf token fallback 2026-03-02 16:44:51 +01:00
Gregor Vostrak
b775aaf1df use api tokens to create e2e test data 2026-03-02 15:47:02 +01:00
Gregor Vostrak
84c4750c9b Add warning for AI slop pull requests
Added a warning about AI slop pull requests and potential bans.
2026-02-27 20:18:44 +01:00
4 changed files with 67 additions and 10 deletions

View File

@@ -37,6 +37,8 @@ If you have a **feature request**, please [**create a discussion**](https://gith
Please open an issue or start a discussion and wait for approval before submitting a pull request. This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons. Please open an issue or start a discussion and wait for approval before submitting a pull request. This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons.
**If you submit an AI slop pull request (especially without following the proper procedure), you will be banned from future contributions to solidtime.**
Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) before sumbitting a Pull Request. Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) before sumbitting a Pull Request.
We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides. We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides.

View File

@@ -608,7 +608,7 @@ test('test that billable icon shows dollar sign for USD currency on time entry r
page, page,
ctx, ctx,
}) => { }) => {
await updateOrganizationCurrencyViaWeb(ctx, 'USD'); await updateOrganizationCurrencyViaWeb(page, ctx, 'USD');
await goToTimeOverview(page); await goToTimeOverview(page);
await createEmptyTimeEntry(page); await createEmptyTimeEntry(page);
const timeEntryRow = page.locator('[data-testid="time_entry_row"]').first(); const timeEntryRow = page.locator('[data-testid="time_entry_row"]').first();
@@ -621,7 +621,7 @@ test('test that billable icon shows euro sign for EUR currency on time entry row
page, page,
ctx, ctx,
}) => { }) => {
await updateOrganizationCurrencyViaWeb(ctx, 'EUR'); await updateOrganizationCurrencyViaWeb(page, ctx, 'EUR');
await goToTimeOverview(page); await goToTimeOverview(page);
await createEmptyTimeEntry(page); await createEmptyTimeEntry(page);
const timeEntryRow = page.locator('[data-testid="time_entry_row"]').first(); const timeEntryRow = page.locator('[data-testid="time_entry_row"]').first();

View File

@@ -30,7 +30,7 @@ test('test that starting and stopping a timer without description and project wo
}); });
test('test that billable icon shows dollar sign for USD currency', async ({ page, ctx }) => { test('test that billable icon shows dollar sign for USD currency', async ({ page, ctx }) => {
await updateOrganizationCurrencyViaWeb(ctx, 'USD'); await updateOrganizationCurrencyViaWeb(page, ctx, 'USD');
await goToDashboard(page); await goToDashboard(page);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
const billableButton = page.getByRole('button', { name: 'Non Billable' }).first(); const billableButton = page.getByRole('button', { name: 'Non Billable' }).first();
@@ -39,7 +39,7 @@ test('test that billable icon shows dollar sign for USD currency', async ({ page
}); });
test('test that billable icon shows euro sign for EUR currency', async ({ page, ctx }) => { test('test that billable icon shows euro sign for EUR currency', async ({ page, ctx }) => {
await updateOrganizationCurrencyViaWeb(ctx, 'EUR'); await updateOrganizationCurrencyViaWeb(page, ctx, 'EUR');
await goToDashboard(page); await goToDashboard(page);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
const billableButton = page.getByRole('button', { name: 'Non Billable' }).first(); const billableButton = page.getByRole('button', { name: 'Non Billable' }).first();

View File

@@ -16,12 +16,59 @@ export interface TestContext {
// Auth helpers // Auth helpers
// ────────────────────────────────────────────────── // ──────────────────────────────────────────────────
async function getApiHeaders(page: Page): Promise<Record<string, string>> { /**
const cookies = await page.context().cookies(); * Create a Passport API token by calling the token endpoint from the browser.
const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN'); *
* The browser's native fetch includes the laravel_token cookie (set by
* CreateFreshApiToken during the dashboard page load), so authentication
* is handled by the browser's own cookie jar. The returned Bearer token is
* then used for all subsequent API calls, making them 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.
*/
async function createApiToken(page: Page): Promise<string> {
for (let attempt = 0; attempt < 3; attempt++) {
const result = await page.evaluate(async (baseUrl) => {
const xsrfCookie = document.cookie.split('; ').find((c) => c.startsWith('XSRF-TOKEN='));
const xsrfToken = xsrfCookie
? decodeURIComponent(xsrfCookie.split('=').slice(1).join('='))
: '';
const res = await fetch(`${baseUrl}/api/v1/users/me/api-tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-XSRF-TOKEN': xsrfToken,
},
body: JSON.stringify({ name: 'playwright-test' }),
});
if (!res.ok) {
return null;
}
const body = await res.json();
return body.data.access_token as string;
}, PLAYWRIGHT_BASE_URL);
if (result) {
return result;
}
// Reload to get a fresh laravel_token cookie and retry.
// networkidle gives Octane time to fully commit the session.
await page.reload({ waitUntil: 'networkidle' });
}
throw new Error('Failed to create API token after retries');
}
function bearerHeaders(token: string): Record<string, string> {
return { return {
Accept: 'application/json', Accept: 'application/json',
...(xsrfCookie ? { 'X-XSRF-TOKEN': decodeURIComponent(xsrfCookie.value) } : {}), Authorization: `Bearer ${token}`,
}; };
} }
@@ -30,8 +77,10 @@ async function getApiHeaders(page: Page): Promise<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 request = page.request; const request = page.request;
const headers = await getApiHeaders(page); const headers = bearerHeaders(token);
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);
return { request: createAuthenticatedRequest(request, headers), orgId, memberId }; return { request: createAuthenticatedRequest(request, headers), orgId, memberId };
@@ -491,11 +540,17 @@ export async function updateOrganizationSettingViaApi(
} }
export async function updateOrganizationCurrencyViaWeb( export async function updateOrganizationCurrencyViaWeb(
page: Page,
ctx: TestContext, ctx: TestContext,
currency: string, currency: string,
name: string = 'Test Organization' name: string = 'Test Organization'
) { ) {
const response = await ctx.request.put(`${PLAYWRIGHT_BASE_URL}/teams/${ctx.orgId}`, { const cookies = await page.context().cookies();
const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN');
const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';
const response = await page.request.put(`${PLAYWRIGHT_BASE_URL}/teams/${ctx.orgId}`, {
headers: { 'X-XSRF-TOKEN': xsrfToken },
data: { name, currency }, data: { name, currency },
}); });
expect(response.status()).toBe(200); expect(response.status()).toBe(200);