The owner took @thepra, which the deploy had made and kept under its own login deploy-smoke for its signed-in check, for the administrator's login (owner decision 2026-10-17). No persona could change login, so: - admin move-persona <persona> <root> re-points the persona's link first, so the old login can no longer exchange a token for it, then ends its Mastodon tokens and authorizations. Its actor, posts and followers stay; nobody outside is told, since nothing they hold changes. - admin delete-root <root> deletes a login as the client deletes its own (RootRemoval): every persona it still has goes with it, announced deleted, and its sessions end. - admin smoke and the deploy's signed-in step are gone, since the step would make deploy-smoke again and fail to make @thepra under it. The deploy keeps its anonymous checks; tools/smoke/oauth.sh stays for the pasture's privapub_token. Tests: move-persona (the persona listed under its new login only, its old token refused, unknown persona or login exit 1) and delete-root (the login and its persona deleted, the login refused); the whole suite passes (963). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
5.7 KiB
Bash
82 lines
5.7 KiB
Bash
# Shared by interop.sh and the scenarios: check helpers, PrivaPub personas and tokens, the statistics check.
|
|
P=http://127.0.0.1:${PASTURE_PORT:-6971}
|
|
work=$(mktemp -d); trap 'rm -rf "$work"' EXIT
|
|
pass=0; fail=0; expected=0
|
|
# every check is also appended to $INTEROP_JSONL (out/scenarios.jsonl), which the town's report reads next to its own
|
|
INTEROP_JSONL=${INTEROP_JSONL:-$here/out/scenarios.jsonl}
|
|
mkdir -p "$(dirname "$INTEROP_JSONL")"
|
|
record() { python3 -c 'import json,sys,time; print(json.dumps({"source":"scenario","peer":sys.argv[1],"check":sys.argv[2],"status":sys.argv[3],"t":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime())}))' \
|
|
"${INTEROP_PEER:-}" "$1" "$2" >> "$INTEROP_JSONL"; }
|
|
ok() { echo " ok $*"; pass=$((pass+1)); record "$*" pass; }
|
|
ko() { echo " FAIL $*"; fail=$((fail+1)); record "$*" fail; }
|
|
xf() { echo " xf $* (expected to fail until a later phase)"; expected=$((expected+1)); record "$*" xfail; }
|
|
j() { python3 -c "import sys,json
|
|
try: d=json.load(sys.stdin)
|
|
except Exception: d=None
|
|
$1" 2>/dev/null; }
|
|
until_true() { local tries=$1; shift; for _ in $(seq 1 "$tries"); do if eval "$@"; then return 0; fi; sleep 2; done; return 1; }
|
|
site() { curl -k --resolve "$1:6443:127.0.0.1" "${@:2}"; }
|
|
# fetches one of PrivaPub's own https URIs (ids, scribbles) from the workstation, through Caddy
|
|
pfetch() { curl -sk --connect-to privapub.test:443:127.0.0.1:6443 "$@"; }
|
|
|
|
# make_png <path>: an 8x8 red PNG, for uploads
|
|
# the status an unsigned ActivityPub GET of a PrivaPub document gets
|
|
pstatus() { pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "$1"; }
|
|
# SecureMode (PRIVAPUB_ENV="Federation__SecureMode=true") answers every unsigned GET but the instance actor's 401, so a
|
|
# post's author answering 401 means it is on; then 401 is what "not served" and "gone" look like to an unsigned reader
|
|
secure_mode() { [ "$(pstatus "${1%%/scribbles/*}")" = "401" ]; }
|
|
unserved() { local code; code=$(pstatus "$1"); if secure_mode "$1"; then [ "$code" = 401 ]; else [ "$code" = 404 ]; fi; }
|
|
gone_unsigned() { local code; code=$(pstatus "$1"); if secure_mode "$1"; then [ "$code" = 401 ]; else [ "$code" = 410 ]; fi; }
|
|
make_png() { python3 -c "
|
|
import struct,zlib
|
|
w=h=8
|
|
raw=b''.join(b'\x00'+bytes([200,60,60])*w for _ in range(h))
|
|
png=b'\x89PNG\r\n\x1a\n'+b''.join(struct.pack('>I',len(c))+t+c+struct.pack('>I',zlib.crc32(t+c)&0xffffffff) for t,c in [(b'IHDR',struct.pack('>IIBBBBB',w,h,8,2,0,0,0)),(b'IDAT',zlib.compress(raw)),(b'IEND',b'')])
|
|
open('$1','wb').write(png)"; }
|
|
|
|
ROOT_USER=pastureroot; ROOT_PASS='Pasture-Pass-1!'
|
|
privapub_root() {
|
|
local root
|
|
root=$(curl -s -X POST $P/clientapi/user/signup -H 'Content-Type: application/json' -d "{\"userName\":\"$ROOT_USER\",\"password\":\"$ROOT_PASS\"}")
|
|
[ -n "$(echo "$root" | j "print(d['token'])")" ] || root=$(curl -s -X POST $P/clientapi/user/login -H 'Content-Type: application/json' -d "{\"userName\":\"$ROOT_USER\",\"password\":\"$ROOT_PASS\"}")
|
|
echo "$root" | j "print(d['token'])"
|
|
}
|
|
|
|
# privapub_token <persona>: creates the persona under the pasture root if needed and returns a Mastodon token for it,
|
|
# through the real OAuth flow (tools/smoke/oauth.sh).
|
|
privapub_token() {
|
|
local persona=$1 jwt
|
|
jwt=$(privapub_root)
|
|
curl -s -o /dev/null -X POST $P/clientapi/avatar/private/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"userName\":\"$persona\",\"name\":\"$persona of PrivaPub\",\"biography\":\"testing federation\"}"
|
|
"$here/../smoke/oauth.sh" "$P" "$ROOT_USER" "$ROOT_PASS" "$persona" "read write follow" | cut -d' ' -f1
|
|
}
|
|
|
|
# p_report <authorization header> <account id> <reason> <status id...>: a persona reports an account's posts and asks for
|
|
# the report to be forwarded; prints whether PrivaPub says it was (Mastodon's "forwarded")
|
|
p_report() {
|
|
local auth=$1 account=$2 reason=$3; shift 3
|
|
local ids=() id
|
|
for id in "$@"; do ids+=(-d "status_ids[]=$id"); done
|
|
curl -s -X POST -H "$auth" "$P/api/v1/reports" -d "account_id=$account" "${ids[@]}" --data-urlencode "comment=$reason" \
|
|
-d 'category=other' -d 'forward=true' | j "print(d['forwarded'])"
|
|
}
|
|
# the anonymous Service that carries PrivaPub's reports to the servers that take them only from a person or a service
|
|
P_REPORTER=https://privapub.test/peasants/privapub_reports
|
|
|
|
# stats_check <host> <software>: the admin statistics name the peer's software and count traffic both ways.
|
|
stats_check() {
|
|
local host=$1 software=$2 admin found
|
|
podman exec -w /app pasture-privapub /app/PrivaPub admin promote "$ROOT_USER" >/dev/null 2>&1 || true
|
|
admin=$(curl -s -X POST $P/clientapi/user/login -H 'Content-Type: application/json' -d "{\"userName\":\"$ROOT_USER\",\"password\":\"$ROOT_PASS\"}" | j "print(d['token'])")
|
|
until_true 30 'found=$(curl -s -H "Authorization: Bearer $admin" "$P/clientapi/admin/statistics/hosts/$host?days=1"); [ "$(echo "$found" | j "print((d[\"instance\"] or {}).get(\"software\"))")" = "$software" ]' \
|
|
&& ok "statistics describe $host as $software" || ko "statistics do not describe $host as $software"
|
|
found=$(curl -s -H "Authorization: Bearer $admin" "$P/clientapi/admin/statistics/hosts/$host?days=1")
|
|
[ "$(echo "$found" | j "print(any(k.startswith('in:') for day in d['days'] for k in day['counters']))")" = "True" ] \
|
|
&& ok "statistics count what $host sent" || ko "no inbound statistics for $host"
|
|
[ "$(echo "$found" | j "print(any(k.startswith('out:') and ':ok' in k for day in d['days'] for k in day['counters']))")" = "True" ] \
|
|
&& ok "statistics count what we delivered to $host" || ko "no outbound statistics for $host"
|
|
[ "$(echo "$found" | j "print('$ROOT_USER' not in json.dumps(d['events']) and 'alice' not in json.dumps(d['events']))")" = "True" ] \
|
|
&& ok "statistics for $host name no account" || ko "statistics for $host name an account"
|
|
}
|