From 3a73ea59d2d5a7576e1a0602b7d8521e788647b9 Mon Sep 17 00:00:00 2001 From: "pithead-developer[bot]" <4933481+pithead-developer[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:07:29 -0500 Subject: [PATCH 1/3] fix(tests): keep contract phase credentials out of process arguments --- tests/README.md | 6 +- tests/e2e-pithead-tokens.sh | 18 ++-- tests/e2e-pithead.sh | 8 +- tests/test-contract-credentials.sh | 168 +++++++++++++++++++++++++++++ tests/test-e2e-gate-fail-closed.sh | 4 + 5 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 tests/test-contract-credentials.sh diff --git a/tests/README.md b/tests/README.md index 1543050..21a2f31 100644 --- a/tests/README.md +++ b/tests/README.md @@ -37,8 +37,10 @@ without a successful refresh and CPU begin/end from the same process fails diagn No arguments, process names or payloads are exported. Five-second observations record timer/service state and the direct feed for five minutes around the apply; the changed ceiling must still appear within 90 seconds. -Control-phase requests pass the temporary bearer token through curl's stdin configuration, -keeping it out of process arguments. Each POST uses its own securely allocated response file, +Control and access-token requests pass bearer tokens through curl's stdin configuration. +Access-token setup and stratum password setup/rotation use stdin with constant jq programs; +the derived read bearer uses Python's standard-library HMAC with its key on stdin. +These paths keep credentials out of process arguments and logs; the EXIT trap restores the original config. Each POST uses its own securely allocated response file, removed after reading the change identifier. The drop-in is written in a private staging file and published atomically with `link` only after the write succeeds. A partial write fails setup and cleanup removes the staging file. Publication refuses diff --git a/tests/e2e-pithead-tokens.sh b/tests/e2e-pithead-tokens.sh index 82494c5..a99eb60 100644 --- a/tests/e2e-pithead-tokens.sh +++ b/tests/e2e-pithead-tokens.sh @@ -9,7 +9,7 @@ phase_access_tokens() { bench=$(head -c 32 /dev/urandom | xxd -p -c 256) cur=$(jq -r '.DONATION // 1' "$CFG") new=$(((cur + 1) % 101)) - set_cfg ".api=\"enabled\" | .control=\"enabled\" | .ACCESS_TOKEN=\"$master\" | .ACCESS_TOKENS={\"bench\":\"$bench\"} | .api_allow_from=\"127.0.0.1/32\"" soft || + printf '{"master":"%s","bench":"%s"}' "$master" "$bench" | set_cfg '.api="enabled" | .control="enabled" | .ACCESS_TOKEN=$tokens[0].master | .ACCESS_TOKENS={"bench":$tokens[0].bench} | .api_allow_from="127.0.0.1/32"' soft --slurpfile tokens /dev/stdin || bad "could not enable api+control with a named ACCESS_TOKENS entry" sleep 3 # let the sister API + control services (restarted by apply) settle port=$(jq -r '.control_port // 8082' "$CFG") @@ -17,26 +17,26 @@ phase_access_tokens() { # :8080 stays master-only — a named entry must not reach xmrig's own API. The refusal code is a # present-but-wrong Bearer, not a missing one, and varies by xmrig version (same reasoning as # e2e-pithead.sh's phase_worker_api restricted-PUT check); != 200 is the contract. - code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -H "Authorization: Bearer $bench" http://127.0.0.1:8080/2/summary 2>/dev/null || true) + code=$(_control_curl "$bench" -s -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:8080/2/summary 2>/dev/null || true) [ -n "$code" ] && [ "$code" != 200 ] && ok "the bench entry is refused on :8080 (master-only, $code, #516)" || bad ":8080 answered $code to a non-master entry (expected refusal)" # :8081 — the sister API accepts the bench entry raw, and its own derived read bearer. - body=$(curl -fsS --max-time 10 -H "Authorization: Bearer $bench" "http://127.0.0.1:$(jq -r '.api_port // 8081' "$CFG")/health" 2>/dev/null || true) + body=$(_control_curl "$bench" -fsS --max-time 10 "http://127.0.0.1:$(jq -r '.api_port // 8081' "$CFG")/health" 2>/dev/null || true) printf '%s' "$body" | jq -e . >/dev/null 2>&1 && ok "a non-master ACCESS_TOKENS entry authenticates :8081 raw (#516)" || bad ":8081 refused the bench entry raw" - bread=$(printf 'rigforge:api-read:v1' | openssl dgst -sha256 -hmac "$bench" | awk '{print $NF}') - body=$(curl -fsS --max-time 10 -H "Authorization: Bearer $bread" "http://127.0.0.1:$(jq -r '.api_port // 8081' "$CFG")/health" 2>/dev/null || true) + bread=$(printf '%s' "$bench" | python3 -c 'import hmac,sys; print(hmac.new(sys.stdin.buffer.read(), b"rigforge:api-read:v1", "sha256").hexdigest())') + body=$(_control_curl "$bread" -fsS --max-time 10 "http://127.0.0.1:$(jq -r '.api_port // 8081' "$CFG")/health" 2>/dev/null || true) printf '%s' "$body" | jq -e . >/dev/null 2>&1 && ok "the bench entry's derived read bearer authenticates :8081 too (#516)" || bad ":8081 refused the bench entry's derived read bearer" # :8082 — a full apply round trip authenticated with the bench entry, never the master. resp="$(mktemp)" - code=$(curl -s -o "$resp" -w '%{http_code}' --max-time 10 -H "Authorization: Bearer $bench" \ + code=$(_control_curl "$bench" -s -o "$resp" -w '%{http_code}' --max-time 10 \ -H "Content-Type: application/json" -d "{\"DONATION\": $new}" "http://127.0.0.1:$port/apply" 2>/dev/null || true) cid=$(jq -r '.change_id // empty' "$resp" 2>/dev/null || true) rm -f "$resp" [ "$code" = 202 ] && [ -n "$cid" ] && ok "a non-master ACCESS_TOKENS entry authenticates :8082's POST /apply (#516, change_id=$cid)" || bad ":8082 POST /apply with the bench entry returned HTTP '$code' (expected 202)" while [ "$waited" -lt 300 ]; do - body=$(curl -fsS --max-time 5 -H "Authorization: Bearer $bench" "http://127.0.0.1:$port/status?change_id=$cid" 2>/dev/null || true) + body=$(_control_curl "$bench" -fsS --max-time 5 "http://127.0.0.1:$port/status?change_id=$cid" 2>/dev/null || true) st=$(printf '%s' "$body" | jq -r '.status // empty' 2>/dev/null || true) case "$st" in applied | rejected | rolled_back | failed) break ;; esac sleep 5 @@ -48,9 +48,9 @@ phase_access_tokens() { # Revocation: drop the entry and confirm it stops on both ports (red before / green after #516). set_cfg '.ACCESS_TOKENS={}' soft || bad "could not revoke the bench ACCESS_TOKENS entry" sleep 3 - code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 -H "Authorization: Bearer $bench" "http://127.0.0.1:$(jq -r '.api_port // 8081' "$CFG")/health" 2>/dev/null || true) + code=$(_control_curl "$bench" -s -o /dev/null -w '%{http_code}' --max-time 10 "http://127.0.0.1:$(jq -r '.api_port // 8081' "$CFG")/health" 2>/dev/null || true) [ "$code" = 401 ] && ok "a revoked ACCESS_TOKENS entry stops authenticating :8081 (#516)" || bad "revoked entry still answered $code on :8081" - code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST -H "Authorization: Bearer $bench" -H 'Content-Type: application/json' -d '{"DONATION":1}' "http://127.0.0.1:$port/apply" 2>/dev/null || true) + code=$(_control_curl "$bench" -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST -H 'Content-Type: application/json' -d '{"DONATION":1}' "http://127.0.0.1:$port/apply" 2>/dev/null || true) [ "$code" = 401 ] && ok "a revoked ACCESS_TOKENS entry stops authenticating :8082 (#516)" || bad "revoked entry still answered $code on :8082" # No further restoration here: _cleanup's EXIT trap restores DONATION, the tokens and the # control state from the snapshot, same convention as phase_control. diff --git a/tests/e2e-pithead.sh b/tests/e2e-pithead.sh index e8fe4fe..14208de 100644 --- a/tests/e2e-pithead.sh +++ b/tests/e2e-pithead.sh @@ -129,10 +129,10 @@ snapshot_config() { trap 'E2E_EXIT_RC=$?; trap - EXIT; _cleanup || [ "$E2E_EXIT_RC" -ne 0 ] || E2E_EXIT_RC=1; exit "$E2E_EXIT_RC"' EXIT } -set_cfg() { # [soft] -- soft: return 1 instead of dying when the edit or the apply fails (#514) +set_cfg() { # [soft] [jq options...] -- soft: return 1 instead of dying when the edit or the apply fails (#514) local tmp err stage=edit tmp="$(mktemp)" - if jq "$1" "$CFG" >"$tmp" && [ -s "$tmp" ] && mv "$tmp" "$CFG" && stage=apply && err=$("$RIGFORGE" apply 2>&1 >/dev/null); then return 0; fi + if jq "${@:3}" "$1" "$CFG" >"$tmp" && [ -s "$tmp" ] && mv "$tmp" "$CFG" && stage=apply && err=$("$RIGFORGE" apply 2>&1 >/dev/null); then return 0; fi rm -f "$tmp" [ -n "${err:-}" ] && echo "set_cfg: $stage failed: $err" >&2 [ "${2:-}" = soft ] && return 1 @@ -417,7 +417,7 @@ phase_stratum_auth() { skip "E2E_STRATUM_PASS not set (stack auth off, or secret not provided) — phases skipped" return 0 fi - set_cfg ".pools[0].pass = \"$E2E_STRATUM_PASS\"" + printf '%s' "$E2E_STRATUM_PASS" | set_cfg '.pools[0].pass = $password' '' --rawfile password /dev/stdin : >"$WLOG" || true "$RIGFORGE" restart >/dev/null 2>&1 if wait_for_job 60; then @@ -443,7 +443,7 @@ phase_stratum_auth() { else ok "wrong pass: no jobs delivered" fi - set_cfg ".pools[0].pass = \"$E2E_STRATUM_PASS\"" # the #113 rotation runbook, proven mechanically + printf '%s' "$E2E_STRATUM_PASS" | set_cfg '.pools[0].pass = $password' '' --rawfile password /dev/stdin # the #113 rotation runbook, proven mechanically : >"$WLOG" || true "$RIGFORGE" restart >/dev/null 2>&1 if wait_for_job 60; then diff --git a/tests/test-contract-credentials.sh b/tests/test-contract-credentials.sh new file mode 100644 index 0000000..851b9b5 --- /dev/null +++ b/tests/test-contract-credentials.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# Hardware-free argv, authentication and restoration regression for #559. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +if [ "$#" = 0 ]; then + TEST_CREDENTIAL_DIR=$(mktemp -d) + export TEST_CREDENTIAL_DIR + trap 'rm -rf "$TEST_CREDENTIAL_DIR"' EXIT + for mode in tokens stratum edit-failed apply-failed; do + if bash "$0" "$mode" >"$TEST_CREDENTIAL_DIR/$mode.log" 2>&1; then + case "$mode" in *-failed) + echo "$mode unexpectedly succeeded" + exit 1 + ;; + esac + else + case "$mode" in *-failed) ;; *) + cat "$TEST_CREDENTIAL_DIR/$mode.log" + exit 1 + ;; + esac + fi + case "$mode" in + edit-failed | apply-failed) + grep -q 'could not enable api+control' "$TEST_CREDENTIAL_DIR/$mode.log" + [ ! -s "$TEST_CREDENTIAL_DIR/$mode.requests" ] + ;; + esac + cmp "$TEST_CREDENTIAL_DIR/$mode.json" "$TEST_CREDENTIAL_DIR/$mode.original" + done + ! grep -qE 'a{64}|b{64}|stratum-secret' "$TEST_CREDENTIAL_DIR"/*.log + printf 'contract credential regressions: PASS\n' + exit 0 +fi +mode="$1" +CFG="$TEST_CREDENTIAL_DIR/$mode.json" +WLOG="$TEST_CREDENTIAL_DIR/worker.log" +# shellcheck disable=SC2034 # consumed by extracted editor/cleanup functions +RIGFORGE=fixture_rigforge +# shellcheck disable=SC2034 # consumed by extracted cleanup function +HAMMER_PIDS='' RIG_LOCK_HOLDER="$TEST_CREDENTIAL_DIR/holder" +E2E_STRATUM_PASS=$'stratum-secret"\\\nsecond line' +master_fixture=$(printf 'a%.0s' {1..64}) +bench_fixture=$(printf 'b%.0s' {1..64}) +read_fixture=5ce653a2e6f6e9ece58f79eedbfcaf87ab80b78b0b0019dd340a758f0badded9 +printf '{"DONATION":1,"pools":[{"pass":"original"}],"keep":true}\n' >"$CFG" +cp "$CFG" "$TEST_CREDENTIAL_DIR/$mode.original" +: >"$WLOG" +: >"$TEST_CREDENTIAL_DIR/$mode.requests" +: >"$TEST_CREDENTIAL_DIR/$mode.edits" +phase() { :; } +ok() { printf 'ok\n' >>"$TEST_CREDENTIAL_DIR/$mode.ok"; } +bad() { + echo "$1" >&2 + exit 1 +} +die() { + echo "$1" >&2 + exit 2 +} +skip() { bad 'unexpected skip'; } +sleep() { :; } +refresh_profile_finish() { :; } +_restore_xmrig() { :; } +systemctl() { [ "$1" = is-active ]; } +# Run the actual editor, phases and EXIT restoration without preflight or hardware. +eval "$(sed -n '/^set_cfg()/,/^}/p; /^_cleanup()/,/^}/p; /^snapshot_config()/,/^}/p; /^phase_stratum_auth()/,/^}/p' "$ROOT/tests/e2e-pithead.sh")" +source "$ROOT/tests/e2e-pithead-control.sh" +source "$ROOT/tests/e2e-pithead-tokens.sh" +check_args() { + local arg + for arg in "$@"; do + case "$arg" in + *"$master_fixture"* | *"$bench_fixture"* | *"$read_fixture"* | *stratum-secret*) bad 'credential appeared in process arguments' ;; + esac + done +} +jq() { + check_args "$@" + [ "$mode" != edit-failed ] || [ "${1:-}" != --slurpfile ] || return 1 + command jq "$@" +} +python3() { + check_args "$@" + command python3 "$@" +} +openssl() { + check_args "$@" + bad 'unexpected openssl invocation' +} +xxd() { + cat >/dev/null + # xxd runs in command substitutions: persist the count outside the subshell. + if [ -e "$TEST_CREDENTIAL_DIR/$mode.generated" ]; then + printf '%s\n' "$bench_fixture" + else + : >"$TEST_CREDENTIAL_DIR/$mode.generated" + printf '%s\n' "$master_fixture" + fi +} +fixture_rigforge() { + if [ "$1" = restart ]; then + if command jq -e '.pools[0].pass == "wrong-114"' "$CFG" >/dev/null; then + printf 'login error\n' >"$WLOG" + else + printf 'new job from fixture\n' >"$WLOG" + fi + return + fi + [ "$1" = apply ] + cmp -s "$CFG" "$SAVED_CFG" && return 0 # restoration + [ "$mode" != apply-failed ] || return 1 + if [ "$mode" = stratum ]; then + # Compare private stdin to the edited password, including quotes, slash and newline. + if ! command jq -e '.pools[0].pass == "wrong-114"' "$CFG" >/dev/null; then + printf '%s' "$E2E_STRATUM_PASS" | command jq -e --rawfile pass /dev/stdin '.pools[0].pass == $pass and .keep' "$CFG" >/dev/null + fi + else + command jq -e '.ACCESS_TOKEN == ("a" * 64) and .api == "enabled" and .control == "enabled" and .api_allow_from == "127.0.0.1/32" and .keep and (.ACCESS_TOKENS == {"bench":("b" * 64)} or .ACCESS_TOKENS == {})' "$CFG" >/dev/null + fi + echo edit >>"$TEST_CREDENTIAL_DIR/$mode.edits" +} +wait_for_job() { grep -q 'new job from' "$WLOG"; } +curl() { + check_args "$@" + local config arg url='' response='' count expected + config=$(cat) + count=$(wc -l <"$TEST_CREDENTIAL_DIR/$mode.requests" | tr -d ' ') + echo request >>"$TEST_CREDENTIAL_DIR/$mode.requests" + expected="$bench_fixture" + [ "$count" != 2 ] || expected="$read_fixture" + [ "$config" = "header = \"Authorization: Bearer $expected\"" ] || bad 'wrong bearer transport or derivation' + while [ "$#" -gt 0 ]; do + arg="$1" + shift + case "$arg" in -o) + response="$1" + shift + ;; + http://*) url="$arg" ;; esac + done + case "$count:$url" in + 0:*/2/summary) printf 401 ;; + 1:*/health | 2:*/health) printf '{}' ;; + 3:*/apply) + printf '{"change_id":"fixture"}' >"$response" + printf 202 + ;; + 4:*/status?change_id=fixture) printf '{"status":"applied"}' ;; + 5:*/health | 6:*/apply) + command jq -e '.ACCESS_TOKENS == {}' "$CFG" >/dev/null + printf 401 + ;; + *) bad 'unexpected request sequence' ;; + esac +} +snapshot_config +if [ "$mode" = stratum ]; then + phase_stratum_auth + [ "$(wc -l <"$TEST_CREDENTIAL_DIR/$mode.edits" | tr -d ' ')" = 3 ] + [ "$(wc -l <"$TEST_CREDENTIAL_DIR/$mode.ok" | tr -d ' ')" = 4 ] +else + phase_access_tokens + [ "$(wc -l <"$TEST_CREDENTIAL_DIR/$mode.requests" | tr -d ' ')" = 7 ] + [ "$(wc -l <"$TEST_CREDENTIAL_DIR/$mode.ok" | tr -d ' ')" = 7 ] +fi +_cleanup +trap - EXIT diff --git a/tests/test-e2e-gate-fail-closed.sh b/tests/test-e2e-gate-fail-closed.sh index c78d8c6..91edcc3 100644 --- a/tests/test-e2e-gate-fail-closed.sh +++ b/tests/test-e2e-gate-fail-closed.sh @@ -366,3 +366,7 @@ watchdog_active_case491() { ) } assert_eq "watchdog cleanup restores and verifies an originally active miner" "$(watchdog_active_case491)" "0:active:1" + +# #559: exercise credential setup/transport with argv inspection and real restoration. +bash "$ROOT/tests/test-contract-credentials.sh" +assert_rc "contract harness credentials stay out of arguments and restore config" "$?" 0 From 6664948b5efd6916dc69bd5bfec02ec9fd44f4fc Mon Sep 17 00:00:00 2001 From: "pithead-developer[bot]" <4933481+pithead-developer[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:32:31 -0500 Subject: [PATCH 2/3] test: require credential regression outside Bash-only kcov suite --- Makefile | 1 + tests/README.md | 5 ++++- tests/test-e2e-gate-fail-closed.sh | 4 ---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 4e80d36..d3d4005 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ test: lint test-suite ## Lint + the dependency-free suite (runs on macOS or Linu test-suite: ## rigforge test suite: unit + black-box, every CPU/OS profile simulated bash tests/run.sh + bash tests/test-contract-credentials.sh test-e2e: ## Full end-to-end run in disposable Linux containers (needs Docker) bash tests/e2e/linux.sh diff --git a/tests/README.md b/tests/README.md index 21a2f31..255ed60 100644 --- a/tests/README.md +++ b/tests/README.md @@ -3,6 +3,9 @@ RigForge is one self-contained script, so its tests are layered by how much they exercise for real, from a dependency-free suite that runs anywhere up to a real-hardware gate that actually compiles XMRig and mines. Each layer covers what the one below it has to stub. +`make test-suite` requires both the Bash suite and the Python-dependent contract credential regression. +The kcov container measures the Bash suite only; the credential regression always runs in the +GitHub Test suite job, including argument inspection, password rotation and EXIT restoration. > The split exists so CI proves everything it can on a GitHub runner, while the things a runner > physically can't do (compile XMRig, reserve HugePages, write MSRs, set the governor, hash) are @@ -12,7 +15,7 @@ compiles XMRig and mines. Each layer covers what the one below it has to stub. | Layer | File | Runs | What it proves | How to run | |---|---|---|---|---| -| **Unit + black-box suite** | [`run.sh`](run.sh) | Any host (macOS/Linux), no Docker. **In CI.** | Config parsing, the XMRig-config generation matrix (every CPU/OS profile, simulated via PATH stubs), GRUB/HugePages math, the command surface, tune search, doctor: everything that doesn't need a real `/etc` or real hardware. The bulk of coverage. | `make test` (lint + suite) or `bash tests/run.sh` | +| **Unit + black-box suite** | [`run.sh`](run.sh) + [`test-contract-credentials.sh`](test-contract-credentials.sh) | Any host (macOS/Linux), no Docker; credential regression needs Python 3. **In CI.** | Config parsing, the XMRig-config generation matrix (every CPU/OS profile, simulated via PATH stubs), GRUB/HugePages math, the command surface, tune search, doctor: everything that doesn't need a real `/etc` or real hardware. The bulk of coverage. | `make test` (lint + suite) or `make test-suite` | | **Linux container e2e** | [`e2e/linux.sh`](e2e/linux.sh) → [`e2e/in-container.sh`](e2e/in-container.sh) | Disposable Ubuntu container, **needs Docker**. **In CI.** | The genuine Linux deploy path against a real (throwaway) `/etc` with real GNU tools (`sed -i`, `tee`, `envsubst`) + idempotency on re-run. Only the heavy/privileged bits (compile, package install, `systemctl`/`mount`) are stubbed. A second pass re-runs the container with `RIGFORGE_APPLIANCE=1` (#348) and proves the appliance contracts on that real `/etc`: units land in `/run/systemd/system`, no package installs, no fstab/limits.conf/logrotate writes, every enable carries `--runtime`. | `make test-e2e` | | **Native macOS e2e** | [`e2e/macos.sh`](e2e/macos.sh) | A real Mac, **manual, local only. Not in CI** (macOS is deprecated, #493). | The macOS deploy path with genuine BSD tools the Linux CI can only stub: BSD `sed`, the macOS config profile, `mac_*` process control (real `nohup` + PID file), the launchd login agent, `backup`/`restore`. | `make test-e2e-macos` | | **Coverage gate** | [`coverage.sh`](coverage.sh) | kcov in **Docker**. **In CI.** | Line coverage of `rigforge.sh` + `util/proposed-grub.sh` by running `run.sh` under kcov; enforces the committed floor ([`coverage-floor.txt`](coverage-floor.txt)) plus a patch-coverage gate (diff-cover) on changed lines. | `make coverage` | diff --git a/tests/test-e2e-gate-fail-closed.sh b/tests/test-e2e-gate-fail-closed.sh index 91edcc3..c78d8c6 100644 --- a/tests/test-e2e-gate-fail-closed.sh +++ b/tests/test-e2e-gate-fail-closed.sh @@ -366,7 +366,3 @@ watchdog_active_case491() { ) } assert_eq "watchdog cleanup restores and verifies an originally active miner" "$(watchdog_active_case491)" "0:active:1" - -# #559: exercise credential setup/transport with argv inspection and real restoration. -bash "$ROOT/tests/test-contract-credentials.sh" -assert_rc "contract harness credentials stay out of arguments and restore config" "$?" 0 From 05341b4ad9f0b9c0b55f95669d28d01722740730 Mon Sep 17 00:00:00 2001 From: "pithead-developer[bot]" <4933481+pithead-developer[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:57:11 -0500 Subject: [PATCH 3/3] test: retain sanitized diagnostics for credential gate failures --- tests/README.md | 3 +++ tests/e2e-pithead-tokens.sh | 12 +++++++--- tests/e2e-pithead.sh | 2 +- tests/test-contract-credentials.sh | 35 +++++++++++++++++++++++++----- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/tests/README.md b/tests/README.md index 1b83389..3e51c97 100644 --- a/tests/README.md +++ b/tests/README.md @@ -44,6 +44,9 @@ Control and access-token requests pass bearer tokens through curl's stdin config Control setup, access-token setup and stratum password setup/rotation use stdin with constant jq programs; the derived read bearer uses Python's standard-library HMAC with its key on stdin. These paths keep credentials out of process arguments and logs; the EXIT trap restores the original config. +A failed control revocation request records only curl's numeric exit status and control-service +activity; a failed wrong-password gate reuses fixed-schema XMRig state and classified log counts. +These diagnostics preserve the failing verdict and never export config, log text or credentials. Each POST uses its own securely allocated response file, removed after reading the change identifier. The drop-in is written in a private staging file and published atomically with `link` only after the diff --git a/tests/e2e-pithead-tokens.sh b/tests/e2e-pithead-tokens.sh index a99eb60..6bfb9fe 100644 --- a/tests/e2e-pithead-tokens.sh +++ b/tests/e2e-pithead-tokens.sh @@ -4,7 +4,7 @@ # e2e-pithead.sh AFTER phase()/ok()/bad()/set_cfg()/$CFG are defined; must not be run standalone. phase_access_tokens() { phase "access-tokens — a non-master ACCESS_TOKENS entry authenticates :8081 and :8082 (#516)" - local master bench cur new port resp code cid st waited=0 body bread + local master bench cur new port resp code cid st waited=0 body bread curl_rc=0 control_active=0 master=$(head -c 32 /dev/urandom | xxd -p -c 256) bench=$(head -c 32 /dev/urandom | xxd -p -c 256) cur=$(jq -r '.DONATION // 1' "$CFG") @@ -50,8 +50,14 @@ phase_access_tokens() { sleep 3 code=$(_control_curl "$bench" -s -o /dev/null -w '%{http_code}' --max-time 10 "http://127.0.0.1:$(jq -r '.api_port // 8081' "$CFG")/health" 2>/dev/null || true) [ "$code" = 401 ] && ok "a revoked ACCESS_TOKENS entry stops authenticating :8081 (#516)" || bad "revoked entry still answered $code on :8081" - code=$(_control_curl "$bench" -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST -H 'Content-Type: application/json' -d '{"DONATION":1}' "http://127.0.0.1:$port/apply" 2>/dev/null || true) - [ "$code" = 401 ] && ok "a revoked ACCESS_TOKENS entry stops authenticating :8082 (#516)" || bad "revoked entry still answered $code on :8082" + code=$(_control_curl "$bench" -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST -H 'Content-Type: application/json' -d '{"DONATION":1}' "http://127.0.0.1:$port/apply" 2>/dev/null) || curl_rc=$? + if [ "$code" = 401 ]; then + ok "a revoked ACCESS_TOKENS entry stops authenticating :8082 (#516)" + else + systemctl is-active --quiet rigforge-control.service && control_active=1 || true + printf 'access-token-diagnostic: control_transport_rc=%s control_active=%s\n' "$curl_rc" "$control_active" + bad "revoked entry still answered $code on :8082" + fi # No further restoration here: _cleanup's EXIT trap restores DONATION, the tokens and the # control state from the snapshot, same convention as phase_control. } diff --git a/tests/e2e-pithead.sh b/tests/e2e-pithead.sh index 8b33d20..06fc897 100644 --- a/tests/e2e-pithead.sh +++ b/tests/e2e-pithead.sh @@ -436,7 +436,7 @@ phase_stratum_auth() { if grep -qi 'permission denied\|login error' "$WLOG" 2>/dev/null; then ok "wrong pass: rejected by the proxy" else - bad "wrong pass: no rejection within 60s" + _connect_bad "wrong pass: no rejection within 60s" fi if grep -q 'new job from' "$WLOG" 2>/dev/null; then bad "wrong pass: worker still received jobs (auth not enforced?)" diff --git a/tests/test-contract-credentials.sh b/tests/test-contract-credentials.sh index 851b9b5..090e585 100644 --- a/tests/test-contract-credentials.sh +++ b/tests/test-contract-credentials.sh @@ -6,7 +6,7 @@ if [ "$#" = 0 ]; then TEST_CREDENTIAL_DIR=$(mktemp -d) export TEST_CREDENTIAL_DIR trap 'rm -rf "$TEST_CREDENTIAL_DIR"' EXIT - for mode in tokens stratum edit-failed apply-failed; do + for mode in tokens stratum edit-failed apply-failed tokens-failed stratum-failed; do if bash "$0" "$mode" >"$TEST_CREDENTIAL_DIR/$mode.log" 2>&1; then case "$mode" in *-failed) echo "$mode unexpectedly succeeded" @@ -26,6 +26,16 @@ if [ "$#" = 0 ]; then [ ! -s "$TEST_CREDENTIAL_DIR/$mode.requests" ] ;; esac + case "$mode" in + tokens-failed) + grep -q '^access-token-diagnostic: control_transport_rc=7 control_active=1$' "$TEST_CREDENTIAL_DIR/$mode.log" + grep -q 'revoked entry still answered 000' "$TEST_CREDENTIAL_DIR/$mode.log" + ;; + stratum-failed) + grep -q '^xmrig-diagnostic: .*network_errors=1 .*auth_errors=0 ' "$TEST_CREDENTIAL_DIR/$mode.log" + grep -q 'wrong pass: no rejection within 60s' "$TEST_CREDENTIAL_DIR/$mode.log" + ;; + esac cmp "$TEST_CREDENTIAL_DIR/$mode.json" "$TEST_CREDENTIAL_DIR/$mode.original" done ! grep -qE 'a{64}|b{64}|stratum-secret' "$TEST_CREDENTIAL_DIR"/*.log @@ -62,9 +72,16 @@ skip() { bad 'unexpected skip'; } sleep() { :; } refresh_profile_finish() { :; } _restore_xmrig() { :; } -systemctl() { [ "$1" = is-active ]; } +systemctl() { + case "$1" in + is-active) return 0 ;; + show) printf 'ActiveState=active\nSubState=running\nResult=success\nExecMainStatus=0\n' ;; + *) return 1 ;; + esac +} # Run the actual editor, phases and EXIT restoration without preflight or hardware. eval "$(sed -n '/^set_cfg()/,/^}/p; /^_cleanup()/,/^}/p; /^snapshot_config()/,/^}/p; /^phase_stratum_auth()/,/^}/p' "$ROOT/tests/e2e-pithead.sh")" +eval "$(sed -n '/^_xmrig_diagnostics()/,/^}/p; /^_connect_bad()/,/^}/p' "$ROOT/tests/e2e-control-replay.sh")" source "$ROOT/tests/e2e-pithead-control.sh" source "$ROOT/tests/e2e-pithead-tokens.sh" check_args() { @@ -101,7 +118,11 @@ xxd() { fixture_rigforge() { if [ "$1" = restart ]; then if command jq -e '.pools[0].pass == "wrong-114"' "$CFG" >/dev/null; then - printf 'login error\n' >"$WLOG" + if [ "$mode" = stratum-failed ]; then + printf 'connect error: stratum-secret\n' >"$WLOG" + else + printf 'login error\n' >"$WLOG" + fi else printf 'new job from fixture\n' >"$WLOG" fi @@ -110,7 +131,7 @@ fixture_rigforge() { [ "$1" = apply ] cmp -s "$CFG" "$SAVED_CFG" && return 0 # restoration [ "$mode" != apply-failed ] || return 1 - if [ "$mode" = stratum ]; then + if [[ "$mode" = stratum* ]]; then # Compare private stdin to the edited password, including quotes, slash and newline. if ! command jq -e '.pools[0].pass == "wrong-114"' "$CFG" >/dev/null; then printf '%s' "$E2E_STRATUM_PASS" | command jq -e --rawfile pass /dev/stdin '.pools[0].pass == $pass and .keep' "$CFG" >/dev/null @@ -149,13 +170,17 @@ curl() { 4:*/status?change_id=fixture) printf '{"status":"applied"}' ;; 5:*/health | 6:*/apply) command jq -e '.ACCESS_TOKENS == {}' "$CFG" >/dev/null + if [ "$mode:$count" = tokens-failed:6 ]; then + printf 000 + return 7 + fi printf 401 ;; *) bad 'unexpected request sequence' ;; esac } snapshot_config -if [ "$mode" = stratum ]; then +if [[ "$mode" = stratum* ]]; then phase_stratum_auth [ "$(wc -l <"$TEST_CREDENTIAL_DIR/$mode.edits" | tr -d ' ')" = 3 ] [ "$(wc -l <"$TEST_CREDENTIAL_DIR/$mode.ok" | tr -d ' ')" = 4 ]