Files

136 lines
6.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
repo=$(cd "$(dirname "$0")/../../.." && pwd)
image=${1:-localhost/archipelago-qortal:6.1.9-archy.1}
name="archy-qortal-fixture-$$"
volume="$name-data"
mode=${2:-fresh}
case "$mode" in fresh|initialized-fixture) ;; *) echo 'Expected fresh or initialized-fixture' >&2; exit 2 ;; esac
work=$(mktemp -d)
cleanup() {
podman rm -f "$name" "$name-probe" >/dev/null 2>&1 || true
podman volume rm -f "$volume" >/dev/null 2>&1 || true
rm -rf -- "$work"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
common=(--pull=never --image-volume=ignore --network none --pid=private --ipc=private --http-proxy=false --cpus 2 --memory 2g --memory-swap 2g --pids-limit 256 --timeout=360
--cap-drop all --security-opt no-new-privileges --read-only
--tmpfs /tmp:rw,notmpcopyup,nosuid,nodev,size=512m)
# Optional unpublished test overlay. Never modifies the original image/JAR.
if [[ -n ${QORTAL_FIXTURE_JAR:-} ]]; then
[[ -f $QORTAL_FIXTURE_JAR && ! -L $QORTAL_FIXTURE_JAR ]]
[[ ${QORTAL_FIXTURE_JAR_SHA256:-} =~ ^[a-f0-9]{64}$ ]]
[[ $(sha256sum "$QORTAL_FIXTURE_JAR" | cut -d' ' -f1) == "$QORTAL_FIXTURE_JAR_SHA256" ]]
common+=(--volume "$QORTAL_FIXTURE_JAR:/usr/local/qortal/qortal.jar:ro")
fi
test "$(podman image inspect --format '{{.Config.User}}' "$image")" = '1000:100'
podman volume create "$volume" >/dev/null
podman run -i --rm --name "$name-probe" "${common[@]}" --volume "$volume:/qortal:U" \
--entrypoint python3 "$image" - <<'PY'
import json, os
from pathlib import Path
os.umask(0o077)
settings = {"bootstrap": False, "autoUpdateEnabled": False,
"localAuthBypassEnabled": False, "apiRestricted": True,
"apiWhitelistEnabled": False, "gatewayEnabled": True,
"gatewayLoopbackEnabled": True, "gatewayPort": 12380}
Path('/qortal/settings.json').write_text(json.dumps(settings))
Path('/qortal/operator-sentinel').write_bytes(b'preserve operator data\n')
PY
start() {
local log_offset
log_offset=$(podman run --rm --name "$name-probe" "${common[@]}" --volume "$volume:/qortal:U" \
--entrypoint python3 "$image" -c 'from pathlib import Path;p=Path("/qortal/qortal.log");print(p.stat().st_size if p.exists() else 0)')
podman run -d --name "$name" "${common[@]}" --volume "$volume:/qortal:U" \
--volume "$repo/tests/apps/qortal:/fixture:ro" \
--env QORTAL_API_KEY="$(printf '11%.0s' {1..32})" \
--env 'QORTAL_JVM_ARGS=-Xmx768m -XX:ActiveProcessorCount=2 -Xss512k' "$image" >/dev/null
for _ in $(seq 1 90); do
if podman exec "$name" curl -fsS -m 1 -o /dev/null http://127.0.0.1:12391/admin/info 2>/dev/null; then return; fi
if podman exec --env "FIXTURE_LOG_OFFSET=$log_offset" "$name" python3 -c 'import os;from pathlib import Path;p=Path("/qortal/qortal.log");offset=int(os.environ["FIXTURE_LOG_OFFSET"]);raise SystemExit(0 if p.exists() and b"statement is not in batch mode" in p.read_bytes()[offset:] else 1)' 2>/dev/null; then
echo 'REPRODUCED: upstream empty-genesis cache migration fails before API startup' >&2
return 2
fi
sleep 1
done
podman logs "$name" >&2 || true
podman exec "$name" python3 -c 'from pathlib import Path;print(Path("/qortal/qortal.log").read_text()[-6000:])' >&2 || true
return 1
}
check_and_wait() {
podman exec "$name" /usr/local/bin/qortal-health
podman exec "$name" python3 /fixture/protocol.py
for _ in $(seq 1 60); do
if test "$(podman inspect --format '{{.State.Running}}' "$name")" = false; then
test "$(podman inspect --format '{{.State.ExitCode}}' "$name")" = 0
return
fi
sleep 1
done
echo 'Core did not gracefully stop within the fixture deadline' >&2
return 1
}
if test "$mode" = initialized-fixture; then
# Compile only our standard-library helper, with annotation processors off.
# Never compile or execute proposal/application code on the host.
mkdir "$work/classes"
javac -proc:none --release 17 -d "$work/classes" "$repo/tests/apps/qortal/InitializedGenesis.java"
if start; then
echo 'Expected the documented empty-genesis failure; re-evaluate fixture rather than mutating a healthy DB' >&2
exit 1
else
test "$?" = 2
fi
podman stop -t 15 "$name" >/dev/null
podman rm "$name" >/dev/null
podman run --rm --name "$name-probe" "${common[@]}" --volume "$volume:/qortal:U" \
--volume "$work/classes:/fixture-classes:ro" --env ARCHY_QORTAL_SYNTHETIC_FIXTURE=1 \
--entrypoint java "$image" -Xmx256m -XX:ActiveProcessorCount=2 \
-cp /usr/local/qortal/qortal.jar:/fixture-classes InitializedGenesis
# Core also rebuilds every height<3 repository when bootstrap=false. Model
# an initialized, non-archival repository explicitly; network stays NONE.
# This is test setup, never an operator-settings repair or fresh-boot pass.
podman run --rm --name "$name-probe" "${common[@]}" --volume "$volume:/qortal:U" \
--entrypoint python3 "$image" -c 'import json;from pathlib import Path;p=Path("/qortal/settings.json");d=json.loads(p.read_text());d.update(bootstrap=True,archiveEnabled=False);p.write_text(json.dumps(d))'
fi
start
check_and_wait
podman rm "$name" >/dev/null
start
check_and_wait
podman rm "$name" >/dev/null
# A changed generated secret must refuse before either child starts, preserving
# the original key/settings rather than silently rotating or resetting them.
podman run -d --name "$name" "${common[@]}" --volume "$volume:/qortal:U" \
--env QORTAL_API_KEY="$(printf '22%.0s' {1..32})" "$image" >/dev/null
# Preparing the verified runtime JAR precedes credential validation. Allow that
# bounded work to finish on constrained CPUs before judging the refusal.
for _ in $(seq 1 120); do
test "$(podman inspect --format '{{.State.Running}}' "$name")" = false && break
sleep 1
done
if test "$(podman inspect --format '{{.State.Running}}' "$name")" != false; then
echo 'Credential-conflict refusal did not finish within 120 seconds' >&2
podman logs "$name" >&2
exit 1
fi
test "$(podman inspect --format '{{.State.ExitCode}}' "$name")" = 78
podman logs "$name" > "$work/refusal.log" 2>&1
if grep -Fq "$(printf '22%.0s' {1..32})" "$work/refusal.log"; then
echo 'Refusal leaked the synthetic replacement key' >&2
exit 1
fi
podman run -i --rm --name "$name-probe" "${common[@]}" --volume "$volume:/qortal:U" \
--entrypoint python3 "$image" - <<'PY'
import hashlib
from pathlib import Path
key=Path('/qortal/apikey.txt')
assert key.read_text() == '11' * 32
assert key.stat().st_ino == int(Path('/qortal/fixture-key-inode').read_text())
assert hashlib.sha256(Path('/qortal/settings.json').read_bytes()).hexdigest() == Path('/qortal/fixture-settings-sha256').read_text()
print('PASS changed-secret refusal preserves original key inode/bytes and operator settings')
PY
printf 'PASS disposable Qortal (%s): actual Core auth, restricted UI proxy, private key and operator data across recreation\n' "$mode"