From f6037c90d3f7cf0ebe3742117e2bef9956f43986 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 20 Feb 2026 03:59:33 +0100 Subject: [PATCH] Rework e2e test setup for full isolation Always start an isolated ephemeral Gitea instance with its own temp directory, SQLite database, and config file. This addresses review feedback that using the developer's existing instance is unreliable. - Rewrite test-e2e.sh to create a temp workdir, find a free port, write a minimal app.ini, start the server, and clean up on exit - Build a separate gitea-e2e binary using TEST_TAGS (includes sqlite) - Simplify CI workflow: remove manual app.ini, server start, and redundant build steps - Rename all env vars to use GITEA_TEST_E2E_* prefix - Rename test user from "e2e" to "e2e-user" Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull-e2e-tests.yml | 26 +----- .gitignore | 1 + CONTRIBUTING.md | 5 +- Makefile | 7 +- playwright.config.ts | 2 +- tests/e2e/env.d.ts | 7 +- tests/e2e/milestone.test.ts | 4 +- tests/e2e/readme.test.ts | 4 +- tests/e2e/register.test.ts | 4 +- tests/e2e/repo.test.ts | 4 +- tests/e2e/utils.ts | 6 +- tools/test-e2e.sh | 123 ++++++++++++++------------- 12 files changed, 87 insertions(+), 106 deletions(-) diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index 4bee5c4c26..aac5602e6a 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -31,32 +31,10 @@ jobs: node-version: 24 cache: pnpm cache-dependency-path: pnpm-lock.yaml - - run: make deps-backend - - run: make backend - env: - TAGS: bindata sqlite sqlite_unlock_notify - run: make deps-frontend - run: make frontend - - run: | - mkdir -p custom/conf - cat <<'EOF' > custom/conf/app.ini - [database] - DB_TYPE = sqlite3 - - [server] - HTTP_PORT = 3000 - ROOT_URL = http://localhost:3000 - - [service] - ENABLE_CAPTCHA = false - - [security] - INSTALL_LOCK = true - EOF - - run: ./gitea web & - - run: make playwright - - run: E2E_URL=http://localhost:3000 make test-e2e + - run: make deps-backend + - run: make test-e2e timeout-minutes: 10 env: FORCE_COLOR: 1 - TAGS: bindata sqlite sqlite_unlock_notify diff --git a/.gitignore b/.gitignore index 87051babc1..45e8e9295f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ cpu.out *.log.*.gz /gitea +/gitea-e2e /gitea-vet /debug /integrations.test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index baa288061f..ad892d6adb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -184,9 +184,8 @@ Here's how to run the test suite: | Variable | Description | | :-------------- | :-------------------------------------------------------------------------- | -|``E2E_URL`` | URL of the Gitea server to test against (default: read from ``app.ini``) | -|``E2E_DEBUG`` | When set, show Gitea server output (only for auto-started server) | -|``E2E_FLAGS`` | Additional flags passed to Playwright (e.g. ``--headed --debug``) | +|``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output | +|``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright (e.g. ``--headed --debug``) | ## Translation diff --git a/Makefile b/Makefile index cf5a54f8ae..2a597790fc 100644 --- a/Makefile +++ b/Makefile @@ -200,7 +200,7 @@ clean-all: clean ## delete backend, frontend and integration files .PHONY: clean clean: ## delete backend and integration files - rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \ + rm -rf $(EXECUTABLE) gitea-e2e $(DIST) $(BINDATA_DEST_WILDCARD) \ integrations*.test \ tests/integration/gitea-integration-* \ tests/integration/indexers-* \ @@ -534,8 +534,9 @@ playwright: deps-frontend @$(NODE_VARS) pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium $(PLAYWRIGHT_FLAGS) .PHONY: test-e2e -test-e2e: playwright $(EXECUTABLE) - @EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh $(E2E_FLAGS) +test-e2e: playwright + $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o gitea-e2e + @EXECUTABLE=gitea-e2e ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS) .PHONY: bench-sqlite bench-sqlite: integrations.sqlite.test generate-ini-sqlite diff --git a/playwright.config.ts b/playwright.config.ts index 4df9213440..68f1dbaa63 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ timeout: env.CI ? 6000 : 3000, }, use: { - baseURL: env.E2E_URL?.replace?.(/\/$/g, ''), + baseURL: env.GITEA_TEST_E2E_URL?.replace?.(/\/$/g, ''), locale: 'en-US', actionTimeout: env.CI ? 6000 : 3000, navigationTimeout: env.CI ? 12000 : 6000, diff --git a/tests/e2e/env.d.ts b/tests/e2e/env.d.ts index d2cbaf0f6d..ff8898ef0e 100644 --- a/tests/e2e/env.d.ts +++ b/tests/e2e/env.d.ts @@ -1,7 +1,8 @@ declare namespace NodeJS { interface ProcessEnv { - E2E_USER: string; - E2E_PASSWORD: string; - E2E_URL: string; + GITEA_TEST_E2E_USER: string; + GITEA_TEST_E2E_EMAIL: string; + GITEA_TEST_E2E_PASSWORD: string; + GITEA_TEST_E2E_URL: string; } } diff --git a/tests/e2e/milestone.test.ts b/tests/e2e/milestone.test.ts index 68d5e9b3d0..d63aee0cf2 100644 --- a/tests/e2e/milestone.test.ts +++ b/tests/e2e/milestone.test.ts @@ -6,9 +6,9 @@ test('create a milestone', async ({page}) => { const repoName = `e2e-milestone-${Date.now()}`; await login(page); await apiCreateRepo(page.request, {name: repoName}); - await page.goto(`/${env.E2E_USER}/${repoName}/milestones/new`); + await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`); await page.getByPlaceholder('Title').fill('Test Milestone'); await page.getByRole('button', {name: 'Create Milestone'}).click(); await expect(page.locator('.milestone-list')).toContainText('Test Milestone'); - await apiDeleteRepo(page.request, env.E2E_USER, repoName); + await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName); }); diff --git a/tests/e2e/readme.test.ts b/tests/e2e/readme.test.ts index 999a280f1e..9dc291d5c5 100644 --- a/tests/e2e/readme.test.ts +++ b/tests/e2e/readme.test.ts @@ -5,7 +5,7 @@ import {apiCreateRepo, apiDeleteRepo} from './utils.ts'; test('README renders on repository page', async ({page}) => { const repoName = `e2e-readme-${Date.now()}`; await apiCreateRepo(page.request, {name: repoName}); - await page.goto(`/${env.E2E_USER}/${repoName}`); + await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}`); await expect(page.locator('#readme')).toContainText(repoName); - await apiDeleteRepo(page.request, env.E2E_USER, repoName); + await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName); }); diff --git a/tests/e2e/register.test.ts b/tests/e2e/register.test.ts index d854b345d6..dd42d5113e 100644 --- a/tests/e2e/register.test.ts +++ b/tests/e2e/register.test.ts @@ -51,13 +51,13 @@ test('register then login', async ({page}) => { // delete via API because of issues related to form-fetch-action const response = await page.request.delete(`/api/v1/admin/users/${username}?purge=true`, { - headers: {Authorization: `Basic ${btoa(`${env.E2E_USER}:${env.E2E_PASSWORD}`)}`}, + headers: {Authorization: `Basic ${btoa(`${env.GITEA_TEST_E2E_USER}:${env.GITEA_TEST_E2E_PASSWORD}`)}`}, }); expect(response.ok()).toBeTruthy(); }); test('register with existing username shows error', async ({page}) => { - await page.getByLabel('Username').fill('e2e'); + await page.getByLabel('Username').fill('e2e-user'); await page.getByLabel('Email Address').fill('e2e-duplicate@e2e.gitea.com'); await page.getByLabel('Password', {exact: true}).fill('password123!'); await page.getByLabel('Confirm Password').fill('password123!'); diff --git a/tests/e2e/repo.test.ts b/tests/e2e/repo.test.ts index 1df024511c..cca59d612d 100644 --- a/tests/e2e/repo.test.ts +++ b/tests/e2e/repo.test.ts @@ -8,6 +8,6 @@ test('create a repository', async ({page}) => { await page.goto('/repo/create'); await page.locator('input[name="repo_name"]').fill(repoName); await page.getByRole('button', {name: 'Create Repository'}).click(); - await page.waitForURL(new RegExp(`/${env.E2E_USER}/${repoName}$`)); - await apiDeleteRepo(page.request, env.E2E_USER, repoName); + await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`)); + await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName); }); diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 4ba8cc9d73..6ee16b32f8 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -3,11 +3,11 @@ import {expect} from '@playwright/test'; import type {APIRequestContext, Locator, Page} from '@playwright/test'; export function apiBaseUrl() { - return env.E2E_URL?.replace(/\/$/g, ''); + return env.GITEA_TEST_E2E_URL?.replace(/\/$/g, ''); } export function apiHeaders() { - return {Authorization: `Basic ${globalThis.btoa(`${env.E2E_USER}:${env.E2E_PASSWORD}`)}`}; + return {Authorization: `Basic ${globalThis.btoa(`${env.GITEA_TEST_E2E_USER}:${env.GITEA_TEST_E2E_PASSWORD}`)}`}; } async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => number; text: () => Promise}>, label: string) { @@ -48,7 +48,7 @@ export async function clickDropdownItem(page: Page, trigger: Locator, itemText: await page.getByText(itemText).click(); } -export async function login(page: Page, username = env.E2E_USER, password = env.E2E_PASSWORD) { +export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) { await page.goto('/user/login'); await page.getByLabel('Username or Email Address').fill(username); await page.getByLabel('Password').fill(password); diff --git a/tools/test-e2e.sh b/tools/test-e2e.sh index b5463d7cf5..616bd04d33 100755 --- a/tools/test-e2e.sh +++ b/tools/test-e2e.sh @@ -1,89 +1,90 @@ #!/bin/bash set -euo pipefail -# Determine the Gitea server URL, either from E2E_URL env var or from custom/conf/app.ini -if [ -z "${E2E_URL:-}" ]; then - INI_FILE="custom/conf/app.ini" - if [ ! -f "$INI_FILE" ]; then - echo "error: $INI_FILE not found and E2E_URL not set" >&2 - echo "Either start Gitea with a config or set E2E_URL explicitly:" >&2 - echo " E2E_URL=http://localhost:3000 make test-e2e" >&2 - exit 1 - fi - # Note: this does not respect INI sections, assumes ROOT_URL only appears under [server] - ROOT_URL=$(sed -n 's/^ROOT_URL\s*=\s*//p' "$INI_FILE" | tr -d '[:space:]') - if [ -z "$ROOT_URL" ]; then - echo "error: ROOT_URL not found in $INI_FILE" >&2 - exit 1 - fi - E2E_URL="$ROOT_URL" -fi +# Create isolated work directory +WORK_DIR=$(mktemp -d) -# Normalize URL: trim trailing slash to avoid double slashes when appending paths -E2E_URL="${E2E_URL%/}" +# Find a random free port +FREE_PORT=$(node -e "const s=require('net').createServer();s.listen(0,'127.0.0.1',()=>{console.log(s.address().port);s.close()})") -echo "Using Gitea server: $E2E_URL" - -# Disable CAPTCHA for e2e tests -export GITEA__service__ENABLE_CAPTCHA=false - -SERVER_PID="" cleanup() { - if [ -n "$SERVER_PID" ]; then - echo "Stopping temporary Gitea server (PID $SERVER_PID)..." + if [ -n "${SERVER_PID:-}" ]; then kill "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true fi + rm -rf "$WORK_DIR" } trap cleanup EXIT -# For local development, if no gitea server is running, start a temporary one. -if [ -z "${CI:-}" ] && ! curl -sf --max-time 5 "$E2E_URL" > /dev/null 2>&1; then - if [ ! -x "./$EXECUTABLE" ]; then - echo "error: ./$EXECUTABLE not found or not executable, run 'make backend' first" >&2 - exit 1 - fi - echo "Starting temporary Gitea server..." - if [ -n "${E2E_DEBUG:-}" ]; then - "./$EXECUTABLE" web & - else - "./$EXECUTABLE" web > /dev/null 2>&1 & - fi - SERVER_PID=$! -fi +# Write config file for isolated instance +mkdir -p "$WORK_DIR/custom/conf" +cat > "$WORK_DIR/custom/conf/app.ini" < "$WORK_DIR/server.log" 2>&1 & +fi +SERVER_PID=$! + +# Wait for server to be reachable +E2E_URL="http://localhost:$FREE_PORT" MAX_WAIT=120 ELAPSED=0 while ! curl -sf --max-time 5 "$E2E_URL" > /dev/null 2>&1; do - if [ -n "$SERVER_PID" ] && ! kill -0 "$SERVER_PID" 2>/dev/null; then - echo "error: Gitea server process exited unexpectedly" >&2 + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + echo "error: Gitea server process exited unexpectedly. Server log:" >&2 + cat "$WORK_DIR/server.log" 2>/dev/null >&2 || true exit 1 fi if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then - echo "error: Gitea server at $E2E_URL is not reachable after ${MAX_WAIT}s" >&2 + echo "error: Gitea server not reachable after ${MAX_WAIT}s. Server log:" >&2 + cat "$WORK_DIR/server.log" 2>/dev/null >&2 || true exit 1 fi sleep 2 ELAPSED=$((ELAPSED + 2)) done -# Create e2e test user if it does not already exist -E2E_USER="e2e" -E2E_EMAIL="e2e@e2e.gitea.com" -E2E_PASSWORD="password" -if ! curl -sf --max-time 5 "$E2E_URL/api/v1/users/$E2E_USER" > /dev/null 2>&1; then - echo "Creating e2e test user..." - if "./$EXECUTABLE" admin user create --username "$E2E_USER" --email "$E2E_EMAIL" --password "$E2E_PASSWORD" --must-change-password=false --admin; then - echo "User '$E2E_USER' created" - else - echo "error: failed to create user '$E2E_USER'" >&2 - exit 1 - fi -fi +echo "Gitea server is ready at $E2E_URL" -export E2E_URL -export E2E_USER -export E2E_PASSWORD +# Create admin test user +GITEA_TEST_E2E_USER="e2e-user" +GITEA_TEST_E2E_EMAIL="e2e-user@e2e.gitea.com" +GITEA_TEST_E2E_PASSWORD="password" +"./$EXECUTABLE" admin user create \ + --username "$GITEA_TEST_E2E_USER" \ + --email "$GITEA_TEST_E2E_EMAIL" \ + --password "$GITEA_TEST_E2E_PASSWORD" \ + --must-change-password=false \ + --admin + +export GITEA_TEST_E2E_URL="$E2E_URL" +export GITEA_TEST_E2E_USER +export GITEA_TEST_E2E_EMAIL +export GITEA_TEST_E2E_PASSWORD pnpm exec playwright test "$@"