Files
small-scripts/tests/test-wait-for-release.sh
Paul O'Reilly 5a1b4b14bc Add wait-for-release: poll a Gitea repo's releases until a glob matches
Polls /api/v1/repos/<owner>/<repo>/releases and matches each tag_name
against a shell-style glob (e.g. v1.2.3.*). Default 600s timeout, 5s
interval, both overridable. Exits 0 with the matched tag on stdout,
1 on timeout, 2 on usage error, 3 on terminal API error (401/403/404).
Intended to run in the background of a Claude Code session while CI
produces the release.

Includes spec and 42-assertion test using a PATH-shadowed mock curl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 20:06:24 +12:00

257 lines
7.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
WAIT="$SCRIPT_DIR/scripts/wait-for-release"
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
pass=0
fail=0
assert_eq() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" == "$actual" ]]; then
echo -e "${GREEN}PASS${NC}: $desc"
((pass++)) || true
else
echo -e "${RED}FAIL${NC}: $desc"
echo " expected: $expected"
echo " actual: $actual"
((fail++)) || true
fi
}
assert_match() {
local desc="$1" pattern="$2" actual="$3"
if [[ "$actual" =~ $pattern ]]; then
echo -e "${GREEN}PASS${NC}: $desc"
((pass++)) || true
else
echo -e "${RED}FAIL${NC}: $desc"
echo " pattern: $pattern"
echo " actual: $actual"
((fail++)) || true
fi
}
assert_exit() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" -eq "$actual" ]]; then
echo -e "${GREEN}PASS${NC}: $desc"
((pass++)) || true
else
echo -e "${RED}FAIL${NC}: $desc"
echo " expected exit: $expected"
echo " actual exit: $actual"
((fail++)) || true
fi
}
assert_between() {
local desc="$1" lo="$2" hi="$3" val="$4"
if (( val >= lo && val <= hi )); then
echo -e "${GREEN}PASS${NC}: $desc (${val} in [${lo},${hi}])"
((pass++)) || true
else
echo -e "${RED}FAIL${NC}: $desc (${val} not in [${lo},${hi}])"
((fail++)) || true
fi
}
# --- test env setup ---
tmp_root="$(mktemp -d)"
trap 'rm -rf "$tmp_root"' EXIT
MOCK_DIR="$tmp_root/bin"
mkdir -p "$MOCK_DIR"
# Mock curl: emits fixture body then "\n<code>", ignoring all flags except URL.
cat > "$MOCK_DIR/curl" <<'MOCK'
#!/usr/bin/env bash
url=""
while [[ $# -gt 0 ]]; do
case "$1" in
http://*|https://*) url="$1"; shift ;;
*) shift ;;
esac
done
[[ -n "${MOCK_CURL_LOG:-}" ]] && echo "$url" >> "$MOCK_CURL_LOG"
[[ -n "${MOCK_CURL_FAIL:-}" ]] && exit 7
if [[ -n "${MOCK_CURL_BODY:-}" && -f "$MOCK_CURL_BODY" ]]; then
cat "$MOCK_CURL_BODY"
fi
printf '\n%s' "${MOCK_CURL_CODE:-200}"
MOCK
chmod +x "$MOCK_DIR/curl"
run_with_mock() {
PATH="$MOCK_DIR:$PATH" "$WAIT" "$@"
}
# Fixtures
FIX_MATCH="$tmp_root/fix_match.json"
cat > "$FIX_MATCH" <<'JSON'
[
{"id":10,"tag_name":"v1.2.3.45","name":"v1.2.3.45"},
{"id":9,"tag_name":"v1.2.2.40","name":"v1.2.2.40"},
{"id":8,"tag_name":"v1.1.0.30","name":"v1.1.0.30"}
]
JSON
FIX_NO_MATCH="$tmp_root/fix_no_match.json"
cat > "$FIX_NO_MATCH" <<'JSON'
[
{"id":1,"tag_name":"v0.1.0.3","name":"v0.1.0.3"}
]
JSON
FIX_EMPTY="$tmp_root/fix_empty.json"
echo "[]" > "$FIX_EMPTY"
echo "=== wait-for-release tests ==="
echo
# --- Help ---
out=$("$WAIT" --help 2>&1); rc=$?
assert_exit "help exits 0" 0 "$rc"
assert_match "help mentions REPO" "REPO" "$out"
assert_match "help mentions PATTERN" "PATTERN" "$out"
assert_match "help lists exit codes" "Exit codes" "$out"
# --- Usage errors ---
out=$("$WAIT" 2>&1); rc=$?
assert_exit "no args exits 2" 2 "$rc"
assert_match "no args error" "REPO is required" "$out"
out=$("$WAIT" owner/repo 2>&1); rc=$?
assert_exit "no pattern exits 2" 2 "$rc"
assert_match "no pattern error" "PATTERN is required" "$out"
out=$("$WAIT" noslash 'v*' 2>&1); rc=$?
assert_exit "REPO without slash exits 2" 2 "$rc"
assert_match "REPO slash error" "owner/name" "$out"
out=$("$WAIT" --timeout abc owner/repo 'v*' 2>&1); rc=$?
assert_exit "non-numeric timeout exits 2" 2 "$rc"
out=$("$WAIT" --interval 0 owner/repo 'v*' 2>&1); rc=$?
assert_exit "interval 0 exits 2" 2 "$rc"
out=$("$WAIT" --unknown-flag owner/repo 'v*' 2>&1); rc=$?
assert_exit "unknown flag exits 2" 2 "$rc"
# --- Dryrun ---
out=$("$WAIT" --dryrun myorg/myrepo 'v1.2.3.*')
rc=$?
assert_exit "dryrun exits 0" 0 "$rc"
assert_match "dryrun shows server" "Server: " "$out"
assert_match "dryrun shows repo" "Repo: myorg/myrepo" "$out"
assert_match "dryrun shows pattern" 'Pattern: v1\.2\.3\.\*' "$out"
assert_match "dryrun shows timeout default" "Timeout: 600s" "$out"
assert_match "dryrun shows interval default" "Interval: 5s" "$out"
assert_match "dryrun shows API URL" "api/v1/repos/myorg/myrepo/releases" "$out"
# Auth: no (no token anywhere)
out=$(env -u GITEA_TOKEN -u GITHUB_TOKEN "$WAIT" --dryrun myorg/myrepo 'v*')
assert_match "no token -> Auth: no" "Auth: no" "$out"
# Auth: yes via --token
out=$(env -u GITEA_TOKEN -u GITHUB_TOKEN "$WAIT" --dryrun -t tok123 myorg/myrepo 'v*')
assert_match "--token -> Auth: yes" "Auth: yes" "$out"
# Auth: yes via GITEA_TOKEN env
out=$(env -u GITHUB_TOKEN GITEA_TOKEN=xyz "$WAIT" --dryrun myorg/myrepo 'v*')
assert_match "GITEA_TOKEN env -> Auth: yes" "Auth: yes" "$out"
# --server override appears in URL
out=$("$WAIT" --dryrun --server https://example.test myorg/myrepo 'v*')
assert_match "custom --server in URL" "https://example\.test/api/v1/repos/myorg/myrepo/releases" "$out"
# --- Real match via mock curl ---
export MOCK_CURL_BODY="$FIX_MATCH"
export MOCK_CURL_CODE=200
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v1.2.3.*')
rc=$?
assert_exit "immediate match exits 0" 0 "$rc"
assert_eq "immediate match prints tag" "v1.2.3.45" "$out"
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v1.*')
assert_eq "wildcard v1.* matches newest" "v1.2.3.45" "$out"
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v1.2.2.40')
assert_eq "exact tag match" "v1.2.2.40" "$out"
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v1.1.?.30')
assert_eq "? wildcard matches" "v1.1.0.30" "$out"
# --- No match -> timeout (short) ---
export MOCK_CURL_BODY="$FIX_NO_MATCH"
start_t=$(date +%s)
out=$(run_with_mock --timeout 2 --interval 1 test/repo 'v9.*' 2>&1); rc=$?
end_t=$(date +%s)
elapsed=$(( end_t - start_t ))
assert_exit "no match exits 1" 1 "$rc"
assert_match "no match: timeout error" "timeout after 2s" "$out"
assert_between "timeout elapsed within tolerance" 2 4 "$elapsed"
# --- Empty releases list -> timeout ---
export MOCK_CURL_BODY="$FIX_EMPTY"
out=$(run_with_mock --timeout 1 --interval 1 test/repo 'v1.*' 2>&1); rc=$?
assert_exit "empty list exits 1 (timeout)" 1 "$rc"
# --- 401 -> exit 3 ---
export MOCK_CURL_BODY="$FIX_MATCH"
export MOCK_CURL_CODE=401
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v*' 2>&1); rc=$?
assert_exit "401 exits 3" 3 "$rc"
assert_match "401 error message" "authentication failed" "$out"
# --- 403 -> exit 3 ---
export MOCK_CURL_CODE=403
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v*' 2>&1); rc=$?
assert_exit "403 exits 3" 3 "$rc"
# --- 404 -> exit 3 ---
export MOCK_CURL_CODE=404
out=$(run_with_mock --timeout 5 --interval 1 test/repo 'v*' 2>&1); rc=$?
assert_exit "404 exits 3" 3 "$rc"
assert_match "404 error message" "not found" "$out"
# --- Transient 500 -> retry until timeout ---
export MOCK_CURL_CODE=500
out=$(run_with_mock --timeout 2 --interval 1 test/repo 'v*' 2>&1); rc=$?
assert_exit "persistent 500 exits 1 (timeout)" 1 "$rc"
# --- Network error (curl fails) -> retry until timeout ---
export MOCK_CURL_FAIL=1
unset MOCK_CURL_CODE
out=$(run_with_mock --timeout 2 --interval 1 test/repo 'v*' 2>&1); rc=$?
assert_exit "network error exits 1 (timeout)" 1 "$rc"
unset MOCK_CURL_FAIL
# --- Verbose emits polling info to stderr ---
export MOCK_CURL_BODY="$FIX_NO_MATCH"
export MOCK_CURL_CODE=200
stderr_out=$(run_with_mock --verbose --timeout 1 --interval 1 test/repo 'v99.*' 2>&1 >/dev/null || true)
assert_match "verbose mentions poll" "poll " "$stderr_out"
# --- Polling still succeeds when --token is provided ---
export MOCK_CURL_BODY="$FIX_MATCH"
export MOCK_CURL_CODE=200
out=$(run_with_mock --token secret123 --timeout 5 --interval 1 test/repo 'v1.2.3.*')
assert_eq "polling with token still matches" "v1.2.3.45" "$out"
# --- Summary ---
echo
total=$(( pass + fail ))
echo -e "Results: ${GREEN}${pass}${NC}/${total} passed"
if (( fail > 0 )); then
echo -e "${RED}${fail} test(s) failed${NC}"
exit 1
fi