#!/usr/bin/env bash # The policy dashboard against real servers: a didbot-pds to be governed, the # operator's own PDS, the built site, and both halves of the scenario run # against them. The operator's PDS is the stand-in crates/didbot/tests/scenarios # runs, because an operator's records live in a repository somebody else hosts # and a didbot server only reads. # # What runs here that nothing else runs: # # 1. crates/didbot-swarm/tests/policy_rollout.rs -- an operator claims the # server, a policy and a binding are deployed with one applyWrites, and # the operator nudges the server and watches a write that policy refuses # be refused by the policy's own reason. Needs the servers; skips without # them. # 2. policy-site/tests/e2e-policy.mjs -- the built page in a real Chrome. It # signs in to the operator's PDS and makes that deploy, then compares the # digest its own wasm computes over the two records with the digest the # governed server published. Needs Chrome; skips without it. # # Opt-in, not a commit hook: it compiles, starts servers, and wants a browser. # scripts/ci.sh runs it when DIDBOT_E2E=1. # # Usage: scripts/test-policy-e2e.sh set -euo pipefail cd "$(dirname "$0")/.." if [ "$#" -ne 0 ]; then echo "usage: $0" >&2 exit 2 fi # shellcheck source=scripts/dev-pidfile.sh . scripts/dev-pidfile.sh say() { printf '\n\033[1m=== %s ===\033[0m\n' "$1" >&2; } WORK="$(mktemp -d "${TMPDIR:-/tmp}/didbot-policy-e2e.XXXXXX")" # One stable place for the page's screenshots, overwritten each run, so the # evidence outlives the working directory below. SHOTS="${DIDBOT_E2E_SHOTS:-${TMPDIR:-/tmp}/didbot-policy-e2e-shots}" PIDS=() # Every child is killed on the way out, however this ends: a failed assertion # leaves two servers and a Vite process behind otherwise, and the next run # picks different ports and leaves two more. Each is started under `setsid`, so # signalling the negative pid reaches the whole group -- `npm run preview` is a # wrapper around the process that actually holds the port. cleanup() { local status=$? for pid in ${PIDS[@]+"${PIDS[@]}"}; do kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true done for pid in ${PIDS[@]+"${PIDS[@]}"}; do wait "$pid" 2>/dev/null || true done # A server that answers SIGTERM by draining, inside a process that goes on # after it -- the stand-in's test does -- outlives the group leader that # was waited on above. Whatever of the group is left goes now. for pid in ${PIDS[@]+"${PIDS[@]}"}; do kill -KILL "-$pid" 2>/dev/null || true done forget_pid policy-e2e-governed "${GOVERNED_PORT:-0}" if [ "$status" -ne 0 ]; then echo "test-policy-e2e: failed; logs are in $WORK" >&2 else rm -rf "$WORK" fi exit "$status" } trap cleanup EXIT INT TERM # A port nobody chose: the kernel hands one out and we take the number. Two # runs at once, or a run alongside a dev stack on 3000, never collide. free_port() { python3 - <<'PY' import socket s = socket.socket() s.bind(("127.0.0.1", 0)) print(s.getsockname()[1]) s.close() PY } # Waits for a server to answer /health, naming it if it never does. await_health() { local name="$1" base="$2" log="$3" waited=0 while ! curl -sf --cacert "$CA" "$base/health" >/dev/null 2>&1; do waited=$((waited + 1)) if [ "$waited" -gt 200 ]; then echo "test-policy-e2e: $name never answered at $base" >&2 tail -30 "$log" >&2 return 1 fi sleep 0.25 done } export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target}" # One certificate authority for this run, in the working directory: every # server signs from it and every client here trusts it. Not the machine's # own (scripts/dev-profile.sh) so a run leaves nothing behind. CA_DIR="$WORK/local-ca" CA="$CA_DIR/ca.pem" export DIDBOT_EXTRA_CA_CERTS="$CA" # The stand-in signs from the same authority; see `local_ca` in # crates/didbot/tests/scenarios.rs. export DIDBOT_LOCAL_CA="$CA_DIR" say "build: didbot-pds, the operator's stand-in PDS and the test that drives them" cargo build --quiet -p didbot-serve --bin didbot-pds cargo build --quiet --tests -p didbot-swarm cargo test --quiet --no-run -p didbot --test scenarios PDS="$CARGO_TARGET_DIR/debug/didbot-pds" say "build: policy-site/dist, the page and the wasm it checks with" scripts/build-policy-site.sh OPERATOR_PORT="$(free_port)" GOVERNED_PORT="$(free_port)" SITE_PORT="$(free_port)" # The operator is `did:web:operator.localhost` and not the stand-in's usual # `did:web:localhost%3A`: the atproto libraries the page signs in with # read a DID on bare `localhost` over plain http. OPERATOR_HOST=operator.localhost GOVERNED_URL="https://govzone.localhost:$GOVERNED_PORT" SITE_URL="http://127.0.0.1:$SITE_PORT" # A did:web carries no port, so resolving one means 443 -- and one machine # has one of those. Each zone says where it answers, and every tool here # resolves accordingly. export DIDBOT_RESOLVE_PORTS="$OPERATOR_HOST=$OPERATOR_PORT,govzone.localhost=$GOVERNED_PORT" say "the operator's own PDS" # The scenarios' stand-in, left running: its PDS, and an OpenID Connect issuer # and a token mint this run does not use, each on a port nobody chose. NEWCOMER_CONFIG="$WORK/operator-config" \ NEWCOMER_HUMAN_HOST="$OPERATOR_HOST" \ NEWCOMER_HUMAN_PORT="$OPERATOR_PORT" \ NEWCOMER_ISSUER_PORT="$(free_port)" \ NEWCOMER_MINT_PORT="$(free_port)" \ setsid cargo test --quiet -p didbot --test scenarios the_stand_ins_by_hand \ -- --ignored --nocapture >"$WORK/operator.log" 2>&1 & PIDS+=($!) waited=0 until grep -q '^human ' "$WORK/operator.log" 2>/dev/null; do waited=$((waited + 1)) if [ "$waited" -gt 400 ]; then echo "test-policy-e2e: the operator's PDS never started" >&2 tail -30 "$WORK/operator.log" >&2 exit 1 fi sleep 0.25 done # `human at ` read -r _ OPERATOR_DID _ OPERATOR_URL <&2 # Only now: the stand-in has just made the authority, and node warns about a # file that is not there yet on every start before this. export NODE_EXTRA_CA_CERTS="$CA" say "the governed server on $GOVERNED_PORT, operated by that account" claim_port policy-e2e-governed "$GOVERNED_PORT" didbot-pds DIDBOT_PDS_DATA= NO_COLOR=1 setsid "$PDS" \ --port "$GOVERNED_PORT" --zone govzone.localhost --tls-ca "$CA_DIR" \ --operator "$OPERATOR_DID" \ >"$WORK/governed.log" 2>&1 & PIDS+=($!) record_pid policy-e2e-governed "$GOVERNED_PORT" "$!" await_health "the governed server" "$GOVERNED_URL" "$WORK/governed.log" say "the site on $SITE_PORT" # `vite preview` and not a plain file server: it serves dist/ with the deployed # CSP and runs infra/policy-site/viewer-request.js over every path, so a route # like /rollouts resolves the way CloudFront resolves it. 127.0.0.1 and not # localhost, because the atproto loopback client redirects to the former. setsid npm --prefix policy-site run preview -- --port "$SITE_PORT" --strictPort \ >"$WORK/site.log" 2>&1 & PIDS+=($!) waited=0 while ! curl -sf "$SITE_URL/" >/dev/null 2>&1; do waited=$((waited + 1)) if [ "$waited" -gt 200 ]; then echo "test-policy-e2e: the site never answered at $SITE_URL" >&2 tail -30 "$WORK/site.log" >&2 exit 1 fi sleep 0.25 done say "the scenario: claim, deploy through the page, roll out, enforce" # The two halves run at once and meet in $HANDOFF: the Rust half claims the # server and writes the post the policy will refuse, the page signs in and # deploys, and the Rust half then watches it enforced. With no browser the # page says so there, and the Rust half deploys the same batch itself. HANDOFF="$WORK/handoff" mkdir -p "$HANDOFF" DIDBOT_E2E_OPERATOR_URL="$OPERATOR_URL" \ DIDBOT_E2E_GOVERNED_URL="$GOVERNED_URL" \ DIDBOT_E2E_OPERATOR_DID="$OPERATOR_DID" \ DIDBOT_E2E_HANDOFF="$HANDOFF" \ DIDBOT_E2E_OUT="$WORK/deployed.json" \ setsid cargo test --quiet -p didbot-swarm --test policy_rollout -- --nocapture & ROLLOUT=$! PIDS+=("$ROLLOUT") # What the browser is told instead of DIDBOT_RESOLVE_PORTS, which it cannot # read: a did:web resolves on 443, and these two zones are not there. RESOLVER_RULES="MAP $OPERATOR_HOST:443 127.0.0.1:$OPERATOR_PORT,MAP govzone.localhost:443 127.0.0.1:$GOVERNED_PORT" DIDBOT_E2E_SITE_URL="$SITE_URL" \ DIDBOT_E2E_HANDOFF="$HANDOFF" \ DIDBOT_E2E_DEPLOYED="$WORK/deployed.json" \ DIDBOT_E2E_SHOTS="$SHOTS" \ DIDBOT_E2E_CA="$CA" \ DIDBOT_E2E_RESOLVE="$RESOLVER_RULES" \ node --test policy-site/tests/e2e-policy.mjs wait "$ROLLOUT" say "done" echo "screenshots: $SHOTS" >&2