T9: the pasture as plugins
tools/pasture/run.sh up [peer...] | down | logs | ps and interop.sh [peer...] are now built from parts: - lib/pasture.sh: network, Caddy with its CA in a volume and copied to .ca/root.crt, Mongo, PrivaPub; - peers/<name>.sh: each peer's <name>_up; - lib/interop.sh: ok, ko, and xf for checks expected to fail until a later phase; privapub_token <persona>, which gives each peer its own persona under one root; and stats_check; - scenarios/<name>.sh: each peer's checks. GoToSocial is pinned at 0.22.1, the version the 33 checks were proven on. Its scenario gains four statistics checks: - the admin statistics describe gts.test as gotosocial; - they count inbound and outbound traffic; - they name no account. 37/37 passed three runs in a row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
1 parent
6ef7e2891a
commit
c301f0c498
7 files changed
+269
-177
No files matched your search
@@ -403,3 +403,5 @@ PrivaPub/media-store/
|
||||
PrivaPub/media-store-proxy/
|
||||
tools/pasture/.publish/
|
||||
.claude/worktrees/
|
||||
tools/pasture/.ca/
|
||||
tools/pasture/.state/
|
||||
+10
-140
@@ -1,145 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drives PrivaPub and GoToSocial (started by run.sh) through each other's federation, as their users would.
|
||||
# Drives PrivaPub and each peer (started by run.sh) through each other's federation, as their users would, then checks
|
||||
# that PrivaPub's statistics saw it all without naming anyone.
|
||||
# usage: tools/pasture/interop.sh [peer...] peers: gts (default)
|
||||
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; }
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
. "$here/lib/interop.sh"
|
||||
|
||||
# --- 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'<input type="hidden" name="([^"]*)" value="([^"]*)"',s)]
|
||||
print(urllib.parse.urlencode(pairs+[("avatarId",re.findall(r'name="avatarId" value="([^"]*)"',s)[0]),("decision","allow")]))
|
||||
PY
|
||||
)
|
||||
code=$(curl -s -c $work/pj -b $work/pj -X POST $P/oauth/authorize --data "$form" | grep -o '<code>[^<]*</code>' | 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; }
|
||||
for peer in "${@:-gts}"; do
|
||||
[ -f "$here/scenarios/$peer.sh" ] || { echo "no scenario for $peer" >&2; exit 2; }
|
||||
. "$here/scenarios/$peer.sh"
|
||||
done
|
||||
|
||||
# --- 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"
|
||||
echo; echo "$pass passed, $fail failed, $expected expected failures"
|
||||
[ "$fail" = 0 ]
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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}"; }
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Shared by run.sh: the network, Caddy (its CA kept in a volume and copied to .ca/root.crt), Mongo and PrivaPub.
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; repo="$(cd "$here/../.." && pwd)"
|
||||
net=privapub-pasture; publish="$here/.publish"; ca="$here/.ca"
|
||||
|
||||
wait_http() { # url tries
|
||||
for _ in $(seq 1 "${2:-60}"); do curl -fsk -o /dev/null "$1" && return 0; sleep 2; done
|
||||
echo "timed out waiting for $1" >&2; return 1
|
||||
}
|
||||
|
||||
pasture_base_up() {
|
||||
local dotnet=${DOTNET:-$(command -v dotnet || echo ~/.dotnet/dotnet)}
|
||||
"$dotnet" publish "$repo/PrivaPub/PrivaPub.csproj" -c Release -r linux-x64 --self-contained true -o "$publish" -v quiet
|
||||
cp "$here/appsettings.Pasture.json" "$publish/"
|
||||
podman network exists $net || podman network create $net >/dev/null
|
||||
podman volume exists pasture-caddy-data || podman volume create pasture-caddy-data >/dev/null
|
||||
podman run -d --replace --name pasture-mongo --network $net --network-alias mongo docker.io/library/mongo:8 --quiet >/dev/null
|
||||
local aliases=""
|
||||
for site in privapub gts mastodon akkoma misskey sharkey lemmy; do aliases="$aliases --network-alias $site.test"; done
|
||||
# shellcheck disable=SC2086
|
||||
podman run -d --replace --name pasture-caddy --network $net $aliases \
|
||||
-p 127.0.0.1:6443:443 --sysctl net.ipv4.ip_unprivileged_port_start=0 -v "$here/Caddyfile:/etc/caddy/Caddyfile:Z,ro" \
|
||||
-v pasture-caddy-data:/data docker.io/library/caddy:2 >/dev/null
|
||||
podman run -d --replace --name pasture-privapub --network $net -p 127.0.0.1:6971:80 \
|
||||
--sysctl net.ipv4.ip_unprivileged_port_start=0 -e ASPNETCORE_ENVIRONMENT=Pasture -w /app -v "$publish:/app:Z,ro" \
|
||||
mcr.microsoft.com/dotnet/runtime-deps:10.0 /app/PrivaPub >/dev/null
|
||||
wait_http http://127.0.0.1:6971/build.json
|
||||
mkdir -p "$ca"
|
||||
for _ in $(seq 1 30); do podman cp pasture-caddy:/data/caddy/pki/authorities/local/root.crt "$ca/root.crt" 2>/dev/null && break; sleep 1; done
|
||||
}
|
||||
|
||||
pasture_down() {
|
||||
podman ps -a --format '{{.Names}}' | grep '^pasture-' | xargs -r podman rm -f >/dev/null 2>&1 || true
|
||||
podman volume ls --format '{{.Name}}' | grep '^pasture-' | xargs -r podman volume rm -f >/dev/null 2>&1 || true
|
||||
podman network rm $net >/dev/null 2>&1 || true
|
||||
rm -rf "$here/.state"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# GoToSocial: sqlite, told to accept Caddy's certificate and to reach private addresses.
|
||||
GTS_IMAGE=docker.io/superseriousbusiness/gotosocial:0.22.1
|
||||
|
||||
gts_up() {
|
||||
podman run -d --replace --name pasture-gts --network $net -p 127.0.0.1:6972:80 \
|
||||
--sysctl net.ipv4.ip_unprivileged_port_start=0 \
|
||||
-e GTS_HOST=gts.test -e GTS_PROTOCOL=https -e GTS_PORT=80 -e GTS_BIND_ADDRESS=0.0.0.0 -e GTS_HTTP_CLIENT_TLS_INSECURE_SKIP_VERIFY=true \
|
||||
-e GTS_DB_TYPE=sqlite -e GTS_DB_ADDRESS=/gotosocial/storage/sqlite.db -e GTS_STORAGE_LOCAL_BASE_PATH=/gotosocial/storage \
|
||||
-e GTS_LETSENCRYPT_ENABLED=false -e GTS_HTTP_CLIENT_ALLOW_IPS=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 \
|
||||
-e GTS_TRUSTED_PROXIES=0.0.0.0/0 -e GTS_LOG_LEVEL=info -e GTS_INSTANCE_EXPOSE_PUBLIC_TIMELINE=true \
|
||||
$GTS_IMAGE >/dev/null
|
||||
wait_http http://127.0.0.1:6972/api/v1/instance
|
||||
podman exec pasture-gts /gotosocial/gotosocial admin account create --username gtsuser --email gtsuser@gts.test --password 'Gts-Pasture-Pass-1!' >/dev/null 2>&1 || true
|
||||
podman exec pasture-gts /gotosocial/gotosocial admin account confirm --username gtsuser >/dev/null 2>&1 || true
|
||||
podman restart pasture-gts >/dev/null
|
||||
wait_http http://127.0.0.1:6972/api/v1/instance
|
||||
echo "gotosocial: https://gts.test:6443"
|
||||
}
|
||||
+18
-37
@@ -1,44 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# A private test fediverse on one podman network: PrivaPub (privapub.test) and GoToSocial (gts.test) behind Caddy,
|
||||
# whose internal CA both sides are told to accept. From the workstation: PrivaPub's API at http://127.0.0.1:6971,
|
||||
# both sites at https://{privapub,gts}.test:6443 (curl --resolve ...:6443:127.0.0.1 -k).
|
||||
# usage: tools/pasture/run.sh up | down | logs <name>
|
||||
# A private test fediverse on one podman network: PrivaPub (privapub.test) and the peers asked for, behind one Caddy
|
||||
# whose internal CA every side is told to accept (its root is copied to .ca/root.crt). From the workstation: PrivaPub's
|
||||
# API at http://127.0.0.1:6971, every site at https://<name>.test:6443 (curl --resolve <name>.test:6443:127.0.0.1 -k).
|
||||
# usage: tools/pasture/run.sh up [peer...] | down | logs <name> | ps peers: gts (default)
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; repo="$(cd "$here/../.." && pwd)"
|
||||
net=privapub-pasture; publish="$here/.publish"
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pasture.sh"
|
||||
|
||||
case "${1:-up}" in
|
||||
up)
|
||||
dotnet=${DOTNET:-$(command -v dotnet || echo ~/.dotnet/dotnet)}
|
||||
"$dotnet" publish "$repo/PrivaPub/PrivaPub.csproj" -c Release -r linux-x64 --self-contained true -o "$publish" -v quiet
|
||||
cp "$here/appsettings.Pasture.json" "$publish/"
|
||||
podman network exists $net || podman network create $net >/dev/null
|
||||
podman run -d --replace --name pasture-mongo --network $net --network-alias mongo docker.io/library/mongo:8 --quiet >/dev/null
|
||||
podman run -d --replace --name pasture-caddy --network $net --network-alias privapub.test --network-alias gts.test \
|
||||
-p 127.0.0.1:6443:443 --sysctl net.ipv4.ip_unprivileged_port_start=0 -v "$here/Caddyfile:/etc/caddy/Caddyfile:Z,ro" \
|
||||
docker.io/library/caddy:2 >/dev/null
|
||||
podman run -d --replace --name pasture-privapub --network $net -p 127.0.0.1:6971:80 \
|
||||
--sysctl net.ipv4.ip_unprivileged_port_start=0 -e ASPNETCORE_ENVIRONMENT=Pasture -w /app -v "$publish:/app:Z,ro" \
|
||||
mcr.microsoft.com/dotnet/runtime-deps:10.0 /app/PrivaPub >/dev/null
|
||||
podman run -d --replace --name pasture-gts --network $net -p 127.0.0.1:6972:80 \
|
||||
--sysctl net.ipv4.ip_unprivileged_port_start=0 \
|
||||
-e GTS_HOST=gts.test -e GTS_PROTOCOL=https -e GTS_PORT=80 -e GTS_BIND_ADDRESS=0.0.0.0 -e GTS_HTTP_CLIENT_TLS_INSECURE_SKIP_VERIFY=true \
|
||||
-e GTS_DB_TYPE=sqlite -e GTS_DB_ADDRESS=/gotosocial/storage/sqlite.db -e GTS_STORAGE_LOCAL_BASE_PATH=/gotosocial/storage \
|
||||
-e GTS_LETSENCRYPT_ENABLED=false -e GTS_HTTP_CLIENT_ALLOW_IPS=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 \
|
||||
-e GTS_TRUSTED_PROXIES=0.0.0.0/0 -e GTS_LOG_LEVEL=info -e GTS_INSTANCE_EXPOSE_PUBLIC_TIMELINE=true \
|
||||
docker.io/superseriousbusiness/gotosocial:latest >/dev/null
|
||||
for i in $(seq 1 60); do curl -fs -o /dev/null http://127.0.0.1:6971/build.json && curl -fs -o /dev/null http://127.0.0.1:6972/api/v1/instance && break; sleep 2; done
|
||||
podman exec pasture-gts /gotosocial/gotosocial admin account create --username gtsuser --email gtsuser@gts.test --password 'Gts-Pasture-Pass-1!' >/dev/null 2>&1 || true
|
||||
podman exec pasture-gts /gotosocial/gotosocial admin account confirm --username gtsuser >/dev/null 2>&1 || true
|
||||
podman restart pasture-gts >/dev/null
|
||||
for i in $(seq 1 60); do curl -fs -o /dev/null http://127.0.0.1:6972/api/v1/instance && break; sleep 2; done
|
||||
echo "privapub: http://127.0.0.1:6971, https://privapub.test:6443 gotosocial: https://gts.test:6443"
|
||||
;;
|
||||
down)
|
||||
podman rm -f pasture-caddy pasture-privapub pasture-gts pasture-mongo >/dev/null 2>&1 || true
|
||||
podman network rm $net >/dev/null 2>&1 || true
|
||||
;;
|
||||
logs)
|
||||
podman logs --tail 200 "pasture-${2:-privapub}"
|
||||
shift || true
|
||||
peers=("${@:-gts}")
|
||||
pasture_base_up
|
||||
for peer in "${peers[@]}"; do
|
||||
[ -f "$here/peers/$peer.sh" ] || { echo "no such peer: $peer" >&2; exit 1; }
|
||||
. "$here/peers/$peer.sh"
|
||||
"${peer}_up"
|
||||
done
|
||||
echo "privapub: http://127.0.0.1:6971, https://privapub.test:6443"
|
||||
;;
|
||||
down) pasture_down ;;
|
||||
logs) podman logs --tail 200 "pasture-${2:-privapub}" ;;
|
||||
ps) podman ps --filter name=pasture- --format '{{.Names}}\t{{.Status}}' ;;
|
||||
*) echo "usage: $0 up [peer...] | down | logs <name> | ps" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -0,0 +1,115 @@
|
||||
# GoToSocial 0.22: the original 33 checks, both sides driven through their Mastodon APIs.
|
||||
G=https://gts.test:6443
|
||||
gcurl() { curl -k --resolve gts.test:6443:127.0.0.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; }
|
||||
|
||||
echo "gotosocial"
|
||||
PT=$(privapub_token alice)
|
||||
PH="Authorization: Bearer $PT"
|
||||
[ -n "$PT" ] && ok "PrivaPub token for alice" || { ko "PrivaPub token"; return 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')"; return 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 "statistics"
|
||||
stats_check gts.test gotosocial
|
||||
Reference in new issue
Block a user