#!/usr/bin/env bash set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" WAIT="$SCRIPT_DIR/scripts/wait-for-version" 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" # URL-aware mock curl: routes to different fixture files based on URL path. # Environment variables: # MOCK_BRANCH_BODY / MOCK_BRANCH_CODE — for /branches/main # MOCK_RUNS_BODY / MOCK_RUNS_CODE — for /actions/runs # MOCK_RELEASE_BODY / MOCK_RELEASE_CODE — for /releases/latest # MOCK_GLOBAL_CODE — override all codes (for auth/404 tests) # MOCK_CURL_FAIL — make curl exit non-zero 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_FAIL:-}" ]] && exit 7 if [[ -n "${MOCK_GLOBAL_CODE:-}" ]]; then printf '\n%s' "$MOCK_GLOBAL_CODE" exit 0 fi if [[ "$url" == */branches/main ]]; then [[ -n "${MOCK_BRANCH_BODY:-}" && -f "${MOCK_BRANCH_BODY}" ]] && cat "$MOCK_BRANCH_BODY" printf '\n%s' "${MOCK_BRANCH_CODE:-200}" elif [[ "$url" == */actions/runs* ]]; then [[ -n "${MOCK_RUNS_BODY:-}" && -f "${MOCK_RUNS_BODY}" ]] && cat "$MOCK_RUNS_BODY" printf '\n%s' "${MOCK_RUNS_CODE:-200}" elif [[ "$url" == */releases/latest ]]; then [[ -n "${MOCK_RELEASE_BODY:-}" && -f "${MOCK_RELEASE_BODY}" ]] && cat "$MOCK_RELEASE_BODY" printf '\n%s' "${MOCK_RELEASE_CODE:-200}" fi MOCK chmod +x "$MOCK_DIR/curl" run_with_mock() { PATH="$MOCK_DIR:$PATH" "$WAIT" "$@" } # --- Fixtures --- FIX_BRANCH="$tmp_root/branch.json" cat > "$FIX_BRANCH" <<'JSON' {"name":"main","commit":{"id":"abc1234567890def1234567890def1234567890de"}} JSON FIX_RUNS_SUCCESS="$tmp_root/runs_success.json" cat > "$FIX_RUNS_SUCCESS" <<'JSON' {"workflow_runs":[ {"id":42,"name":"version","status":"completed","conclusion":"success"}, {"id":41,"name":"test", "status":"completed","conclusion":"success"} ]} JSON FIX_RUNS_FAILED="$tmp_root/runs_failed.json" cat > "$FIX_RUNS_FAILED" <<'JSON' {"workflow_runs":[ {"id":42,"name":"version","status":"completed","conclusion":"failure"} ]} JSON FIX_RUNS_INPROGRESS="$tmp_root/runs_inprogress.json" cat > "$FIX_RUNS_INPROGRESS" <<'JSON' {"workflow_runs":[ {"id":42,"name":"version","status":"in_progress","conclusion":null} ]} JSON FIX_RUNS_EMPTY="$tmp_root/runs_empty.json" cat > "$FIX_RUNS_EMPTY" <<'JSON' {"workflow_runs":[]} JSON FIX_RUNS_WRONG_WORKFLOW="$tmp_root/runs_wrong.json" cat > "$FIX_RUNS_WRONG_WORKFLOW" <<'JSON' {"workflow_runs":[ {"id":42,"name":"test","status":"completed","conclusion":"success"} ]} JSON FIX_RELEASE="$tmp_root/release.json" cat > "$FIX_RELEASE" <<'JSON' {"id":10,"tag_name":"v1.2.3.45","name":"v1.2.3.45"} JSON echo "=== wait-for-version 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 lists exit codes" "Exit codes" "$out" assert_match "help mentions --workflow" "\-\-workflow" "$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" noslash 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 2>&1); rc=$? assert_exit "non-numeric timeout exits 2" 2 "$rc" out=$("$WAIT" --interval 0 owner/repo 2>&1); rc=$? assert_exit "interval 0 exits 2" 2 "$rc" out=$("$WAIT" --unknown-flag owner/repo 2>&1); rc=$? assert_exit "unknown flag exits 2" 2 "$rc" out=$("$WAIT" owner/repo extra 2>&1); rc=$? assert_exit "extra positional arg exits 2" 2 "$rc" # --- Dryrun --- out=$(env -u GITEA_TOKEN -u GITHUB_TOKEN "$WAIT" --dryrun myorg/myrepo 2>&1); 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 workflow" "Workflow: version" "$out" assert_match "dryrun shows timeout" "Timeout: 300s" "$out" assert_match "dryrun shows interval" "Interval: 15s" "$out" assert_match "dryrun shows Auth: no" "Auth: no" "$out" assert_match "dryrun shows Actions API" "api/v1/repos/myorg/myrepo/actions/runs" "$out" # Auth: yes via --token out=$(env -u GITEA_TOKEN -u GITHUB_TOKEN "$WAIT" --dryrun -t tok123 myorg/myrepo 2>&1) 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 2>&1) assert_match "GITEA_TOKEN -> Auth: yes" "Auth: yes" "$out" # Custom --server appears in Actions API URL out=$("$WAIT" --dryrun --server https://example.test myorg/myrepo 2>&1) assert_match "custom --server in Actions API" "https://example\.test/api/v1/repos/myorg/myrepo/actions/runs" "$out" # Custom --workflow reflected in dryrun out=$("$WAIT" --dryrun --workflow release myorg/myrepo 2>&1) assert_match "custom --workflow shown" "Workflow: release" "$out" # --- Successful flow via mock --- export MOCK_BRANCH_BODY="$FIX_BRANCH" export MOCK_RUNS_BODY="$FIX_RUNS_SUCCESS" export MOCK_RELEASE_BODY="$FIX_RELEASE" unset MOCK_GLOBAL_CODE MOCK_CURL_FAIL MOCK_BRANCH_CODE MOCK_RUNS_CODE MOCK_RELEASE_CODE out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1) rc=$? # stdout is the last line (tag); stderr has the "Waiting..." message tag=$(run_with_mock --timeout 5 --interval 1 test/repo 2>/dev/null) assert_exit "success exits 0" 0 "$rc" assert_eq "success prints tag" "v1.2.3.45" "$tag" # Only the version workflow is selected, not the test workflow out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>/dev/null) assert_eq "selects correct workflow by name" "v1.2.3.45" "$out" # -- Verbose output to stderr --- stderr_out=$(run_with_mock --verbose --timeout 5 --interval 1 test/repo 2>&1 >/dev/null) assert_match "verbose mentions Waiting" "Waiting for" "$stderr_out" # --- CI failure -> exit 1 --- export MOCK_RUNS_BODY="$FIX_RUNS_FAILED" out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$? assert_exit "CI failure exits 1" 1 "$rc" assert_match "CI failure message includes conclusion" "failure" "$out" # --- In-progress -> timeout (no polling loop, just immediate timeout) --- export MOCK_RUNS_BODY="$FIX_RUNS_INPROGRESS" start_t=$(date +%s) out=$(run_with_mock --timeout 2 --interval 1 test/repo 2>&1); rc=$? end_t=$(date +%s) elapsed=$(( end_t - start_t )) assert_exit "in_progress times out -> exit 1" 1 "$rc" assert_match "timeout error message" "timeout after 2s" "$out" assert_between "elapsed within tolerance" 2 4 "$elapsed" # --- No matching workflow in runs -> timeout --- export MOCK_RUNS_BODY="$FIX_RUNS_WRONG_WORKFLOW" out=$(run_with_mock --timeout 2 --interval 1 test/repo 2>&1); rc=$? assert_exit "no matching workflow times out -> exit 1" 1 "$rc" # --- Empty runs list -> timeout --- export MOCK_RUNS_BODY="$FIX_RUNS_EMPTY" out=$(run_with_mock --timeout 1 --interval 1 test/repo 2>&1); rc=$? assert_exit "empty runs times out -> exit 1" 1 "$rc" # --- Branch API auth errors -> exit 3 --- export MOCK_GLOBAL_CODE=401 out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$? assert_exit "401 on branch lookup exits 3" 3 "$rc" assert_match "401 error message" "authentication failed" "$out" export MOCK_GLOBAL_CODE=403 out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$? assert_exit "403 exits 3" 3 "$rc" export MOCK_GLOBAL_CODE=404 out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$? assert_exit "404 exits 3" 3 "$rc" assert_match "404 error message" "not found" "$out" # --- Runs API 401 -> exit 3 --- unset MOCK_GLOBAL_CODE export MOCK_BRANCH_BODY="$FIX_BRANCH" export MOCK_BRANCH_CODE=200 export MOCK_RUNS_CODE=401 out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$? assert_exit "runs 401 exits 3" 3 "$rc" # --- Transient 500 on runs -> retry until timeout --- export MOCK_RUNS_CODE=500 out=$(run_with_mock --timeout 2 --interval 1 test/repo 2>&1); rc=$? assert_exit "persistent 500 on runs exits 1 (timeout)" 1 "$rc" # --- Network error -> retry until timeout --- unset MOCK_RUNS_CODE export MOCK_CURL_FAIL=1 out=$(run_with_mock --timeout 2 --interval 1 test/repo 2>&1); rc=$? assert_exit "network error exits 3 (curl fail on branch)" 3 "$rc" unset MOCK_CURL_FAIL # --- With explicit token --- unset MOCK_BRANCH_CODE MOCK_RUNS_CODE MOCK_GLOBAL_CODE export MOCK_RUNS_BODY="$FIX_RUNS_SUCCESS" export MOCK_RELEASE_BODY="$FIX_RELEASE" out=$(run_with_mock --token secret123 --timeout 5 --interval 1 test/repo 2>/dev/null) assert_eq "polling with --token still works" "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