The mock crontab file was not being created despite PATH precedence working correctly. Replace the mock with the real BusyBox crontab already available in the Forgejo Alpine image. Verify cron entries via 'crontab -l' output instead of checking a mock state file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
359 lines
12 KiB
Bash
359 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
# tests/smoke-init.sh — End-to-end smoke test for disinto init
|
|
#
|
|
# Expects a running Forgejo at SMOKE_FORGE_URL with a bootstrap admin
|
|
# user already created (see .woodpecker/smoke-init.yml for CI setup).
|
|
# Validates the full init flow: Forgejo API, user/token creation,
|
|
# repo setup, labels, TOML generation, and cron installation.
|
|
#
|
|
# Required env: SMOKE_FORGE_URL (default: http://localhost:3000)
|
|
# Required tools: bash, curl, jq, python3, git
|
|
|
|
set -euo pipefail
|
|
|
|
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
FORGE_URL="${SMOKE_FORGE_URL:-http://localhost:3000}"
|
|
SETUP_ADMIN="setup-admin"
|
|
SETUP_PASS="SetupPass-789xyz"
|
|
TEST_SLUG="smoke-org/smoke-repo"
|
|
MOCK_BIN="/tmp/smoke-mock-bin"
|
|
MOCK_STATE="/tmp/smoke-mock-state"
|
|
FAILED=0
|
|
|
|
fail() { printf 'FAIL: %s\n' "$*" >&2; FAILED=1; }
|
|
pass() { printf 'PASS: %s\n' "$*"; }
|
|
|
|
cleanup() {
|
|
rm -rf "$MOCK_BIN" "$MOCK_STATE" /tmp/smoke-test-repo \
|
|
"${FACTORY_ROOT}/projects/smoke-repo.toml" \
|
|
"${FACTORY_ROOT}/docker-compose.yml"
|
|
# 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 (setup_forge writes tokens here)
|
|
printf '' > "${FACTORY_ROOT}/.env"
|
|
|
|
# ── 1. Verify Forgejo is ready ──────────────────────────────────────────────
|
|
echo "=== 1/6 Verifying 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 "Forgejo API not responding after 30s"
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
pass "Forgejo API v${api_version} (${retries}s)"
|
|
|
|
# Verify bootstrap admin user exists
|
|
if curl -sf --max-time 5 "${FORGE_URL}/api/v1/users/${SETUP_ADMIN}" >/dev/null 2>&1; then
|
|
pass "Bootstrap admin '${SETUP_ADMIN}' exists"
|
|
else
|
|
fail "Bootstrap admin '${SETUP_ADMIN}' not found — was Forgejo set up?"
|
|
exit 1
|
|
fi
|
|
|
|
# ── 2. Set up mock binaries ─────────────────────────────────────────────────
|
|
echo "=== 2/6 Setting up mock binaries ==="
|
|
mkdir -p "$MOCK_BIN" "$MOCK_STATE"
|
|
|
|
# Store bootstrap admin credentials for the docker mock
|
|
printf '%s:%s' "${SETUP_ADMIN}" "${SETUP_PASS}" > "$MOCK_STATE/bootstrap_creds"
|
|
|
|
# ── Mock: docker ──
|
|
# Routes 'docker exec' user-creation calls to the Forgejo admin API,
|
|
# using the bootstrap admin's credentials.
|
|
cat > "$MOCK_BIN/docker" << 'DOCKERMOCK'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
FORGE_URL="${SMOKE_FORGE_URL:-http://localhost:3000}"
|
|
MOCK_STATE="/tmp/smoke-mock-state"
|
|
|
|
if [ ! -f "$MOCK_STATE/bootstrap_creds" ]; then
|
|
echo "mock-docker: bootstrap credentials not found" >&2
|
|
exit 1
|
|
fi
|
|
BOOTSTRAP_CREDS="$(cat "$MOCK_STATE/bootstrap_creds")"
|
|
|
|
# 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 \
|
|
-u "$BOOTSTRAP_CREDS" \
|
|
-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 \
|
|
-u "$BOOTSTRAP_CREDS" \
|
|
-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
|
|
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"
|
|
|
|
# No crontab mock — use real BusyBox crontab (available in the Forgejo
|
|
# Alpine image). Cron entries are verified via 'crontab -l' in step 6.
|
|
|
|
export PATH="$MOCK_BIN:$PATH"
|
|
pass "Mock binaries installed (docker, 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 — use bootstrap admin to check
|
|
setup_token=$(curl -sf -X POST \
|
|
-u "${SETUP_ADMIN}:${SETUP_PASS}" \
|
|
-H "Content-Type: application/json" \
|
|
"${FORGE_URL}/api/v1/users/${SETUP_ADMIN}/tokens" \
|
|
-d '{"name":"smoke-verify","scopes":["all"]}' 2>/dev/null \
|
|
| jq -r '.sha1 // empty') || setup_token=""
|
|
|
|
if [ -n "$setup_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 ${setup_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 from bootstrap admin"
|
|
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
|
|
|
|
# ── Summary ──────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
echo "=== SMOKE-INIT TEST FAILED ==="
|
|
exit 1
|
|
fi
|
|
echo "=== SMOKE-INIT TEST PASSED ==="
|