#!/usr/bin/env bats # Bats tests for scripts/proxmox/firstboot-hook.sh (praxis first-boot hookscript). # # Run: bats scripts/proxmox/test/firstboot-hook.bats # # firstboot-hook.sh is invoked by Proxmox at CT lifecycle phases on the # PVE HOST. Only the `post-start` phase does work (other phases exit 0). # In post-start it: # 1. Idempotency check: skip if /opt/praxis/.git exists + praxis # service is active (via pct exec). # 2. Install Docker + docker-compose-v2 + git + curl inside the CT. # 3. Clone the praxis repo from Gitea into /opt/praxis (with branch # fallback to main). # 4. Run scripts/install-service.sh inside the CT. # # These tests exercise the real firstboot-hook.sh with a mocked `pct` # on PATH (records exec invocations + returns controllable exit codes) # so the phase-gating, idempotency skip, Docker-install, and git-clone # steps are verified without a live PVE host or CT. # # G-101: GITEA_TOKEN is baked into this snippet by stage-snippet.sh # (the hookscript runs on the PVE host where lxc.environment is # invisible). The tests set GITEA_TOKEN in the env to model the baked-in # value (stage-snippet.bats verifies the sed bake itself). setup() { SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" HOOK="${SCRIPT_DIR}/firstboot-hook.sh" STUB_DIR="$(mktemp -d)" export STUB_DIR LOG="${STUB_DIR}/calls.log" export CALL_LOG="$LOG" : > "$LOG" 2>/dev/null || true ROOT="${STUB_DIR}/root" mkdir -p "$ROOT" cp "$HOOK" "${ROOT}/firstboot-hook.sh" # Mocked pct — `pct exec -- ` records the full # invocation to $CALL_LOG and exits with STUB_PCT_EXIT (default 0). # Per-call exit overrides via STUB_PCT_EXIT_ (1-based call number) # let the idempotency-check test make call 1 fail (not-yet-installed) # while subsequent calls succeed. cat > "${ROOT}/pct" <<'PSTUB' #!/bin/sh # pct exec -- count_file="${STUB_DIR}/pct.count" n=$(cat "$count_file" 2>/dev/null || echo 0) n=$((n + 1)) echo "$n" > "$count_file" # Record the full invocation (vmid + cmd). shift # drop `exec` vmid="$1"; shift if [ "$1" = "--" ]; then shift; fi printf 'pct:%s exec:%s cmd:%s\n' "$n" "$vmid" "$*" >> "$CALL_LOG" # Per-call exit override. eval "exit \${STUB_PCT_EXIT_${n}:-${STUB_PCT_EXIT:-0}}" PSTUB chmod +x "${ROOT}/pct" export PATH="${ROOT}:${PATH}" # GITEA_TOKEN is baked in by stage-snippet.sh; model it as an env var # the baked snippet would carry. export GITEA_TOKEN="gitea-test-token" export PRAXIS_VERSION="v0.2" export GITEA_HOST="git.cloudinit.dev" # Reset the pct call counter between tests. : > "${STUB_DIR}/pct.count" 2>/dev/null || true } teardown() { [ -n "${STUB_DIR:-}" ] && rm -rf "$STUB_DIR" } @test "hook: non-post-start phase (pre-start) → exit 0 immediately, NO pct exec" { run "${ROOT}/firstboot-hook.sh" 200 pre-start [ "$status" -eq 0 ] # No pct exec invocations (the phase gate exits before any work). ! grep -q '^pct:' "$LOG" } @test "hook: empty phase → exit 0 immediately, NO pct exec (defensive)" { run "${ROOT}/firstboot-hook.sh" 200 [ "$status" -eq 0 ] ! grep -q '^pct:' "$LOG" } @test "hook: post-start phase — runs the idempotency check via pct exec" { # Idempotency check (call 1) fails (not yet installed) → proceeds to # Docker install (call 2) + git clone (call 3) + install-service (call 4). # All subsequent calls succeed. STUB_PCT_EXIT_1=1 export STUB_PCT_EXIT_1 run "${ROOT}/firstboot-hook.sh" 200 post-start [ "$status" -eq 0 ] # The idempotency check ran (pct call 1). [ "$(cat "${STUB_DIR}/pct.count")" -ge 1 ] grep -q 'praxis already installed and active — skipping\|installing Docker inside CT' <<< "$output" } @test "hook: post-start + praxis already installed → idempotency skip, NO Docker install" { # Idempotency check (call 1) succeeds (already installed + active) → # the hook logs "already installed" + exits 0 WITHOUT running Docker # install / git clone / install-service. STUB_PCT_EXIT_1=0 export STUB_PCT_EXIT_1 run "${ROOT}/firstboot-hook.sh" 200 post-start [ "$status" -eq 0 ] grep -q 'praxis already installed and active — skipping' <<< "$output" # Only ONE pct exec call (the idempotency probe). [ "$(cat "${STUB_DIR}/pct.count")" -eq 1 ] ! grep -q 'installing Docker inside CT' <<< "$output" ! grep -q 'cloning praxis repo' <<< "$output" } @test "hook: post-start + not installed → Docker install step runs (apt-get docker.io)" { STUB_PCT_EXIT_1=1 export STUB_PCT_EXIT_1 run "${ROOT}/firstboot-hook.sh" 200 post-start [ "$status" -eq 0 ] grep -q 'installing Docker inside CT' <<< "$output" # The pct exec log records the apt-get install docker.io invocation. grep -q 'apt-get install' "$LOG" grep -q 'docker.io' "$LOG" grep -q 'docker-compose-v2' "$LOG" grep -q 'git' "$LOG" grep -q 'curl' "$LOG" } @test "hook: post-start + not installed → git clone step runs with CLONE_URL containing the baked GITEA_TOKEN" { STUB_PCT_EXIT_1=1 export STUB_PCT_EXIT_1 run "${ROOT}/firstboot-hook.sh" 200 post-start [ "$status" -eq 0 ] grep -q 'cloning praxis repo' <<< "$output" # The git clone invocation records the CLONE_URL with the token. grep -q 'git clone' "$LOG" grep -q 'gitea-test-token@git.cloudinit.dev/coreci/praxis.git' "$LOG" } @test "hook: post-start + not installed → install-service.sh runs inside the CT" { STUB_PCT_EXIT_1=1 export STUB_PCT_EXIT_1 run "${ROOT}/firstboot-hook.sh" 200 post-start [ "$status" -eq 0 ] grep -q 'running install-service inside CT' <<< "$output" # The pct exec log records the install-service.sh invocation. grep -q 'scripts/install-service.sh' "$LOG" } @test "hook: PRAXIS_VERSION flows into the git clone --branch flag" { STUB_PCT_EXIT_1=1 export STUB_PCT_EXIT_1 PRAXIS_VERSION="feature-xyz" run "${ROOT}/firstboot-hook.sh" 200 post-start [ "$status" -eq 0 ] grep -q "git clone --depth 1 --branch 'feature-xyz'" "$LOG" } @test "hook: GITEA_HOST override flows into the CLONE_URL" { STUB_PCT_EXIT_1=1 export STUB_PCT_EXIT_1 GITEA_HOST="git.staging.test" run "${ROOT}/firstboot-hook.sh" 200 post-start [ "$status" -eq 0 ] grep -q 'gitea-test-token@git.staging.test/coreci/praxis.git' "$LOG" } @test "hook: post-start + Docker install fails (pct exit 1) → hook exits non-zero (set -e)" { # Idempotency check (call 1) fails (not installed) → proceeds to Docker # install (call 2) which ALSO fails → set -e propagates → hook exits 1. STUB_PCT_EXIT_1=1 STUB_PCT_EXIT_2=1 export STUB_PCT_EXIT_1 STUB_PCT_EXIT_2 run "${ROOT}/firstboot-hook.sh" 200 post-start [ "$status" -ne 0 ] grep -q 'installing Docker inside CT' <<< "$output" # git clone + install-service NOT reached. ! grep -q 'cloning praxis repo' <<< "$output" ! grep -q 'running install-service' <<< "$output" } @test "hook: VMID is passed through to every pct exec invocation" { STUB_PCT_EXIT_1=1 export STUB_PCT_EXIT_1 run "${ROOT}/firstboot-hook.sh" 300 post-start [ "$status" -eq 0 ] # Every pct exec line records vmid=300. while IFS= read -r line; do case "$line" in pct:*) echo "$line" | grep -q 'exec:300 ' ;; esac done < "$LOG" }