The GoToSocial scenario gains 17 checks: - alice's reply threads under gtsuser's post; - gtsuser's edit arrives with edited_at and two history entries; - unlike and unboost both ways; - images with alt text both ways, theirs through our proxy; - gtsuser follows a PrivaPub community and its announce brings the post; - gtsuser's request to join a circle waits for the owner and is approved, and the circle post is never served unsigned; - a locked persona holds gtsuser's follow, rejects it, then authorizes it; - the deploy's Mastodon smoke check passes, signed in. 54 checks passed, three runs in a row. The circle post reaching gtsuser is an expected failure: GoToSocial keeps no post addressed only to a collection it does not know. It found a bug. An edit made within the second the post was published carries GoToSocial's whole-second updated == published, and IsEdit wanted strictly newer, so the edit was taken as a refresh and lost. A first edit now also counts when it is no older and the text, warning or title actually changed. A bare refresh still never makes a revision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
81 lines
5.4 KiB
Bash
81 lines
5.4 KiB
Bash
# Shared by interop.sh and the scenarios: check helpers, PrivaPub personas and tokens, the statistics check.
|
|
P=http://127.0.0.1:6971
|
|
work=$(mktemp -d); trap 'rm -rf "$work"' EXIT
|
|
pass=0; fail=0; expected=0
|
|
ok() { echo " ok $*"; pass=$((pass+1)); }
|
|
ko() { echo " FAIL $*"; fail=$((fail+1)); }
|
|
xf() { echo " xf $* (expected to fail until a later phase)"; expected=$((expected+1)); }
|
|
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
|
|
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.
|
|
privapub_token() {
|
|
local persona=$1 jwt cid cs q xt form code
|
|
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\"}"
|
|
local app; app=$(curl -s -X POST $P/api/v1/apps -d 'client_name=pasture&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=read+write+follow')
|
|
cid=$(echo "$app" | j "print(d['client_id'])"); cs=$(echo "$app" | j "print(d['client_secret'])")
|
|
q="client_id=$cid&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=read+write+follow"
|
|
local jar="$work/jar-$persona"
|
|
xt=$(curl -s -c "$jar" -b "$jar" "$P/oauth/login?returnUrl=/oauth/authorize?$q" | grep -o 'name="__RequestVerificationToken" type="hidden" value="[^"]*"' | sed 's/.*value="//;s/"//')
|
|
curl -s -o /dev/null -c "$jar" -b "$jar" -X POST $P/oauth/login --data-urlencode "returnUrl=/oauth/authorize?$q" --data-urlencode "__RequestVerificationToken=$xt" \
|
|
--data-urlencode "userName=$ROOT_USER" --data-urlencode "password=$ROOT_PASS"
|
|
curl -s -c "$jar" -b "$jar" "$P/oauth/authorize?$q&signed_in=1" > "$work/choose-$persona.html"
|
|
form=$(python3 - "$work/choose-$persona.html" "$persona" <<'PY'
|
|
import re,sys,urllib.parse,html
|
|
s=open(sys.argv[1]).read()
|
|
pairs=[(k,html.unescape(v)) for k,v in re.findall(r'<input type="hidden" name="([^"]*)" value="([^"]*)"',s)]
|
|
blocks=re.findall(r'<label[^>]*>(.*?)</label>',s,re.S)
|
|
avatar=None
|
|
for b in blocks:
|
|
if '@'+sys.argv[2]+'@' in b or '@'+sys.argv[2]+'<' in b or '>'+sys.argv[2]+'<' in b:
|
|
m=re.search(r'name="avatarId" value="([^"]*)"',b)
|
|
if m: avatar=m.group(1)
|
|
if avatar is None:
|
|
avatar=re.findall(r'name="avatarId" value="([^"]*)"',s)[0]
|
|
print(urllib.parse.urlencode(pairs+[("avatarId",avatar),("decision","allow")]))
|
|
PY
|
|
)
|
|
code=$(curl -s -c "$jar" -b "$jar" -X POST $P/oauth/authorize --data "$form" | grep -o '<code>[^<]*</code>' | sed 's/<[^>]*>//g')
|
|
curl -s -X POST $P/oauth/token -d "grant_type=authorization_code&code=$code&client_id=$cid&client_secret=$cs&redirect_uri=urn:ietf:wg:oauth:2.0:oob" | j "print(d['access_token'])"
|
|
}
|
|
|
|
# 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"
|
|
}
|