Owner decision 2026-10-04: fix the account privacy findings.
- Sign-in. Every failure answers "That username and password do not match." after the same work: an unknown login
is hashed against a decoy, and the comparison is constant-time. "Banned" is told only to someone who gave the right
password. This covers /clientapi/user/login, /invitation/login and /oauth/login.
- Recovery.
- Every request answers the same sentence and queues a SendRecovery job, whether or not the account exists or has an
email. The lookup, the code and SMTP move to RecoveryJob, so neither the answer nor its timing says anything.
- Codes are kept only as a SHA-256 hash, for one hour. Migration _011 drops the plaintext ones, which never expired.
- A recovered password ends every session of the root. RootSessions sets CredentialsChangedAt, which JwtEvents
checks against the JWT's issue time, now stamped as nbf, and revokes each persona's OAuth tokens and authorizations.
- Deleting a root (RootRemoval: the admin route, or the restored self-delete at /clientapi/user/delete, which asks for
the password).
- Its sessions end.
- Each persona and each group it owns sends Delete{Actor} to its followers, its members and the accounts it follows.
- The personas' posts are emptied.
- /peasants/{name} answers 410 with a Tombstone (formerType Person or Group), as do its inbox and WebFinger, through
LocalActorService.Gone. The names stay reserved.
- The root keeps only a unique `deleted-{id}` name; the second deletion on an instance used to collide on
"Deleted user".
Also, from phase 2's pasture: GoToSocial files a circle post like a DM and shows it only to accounts it mentions. Each
member's copy, and a member's refetch, now also mentions that member silently. The GoToSocial scenario checks circle
posts in conversations, like DMs, and they pass there now, as on Mastodon.
657 tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
206 lines
22 KiB
Bash
206 lines
22 KiB
Bash
# 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 "more replies, edits and undos"
|
|
gts_edit=$(gcurl -s -X POST -H "$GH" $G/api/v1/statuses -d 'status=GoToSocial before the edit&visibility=public' | j "print(d['id'])")
|
|
until_true 20 'curl -s -H "$PH" "$P/api/v1/timelines/home" | grep -q "GoToSocial before the edit"' || true
|
|
gts_edit_on_pp=$(curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(next(s['id'] for s in d if 'GoToSocial before the edit' in s['content']))")
|
|
pp_reply=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d "status=@gtsuser@gts.test answering from PrivaPub&in_reply_to_id=$gts_edit_on_pp&visibility=public" | j "print(d['uri'])")
|
|
until_true 20 '[ "$(on_gts "$pp_reply" | j "print(d[\"statuses\"][0][\"in_reply_to_id\"] == \"$gts_edit\")")" = "True" ]' && ok "alice's reply threads under gtsuser's post" || ko "outbound reply not threaded on GoToSocial"
|
|
gcurl -s -o /dev/null -X PUT -H "$GH" $G/api/v1/statuses/$gts_edit -d 'status=GoToSocial after the edit'
|
|
until_true 20 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$gts_edit_on_pp/history" | j "print(len(d))")" = "2" ]' && ok "gtsuser's edit reaches PrivaPub with its history" || ko "GoToSocial's edit not applied"
|
|
[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$gts_edit_on_pp" | j "print(d['edited_at'] is not None)")" = "True" ] && ok "the edited post carries edited_at" || ko "no edited_at on the edited post"
|
|
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses/$gts_edit_on_pp/favourite
|
|
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses/$gts_edit_on_pp/reblog
|
|
until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/statuses/$gts_edit" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "1 1" ]' || true
|
|
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses/$gts_edit_on_pp/unfavourite
|
|
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses/$gts_edit_on_pp/unreblog
|
|
until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/statuses/$gts_edit" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "0 0" ]' && ok "alice's unlike and unboost reach GoToSocial" || ko "undo of like or boost not applied on GoToSocial"
|
|
pp_undo=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d 'status=like me and take it back&visibility=public' | j "print(d['id'] + ' ' + d['uri'])")
|
|
pp_undo_id=${pp_undo% *}; pp_undo_uri=${pp_undo#* }
|
|
until_true 20 '[ -n "$(on_gts "$pp_undo_uri" | j "print(d[\"statuses\"][0][\"id\"])")" ]' || true
|
|
pp_undo_on_gts=$(on_gts "$pp_undo_uri" | j "print(d['statuses'][0]['id'])")
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses/$pp_undo_on_gts/favourite
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses/$pp_undo_on_gts/reblog
|
|
until_true 20 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$pp_undo_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "1 1" ]' || true
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses/$pp_undo_on_gts/unfavourite
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses/$pp_undo_on_gts/unreblog
|
|
until_true 20 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$pp_undo_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "0 0" ]' && ok "gtsuser's unlike and unboost reach PrivaPub" || ko "GoToSocial's undo of like or boost not applied"
|
|
|
|
echo "media"
|
|
make_png "$work/red.png"
|
|
pp_media=$(curl -s -X POST -H "$PH" "$P/api/v2/media" -F "file=@$work/red.png;type=image/png" -F 'description=a red square' | j "print(d['id'])")
|
|
pp_media_uri=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d "status=a picture for GoToSocial&visibility=public&media_ids[]=$pp_media" | j "print(d['uri'])")
|
|
until_true 20 '[ "$(on_gts "$pp_media_uri" | j "print(d[\"statuses\"][0][\"media_attachments\"][0][\"description\"])")" = "a red square" ]' && ok "an image with alt text reaches GoToSocial" || ko "image or alt text missing on GoToSocial"
|
|
g_media=$(gcurl -s -X POST -H "$GH" "$G/api/v2/media" -F "file=@$work/red.png;type=image/png" -F 'description=a red square from GoToSocial' | j "print(d['id'])")
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/statuses -d "status=a picture from GoToSocial&visibility=public&media_ids[]=$g_media"
|
|
until_true 20 'curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(any(\"a picture from GoToSocial\" in s[\"content\"] and s[\"media_attachments\"] and \"/media/proxy/\" in s[\"media_attachments\"][0][\"url\"] and s[\"media_attachments\"][0][\"description\"]==\"a red square from GoToSocial\" for s in d))" | grep -q True' \
|
|
&& ok "an image with alt text from GoToSocial arrives through our proxy" || ko "GoToSocial's image missing, unproxied or without alt text"
|
|
|
|
echo "communities"
|
|
jwt=$(privapub_root)
|
|
alice_id=$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
|
|
community_name="community$(date +%s)"
|
|
community=$(curl -s -X POST $P/clientapi/group/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"userName\":\"$community_name\",\"name\":\"a community\",\"description\":\"for everyone\",\"isCommunity\":true}" | j "print(d['id'])")
|
|
community_on_gts=$(gcurl -s -H "$GH" "$G/api/v2/search?q=@$community_name@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/accounts/$community_on_gts/follow
|
|
until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$community_on_gts" | j "print(d[0][\"following\"])")" = "True" ]' && ok "gtsuser follows a PrivaPub community" || ko "community follow not accepted"
|
|
curl -s -o /dev/null -X POST $P/clientapi/post/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"title\":\"A community thread\",\"text\":\"posted into the community\",\"groupId\":\"$community\"}"
|
|
community_post=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'print(db.Post.findOne({Text:/posted into the community/}).ObjectURI)')
|
|
until_true 20 '[ "$(on_gts "$community_post" | j "print(len(d[\"statuses\"]))")" = "1" ]' && ok "the community's announce brings its post to GoToSocial" || ko "community post missing on GoToSocial"
|
|
|
|
echo "circles"
|
|
circle_name="circle$(date +%s)"
|
|
circle=$(curl -s -X POST $P/clientapi/group/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"userName\":\"$circle_name\",\"name\":\"a circle\",\"description\":\"just us\",\"isCommunity\":false}" | j "print(d['id'])")
|
|
circle_on_gts=$(gcurl -s -H "$GH" "$G/api/v2/search?q=@$circle_name@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/accounts/$circle_on_gts/follow
|
|
until_true 15 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$circle_on_gts" | j "print(d[0][\"requested\"])")" = "True" ]' && ok "gtsuser's request to join a circle waits for its owner" || ko "joining the circle was not held for approval"
|
|
requester=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval "print(db.Follower.findOne({LocalActorId:'$circle', IsAccepted:false}).ActorURI)")
|
|
curl -s -o /dev/null -X POST $P/clientapi/group/approve -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"groupId\":\"$circle\",\"memberActorURI\":\"$requester\"}"
|
|
until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$circle_on_gts" | j "print(d[0][\"following\"])")" = "True" ]' && ok "the owner's approval makes gtsuser a circle member" || ko "circle approval did not reach GoToSocial"
|
|
curl -s -o /dev/null -X POST $P/clientapi/post/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"text\":\"only the circle on GoToSocial sees this\",\"groupId\":\"$circle\"}"
|
|
circle_uri=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'print(db.Post.findOne({Text:/only the circle on GoToSocial/}).ObjectURI)')
|
|
# GoToSocial files a post for neither the public nor the author's followers as a direct message (as it does our DMs),
|
|
# shown only to the accounts it mentions; so each member's copy names and silently mentions that member. Like a DM it
|
|
# is then in gtsuser's conversations, never in a search by URI.
|
|
until_true 15 'gcurl -s -H "$GH" "$G/api/v1/conversations" | grep -q "only the circle on GoToSocial sees this"' \
|
|
&& ok "a circle post, naming its member, reaches its GoToSocial member" || ko "circle post missing on GoToSocial"
|
|
[ "$(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "$circle_uri")" = "404" ] && ok "the circle post is not served unsigned" || ko "circle post served unsigned"
|
|
|
|
echo "locked personas"
|
|
LT=$(privapub_token locked_alice); LH="Authorization: Bearer $LT"
|
|
curl -s -o /dev/null -X PATCH -H "$LH" "$P/api/v1/accounts/update_credentials" -d 'locked=true'
|
|
locked_on_gts=$(gcurl -s -H "$GH" "$G/api/v2/search?q=@locked_alice@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/accounts/$locked_on_gts/follow
|
|
until_true 20 '[ "$(curl -s -H "$LH" "$P/api/v1/follow_requests" | j "print(len(d))")" = "1" ]' && ok "gtsuser's follow waits on a locked persona" || ko "no follow request on the locked persona"
|
|
[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$locked_on_gts" | j "print(d[0][\"requested\"])")" = "True" ] && ok "GoToSocial shows the follow as requested" || ko "GoToSocial does not show the request"
|
|
requester=$(curl -s -H "$LH" "$P/api/v1/follow_requests" | j "print(d[0]['id'])")
|
|
curl -s -o /dev/null -X POST -H "$LH" "$P/api/v1/follow_requests/$requester/reject"
|
|
until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$locked_on_gts" | j "print(d[0][\"requested\"] or d[0][\"following\"])")" = "False" ]' && ok "a rejected request is withdrawn on GoToSocial" || ko "rejection not applied on GoToSocial"
|
|
gcurl -s -o /dev/null -X POST -H "$GH" $G/api/v1/accounts/$locked_on_gts/follow
|
|
until_true 20 '[ "$(curl -s -H "$LH" "$P/api/v1/follow_requests" | j "print(len(d))")" = "1" ]' || true
|
|
requester=$(curl -s -H "$LH" "$P/api/v1/follow_requests" | j "print(d[0]['id'])")
|
|
curl -s -o /dev/null -X POST -H "$LH" "$P/api/v1/follow_requests/$requester/authorize"
|
|
until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$locked_on_gts" | j "print(d[0][\"following\"])")" = "True" ]' && ok "an authorized request makes gtsuser a follower" || ko "authorization not applied on GoToSocial"
|
|
|
|
echo "smoke"
|
|
"$here/../smoke/mastodon-api.sh" "$P" "$PT" >/dev/null 2>&1 && ok "the deploy's Mastodon smoke check passes, signed in" || ko "the Mastodon smoke check fails"
|
|
|
|
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
|