#!/usr/bin/env bash # next/tests/inbox_pipeline.sh — m2 Step 5b test. # # Exercises pipeline:validate_inbound/3(Activity, PeerActorState, # InboxLog) — the federation inbound pipeline that runs # envelope-shape -> peer signature -> replay against the receiving # actor's inbox log. Step 5c wires this into the HTTP handler. set -uo pipefail cd "$(git rev-parse --show-toplevel)" SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" if [ ! -x "$SX_SERVER" ]; then SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" fi if [ ! -x "$SX_SERVER" ]; then echo "ERROR: sx_server.exe not found." >&2 exit 1 fi VERBOSE="${1:-}" PASS=0; FAIL=0; ERRORS="" TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT # Bob (the peer) signs activities with K1. Alice (the recipient) has # PeerAS = Bob's actor-state (with Bob's public key). The InboxLog is # Alice's :actor_inbox bucket. SETUP='K1 = <<1,2,3,4>>, K1S = [{key_id,k1},{algorithm,ed25519},{value,K1}], BobAS = [{public_keys,[[{id,k1},{created,0},{value,K1}]]}], K2 = <<9,9,9,9>>, EvilAS = [{public_keys,[[{id,k1},{created,0},{value,K2}]]}], Env = outbox:construct(note, bob, 1, [{content,hi}]), Signed = outbox:sign(Env, K1S), {ok, FreshInbox} = log:open(alice, <<105,110,98>>),' cat > "$TMPFILE" < ok (epoch 10) (eval "(get (erlang-eval-ast \"${SETUP} pipeline:validate_inbound(Signed, BobAS, FreshInbox) =:= ok\") :name)") ;; Tampered envelope (broken shape) -> {error, invalid_shape} (epoch 11) (eval "(get (erlang-eval-ast \"${SETUP} Bad = [{type,note}], case pipeline:validate_inbound(Bad, BobAS, FreshInbox) of {error, _} -> ok; _ -> bad end\") :name)") ;; Activity sans :signature -> stage_envelope rejects as ;; {missing_field, signature} (short-circuit before sig stage) (epoch 12) (eval "(get (erlang-eval-ast \"${SETUP} Unsigned = Env, case pipeline:validate_inbound(Unsigned, BobAS, FreshInbox) of {error, {missing_field, signature}} -> ok; _ -> bad end\") :name)") ;; Wrong peer AS (EvilAS doesn't carry Bob's key bytes) -> bad_signature (epoch 13) (eval "(get (erlang-eval-ast \"${SETUP} case pipeline:validate_inbound(Signed, EvilAS, FreshInbox) of {error, bad_signature} -> ok; _ -> bad end\") :name)") ;; Pre-populated inbox containing the same activity -> {error, replay} (epoch 14) (eval "(get (erlang-eval-ast \"${SETUP} {ok, InboxWithMsg, _} = log:append(FreshInbox, Signed), case pipeline:validate_inbound(Signed, BobAS, InboxWithMsg) of {error, replay} -> ok; _ -> bad end\") :name)") ;; Inbox with a DIFFERENT activity doesn't trigger replay (epoch 15) (eval "(get (erlang-eval-ast \"${SETUP} Other = [{type,note},{object,[{content,hello}]},{id,<<200,1>>}], {ok, InboxWithOther, _} = log:append(FreshInbox, Other), pipeline:validate_inbound(Signed, BobAS, InboxWithOther) =:= ok\") :name)") ;; inbound_stages/2 returns 3 stages (epoch 16) (eval "(get (erlang-eval-ast \"${SETUP} length(pipeline:inbound_stages(BobAS, FreshInbox)) =:= 3\") :name)") ;; inbound_stages/0 stays at 1 stage (back-compat for outbox-side callers) (epoch 17) (eval "(get (erlang-eval-ast \"length(pipeline:inbound_stages()) =:= 1\") :name)") ;; validate_inbound/1 still works (envelope-only fast path) (epoch 18) (eval "(get (erlang-eval-ast \"${SETUP} pipeline:validate_inbound(Signed) =:= ok\") :name)") ;; Stages compose: envelope failure short-circuits before sig (epoch 19) (eval "(get (erlang-eval-ast \"${SETUP} BadShape = [{type,note}], case pipeline:validate_inbound(BadShape, EvilAS, FreshInbox) of {error, _} -> ok; _ -> bad end\") :name)") ;; Sig failure short-circuits before replay (epoch 20) (eval "(get (erlang-eval-ast \"${SETUP} {ok, InboxWithMsg, _} = log:append(FreshInbox, Signed), case pipeline:validate_inbound(Signed, EvilAS, InboxWithMsg) of {error, bad_signature} -> ok; _ -> bad end\") :name)") ;; Two distinct peer activities both verify (different :published seq -> different :id) (epoch 21) (eval "(get (erlang-eval-ast \"${SETUP} Env2 = outbox:construct(note, bob, 2, [{content,hi}]), Signed2 = outbox:sign(Env2, K1S), pipeline:validate_inbound(Signed, BobAS, FreshInbox) =:= ok andalso pipeline:validate_inbound(Signed2, BobAS, FreshInbox) =:= ok\") :name)") ;; Inbox with peer1's activity doesn't replay peer2's (epoch 22) (eval "(get (erlang-eval-ast \"${SETUP} Env2 = outbox:construct(note, bob, 2, [{content,hi}]), Signed2 = outbox:sign(Env2, K1S), {ok, InboxA, _} = log:append(FreshInbox, Signed), pipeline:validate_inbound(Signed2, BobAS, InboxA) =:= ok\") :name)") EPOCHS OUTPUT=$(timeout 240 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) check() { local epoch="$1" desc="$2" expected="$3" local actual actual=$(echo "$OUTPUT" | awk -v e="$epoch" ' $0 ~ "^\\(ok-len " e " " { getline; print; exit } $0 ~ "^\\(ok " e " " { print; exit } $0 ~ "^\\(error " e " " { print; exit } ') [ -z "$actual" ] && actual="" if echo "$actual" | grep -qF -- "$expected"; then PASS=$((PASS+1)) [ "$VERBOSE" = "-v" ] && echo " ok $desc" else FAIL=$((FAIL+1)) ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual " fi } check 4 "pipeline module loaded" "pipeline" check 10 "happy path -> ok" "true" check 11 "bad envelope shape -> {error, _}" "ok" check 12 "unsigned -> missing_field rejection" "ok" check 13 "wrong peer AS -> bad_signature" "ok" check 14 "duplicate activity -> replay" "ok" check 15 "different activity, no replay" "true" check 16 "inbound_stages/2 -> 3 stages" "true" check 17 "inbound_stages/0 -> 1 stage" "true" check 18 "validate_inbound/1 still works" "true" check 19 "shape fail short-circuits sig" "ok" check 20 "sig fail short-circuits replay" "ok" check 21 "two distinct activities verify" "true" check 22 "inbox-of-one doesn't replay other" "true" TOTAL=$((PASS+FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL next/tests/inbox_pipeline.sh passed" else echo "FAIL $PASS/$TOTAL passed, $FAIL failed:" echo "$ERRORS" fi [ $FAIL -eq 0 ]