#!/usr/bin/env bash # Drives PrivaPub and GoToSocial (started by run.sh) through each other's federation, as their users would. set -uo pipefail P=http://127.0.0.1:6971; G=https://gts.test:6443 gcurl() { curl -k --resolve gts.test:6443:127.0.0.1 "$@"; } work=$(mktemp -d); trap 'rm -rf "$work"' EXIT pass=0; fail=0 ok() { echo " ok $*"; pass=$((pass+1)); } ko() { echo " FAIL $*"; fail=$((fail+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 i in $(seq 1 "$tries"); do if eval "$@"; then return 0; fi; sleep 2; done; return 1; } # GoToSocial's cached home timeline can stop taking new posts after its first read, so a delivery is checked by looking # the object up by URI with resolve=false: that answers from GoToSocial's database and never fetches from us. on_gts() { gcurl -s -G -H "$GH" "$G/api/v2/search" --data-urlencode "q=$1" -d resolve=false -d type=statuses; } # --- PrivaPub persona and token root=$(curl -s -X POST $P/clientapi/user/signup -H 'Content-Type: application/json' -d '{"userName":"pastureroot","password":"Pasture-Pass-1!"}') jwt=$(echo "$root" | j "print(d['token'])"); rootid=$(echo "$root" | j "print(d['userId'])") if [ -z "$jwt" ]; then jwt=$(curl -s -X POST $P/clientapi/user/login -H 'Content-Type: application/json' -d '{"userName":"pastureroot","password":"Pasture-Pass-1!"}' | j "print(d['token'])"); rootid=$(curl -s -X POST $P/clientapi/user/login -H 'Content-Type: application/json' -d '{"userName":"pastureroot","password":"Pasture-Pass-1!"}' | j "print(d['userId'])"); fi curl -s -o /dev/null -X POST $P/clientapi/avatar/private/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \ -d "{\"rootId\":\"$rootid\",\"userName\":\"alice\",\"name\":\"Alice of PrivaPub\",\"biography\":\"testing federation\"}" 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" xt=$(curl -s -c $work/pj -b $work/pj "$P/oauth/login?returnUrl=/oauth/authorize?$q" | grep -o 'name="__RequestVerificationToken" type="hidden" value="[^"]*"' | sed 's/.*value="//;s/"//') curl -s -o /dev/null -c $work/pj -b $work/pj -X POST $P/oauth/login --data-urlencode "returnUrl=/oauth/authorize?$q" --data-urlencode "__RequestVerificationToken=$xt" -d 'userName=pastureroot&password=Pasture-Pass-1!' curl -s -c $work/pj -b $work/pj "$P/oauth/authorize?$q&signed_in=1" > $work/choose.html form=$(python3 - "$work/choose.html" <<'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'[^<]*' | sed 's/<[^>]*>//g') PT=$(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'])") PH="Authorization: Bearer $PT" [ -n "$PT" ] && ok "PrivaPub token for alice" || { ko "PrivaPub token"; exit 1; } # --- GoToSocial token gapp=$(gcurl -s -X POST $G/api/v1/apps -d 'client_name=pasture&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=read+write+follow') gid=$(echo "$gapp" | j "print(d['client_id'])"); gs=$(echo "$gapp" | j "print(d['client_secret'])") gcurl -s -o /dev/null -c $work/gj -b $work/gj "$G/oauth/authorize?client_id=$gid&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=read+write+follow" gcurl -s -o /dev/null -c $work/gj -b $work/gj -X POST $G/auth/sign_in --data-urlencode 'username=gtsuser@gts.test' --data-urlencode 'password=Gts-Pasture-Pass-1!' gcode=$(gcurl -s -o /dev/null -w '%{redirect_url}' -c $work/gj -b $work/gj -X POST "$G/oauth/authorize" | sed -n 's/.*[?&]code=\([^&]*\).*/\1/p') GT=$(gcurl -s -X POST $G/oauth/token -d "grant_type=authorization_code&code=$gcode&client_id=$gid&client_secret=$gs&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=read+write+follow" | j "print(d['access_token'])") GH="Authorization: Bearer $GT" [ -n "$GT" ] && ok "GoToSocial token for gtsuser" || { ko "GoToSocial token (code '$gcode')"; exit 1; } echo "discovery" alice_on_gts=$(gcurl -s -H "$GH" "$G/api/v2/search?q=@alice@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])") [ -n "$alice_on_gts" ] && ok "GoToSocial resolves @alice@privapub.test" || ko "GoToSocial cannot resolve alice" gts_on_pp=$(curl -s -H "$PH" "$P/api/v2/search?q=gtsuser@gts.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])") [ -n "$gts_on_pp" ] && ok "PrivaPub resolves @gtsuser@gts.test" || ko "PrivaPub cannot resolve gtsuser" echo "follows" gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/accounts/$alice_on_gts/follow until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$alice_on_gts" | j "print(d[0][\"following\"])")" = "True" ]' && ok "gtsuser follows alice (Accept arrived)" || ko "gtsuser's follow of alice not accepted" curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/accounts/$gts_on_pp/follow until_true 10 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$gts_on_pp" | j "print(d[0][\"requested\"])")" = "True" ]' && ok "alice's follow of locked gtsuser waits as a request" || ko "alice's follow is not shown as requested" until_true 20 'gcurl -s -H "$GH" "$G/api/v1/follow_requests" | j "print(any(a[\"id\"]==\"$alice_on_gts\" for a in d))" | grep -q True' && ok "the request reaches gtsuser's follow requests" || ko "follow request missing on GoToSocial" gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/follow_requests/$alice_on_gts/authorize until_true 20 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$gts_on_pp" | j "print(d[0][\"following\"])")" = "True" ]' && ok "alice follows gtsuser (Accept arrived)" || ko "alice's follow of gtsuser not accepted" echo "posts" pp_status=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d 'status=Hello from PrivaPub #pasture&visibility=public') pp_post=$(echo "$pp_status" | j "print(d['id'])"); pp_uri=$(echo "$pp_status" | j "print(d['uri'])") until_true 20 'on_gts "$pp_uri" | j "print(len(d[\"statuses\"]))" | grep -q 1' && ok "alice's post reaches GoToSocial" || ko "alice's post missing on GoToSocial" gts_post=$(gcurl -s -X POST -H "$GH" $G/api/v1/statuses -d 'status=Hello from GoToSocial&visibility=public' | j "print(d['id'])") until_true 20 'curl -s -H "$PH" "$P/api/v1/timelines/home" | grep -q "Hello from GoToSocial"' && ok "gtsuser's post reaches alice's home" || ko "gtsuser's post missing on PrivaPub" [ "$(curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(next(s['edited_at'] for s in d if 'Hello from GoToSocial' in s['content']))")" = "None" ] && ok "an unedited remote post carries no edited_at" || ko "unedited remote post reports an edit" cw_status=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d 'status=behind a warning&spoiler_text=spoilers&visibility=public') cw=$(echo "$cw_status" | j "print(d['id'])"); cw_uri=$(echo "$cw_status" | j "print(d['uri'])") until_true 20 'on_gts "$cw_uri" | j "print(any(s[\"spoiler_text\"]==\"spoilers\" and s[\"sensitive\"] for s in d[\"statuses\"]))" | grep -q True' && ok "content warning survives to GoToSocial" || ko "content warning lost" cw_on_gts=$(on_gts "$cw_uri" | j "print(d['statuses'][0]['id'])") echo "replies and mentions" pp_on_gts=$(on_gts "$pp_uri" | j "print(d['statuses'][0]['id'])") gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses -d "status=@alice@privapub.test nice to meet you&in_reply_to_id=$pp_on_gts&visibility=public" until_true 20 'curl -s -H "$PH" "$P/api/v1/notifications" | j "print(any(n[\"type\"]==\"mention\" for n in d))" | grep -q True' && ok "gtsuser's reply notifies alice" || ko "reply did not notify alice" until_true 10 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$pp_post/context" | j "print(len(d[\"descendants\"]))")" -ge 1 ]' && ok "the reply threads under alice's post" || ko "reply not threaded" echo "likes and boosts" gts_on_pp_post=$(curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(next(s['id'] for s in d if 'Hello from GoToSocial' in s['content']))") curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses/$gts_on_pp_post/favourite curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses/$gts_on_pp_post/reblog until_true 20 'gcurl -s -H "$GH" "$G/api/v1/notifications" | j "print(any(n[\"type\"]==\"favourite\" for n in d))" | grep -q True' && ok "alice's like reaches GoToSocial" || ko "like not received" until_true 20 'gcurl -s -H "$GH" "$G/api/v1/notifications" | j "print(any(n[\"type\"]==\"reblog\" for n in d))" | grep -q True' && ok "alice's boost reaches GoToSocial" || ko "boost not received" gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses/$pp_on_gts/favourite gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses/$pp_on_gts/reblog until_true 20 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$pp_post" | j "print(d[\"favourites_count\"])")" = "1" ]' && ok "gtsuser's like counts on PrivaPub" || ko "like not counted" until_true 20 'curl -s -H "$PH" "$P/api/v1/notifications" | j "print(any(n[\"type\"]==\"reblog\" for n in d))" | grep -q True' && ok "gtsuser's boost notifies alice" || ko "boost not notified" echo "direct messages" curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d 'status=@gtsuser@gts.test a secret&visibility=direct' until_true 20 'gcurl -s -H "$GH" "$G/api/v1/conversations" | grep -q "a secret"' && ok "alice's DM reaches gtsuser" || ko "DM missing on GoToSocial" gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses -d 'status=@alice@privapub.test a secret back&visibility=direct' until_true 20 'curl -s -H "$PH" "$P/api/v1/conversations" | grep -q "a secret back"' && ok "gtsuser's DM reaches alice" || ko "DM missing on PrivaPub" until_true 5 '! gcurl -s -H "$GH" "$G/api/v1/timelines/public?local=false" | grep -q "a secret"' && ok "the DM is not public on GoToSocial" || ko "DM leaked to a public timeline" echo "polls" pp_poll=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d 'status=tea or coffee&visibility=public&poll[options][]=tea&poll[options][]=coffee&poll[expires_in]=3600' | j "print(d['uri'])") until_true 20 'on_gts "$pp_poll" | j "print(len(d[\"statuses\"][0][\"poll\"][\"options\"]))" | grep -q 2' && ok "alice's poll reaches GoToSocial as a poll" || ko "poll missing on GoToSocial" pp_poll_on_gts=$(on_gts "$pp_poll" | j "print(d['statuses'][0]['poll']['id'])") gcurl -s -o /dev/null -X POST -H "$GH" "$G/api/v1/polls/$pp_poll_on_gts/votes" -d 'choices[]=1' pp_poll_id=$(curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(next(s['id'] for s in d if s['poll'] and s['uri']=='$pp_poll'))") until_true 20 '[ "$(curl -s -H "$PH" "$P/api/v1/polls/$pp_poll_id" | j "print(d[\"options\"][1][\"votes_count\"])")" = "1" ]' && ok "gtsuser's vote counts on PrivaPub" || ko "vote not counted on PrivaPub" gts_poll=$(gcurl -s -X POST -H "$GH" $G/api/v1/statuses -d 'status=red or blue&visibility=public&poll[options][]=red&poll[options][]=blue&poll[expires_in]=3600' | j "print(d['id'])") until_true 20 'curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(any(s[\"poll\"] and s[\"content\"].find(\"red or blue\")>=0 for s in d))" | grep -q True' && ok "gtsuser's poll reaches alice as a poll" || ko "poll missing on PrivaPub" gts_poll_on_pp=$(curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(next(s['poll']['id'] for s in d if s['poll'] and 'red or blue' in s['content']))") curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/polls/$gts_poll_on_pp/votes" -d 'choices[]=0' until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/statuses/$gts_poll" | j "print(d[\"poll\"][\"options\"][0][\"votes_count\"])")" = "1" ]' && ok "alice's vote counts on GoToSocial" || ko "vote not counted on GoToSocial" echo "quotes" quote_target=$(curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(next(s['id'] for s in d if 'red or blue' in s['content']))") [ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$PH" $P/api/v1/statuses -d "status=quoting"ed_status_id=$quote_target")" = "422" ] \ && ok "GoToSocial's author-only quote policy is respected" || ko "quoted a post whose author forbids it" echo "link previews" linked=$(curl -s -X POST -H "$PH" $P/api/v1/statuses --data-urlencode 'status=have a look https://gts.test/@gtsuser' -d 'visibility=public' | j "print(d['id'])") until_true 45 '[ -n "$(curl -s -H "$PH" "$P/api/v1/statuses/$linked" | j "print((d[\"card\"] or {}).get(\"title\") or \"\")")" ]' && ok "the server builds a card for a linked page" || ko "no card for the linked page" echo "edits and deletes" curl -s -o /dev/null -X PUT -H "$PH" $P/api/v1/statuses/$pp_post -d 'status=Hello from PrivaPub, edited' until_true 20 'gcurl -s -H "$GH" "$G/api/v1/statuses/$pp_on_gts" | grep -q "edited"' && ok "alice's edit reaches GoToSocial" || ko "edit not applied" curl -s -o /dev/null -X DELETE -H "$PH" $P/api/v1/statuses/$cw [ -n "$cw_on_gts" ] && until_true 20 '[ "$(gcurl -s -o /dev/null -w "%{http_code}" -H "$GH" "$G/api/v1/statuses/$cw_on_gts")" = "404" ]' && ok "alice's delete reaches GoToSocial" || ko "delete not applied" gcurl -s -o /dev/null -X DELETE -H "$GH" $G/api/v1/statuses/$gts_post until_true 20 '! curl -s -H "$PH" "$P/api/v1/timelines/home" | grep -q "Hello from GoToSocial"' && ok "gtsuser's delete reaches PrivaPub" || ko "remote delete not applied" echo "unfollow" curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/accounts/$gts_on_pp/unfollow until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$alice_on_gts" | j "print(d[0][\"followed_by\"])")" = "False" ]' && ok "alice's unfollow reaches GoToSocial" || ko "unfollow not applied" echo "blocks" curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/accounts/$gts_on_pp/block until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$alice_on_gts" | j "print(d[0][\"blocked_by\"])")" = "True" ]' && ok "alice's block reaches GoToSocial" || ko "block not applied" curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/accounts/$gts_on_pp/unblock until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$alice_on_gts" | j "print(d[0][\"blocked_by\"])")" = "False" ]' && ok "alice's unblock reaches GoToSocial" || ko "unblock not applied" echo; echo "$pass passed, $fail failed" [ "$fail" = 0 ]