Add E2E testing (#201)

* feat: add E2E testing infrastructure with fake GitHub, Playwright, and CI workflow

- Add fake GitHub API server (tests/e2e/fake-github-server.ts) with
  management API for seeding test data
- Add Playwright E2E test suite covering full mirror workflow:
  service health checks, user registration, config, sync, verify
- Add Docker Compose for E2E Gitea instance
- Add orchestrator script (run-e2e.sh) with cleanup
- Add GitHub Actions workflow (e2e-tests.yml) with Gitea service container
- Make GITHUB_API_URL configurable via env var for testing
- Add npm scripts: test:e2e, test:e2e:ci, test:e2e:keep, test:e2e:cleanup

* feat: add real git repos + backup config testing to E2E suite

- Create programmatic test git repos (create-test-repos.ts) with real
  commits, branches (main, develop, feature/*), and tags (v1.0.0, v1.1.0)
- Add git-server container to docker-compose serving bare repos via
  dumb HTTP protocol so Gitea can actually clone them
- Update fake GitHub server to emit reachable clone_url fields pointing
  to the git-server container (configurable via GIT_SERVER_URL env var)
- Add management endpoint POST /___mgmt/set-clone-url for runtime config
- Update E2E spec with real mirroring verification:
  * Verify repos appear in Gitea with actual content
  * Check branches, tags, commits, file content
  * Verify 4/4 repos mirrored successfully
- Add backup configuration test suite:
  * Enable/disable backupBeforeSync config
  * Toggle blockSyncOnBackupFailure
  * Trigger re-sync with backup enabled and verify activities
  * Verify config persistence across changes
- Update CI workflow to use docker compose (not service containers)
  matching the local run-e2e.sh approach
- Update cleanup.sh for git-repos directory and git-server port
- All 22 tests passing with real git content verification

* refactor: split E2E tests into focused files + add force-push tests

Split the monolithic e2e.spec.ts (1335 lines) into 5 focused spec files
and a shared helpers module:

  helpers.ts                 — constants, GiteaAPI, auth, saveConfig, utilities
  01-health.spec.ts          — service health checks (4 tests)
  02-mirror-workflow.spec.ts — full first-mirror journey (8 tests)
  03-backup.spec.ts          — backup config toggling (6 tests)
  04-force-push.spec.ts      — force-push simulation & backup verification (9 tests)
  05-sync-verification.spec.ts — dynamic repos, content integrity, reset (5 tests)

The force-push tests are the critical addition:
  F0: Record original state (commit SHAs, file content)
  F1: Rewrite source repo history (simulate force-push)
  F2: Sync to Gitea WITHOUT backup
  F3: Verify data loss — LICENSE file gone, README overwritten
  F4: Restore source, re-mirror to clean state
  F5: Enable backup, force-push again, sync through app
  F6: Verify Gitea reflects the force-push
  F7: Verify backup system was invoked (snapshot activities logged)
  F8: Restore source repo for subsequent tests

Also added to helpers.ts:
  - GiteaAPI.getBranch(), .getCommit(), .triggerMirrorSync()
  - getRepositoryIds(), triggerMirrorJobs(), triggerSyncRepo()

All 32 tests passing.

* Try to fix actions

* Try to fix the other action

* Add debug info to check why e2e action is failing

* More debug info

* Even more debug info

* E2E fix attempt #1

* E2E fix attempt #2

* more debug again

* E2E fix attempt #3

* E2E fix attempt #4

* Remove a bunch of debug info

* Hopefully fix backup bug

* Force backups to succeed
This commit is contained in:
Xyndra
2026-03-01 03:05:13 +01:00
committed by GitHub
parent 61841dd7a5
commit 2e00a610cb
19 changed files with 5365 additions and 59 deletions

455
tests/e2e/run-e2e.sh Executable file
View File

@@ -0,0 +1,455 @@
#!/usr/bin/env bash
# ────────────────────────────────────────────────────────────────────────────────
# E2E Test Orchestrator
#
# Starts all required services, runs Playwright E2E tests, and tears down.
#
# Services managed:
# 1. Gitea instance (Docker/Podman on port 3333)
# 2. Fake GitHub API (Node.js on port 4580)
# 3. gitea-mirror app (Astro dev server on port 4321)
#
# Usage:
# ./tests/e2e/run-e2e.sh # full run (cleanup → start → test → teardown)
# ./tests/e2e/run-e2e.sh --no-build # skip the Astro build step
# ./tests/e2e/run-e2e.sh --keep # don't tear down services after tests
# ./tests/e2e/run-e2e.sh --ci # CI-friendly mode (stricter, no --keep)
#
# Environment variables:
# GITEA_PORT (default: 3333)
# FAKE_GITHUB_PORT (default: 4580)
# APP_PORT (default: 4321)
# SKIP_CLEANUP (default: false) set "true" to skip initial cleanup
# BUN_CMD (default: auto-detected bun or "npx --yes bun")
# ────────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# ─── Resolve paths ────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.e2e.yml"
# ─── Configuration ────────────────────────────────────────────────────────────
GITEA_PORT="${GITEA_PORT:-3333}"
FAKE_GITHUB_PORT="${FAKE_GITHUB_PORT:-4580}"
APP_PORT="${APP_PORT:-4321}"
GIT_SERVER_PORT="${GIT_SERVER_PORT:-4590}"
GITEA_URL="http://localhost:${GITEA_PORT}"
FAKE_GITHUB_URL="http://localhost:${FAKE_GITHUB_PORT}"
APP_URL="http://localhost:${APP_PORT}"
GIT_SERVER_URL="http://localhost:${GIT_SERVER_PORT}"
# URL that Gitea (inside Docker) uses to reach the git-server container
GIT_SERVER_INTERNAL_URL="http://git-server"
NO_BUILD=false
KEEP_RUNNING=false
CI_MODE=false
for arg in "$@"; do
case "$arg" in
--no-build) NO_BUILD=true ;;
--keep) KEEP_RUNNING=true ;;
--ci) CI_MODE=true ;;
--help|-h)
echo "Usage: $0 [--no-build] [--keep] [--ci]"
exit 0
;;
esac
done
# ─── Detect tools ─────────────────────────────────────────────────────────────
# Container runtime
COMPOSE_CMD=""
CONTAINER_CMD=""
if command -v podman-compose &>/dev/null; then
COMPOSE_CMD="podman-compose"
CONTAINER_CMD="podman"
elif command -v docker-compose &>/dev/null; then
COMPOSE_CMD="docker-compose"
CONTAINER_CMD="docker"
elif command -v docker &>/dev/null && docker compose version &>/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
CONTAINER_CMD="docker"
else
echo "ERROR: No container compose tool found. Install docker-compose or podman-compose."
exit 1
fi
# Bun or fallback
if command -v bun &>/dev/null; then
BUN_CMD="${BUN_CMD:-bun}"
elif command -v npx &>/dev/null; then
# Use npx to run bun commands works on CI with setup-bun action
BUN_CMD="${BUN_CMD:-npx --yes bun}"
else
echo "ERROR: Neither bun nor npx found."
exit 1
fi
# Node/tsx for the fake GitHub server
if command -v tsx &>/dev/null; then
TSX_CMD="tsx"
elif command -v npx &>/dev/null; then
TSX_CMD="npx --yes tsx"
else
echo "ERROR: Neither tsx nor npx found."
exit 1
fi
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ E2E Test Orchestrator ║"
echo "╠══════════════════════════════════════════════════════════════╣"
echo "║ Container runtime : $COMPOSE_CMD"
echo "║ Bun command : $BUN_CMD"
echo "║ TSX command : $TSX_CMD"
echo "║ Gitea URL : $GITEA_URL"
echo "║ Fake GitHub URL : $FAKE_GITHUB_URL"
echo "║ App URL : $APP_URL"
echo "║ Git Server URL : $GIT_SERVER_URL"
echo "║ Git Server (int) : $GIT_SERVER_INTERNAL_URL"
echo "║ CI mode : $CI_MODE"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# ─── PID tracking for cleanup ─────────────────────────────────────────────────
FAKE_GITHUB_PID=""
APP_PID=""
EXIT_CODE=0
cleanup_on_exit() {
local code=$?
echo ""
echo "────────────────────────────────────────────────────────────────"
echo "[teardown] Cleaning up..."
# Kill fake GitHub server
if [[ -n "$FAKE_GITHUB_PID" ]] && kill -0 "$FAKE_GITHUB_PID" 2>/dev/null; then
echo "[teardown] Stopping fake GitHub server (PID $FAKE_GITHUB_PID)..."
kill "$FAKE_GITHUB_PID" 2>/dev/null || true
wait "$FAKE_GITHUB_PID" 2>/dev/null || true
fi
rm -f "$SCRIPT_DIR/.fake-github.pid"
# Kill app server
if [[ -n "$APP_PID" ]] && kill -0 "$APP_PID" 2>/dev/null; then
echo "[teardown] Stopping gitea-mirror app (PID $APP_PID)..."
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
fi
rm -f "$SCRIPT_DIR/.app.pid"
# Stop containers (unless --keep)
if [[ "$KEEP_RUNNING" == false ]]; then
if [[ -n "$COMPOSE_CMD" ]] && [[ -f "$COMPOSE_FILE" ]]; then
echo "[teardown] Stopping Gitea container..."
$COMPOSE_CMD -f "$COMPOSE_FILE" down --volumes --remove-orphans 2>/dev/null || true
fi
else
echo "[teardown] --keep flag set, leaving services running"
fi
echo "[teardown] Done."
# Use the test exit code, not the cleanup exit code
if [[ $EXIT_CODE -ne 0 ]]; then
exit $EXIT_CODE
fi
exit $code
}
trap cleanup_on_exit EXIT INT TERM
# ─── Step 0: Cleanup previous run ────────────────────────────────────────────
if [[ "${SKIP_CLEANUP:-false}" != "true" ]]; then
echo "┌──────────────────────────────────────────────────────────────┐"
echo "│ Step 0: Cleanup previous E2E run │"
echo "└──────────────────────────────────────────────────────────────┘"
bash "$SCRIPT_DIR/cleanup.sh" --soft 2>/dev/null || true
echo ""
fi
# ─── Step 1: Install dependencies ────────────────────────────────────────────
echo "┌──────────────────────────────────────────────────────────────┐"
echo "│ Step 1: Install dependencies │"
echo "└──────────────────────────────────────────────────────────────┘"
cd "$PROJECT_ROOT"
$BUN_CMD install 2>&1 | tail -5
echo "[deps] ✓ Dependencies installed"
# Install Playwright browsers if needed
if ! npx playwright install --dry-run chromium &>/dev/null 2>&1; then
echo "[deps] Installing Playwright browsers..."
npx playwright install chromium 2>&1 | tail -3
fi
# Always ensure system deps are available (needed in CI/fresh environments)
if [[ "$CI_MODE" == true ]]; then
echo "[deps] Installing Playwright system dependencies..."
npx playwright install-deps chromium 2>&1 | tail -5 || true
fi
echo "[deps] ✓ Playwright ready"
echo ""
# ─── Step 1.5: Create test git repositories ─────────────────────────────────
echo "┌──────────────────────────────────────────────────────────────┐"
echo "│ Step 1.5: Create test git repositories │"
echo "└──────────────────────────────────────────────────────────────┘"
GIT_REPOS_DIR="$SCRIPT_DIR/git-repos"
echo "[git-repos] Creating bare git repos in $GIT_REPOS_DIR ..."
$BUN_CMD run "$SCRIPT_DIR/create-test-repos.ts" --output-dir "$GIT_REPOS_DIR" 2>&1
if [[ ! -f "$GIT_REPOS_DIR/manifest.json" ]]; then
echo "ERROR: Test git repos were not created (manifest.json missing)"
EXIT_CODE=1
exit 1
fi
echo "[git-repos] ✓ Test repositories created"
echo ""
# ─── Step 2: Build the app ──────────────────────────────────────────────────
if [[ "$NO_BUILD" == false ]]; then
echo "┌──────────────────────────────────────────────────────────────┐"
echo "│ Step 2: Build gitea-mirror │"
echo "└──────────────────────────────────────────────────────────────┘"
cd "$PROJECT_ROOT"
# Initialize the database
echo "[build] Initializing database..."
$BUN_CMD run manage-db init 2>&1 | tail -3 || true
# Build the Astro project
echo "[build] Building Astro project..."
GITHUB_API_URL="$FAKE_GITHUB_URL" \
BETTER_AUTH_SECRET="e2e-test-secret" \
$BUN_CMD run build 2>&1 | tail -10
echo "[build] ✓ Build complete"
echo ""
else
echo "[build] Skipped (--no-build flag)"
echo ""
fi
# ─── Step 3: Start Gitea container ──────────────────────────────────────────
echo "┌──────────────────────────────────────────────────────────────┐"
echo "│ Step 3: Start Gitea container │"
echo "└──────────────────────────────────────────────────────────────┘"
$COMPOSE_CMD -f "$COMPOSE_FILE" up -d 2>&1
# Wait for git-server to be healthy first (Gitea depends on it)
echo "[git-server] Waiting for git HTTP server..."
GIT_SERVER_READY=false
for i in $(seq 1 30); do
if curl -sf "${GIT_SERVER_URL}/manifest.json" &>/dev/null; then
GIT_SERVER_READY=true
break
fi
printf "."
sleep 1
done
echo ""
if [[ "$GIT_SERVER_READY" != true ]]; then
echo "ERROR: Git HTTP server did not start within 30 seconds"
echo "[git-server] Container logs:"
$COMPOSE_CMD -f "$COMPOSE_FILE" logs git-server --tail=20 2>/dev/null || true
EXIT_CODE=1
exit 1
fi
echo "[git-server] ✓ Git HTTP server is ready on $GIT_SERVER_URL"
echo "[gitea] Waiting for Gitea to become healthy..."
GITEA_READY=false
for i in $(seq 1 60); do
if curl -sf "${GITEA_URL}/api/v1/version" &>/dev/null; then
GITEA_READY=true
break
fi
printf "."
sleep 2
done
echo ""
if [[ "$GITEA_READY" != true ]]; then
echo "ERROR: Gitea did not become healthy within 120 seconds"
echo "[gitea] Container logs:"
$COMPOSE_CMD -f "$COMPOSE_FILE" logs gitea-e2e --tail=30 2>/dev/null || true
EXIT_CODE=1
exit 1
fi
GITEA_VERSION=$(curl -sf "${GITEA_URL}/api/v1/version" | grep -o '"version":"[^"]*"' | cut -d'"' -f4)
echo "[gitea] ✓ Gitea is ready (version: ${GITEA_VERSION:-unknown})"
echo ""
# ─── Step 4: Start fake GitHub API ──────────────────────────────────────────
echo "┌──────────────────────────────────────────────────────────────┐"
echo "│ Step 4: Start fake GitHub API server │"
echo "└──────────────────────────────────────────────────────────────┘"
PORT=$FAKE_GITHUB_PORT GIT_SERVER_URL="$GIT_SERVER_INTERNAL_URL" \
$TSX_CMD "$SCRIPT_DIR/fake-github-server.ts" &
FAKE_GITHUB_PID=$!
echo "$FAKE_GITHUB_PID" > "$SCRIPT_DIR/.fake-github.pid"
echo "[fake-github] Started (PID: $FAKE_GITHUB_PID)"
echo "[fake-github] Waiting for server to be ready..."
FAKE_READY=false
for i in $(seq 1 30); do
if curl -sf "${FAKE_GITHUB_URL}/___mgmt/health" &>/dev/null; then
FAKE_READY=true
break
fi
# Check if process died
if ! kill -0 "$FAKE_GITHUB_PID" 2>/dev/null; then
echo "ERROR: Fake GitHub server process died"
EXIT_CODE=1
exit 1
fi
printf "."
sleep 1
done
echo ""
if [[ "$FAKE_READY" != true ]]; then
echo "ERROR: Fake GitHub server did not start within 30 seconds"
EXIT_CODE=1
exit 1
fi
echo "[fake-github] ✓ Fake GitHub API is ready on $FAKE_GITHUB_URL"
# Tell the fake GitHub server to use the git-server container URL for clone_url
# (This updates existing repos in the store so Gitea can actually clone them)
echo "[fake-github] Setting clone URL base to $GIT_SERVER_INTERNAL_URL ..."
curl -sf -X POST "${FAKE_GITHUB_URL}/___mgmt/set-clone-url" \
-H "Content-Type: application/json" \
-d "{\"url\": \"${GIT_SERVER_INTERNAL_URL}\"}" || true
echo "[fake-github] ✓ Clone URLs configured"
echo ""
# ─── Step 5: Start gitea-mirror app ────────────────────────────────────────
echo "┌──────────────────────────────────────────────────────────────┐"
echo "│ Step 5: Start gitea-mirror application │"
echo "└──────────────────────────────────────────────────────────────┘"
cd "$PROJECT_ROOT"
# Reinitialize the database in case build step reset it
$BUN_CMD run manage-db init 2>&1 | tail -2 || true
# Start the app with E2E environment
GITHUB_API_URL="$FAKE_GITHUB_URL" \
BETTER_AUTH_SECRET="e2e-test-secret" \
BETTER_AUTH_URL="$APP_URL" \
DATABASE_URL="file:data/gitea-mirror.db" \
HOST="0.0.0.0" \
PORT="$APP_PORT" \
NODE_ENV="production" \
PRE_SYNC_BACKUP_ENABLED="false" \
ENCRYPTION_SECRET="e2e-encryption-secret-32char!!" \
$BUN_CMD run start &
APP_PID=$!
echo "$APP_PID" > "$SCRIPT_DIR/.app.pid"
echo "[app] Started (PID: $APP_PID)"
echo "[app] Waiting for app to be ready..."
APP_READY=false
for i in $(seq 1 90); do
# Try the health endpoint first, then fall back to root
if curl -sf "${APP_URL}/api/health" &>/dev/null 2>&1 || \
curl -sf -o /dev/null -w "%{http_code}" "${APP_URL}/" 2>/dev/null | grep -q "^[23]"; then
APP_READY=true
break
fi
# Check if process died
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo ""
echo "ERROR: gitea-mirror app process died"
EXIT_CODE=1
exit 1
fi
printf "."
sleep 2
done
echo ""
if [[ "$APP_READY" != true ]]; then
echo "ERROR: gitea-mirror app did not start within 180 seconds"
EXIT_CODE=1
exit 1
fi
echo "[app] ✓ gitea-mirror app is ready on $APP_URL"
echo ""
# ─── Step 6: Run Playwright E2E tests ──────────────────────────────────────
echo "┌──────────────────────────────────────────────────────────────┐"
echo "│ Step 6: Run Playwright E2E tests │"
echo "└──────────────────────────────────────────────────────────────┘"
cd "$PROJECT_ROOT"
# Ensure test-results directory exists
mkdir -p "$SCRIPT_DIR/test-results"
# Run Playwright
set +e
APP_URL="$APP_URL" \
GITEA_URL="$GITEA_URL" \
FAKE_GITHUB_URL="$FAKE_GITHUB_URL" \
npx playwright test \
--config "$SCRIPT_DIR/playwright.config.ts" \
--reporter=list
PLAYWRIGHT_EXIT=$?
set -e
echo ""
if [[ $PLAYWRIGHT_EXIT -eq 0 ]]; then
echo "═══════════════════════════════════════════════════════════════"
echo " ✅ E2E tests PASSED"
echo "═══════════════════════════════════════════════════════════════"
else
echo "═══════════════════════════════════════════════════════════════"
echo " ❌ E2E tests FAILED (exit code: $PLAYWRIGHT_EXIT)"
echo "═══════════════════════════════════════════════════════════════"
# On failure, dump some diagnostic info
echo ""
echo "[diag] Gitea container status:"
$COMPOSE_CMD -f "$COMPOSE_FILE" ps 2>/dev/null || true
echo ""
echo "[diag] Gitea container logs (last 20 lines):"
$COMPOSE_CMD -f "$COMPOSE_FILE" logs gitea-e2e --tail=20 2>/dev/null || true
echo ""
echo "[diag] Git server logs (last 10 lines):"
$COMPOSE_CMD -f "$COMPOSE_FILE" logs git-server --tail=10 2>/dev/null || true
echo ""
echo "[diag] Git server health:"
curl -sf "${GIT_SERVER_URL}/manifest.json" 2>/dev/null || echo "(unreachable)"
echo ""
echo "[diag] Fake GitHub health:"
curl -sf "${FAKE_GITHUB_URL}/___mgmt/health" 2>/dev/null || echo "(unreachable)"
echo ""
echo "[diag] App health:"
curl -sf "${APP_URL}/api/health" 2>/dev/null || echo "(unreachable)"
echo ""
# Point to HTML report
if [[ -d "$SCRIPT_DIR/playwright-report" ]]; then
echo "[diag] HTML report: $SCRIPT_DIR/playwright-report/index.html"
echo " Run: npx playwright show-report $SCRIPT_DIR/playwright-report"
fi
EXIT_CODE=$PLAYWRIGHT_EXIT
fi
# EXIT_CODE is used by the trap handler
exit $EXIT_CODE