#!/usr/bin/env bats # Bats tests for scripts/proxmox/rollback.sh (praxis CT rollback). # # Run: bats scripts/proxmox/test/rollback.bats # # rollback.sh stops (graceful, then force) and destroys a CT. It is # idempotent (a 404 / already-gone CT is not an error). These tests # exercise the real rollback.sh with a mocked api.sh (pve_curl, pve_get, # pve_poll) so the shutdown → force-stop → destroy sequence + the # 404-tolerant paths are verified without a live Proxmox endpoint. # # Praxis v0.2 (vs coreci) key difference asserted here: # - NO proxy / PROXY_VMID / backend-remove.sh references (the proxy # tier was removed in v0.2). rollback.sh is stop + destroy only. setup() { SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" ROLLBACK="${SCRIPT_DIR}/rollback.sh" STUB_DIR="$(mktemp -d)" export STUB_DIR LOG="${STUB_DIR}/calls.log" export CALL_LOG="$LOG" : > "$LOG" 2>/dev/null || true # Sandbox layout: # /rollback.sh (SCRIPT_DIR) # /api.sh (sourced) ROOT="${STUB_DIR}/root" mkdir -p "$ROOT" cp "$ROLLBACK" "${ROOT}/rollback.sh" # Mocked api.sh — pve_curl records method+path and returns STUB_UPID # (or null); pve_get returns STUB_PVE_GET (so the "still running?" # check fires when status=running); pve_poll no-op. cat > "${ROOT}/api.sh" <<'ASTUB' pve_env() { :; } pve_curl() { method="$1"; path="$2" printf 'pve_curl:%s %s\n' "$method" "$path" >> "$CALL_LOG" printf '%s\n' "${STUB_UPID:-null}" } pve_get() { printf 'pve_get:%s\n' "$1" >> "$CALL_LOG" printf '%s\n' "${STUB_PVE_GET:-}" } pve_poll() { printf 'pve_poll:%s\n' "$1" >> "$CALL_LOG" } pve_tls_insecure() { :; } pve_auth_header() { :; } ASTUB chmod +x "${ROOT}"/*.sh export PROXMOX_API_URL="https://proxmox.test:8006/api2/json" export PROXMOX_API_TOKEN="root@pam!test=secret" export PROXMOX_NODE="testnode" } teardown() { [ -n "${STUB_DIR:-}" ] && rm -rf "$STUB_DIR" } @test "rollback: shutdown → force-stop → destroy sequence (CT running)" { # CT is running → graceful shutdown, then status=running → force stop, then destroy. STUB_UPID="UPID:task:123" STUB_PVE_GET='{"status":"running"}' export STUB_UPID STUB_PVE_GET run "${ROOT}/rollback.sh" 200 [ "$status" -eq 0 ] grep -q 'rollback: cleaning up VMID 200' <<< "$output" # shutdown POST recorded. grep -q '^pve_curl:POST /nodes/testnode/lxc/200/status/shutdown$' "$LOG" # status check via pve_get. grep -q '^pve_get:/nodes/testnode/lxc/200/status/current$' "$LOG" grep -q 'rollback: force-stopping VMID 200' <<< "$output" grep -q '^pve_curl:POST /nodes/testnode/lxc/200/status/stop$' "$LOG" grep -q 'rollback: destroying VMID 200' <<< "$output" grep -q '^pve_curl:DELETE /nodes/testnode/lxc/200$' "$LOG" grep -q 'rollback: VMID 200 cleaned up' <<< "$output" } @test "rollback: CT not running (stopped) → shutdown, no force-stop, destroy" { # CT exists but status=stopped → no force-stop needed; destroy still runs. STUB_UPID="UPID:task:456" STUB_PVE_GET='{"status":"stopped"}' export STUB_UPID STUB_PVE_GET run "${ROOT}/rollback.sh" 200 [ "$status" -eq 0 ] grep -q '^pve_curl:POST /nodes/testnode/lxc/200/status/shutdown$' "$LOG" ! grep -q 'force-stopping' <<< "$output" ! grep -q '^pve_curl:POST /nodes/testnode/lxc/200/status/stop$' "$LOG" grep -q '^pve_curl:DELETE /nodes/testnode/lxc/200$' "$LOG" grep -q 'rollback: VMID 200 cleaned up' <<< "$output" } @test "rollback: 404 (CT already gone) → idempotent, exit 0 (no force-stop, no error)" { # pve_get returns empty (404) → no force-stop; shutdown + destroy both # return null UPID (no poll). Exit 0. STUB_UPID="null" STUB_PVE_GET="" export STUB_UPID STUB_PVE_GET run "${ROOT}/rollback.sh" 200 [ "$status" -eq 0 ] ! grep -q 'force-stopping' <<< "$output" grep -q 'rollback: destroying VMID 200' <<< "$output" grep -q 'rollback: VMID 200 cleaned up' <<< "$output" } @test "rollback: shutdown returns null UPID → no poll, but destroy still runs (404-tolerant)" { # shutdown returns null (CT already stopped) → skip poll; destroy still runs. STUB_UPID="null" STUB_PVE_GET='{"status":"stopped"}' export STUB_UPID STUB_PVE_GET run "${ROOT}/rollback.sh" 200 [ "$status" -eq 0 ] ! grep -q '^pve_poll:' "$LOG" grep -q '^pve_curl:DELETE /nodes/testnode/lxc/200$' "$LOG" } @test "rollback: missing VMID arg → exit non-zero (usage)" { run "${ROOT}/rollback.sh" [ "$status" -ne 0 ] grep -q 'usage: rollback.sh' <<< "$output" } @test "rollback: NO proxy/PROXY_VMID/backend-remove references in CODE (v0.2 proxy tier removed)" { # G-106 / v0.2: the proxy tier was removed. rollback.sh must NOT # reference PROXY_VMID or invoke proxy/backend-remove.sh in its CODE # (the header comment may mention the removal for future readers, but # no executable path references the proxy tier). Assert by grepping the # call log (no backend-remove invocation at runtime) + stripping # comments before grepping the source for PROXY_VMID / backend-remove.sh. STUB_UPID="null" STUB_PVE_GET="" export STUB_UPID STUB_PVE_GET PROXY_VMID=100 run "${ROOT}/rollback.sh" 200 [ "$status" -eq 0 ] ! grep -q 'backend-remove' "$LOG" ! grep -q 'proxy' "$LOG" # Static source guard: strip comment-only lines, then assert no code # references to the proxy tier. code_only=$(grep -v '^[[:space:]]*#' "${ROOT}/rollback.sh") ! printf '%s\n' "$code_only" | grep -q 'PROXY_VMID' ! printf '%s\n' "$code_only" | grep -q 'backend-remove\.sh' } @test "rollback: pve_env fails on missing PROXMOX_NODE → exit non-zero (set -u)" { run env -u PROXMOX_NODE "${ROOT}/rollback.sh" 200 [ "$status" -ne 0 ] }