#!/usr/bin/env bash
# tests/tenant-onboard/run-tests.sh -- focused offline harness for guard behaviors:
# - stage4 duplicate-CIDR guard (2026-07-03 sweep; was fail-open under SIGPIPE)
# - stage3 H2 re-run guards (2026-07-06): app-cred/keypair exists + local secret
# intact -> SKIP rc0; exists WITHOUT usable local secret -> STOP rc1 with
# operator guidance (no implicit credential rotation on a re-run)
# - stage7 H3 canary (2026-07-06): happy path CANARY PASS + teardown rc0;
# ssh-dead path CANARY FAIL rc1 AND teardown still runs (trap EXIT)
# Fake openstack/ssh replay fixtures; fake sleep compresses wait loops; fake HOMEs
# provide stub admin-openrc + CA. Not a full-stage harness -- assertions target
# the guard/canary behaviors only.
# Exit: 0 all pass | 1 any case failed. ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
BIN="$TMP/bin"; H="$TMP/home"; mkdir -p "$BIN" "$H"
printf 'export OS_AUTH_URL=https://fake:5000/v3\nexport OS_CACERT=$HOME/vault-init/vault-ca-root.pem\n' > "$H/admin-openrc"
mkdir -p "$H/vault-init"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -subj /CN=fake-ca \
-keyout "$H/vault-init/ca.key" -out "$H/vault-init/vault-ca-root.pem" -days 2 >/dev/null 2>&1
DOMID="d0000000000000000000000000000001"
PID="b0000000000000000000000000000001"
ACID="e0000000000000000000000000000001"
cat > "$BIN/openstack" <<FB
#!/usr/bin/env bash
case "\$*" in
*"subnet list"*) cat "\$FIXDIR/subnets.txt" ;;
"domain show testco -f value -c id") echo "$DOMID" ;;
"token issue -f value -c project_id") echo "$PID" ;;
"application credential show testco-svc-cred -f value -c id") cat "\$FIXDIR/appcred-id.txt" ;;
"keypair show testco-key -f value -c name") cat "\$FIXDIR/keypair-name.txt" ;;
"image list --public -f json") cat "\$FIXDIR/images.json" ;;
"security group create testco-canary-sg"*) : ;;
"security group rule create testco-canary-sg"*) : ;;
"server create testco-canary"*) : ;;
"server show testco-canary -f value -c status") cat "\$FIXDIR/status.txt" ;;
"floating ip create provider-ext -f value -c floating_ip_address") echo "10.99.0.5" ;;
"server add floating ip testco-canary 10.99.0.5") : ;;
"server delete --wait testco-canary") echo "deleted" >> "\$FIXDIR/teardown.log" ;;
"floating ip delete 10.99.0.5") echo "fip-del" >> "\$FIXDIR/teardown.log" ;;
"security group delete testco-canary-sg") echo "sg-del" >> "\$FIXDIR/teardown.log" ;;
*) echo "fake-openstack: unmatched: \$*" >&2; exit 1 ;;
esac
FB
chmod +x "$BIN/openstack"
cat > "$BIN/ssh" <<'FB'
#!/usr/bin/env bash
exit "$(cat "$FIXDIR/sshrc" 2>/dev/null || echo 1)"
FB
chmod +x "$BIN/ssh"
cat > "$BIN/sleep" <<'FB'
#!/usr/bin/env bash
exit 0
FB
chmod +x "$BIN/sleep"
mk_case_home(){ # mk_case_home <dir> [seed: full|noac|nokey|none]
local h="$1" seed="${2:-full}"
mkdir -p "$h"; cp "$H/admin-openrc" "$h/"; cp -r "$H/vault-init" "$h/"
local d="$h/tenant-testco"; mkdir -p "$d"; chmod 700 "$d"
printf 'username=testco-svc\nproject_id=%s\npassword=x\n' "$PID" > "$d/testco-svc-cred.txt"
printf 'username=testco-cluster\nproject_id=%s\npassword=x\n' "$PID" > "$d/testco-cluster-cred.txt"
if [ "$seed" = "full" ] || [ "$seed" = "nokey" ]; then
printf 'id="%s"\nsecret="fake-secret-value"\n' "$ACID" > "$d/testco-svc-appcred.txt"
fi
if [ "$seed" = "full" ] || [ "$seed" = "noac" ]; then
printf -- '-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----\n' > "$d/testco-key.pem"
fi
chmod 600 "$d"/* 2>/dev/null || true
}
run_stage(){ # run_stage <home> <fixdir> <stage> -> OUT/RC
OUT=$( cd "$TMP" && env HOME="$1" FIXDIR="$2" PATH="$BIN:$PATH" \
TENANT_CIDR="10.20.24.0/24" \
bash "$REPO/scripts/tenant-onboard.sh" testco "$3" 2>&1 ); RC=$?
}
check(){ # check <want_rc|-> <want:regex> <must_not:regex|-> <label>
local wrc="$1" want="$2" not="$3" label="$4"
local ok=1
[ "$wrc" = "-" ] || [ "$RC" = "$wrc" ] || ok=0
grep -qE "$want" <<<"$OUT" || ok=0
[ "$not" = "-" ] || { grep -qE "$not" <<<"$OUT" && ok=0; }
if [ "$ok" = 1 ]; then echo " PASS $label"; PASS=$((PASS+1))
else echo " FAIL $label (rc=$RC want=$wrc)"; echo "$OUT" | tail -5 | sed 's/^/ /'; FAIL=$((FAIL+1)); fi
}
# ---- stage4 CIDR guard (original cases) ----
mkdir -p "$TMP/fx-collide" "$TMP/fx-clear"
printf '10.20.7.0/24\n10.20.24.0/24\n10.20.9.0/24\n' > "$TMP/fx-collide/subnets.txt"
printf '10.20.7.0/24\n10.20.9.0/24\n' > "$TMP/fx-clear/subnets.txt"
run_stage "$H" "$TMP/fx-collide" stage4
check - 'CIDR 10.20.24.0/24 in use' '-' "T1 duplicate-CIDR guard fires (was fail-open under SIGPIPE)"
[ "$RC" -ne 0 ] || { echo " FAIL T1b guard must exit non-zero"; FAIL=$((FAIL+1)); }
run_stage "$H" "$TMP/fx-clear" stage4
check - '== stage4:' 'CIDR .* in use' "T2 clear CIDR passes the guard (no false die)"
# ---- stage3 H2 re-run guards ----
# T3 both exist remotely + both local files intact -> double SKIP, rc0
mk_case_home "$TMP/h3" full; mkdir -p "$TMP/fx3"
echo "$ACID" > "$TMP/fx3/appcred-id.txt"
echo "testco-key" > "$TMP/fx3/keypair-name.txt"
run_stage "$TMP/h3" "$TMP/fx3" stage3
check 0 'app cred testco-svc-cred exists \+ local secret intact -- SKIP' '-' "T3a app-cred re-run SKIPs"
check 0 'keypair testco-key exists \+ local private key intact -- SKIP' '-' "T3b keypair re-run SKIPs"
# T4 app cred exists remotely, local secret file MISSING -> STOP rc1 with guidance
mk_case_home "$TMP/h4" noac; mkdir -p "$TMP/fx4"
echo "$ACID" > "$TMP/fx4/appcred-id.txt"
echo "testco-key" > "$TMP/fx4/keypair-name.txt"
run_stage "$TMP/h4" "$TMP/fx4" stage3
check 1 'OPERATOR DECISION required \(rotates a live credential\)' '-' "T4 lost-secret re-run STOPs with guidance"
# T5 keypair exists remotely, local key MISSING -> STOP rc1 with guidance
mk_case_home "$TMP/h5" nokey; mkdir -p "$TMP/fx5"
printf 'x\n' > "$TMP/fx5/appcred-id.txt" # app cred absent -> would create; but skip via intact file
echo "$ACID" > "$TMP/fx5/appcred-id.txt"
echo "testco-key" > "$TMP/fx5/keypair-name.txt"
run_stage "$TMP/h5" "$TMP/fx5" stage3
check 1 'OPERATOR DECISION required \(breaks existing node access\)' '-' "T5 lost-private-key re-run STOPs with guidance"
# ---- stage7 H3 canary ----
mk_fix7(){ local d="$1"; mkdir -p "$d"
printf '[{"Name":"fedora-coreos-kube-1.28","ID":"11111111-2222-3333-4444-555555555555"}]\n' > "$d/images.json"
echo "ACTIVE" > "$d/status.txt"; : > "$d/teardown.log"
}
# T6 happy path: ACTIVE + ssh ok -> CANARY PASS, teardown ran, rc0
mk_case_home "$TMP/h6" full; mk_fix7 "$TMP/fx6"; echo 0 > "$TMP/fx6/sshrc"
run_stage "$TMP/h6" "$TMP/fx6" stage7
check 0 'CANARY PASS' '-' "T6a canary happy path passes"
check 0 'teardown done' '-' "T6b teardown ran on success"
grep -q 'deleted' "$TMP/fx6/teardown.log" && grep -q 'sg-del' "$TMP/fx6/teardown.log" \
&& { echo " PASS T6c teardown actually deleted server+SG"; PASS=$((PASS+1)); } \
|| { echo " FAIL T6c teardown log incomplete"; FAIL=$((FAIL+1)); }
# T7 ssh dead: CANARY FAIL rc1 AND teardown still ran (trap EXIT)
mk_case_home "$TMP/h7" full; mk_fix7 "$TMP/fx7"; echo 1 > "$TMP/fx7/sshrc"
run_stage "$TMP/h7" "$TMP/fx7" stage7
check 1 'CANARY FAIL: no SSH' '-' "T7a dead SSH fails the canary"
check 1 'teardown done' '-' "T7b teardown still ran on failure"
grep -q 'deleted' "$TMP/fx7/teardown.log" \
&& { echo " PASS T7c server deleted despite failure"; PASS=$((PASS+1)); } \
|| { echo " FAIL T7c no server delete on failure path"; FAIL=$((FAIL+1)); }
echo; echo "RESULT: PASS=$PASS FAIL=$FAIL"
[[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1