#!/usr/bin/env bash # tests/smoke-init.sh — End-to-end smoke test for disinto init using mock Forgejo # # Validates the full init flow: Forgejo API, user/token creation, # repo setup, labels, TOML generation, and cron installation. # # Uses mock Forgejo server (started by .woodpecker/smoke-init.yml). # # Required env: SMOKE_FORGE_URL (default: http://localhost:3000) # Required tools: bash, curl, jq, git set -euo pipefail FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)" FORGE_URL="${SMOKE_FORGE_URL:-http://localhost:3000}" TEST_SLUG="smoke-org/smoke-repo" MOCK_BIN="/tmp/smoke-mock-bin" FAILED=0 fail() { printf 'FAIL: %s\n' "$*" >&2; FAILED=1; } pass() { printf 'PASS: %s\n' "$*"; } cleanup() { rm -rf "$MOCK_BIN" /tmp/smoke-test-repo \ "${FACTORY_ROOT}/projects/smoke-repo.toml" # Restore .env only if we created the backup if [ -f "${FACTORY_ROOT}/.env.smoke-backup" ]; then mv "${FACTORY_ROOT}/.env.smoke-backup" "${FACTORY_ROOT}/.env" else rm -f "${FACTORY_ROOT}/.env" fi } trap cleanup EXIT # Back up existing .env if present if [ -f "${FACTORY_ROOT}/.env" ]; then cp "${FACTORY_ROOT}/.env" "${FACTORY_ROOT}/.env.smoke-backup" fi # Start with a clean .env (init writes tokens here) printf '' > "${FACTORY_ROOT}/.env" # ── 1. Verify mock Forgejo is ready ───────────────────────────────────────── echo "=== 1/6 Verifying mock Forgejo at ${FORGE_URL} ===" retries=0 api_version="" while true; do api_version=$(curl -sf --max-time 3 "${FORGE_URL}/api/v1/version" 2>/dev/null \ | jq -r '.version // empty' 2>/dev/null) || api_version="" if [ -n "$api_version" ]; then break fi retries=$((retries + 1)) if [ "$retries" -gt 30 ]; then fail "Mock Forgejo API not responding after 30s" exit 1 fi sleep 1 done pass "Mock Forgejo API v${api_version} (${retries}s)" # ── 2. Set up mock binaries (claude, tmux, docker) ─────────────────────────── echo "=== 2/6 Setting up mock binaries ===" mkdir -p "$MOCK_BIN" # ── Mock: docker ── # Routes 'docker exec' Forgejo CLI commands to the Forgejo API. cat > "$MOCK_BIN/docker" << 'DOCKERMOCK' #!/usr/bin/env bash set -euo pipefail FORGE_URL="${SMOKE_FORGE_URL:-http://localhost:3000}" # docker ps — return empty (no containers running) if [ "${1:-}" = "ps" ]; then exit 0 fi # docker exec — route to Forgejo API if [ "${1:-}" = "exec" ]; then shift # remove 'exec' # Skip docker exec flags (-u VALUE, -T, -i, etc.) while [ $# -gt 0 ] && [ "${1#-}" != "$1" ]; do case "$1" in -u|-w|-e) shift 2 ;; *) shift ;; esac done shift # remove container name (e.g. disinto-forgejo) # $@ is now: forgejo admin user list|create [flags] if [ "${1:-}" = "forgejo" ] && [ "${2:-}" = "admin" ] && [ "${3:-}" = "user" ]; then subcmd="${4:-}" if [ "$subcmd" = "list" ]; then echo "ID Username Email" exit 0 fi if [ "$subcmd" = "create" ]; then shift 4 # skip 'forgejo admin user create' username="" password="" email="" is_admin="false" while [ $# -gt 0 ]; do case "$1" in --admin) is_admin="true"; shift ;; --username) username="$2"; shift 2 ;; --password) password="$2"; shift 2 ;; --email) email="$2"; shift 2 ;; --must-change-password*) shift ;; *) shift ;; esac done if [ -z "$username" ] || [ -z "$password" ] || [ -z "$email" ]; then echo "mock-docker: missing required args" >&2 exit 1 fi # Create user via Forgejo admin API if ! curl -sf -X POST \ -H "Content-Type: application/json" \ "${FORGE_URL}/api/v1/admin/users" \ -d "{\"username\":\"${username}\",\"password\":\"${password}\",\"email\":\"${email}\",\"must_change_password\":false,\"login_name\":\"${username}\",\"source_id\":0}" \ >/dev/null 2>&1; then echo "mock-docker: failed to create user '${username}'" >&2 exit 1 fi # Patch user: ensure must_change_password is false (Forgejo admin # API POST may ignore it) and promote to admin if requested patch_body="{\"must_change_password\":false,\"login_name\":\"${username}\",\"source_id\":0" if [ "$is_admin" = "true" ]; then patch_body="${patch_body},\"admin\":true" fi patch_body="${patch_body}}" curl -sf -X PATCH \ -H "Content-Type: application/json" \ "${FORGE_URL}/api/v1/admin/users/${username}" \ -d "${patch_body}" \ >/dev/null 2>&1 || true echo "New user '${username}' has been successfully created!" exit 0 fi if [ "$subcmd" = "change-password" ]; then shift 4 # skip 'forgejo admin user change-password' username="" password="" while [ $# -gt 0 ]; do case "$1" in --username) username="$2"; shift 2 ;; --password) password="$2"; shift 2 ;; --must-change-password*) shift ;; --config*) shift ;; *) shift ;; esac done if [ -z "$username" ]; then echo "mock-docker: change-password missing --username" >&2 exit 1 fi # PATCH user via Forgejo admin API to clear must_change_password patch_body="{\"must_change_password\":false,\"login_name\":\"${username}\",\"source_id\":0" if [ -n "$password" ]; then patch_body="${patch_body},\"password\":\"${password}\"" fi patch_body="${patch_body}}" if ! curl -sf -X PATCH \ -H "Content-Type: application/json" \ "${FORGE_URL}/api/v1/admin/users/${username}" \ -d "${patch_body}" \ >/dev/null 2>&1; then echo "mock-docker: failed to change-password for '${username}'" >&2 exit 1 fi exit 0 fi fi echo "mock-docker: unhandled exec: $*" >&2 exit 1 fi echo "mock-docker: unhandled command: $*" >&2 exit 1 DOCKERMOCK chmod +x "$MOCK_BIN/docker" # ── Mock: claude ── cat > "$MOCK_BIN/claude" << 'CLAUDEMOCK' #!/usr/bin/env bash case "$*" in *"auth status"*) printf '{"loggedIn":true}\n' ;; *"--version"*) printf 'claude 1.0.0 (mock)\n' ;; esac exit 0 CLAUDEMOCK chmod +x "$MOCK_BIN/claude" # ── Mock: tmux ── printf '#!/usr/bin/env bash\nexit 0\n' > "$MOCK_BIN/tmux" chmod +x "$MOCK_BIN/tmux" export PATH="$MOCK_BIN:$PATH" pass "Mock binaries installed (claude, tmux)" # ── 3. Run disinto init ───────────────────────────────────────────────────── echo "=== 3/6 Running disinto init ===" rm -f "${FACTORY_ROOT}/projects/smoke-repo.toml" # Configure git identity (needed for git operations) git config --global user.email "smoke@test.local" git config --global user.name "Smoke Test" export SMOKE_FORGE_URL="$FORGE_URL" export FORGE_URL if bash "${FACTORY_ROOT}/bin/disinto" init \ "${TEST_SLUG}" \ --bare --yes \ --forge-url "$FORGE_URL" \ --repo-root "/tmp/smoke-test-repo"; then pass "disinto init completed successfully" else fail "disinto init exited non-zero" fi # ── 4. Verify Forgejo state ───────────────────────────────────────────────── echo "=== 4/6 Verifying Forgejo state ===" # Admin user exists if curl -sf --max-time 5 "${FORGE_URL}/api/v1/users/disinto-admin" >/dev/null 2>&1; then pass "Admin user 'disinto-admin' exists on Forgejo" else fail "Admin user 'disinto-admin' not found on Forgejo" fi # Bot users exist for bot in dev-bot review-bot; do if curl -sf --max-time 5 "${FORGE_URL}/api/v1/users/${bot}" >/dev/null 2>&1; then pass "Bot user '${bot}' exists on Forgejo" else fail "Bot user '${bot}' not found on Forgejo" fi done # Repo exists (try org path, then fallback paths) repo_found=false for repo_path in "${TEST_SLUG}" "dev-bot/smoke-repo" "disinto-admin/smoke-repo"; do if curl -sf --max-time 5 "${FORGE_URL}/api/v1/repos/${repo_path}" >/dev/null 2>&1; then pass "Repo '${repo_path}' exists on Forgejo" repo_found=true break fi done if [ "$repo_found" = false ]; then fail "Repo not found on Forgejo under any expected path" fi # Labels exist on repo # Create a token to check labels (using disinto-admin which was created by init) disinto_admin_pass="Disinto-Admin-456" verify_token=$(curl -sf -X POST \ -u "disinto-admin:${disinto_admin_pass}" \ -H "Content-Type: application/json" \ "${FORGE_URL}/api/v1/users/disinto-admin/tokens" \ -d '{"name":"smoke-verify","scopes":["all"]}' 2>/dev/null \ | jq -r '.sha1 // empty') || verify_token="" if [ -n "$verify_token" ]; then label_count=0 for repo_path in "${TEST_SLUG}" "dev-bot/smoke-repo" "disinto-admin/smoke-repo"; do label_count=$(curl -sf \ -H "Authorization: token ${verify_token}" \ "${FORGE_URL}/api/v1/repos/${repo_path}/labels?limit=50" 2>/dev/null \ | jq 'length' 2>/dev/null) || label_count=0 if [ "$label_count" -gt 0 ]; then break fi done if [ "$label_count" -ge 5 ]; then pass "Labels created on repo (${label_count} labels)" else fail "Expected >= 5 labels, found ${label_count}" fi else fail "Could not obtain verification token" fi # ── 5. Verify local state ─────────────────────────────────────────────────── echo "=== 5/6 Verifying local state ===" # TOML was generated toml_path="${FACTORY_ROOT}/projects/smoke-repo.toml" if [ -f "$toml_path" ]; then toml_name=$(python3 -c " import tomllib, sys with open(sys.argv[1], 'rb') as f: print(tomllib.load(f)['name']) " "$toml_path" 2>/dev/null) || toml_name="" if [ "$toml_name" = "smoke-repo" ]; then pass "TOML generated with correct project name" else fail "TOML name mismatch: expected 'smoke-repo', got '${toml_name}'" fi else fail "TOML not generated at ${toml_path}" fi # .env has tokens env_file="${FACTORY_ROOT}/.env" if [ -f "$env_file" ]; then if grep -q '^FORGE_TOKEN=' "$env_file"; then pass ".env contains FORGE_TOKEN" else fail ".env missing FORGE_TOKEN" fi if grep -q '^FORGE_REVIEW_TOKEN=' "$env_file"; then pass ".env contains FORGE_REVIEW_TOKEN" else fail ".env missing FORGE_REVIEW_TOKEN" fi else fail ".env not found" fi # Repo was cloned if [ -d "/tmp/smoke-test-repo/.git" ]; then pass "Repo cloned to /tmp/smoke-test-repo" else fail "Repo not cloned to /tmp/smoke-test-repo" fi # ── 6. Verify cron setup ──────────────────────────────────────────────────── echo "=== 6/6 Verifying cron setup ===" cron_output=$(crontab -l 2>/dev/null) || cron_output="" if [ -n "$cron_output" ]; then if printf '%s' "$cron_output" | grep -q 'dev-poll.sh'; then pass "Cron includes dev-poll entry" else fail "Cron missing dev-poll entry" fi if printf '%s' "$cron_output" | grep -q 'review-poll.sh'; then pass "Cron includes review-poll entry" else fail "Cron missing review-poll entry" fi if printf '%s' "$cron_output" | grep -q 'gardener-run.sh'; then pass "Cron includes gardener entry" else fail "Cron missing gardener entry" fi else fail "No cron entries found (crontab -l returned empty)" fi # ── Mock state verification ───────────────────────────────────────────────── echo "=== Verifying mock Forgejo state ===" # Query /mock/state to verify all expected API calls were made mock_state=$(curl -sf \ -H "Authorization: token ${verify_token}" \ "${FORGE_URL}/mock/state" 2>/dev/null) || mock_state="" if [ -n "$mock_state" ]; then # Verify users were created users=$(echo "$mock_state" | jq -r '.users | length' 2>/dev/null) || users=0 if [ "$users" -ge 3 ]; then pass "Mock state: ${users} users created (expected >= 3)" else fail "Mock state: expected >= 3 users, found ${users}" fi # Verify repos were created repos=$(echo "$mock_state" | jq -r '.repos | length' 2>/dev/null) || repos=0 if [ "$repos" -ge 1 ]; then pass "Mock state: ${repos} repos created (expected >= 1)" else fail "Mock state: expected >= 1 repos, found ${repos}" fi # Verify labels were created labels_total=$(echo "$mock_state" | jq '[.labels.values[] | length] | add // 0' 2>/dev/null) || labels_total=0 if [ "$labels_total" -ge 5 ]; then pass "Mock state: ${labels_total} labels created (expected >= 5)" else fail "Mock state: expected >= 5 labels, found ${labels_total}" fi # Verify tokens were created tokens=$(echo "$mock_state" | jq -r '.tokens | length' 2>/dev/null) || tokens=0 if [ "$tokens" -ge 1 ]; then pass "Mock state: ${tokens} tokens created (expected >= 1)" else fail "Mock state: expected >= 1 tokens, found ${tokens}" fi else fail "Could not query /mock/state endpoint" fi # ── Summary ────────────────────────────────────────────────────────────────── echo "" if [ "$FAILED" -ne 0 ]; then echo "=== SMOKE-INIT TEST FAILED ===" exit 1 fi echo "=== SMOKE-INIT TEST PASSED ==="