test: add rig, stress and pentest suites with audit
This commit is contained in:
parent
8330b65ff9
commit
1e782310b6
4 changed files with 679 additions and 0 deletions
177
tests/stress.sh
Executable file
177
tests/stress.sh
Executable file
|
|
@ -0,0 +1,177 @@
|
|||
#!/usr/bin/env bash
|
||||
# smolboid stress test: a mail flood against the daily quota and reply
|
||||
# budget, an HTTP load burst against a full feed, and feed-integrity checks
|
||||
# throughout. Real binaries, real Noise sessions.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
FUMI="$ROOT/../fumi/target/debug/fumi"
|
||||
BUNSHIN_BIN="$ROOT/../bunshin/target/debug/bunshin"
|
||||
SMOLBOID="$ROOT/target/debug/smolboid"
|
||||
HOST=127.0.0.1
|
||||
SMTP_PORT=19621
|
||||
HTTP_PORT=18091
|
||||
RIG="$(mktemp -d /tmp/smolboid-stress.XXXXXX)"
|
||||
DATA="$RIG/data"
|
||||
DAEMON_ADDR="twtxt@$HOST:$SMTP_PORT"
|
||||
FEED="$DATA/feeds/tobias.txt"
|
||||
BUNSHIN_PID=""
|
||||
SMOLBOID_PID=""
|
||||
cleanup() {
|
||||
[[ -n "$BUNSHIN_PID" ]] && kill "$BUNSHIN_PID" 2>/dev/null || true
|
||||
[[ -n "$SMOLBOID_PID" ]] && kill "$SMOLBOID_PID" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
step() { printf '\n== %s ==\n' "$*"; }
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
wait_for() {
|
||||
local file=$1 text=$2 tries=${3:-40} i
|
||||
for ((i = 0; i < tries; i++)); do
|
||||
[[ -f $file ]] && grep -q "$text" "$file" && return 0
|
||||
sleep 1
|
||||
done
|
||||
fail "timed out waiting for '$text' in $file"
|
||||
}
|
||||
count_posts() { grep -cE '^[0-9]' "$FEED" 2>/dev/null || echo 0; }
|
||||
|
||||
step "build"
|
||||
cargo build --manifest-path "$ROOT/../bunshin/Cargo.toml" --quiet
|
||||
cargo build --manifest-path "$ROOT/../fumi/Cargo.toml" --quiet
|
||||
cargo build --manifest-path "$ROOT/Cargo.toml" --quiet
|
||||
|
||||
step "bootstrap: bunshin, author tobias, daemon twtxt"
|
||||
mkdir -p "$DATA"
|
||||
"$BUNSHIN_BIN" keygen --key "$RIG/server.key" >"$RIG/keygen.log" 2>&1
|
||||
SERVER_KEY="$(grep -oE '[a-z2-7]{52}' "$RIG/keygen.log" | head -1)"
|
||||
[[ -n "$SERVER_KEY" ]] || fail "no server key"
|
||||
# Raised server rate limits: the flood stresses smolboid's quota, not
|
||||
# bunshin's per-IP throttles — everything here shares 127.0.0.1, and the
|
||||
# daemon's one-second polling already AUTHs 60 times a minute.
|
||||
"$BUNSHIN_BIN" serve --key "$RIG/server.key" --db "$RIG/server.db" \
|
||||
--host "$HOST" --port "$SMTP_PORT" \
|
||||
--rate-connections 1000000 --rate-sends 1000000 --rate-tokens 1000000 \
|
||||
>"$RIG/bunshin.log" 2>&1 &
|
||||
BUNSHIN_PID=$!
|
||||
sleep 1
|
||||
author() { "$FUMI" --key "$RIG/author.key" --db "$RIG/author.db" --timeout 10 "$@"; }
|
||||
daemon_fumi() { "$FUMI" --key "$RIG/master.key" --db "$DATA/mail.db" --timeout 10 "$@"; }
|
||||
author keygen --force >/dev/null
|
||||
author trust "$HOST:$SMTP_PORT" "$SERVER_KEY"
|
||||
author register "tobias@$HOST:$SMTP_PORT" >/dev/null
|
||||
"$SMOLBOID" keygen "$RIG/master.key" >/dev/null
|
||||
daemon_fumi trust "$HOST:$SMTP_PORT" "$SERVER_KEY"
|
||||
daemon_fumi register "$DAEMON_ADDR" >/dev/null
|
||||
|
||||
step "admit"
|
||||
cat >"$RIG/smolboid.toml" <<EOF
|
||||
listen = $HOST:$HTTP_PORT
|
||||
base_url = http://$HOST:$HTTP_PORT
|
||||
smolmail_host = $HOST
|
||||
mailbox = twtxt
|
||||
master = $RIG/master.key
|
||||
data = $DATA
|
||||
poll_secs = 1
|
||||
timeout = 10
|
||||
cap_bytes = 1048576
|
||||
posts_per_day = 25
|
||||
reposts_per_day = 25
|
||||
EOF
|
||||
"$SMOLBOID" admit --config "$RIG/smolboid.toml" "tobias@$HOST:$SMTP_PORT" >"$RIG/admit.log" 2>&1 \
|
||||
|| { cat "$RIG/admit.log"; fail "admit"; }
|
||||
author fetch >/dev/null
|
||||
|
||||
step "serve"
|
||||
"$SMOLBOID" serve --config "$RIG/smolboid.toml" >"$RIG/serve.log" 2>&1 &
|
||||
SMOLBOID_PID=$!
|
||||
sleep 2
|
||||
|
||||
step "mail burst: 40 unpaced posts in a few seconds"
|
||||
START=$(date +%s)
|
||||
for i in $(seq 1 40); do
|
||||
author send "$DAEMON_ADDR" --body "flood post $i" >/dev/null
|
||||
done
|
||||
echo "40 sends in $(( $(date +%s) - START ))s"
|
||||
# Rapid fire means many payload seconds collide; what must hold is
|
||||
# integrity: nothing invalid, the quota never exceeded, the daemon alive.
|
||||
for ((i = 0; i < 40; i++)); do
|
||||
sleep 1
|
||||
[[ "$(count_posts)" -ge 1 ]] && break
|
||||
done
|
||||
[[ "$(count_posts)" -le 25 ]] || fail "quota exceeded under burst: $(count_posts) records"
|
||||
"$SMOLBOID" check "$FEED" >"$RIG/check.log" \
|
||||
|| { cat "$RIG/check.log"; fail "the burst left the feed unparseable"; }
|
||||
grep -qE "0 unsigned, 0 invalid" "$RIG/check.log" \
|
||||
|| { cat "$RIG/check.log"; fail "the burst published invalid records"; }
|
||||
curl -sf "http://$HOST:$HTTP_PORT/twtxt/tobias.txt" >/dev/null || fail "listener unhealthy after the burst"
|
||||
|
||||
step "quota precision: paced posts fill exactly to 25"
|
||||
# One send per second keeps timestamps distinct, so only the daily quota
|
||||
# can stop publication: the feed lands on exactly the configured cap.
|
||||
for i in $(seq 41 70); do
|
||||
author send "$DAEMON_ADDR" --body "flood post $i" >/dev/null
|
||||
sleep 1
|
||||
[[ "$(count_posts)" == 25 ]] && break
|
||||
done
|
||||
for ((i = 0; i < 20; i++)); do
|
||||
[[ "$(count_posts)" == 25 ]] && break
|
||||
sleep 1
|
||||
done
|
||||
[[ "$(count_posts)" == 25 ]] || fail "quota held at $(count_posts) records, want 25"
|
||||
"$SMOLBOID" check "$FEED" >"$RIG/check.log" \
|
||||
|| { cat "$RIG/check.log"; fail "the quota-cut feed does not verify"; }
|
||||
grep -q "25 verified, 0 unsigned, 0 invalid" "$RIG/check.log" \
|
||||
|| { cat "$RIG/check.log"; fail "not all 25 verify"; }
|
||||
author fetch >/dev/null
|
||||
|
||||
step "reply budget: the flood drew at most 10 replies"
|
||||
# The welcome mail also carries a twtxt: subject; only rejections count.
|
||||
author list --requests 2>/dev/null | grep "twtxt:" | grep -cv "feed is ready" \
|
||||
>"$RIG/replies.count" || true
|
||||
REPLIES=$(grep -oE '^[0-9]+' "$RIG/replies.count" | head -1)
|
||||
echo "error replies after the flood: $REPLIES"
|
||||
[[ "$REPLIES" -le 10 ]] || fail "reply budget exceeded: $REPLIES replies in an hour"
|
||||
|
||||
step "http load: 200 requests, 20 parallel"
|
||||
URL="http://$HOST:$HTTP_PORT/twtxt/tobias.txt"
|
||||
ETAG="$(curl -s -D - -o /dev/null "$URL" | tr -d '\r' | awk -F'"' '/^ETag:/ {print $2}')"
|
||||
[[ -n "$ETAG" ]] || fail "no etag"
|
||||
START=$(date +%s%N)
|
||||
seq 1 200 | xargs -P 20 -I{} curl -sf -o /dev/null "$URL" || fail "a load request failed"
|
||||
END=$(date +%s%N)
|
||||
MS=$(((END - START) / 1000000))
|
||||
echo "200 requests in ${MS}ms ($((200000 / MS)) req/s lower bound)"
|
||||
AFTER="$(curl -s -D - -o /dev/null "$URL" | tr -d '\r' | awk -F'"' '/^ETag:/ {print $2}')"
|
||||
[[ "$AFTER" == "$ETAG" ]] || fail "the etag moved under load without a write"
|
||||
|
||||
step "conditional load: 200 requests, 20 parallel, all 304"
|
||||
START=$(date +%s%N)
|
||||
seq 1 200 | xargs -P 20 -I{} \
|
||||
curl -sf -o /dev/null -w '%{http_code}\n' -H "If-None-Match: \"$ETAG\"" "$URL" \
|
||||
| sort | uniq -c >"$RIG/codes.log"
|
||||
END=$(date +%s%N)
|
||||
echo "200 conditional requests in $(((END - START) / 1000000))ms"
|
||||
grep -q "200 304" "$RIG/codes.log" || { cat "$RIG/codes.log"; fail "not all conditional requests were 304"; }
|
||||
|
||||
step "read-during-write: the feed stays parseable while the mail loop runs"
|
||||
author send "$DAEMON_ADDR" --header "Twt-Delete: $(grep -E '^[0-9]' "$FEED" | tail -1 | cut -f1)" --body "" >/dev/null
|
||||
BROKEN=0
|
||||
for i in $(seq 1 60); do
|
||||
curl -sf "$URL" >/dev/null || BROKEN=$((BROKEN + 1))
|
||||
# One per-loop read while the delete lands between polls.
|
||||
sleep 0.2
|
||||
done
|
||||
[[ "$BROKEN" == 0 ]] || fail "$BROKEN reads failed while the feed changed"
|
||||
for ((i = 0; i < 20; i++)); do
|
||||
[[ "$(count_posts)" == 24 ]] && break
|
||||
sleep 1
|
||||
done
|
||||
[[ "$(count_posts)" == 24 ]] || fail "delete did not land"
|
||||
"$SMOLBOID" check "$FEED" >/dev/null || fail "final feed does not verify"
|
||||
[[ "$(count_posts)" == 24 ]] || fail "expected 24 records after the delete, got $(count_posts)"
|
||||
|
||||
step "stress passed"
|
||||
echo "feed: $FEED"
|
||||
echo "logs: $RIG"
|
||||
cleanup
|
||||
trap - EXIT
|
||||
Loading…
Add table
Add a link
Reference in a new issue