#!/usr/bin/env bash # ─── End-to-end regression test for svn-mirror ───────────────── # Run from the project root: bash tests/test_e2e.sh set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" TMP="/tmp/svn-mirror-e2e-$$" SVN_REPO="$TMP/svn-repo" GITEA_REPO="$TMP/gitea/testorg/testrepo.git" MIRROR_DATA="$TMP/mirror-data" CONFIG="$TMP/config.yml" PASS=0 FAIL=0 cleanup() { rm -rf "$TMP"; } trap cleanup EXIT ok() { PASS=$((PASS+1)); echo " ✓ $1"; } fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; } die() { echo "FATAL: $1" >&2; exit 1; } # ─── 1. Setup SVN repo ───────────────────────────────────────── setup_svn() { mkdir -p "$TMP" svnadmin create "$SVN_REPO" mkdir -p "$TMP/layout/trunk" "$TMP/layout/branches" "$TMP/layout/tags" svn import "$TMP/layout" "file://$SVN_REPO" -m "Initial layout" --quiet rm -rf "$TMP/layout" svn co "file://$SVN_REPO/trunk" "$TMP/wc" --quiet echo "v1" > "$TMP/wc/README" svn add "$TMP/wc/README" --quiet svn commit -m "Add README v1" "$TMP/wc" --username dev1 --quiet rm -rf "$TMP/wc" } # ─── 2. Setup Gitea bare repo ────────────────────────────────── setup_gitea() { mkdir -p "$(dirname "$GITEA_REPO")" git init --bare "$GITEA_REPO" 2>/dev/null } # ─── 3. Write config ─────────────────────────────────────────── write_config() { cat > "$CONFIG" <" sync_interval: 10 CONF } # ─── 4. Init (git svn clone) ─────────────────────────────────── test_init() { python -m svn_mirror --config "$CONFIG" init 2>&1 | grep -q "✓ test-e2e" \ && ok "init completed" || fail "init failed" } # ─── 5. SVN→Git sync ────────────────────────────────────────── test_svn_to_git() { # Add SVN commits svn co "file://$SVN_REPO/trunk" "$TMP/wc" --quiet echo "v2 from SVN" > "$TMP/wc/README" svn commit -m "Update README to v2" "$TMP/wc" --username dev1 --quiet rm -rf "$TMP/wc" out=$(python -m svn_mirror --config "$CONFIG" sync --direction svn-to-git 2>&1) echo "$out" | grep -q "SVN rev(s) synced to Git" \ && ok "SVN→Git sync created commits" || fail "SVN→Git sync failed" # Verify Git content git --git-dir="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" show master:README \ | grep -q "v2 from SVN" \ && ok "Master README is v2 from SVN" || fail "Master README wrong" } # ─── 6. Git→SVN push ────────────────────────────────────────── test_git_to_svn() { GIT_DIR="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" export GIT_DIR export GIT_AUTHOR_NAME="Dev One" export GIT_AUTHOR_EMAIL="dev1@example.com" export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" BLOB=$(echo "v3 from Git push" | git hash-object -w --stdin) TREE=$(echo "100644 blob $BLOB README" | git mktree) COMMIT=$(git commit-tree $TREE -p master -m "Git-originated change") git update-ref refs/heads/master "$COMMIT" unset GIT_DIR GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL out=$(python -m svn_mirror --config "$CONFIG" push 2>&1) echo "$out" | grep -q "Git commit(s) pushed to SVN" \ && ok "Git→SVN push created SVN revision" || fail "Git→SVN push failed" svn cat "file://$SVN_REPO/trunk/README" | grep -q "v3 from Git push" \ && ok "SVN README is v3 from Git push" || fail "SVN README wrong" } # ─── 7. Branch sync (SVN branch → Git branch) ────────────────── test_svn_branch() { svn copy "file://$SVN_REPO/trunk" "file://$SVN_REPO/branches/stable" \ -m "Create stable branch" --username dev1 --quiet out=$(python -m svn_mirror --config "$CONFIG" sync --direction svn-to-git 2>&1) echo "$out" | grep -q "SVN rev(s) synced to Git" \ && ok "Branch synced to Git" || fail "Branch sync failed" git --git-dir="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" \ rev-parse --verify --quiet refs/heads/stable >/dev/null \ && ok "Git refs/heads/stable exists" || fail "Git refs/heads/stable missing" } # ─── 8. Tag sync (SVN tag → Git tag) ─────────────────────────── test_svn_tag() { svn copy "file://$SVN_REPO/trunk" "file://$SVN_REPO/tags/v1.0" \ -m "Tag v1.0" --username dev1 --quiet out=$(python -m svn_mirror --config "$CONFIG" sync --direction svn-to-git 2>&1) echo "$out" | grep -q "SVN rev(s) synced to Git" \ && ok "Tag synced to Git" || fail "Tag sync failed" git --git-dir="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" \ rev-parse --verify --quiet refs/tags/v1.0 >/dev/null \ && ok "Git refs/tags/v1.0 exists" || fail "Git refs/tags/v1.0 missing" # Verify tag content matches trunk at the SVN revision it was created TAG_README=$(git --git-dir="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" show refs/tags/v1.0:README) MASTER_README=$(git --git-dir="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" show master:README) [ "$TAG_README" = "$MASTER_README" ] \ && ok "Tag v1.0 content matches trunk HEAD" \ || fail "Tag v1.0 content mismatch" echo " tag README: $TAG_README, master README: $MASTER_README" } # ─── 9. Git tag → SVN tag ────────────────────────────────────── test_git_tag() { export GIT_DIR="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" git tag v2.0 master unset GIT_DIR out=$(python -m svn_mirror --config "$CONFIG" push 2>&1) echo "$out" | grep -q "Git commit(s) pushed to SVN" \ && ok "Git tag pushed to SVN" || fail "Git tag push failed" svn ls "file://$SVN_REPO/tags/" | grep -q "v2.0/" \ && ok "SVN tag v2.0 exists" || fail "SVN tag v2.0 missing" svn cat "file://$SVN_REPO/tags/v2.0/README" | grep -q "v3 from Git push" \ && ok "SVN tag v2.0 content correct" || fail "SVN tag v2.0 content wrong" } # ─── 10. Push to Gitea ───────────────────────────────────────── test_gitea_push() { # Sync then push to Gitea out=$(python -m svn_mirror --config "$CONFIG" sync --direction svn-to-git 2>&1) export GIT_DIR="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" git push "$GITEA_REPO" refs/heads/* refs/tags/* 2>&1 unset GIT_DIR git --git-dir="$GITEA_REPO" rev-parse --verify --quiet refs/heads/master >/dev/null \ && ok "Gitea has master" || fail "Gitea missing master" git --git-dir="$GITEA_REPO" rev-parse --verify --quiet refs/heads/stable >/dev/null \ && ok "Gitea has stable" || fail "Gitea missing stable" git --git-dir="$GITEA_REPO" rev-parse --verify --quiet refs/tags/v1.0 >/dev/null \ && ok "Gitea has tag v1.0" || fail "Gitea missing tag v1.0" git --git-dir="$GITEA_REPO" rev-parse --verify --quiet refs/tags/v2.0 >/dev/null \ && ok "Gitea has tag v2.0" || fail "Gitea missing tag v2.0" } # ─── 11. Gitea webhook → fetch → SVN ─────────────────────────── test_webhook_flow() { # Start webhook server in background python -m svn_mirror --config "$CONFIG" webhook & WEBHOOK_PID=$! sleep 1 # Simulate a developer push to Gitea export GIT_DIR="$GITEA_REPO" export GIT_AUTHOR_NAME="Dev Two" export GIT_AUTHOR_EMAIL="dev2@example.com" export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" BLOB=$(echo "v4 from webhook" | git hash-object -w --stdin) TREE=$(echo "100644 blob $BLOB README" | git mktree) COMMIT=$(git commit-tree $TREE -p master -m "Webhook test commit") git update-ref refs/heads/master "$COMMIT" unset GIT_DIR GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL # Trigger webhook curl -s -X POST http://localhost:8080/webhook \ -H "Content-Type: application/json" \ -d "{ \"ref\": \"refs/heads/master\", \"repository\": { \"owner\": {\"login\": \"testorg\"}, \"name\": \"testrepo\" }, \"pusher\": {\"login\": \"dev2\"} }" 2>/dev/null | grep -q '"status": "ok"' \ && ok "Webhook accepted" || fail "Webhook rejected" sleep 1 svn cat "file://$SVN_REPO/trunk/README" | grep -q "v4 from webhook" \ && ok "Webhook-triggered SVN commit content correct" \ || fail "Webhook-triggered SVN commit wrong" kill "$WEBHOOK_PID" 2>/dev/null; wait "$WEBHOOK_PID" 2>/dev/null || true } # ─── 12. Idempotent: no double-translations ──────────────────── test_idempotent() { out=$(python -m svn_mirror --config "$CONFIG" sync 2>&1) echo "$out" | grep -q "up to date" \ && ok "Second sync reports up to date" || fail "Second sync not up to date" out=$(python -m svn_mirror --config "$CONFIG" push 2>&1) echo "$out" | grep -q "up to date" \ && ok "Second push reports up to date" || fail "Second push not up to date" } # ─── 11. Reconciliation (divergence recovery) ───────────────── test_reconciliation() { # ── Create divergence ───────────────────────────────── # SVN commit (bypassing sync daemon) svn co "file://$SVN_REPO/trunk" "$TMP/wc2" --quiet echo "SVN-only notes" > "$TMP/wc2/NOTES" svn add "$TMP/wc2/NOTES" --quiet svn commit -m "Add NOTES (SVN-only change)" "$TMP/wc2" --username dev1 --quiet rm -rf "$TMP/wc2" # Git commit (bypassing sync — direct to canonical repo) export GIT_DIR="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" export GIT_AUTHOR_NAME="Dev Two" export GIT_AUTHOR_EMAIL="dev2@example.com" export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" PARENT=$(git rev-parse refs/heads/master) BLOB=$(echo "Git projects list" | git hash-object -w --stdin) TREE=$(echo "100644 blob $BLOB PROJECTS" | git mktree) COMMIT=$(git commit-tree $TREE -p "$PARENT" -m "Add PROJECTS (Git-only change)") git update-ref refs/heads/master "$COMMIT" GIT_DEV_HASH="$COMMIT" unset GIT_DIR GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL # ── Assess (dry-run) ───────────────────────────────── out=$(python -m svn_mirror --config "$CONFIG" reconcile --assess --mirror test-e2e 2>&1) echo "$out" | grep -q "Pending SVN revisions" \ && ok "Assess shows pending SVN" || fail "Assess missing SVN" echo "$out" | grep -q "Pending Git commits" \ && ok "Assess shows pending Git" || fail "Assess missing Git" echo "$out" | grep -q "Last synced commit" \ && ok "Assess shows boundary" || fail "Assess missing boundary" echo "$out" | grep -q "Changed only on Git side" \ && ok "Assess shows Git-only files" || fail "Assess missing Git-only" # ── Reconcile ──────────────────────────────────────── out=$(python -m svn_mirror --config "$CONFIG" reconcile --mirror test-e2e 2>&1) echo "$out" | grep -q "Reconciliation complete" \ && ok "Reconciliation complete" || fail "Reconciliation failed" # Verify SVN has both changes svn cat "file://$SVN_REPO/trunk/NOTES" | grep -q "SVN-only notes" \ && ok "SVN NOTES preserved" || fail "SVN NOTES missing after reconcile" svn cat "file://$SVN_REPO/trunk/PROJECTS" | grep -q "Git projects list" \ && ok "Git PROJECTS present in SVN after reconcile" \ || fail "Git PROJECTS missing in SVN" # ── Sync to process pending SVN revs → Git ─────────── out=$(python -m svn_mirror --config "$CONFIG" sync 2>&1) # Verify Git canonical has both changes git --git-dir="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" \ show master:NOTES | grep -q "SVN-only notes" \ && ok "SVN NOTES in Git after sync" || fail "SVN NOTES missing in Git" git --git-dir="$MIRROR_DATA/mirrors/test-e2e/canonical-repo.git" \ show master:PROJECTS | grep -q "Git projects list" \ && ok "Git PROJECTS in canonical after sync" \ || fail "Git PROJECTS missing in canonical" # ── Verify idempotent ──────────────────────────────── out=$(python -m svn_mirror --config "$CONFIG" sync 2>&1) echo "$out" | grep -q "up to date" \ && ok "Sync is up to date after reconciliation" \ || fail "Sync not up to date after reconciliation" out=$(python -m svn_mirror --config "$CONFIG" reconcile --assess --mirror test-e2e 2>&1) echo "$out" | grep -q "already in sync" \ && ok "Reconcile assess shows in sync after fix" \ || fail "Reconcile assess wrong after fix" } # ─── 12. Status display ──────────────────────────────────────── test_status() { out=$(python -m svn_mirror --config "$CONFIG" status --mirror test-e2e 2>&1) echo "$out" | grep -q "Last SVN:" \ && ok "Status shows Last SVN" || fail "Status missing Last SVN" echo "$out" | grep -q "Mappings:" \ && ok "Status shows Mappings" || fail "Status missing Mappings" } # ─── Run all tests ───────────────────────────────────────────── echo "=== Phase 1: Setup ===" setup_svn setup_gitea write_config echo " SVN repo: file://$SVN_REPO" echo " Gitea repo: $GITEA_REPO" echo " Config: $CONFIG" echo "" echo "=== Phase 2: Init ===" test_init echo "" echo "=== Phase 3: SVN→Git sync ===" test_svn_to_git echo "" echo "=== Phase 4: Git→SVN push ===" test_git_to_svn echo "" echo "=== Phase 5: Branch sync ===" test_svn_branch echo "" echo "=== Phase 6: Tag sync ===" test_svn_tag echo "" echo "=== Phase 7: Git tag → SVN tag ===" test_git_tag echo "" echo "=== Phase 8: Gitea push ===" test_gitea_push echo "" echo "=== Phase 9: Webhook flow ===" test_webhook_flow echo "" echo "=== Phase 10: Idempotent ===" test_idempotent test_status echo "" echo "=== Phase 11: Reconciliation ===" test_reconciliation echo "" echo "==============================" echo " $PASS passed, $FAIL failed" echo "==============================" [ "$FAIL" -eq 0 ] || exit 1