Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
bash tests/test-access-token-readiness.sh

test-e2e: ## Full end-to-end run in disposable Linux containers (needs Docker)
Expand Down
16 changes: 12 additions & 4 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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` |
Expand All @@ -37,9 +40,14 @@ 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 setup passes the temporary write token through stdin to jq's `--rawfile`
with a constant program; requests pass it through curl's stdin configuration. Both keep the
token out of process arguments and logs, while the EXIT trap restores the original config. Each POST uses its own securely allocated response file,
Control and access-token requests pass bearer tokens through curl's stdin configuration.
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
write succeeds. A partial write fails setup and cleanup removes the staging file. Publication refuses
Expand Down
28 changes: 17 additions & 11 deletions tests/e2e-pithead-tokens.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ _access_tokens_ready() { # <api|control> <port> <setup|revocation>

phase_access_tokens() {
phase "access-tokens — a non-master ACCESS_TOKENS entry authenticates :8081 and :8082 (#516)"
local master bench cur new port api_port resp code cid st waited=0 body bread
local master bench cur new port api_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")
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"
port=$(jq -r '.control_port // 8082' "$CFG")
api_port=$(jq -r '.api_port // 8081' "$CFG")
Expand All @@ -47,26 +47,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
Expand All @@ -79,10 +79,16 @@ phase_access_tokens() {
set_cfg '.ACCESS_TOKENS={}' soft || bad "could not revoke the bench ACCESS_TOKENS entry"
_access_tokens_ready api "$api_port" revocation || return 1
_access_tokens_ready control "$port" revocation || return 1
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:$api_port/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" = 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.
}
2 changes: 1 addition & 1 deletion tests/e2e-pithead.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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?)"
Expand Down
17 changes: 10 additions & 7 deletions tests/test-access-token-readiness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
TASK_TMP=$(mktemp -d)
trap 'rm -rf "$TASK_TMP"' EXIT
source "$ROOT/tests/e2e-pithead-tokens.sh"
for fn in _restore_xmrig _cleanup snapshot_config; do
source "$ROOT/tests/e2e-pithead-control.sh"
for fn in _restore_xmrig _cleanup snapshot_config set_cfg; do
eval "$(sed -n "/^$fn()/,/^}/p" "$ROOT/tests/e2e-pithead.sh")"
done
check() { [ "$1" = "$2" ] || {
Expand All @@ -24,20 +25,22 @@ bad() {
refresh_profile_finish() { :; }
rigforge_stub() { :; }
systemctl() { return 0; }
set_cfg() {
local next
next=$(jq "$1" "$CFG")
printf '%s\n' "$next" >"$CFG"
}

curl() {
local url="${*: -1}" output='' bearer=0 body=0 n=0 arg
printf '%s\n' "$*" >>"$CASE_DIR/argv"
while [ "$#" -gt 0 ]; do
arg=$1
shift
case "$arg" in
-H)
--config)
[ "$1" = - ] || return 99
shift
[ -n "$(cat)" ] || return 99
bearer=1
;;
-H)
[[ "$1" != Authorization:* ]] || return 99
shift
;;
-o)
Expand Down
Loading