Files
praxis/scripts/proxmox/test/firstboot-hook.bats
T
Praxis CI 93d33ecb0c feat(P01): SLICE-08+09+10 — secret wiring, bats tests (121), e2e verification
SLICE-08 (devops-engineer): .env.example updated with Proxmox deployment
  vars (documented, sourced from ~/coreci/.env.secrets per D-026),
  PRAXIS_CLIENT_DIST for StaticFiles, PRAXIS_SCENARIO. config.json
  secrets.scopes already extended in SPECIFY (proxmox + voice scopes).
SLICE-09 (devops-engineer): 10 bats test files (G-106 fix: 10 not 9)
  covering all proxmox scripts — 121 tests, 114 pass + 7 skipped (e2e).
  Mocked curl/pct/ssh; no live cluster needed for unit tests.
SLICE-10 (devops-engineer): e2e-deploy.sh — sources secrets from both
  coreci + praxis .env.secrets, runs full deploy, verifies /health +
  client HTML serving. REQ-DEPLOY-15 covered.

All 6 grill binding decisions addressed:
  G-101 MUST: GITEA_TOKEN baked into snippet (stage-snippet.sh)
  G-102 MUST: PRAXIS_DB_PATH env read (db/store.py + db/migrate.py)
  G-103 FIX:  all 16 env vars in injection list (install-service.sh)
  G-104 FIX:  health-check timeout 600s (health-check.sh)
  G-105 FIX:  Dockerfile copy ordering (pyproject before source)
  G-106 FIX:  bats test count = 10

REQ-DEPLOY-12, 14, 15 covered. All 20 REQ-IDs now implemented.

---ci---
project: praxis
phase: 1
milestone: v0.2
status: execute
slice: 08-10
wave: 4
---/ci---
2026-08-03 18:17:40 +00:00

194 lines
7.3 KiB
Bash

#!/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 /usr/local/bin/praxis-deploy 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 <vmid> -- <cmd...>` records the full
# invocation to $CALL_LOG and exits with STUB_PCT_EXIT (default 0).
# Per-call exit overrides via STUB_PCT_EXIT_<n> (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 <vmid> -- <cmd...>
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"
}