until_true 30'[ "$(m_status_by_uri "$alice_on_m" "$a_post_uri")" != "null" ]'&& ok "alice_masto's post reaches Mastodon"|| ko "post missing on Mastodon"
m_post=$(mcurl -X POST -H "$MH""$M/api/v1/statuses" -d 'status=Hello PrivaPub from Mastodon&visibility=public'| j "print(d['id'])")
until_true 30'curl -s -H "$AH" "$P/api/v1/timelines/home" | grep -q "Hello PrivaPub from Mastodon"'&& ok "mastouser's post reaches alice_masto's home"|| ko "Mastodon's post missing on PrivaPub"
m_post_on_p=$(curl -s -H "$AH""$P/api/v1/timelines/home"| j "print(next(s['id'] for s in d if 'Hello PrivaPub from Mastodon' in s['content']))")
cw_uri=$(curl -s -X POST -H "$AH"$P/api/v1/statuses -d 'status=behind a warning&spoiler_text=spoilers&visibility=public'| j "print(d['uri'])")
until_true 30'[ "$(m_status_by_uri "$alice_on_m" "$cw_uri" | j "print(d and d[\"spoiler_text\"]==\"spoilers\" and d[\"sensitive\"])")" = "True" ]'&& ok "a content warning survives to Mastodon"|| ko "content warning lost on Mastodon"
fo_uri=$(curl -s -X POST -H "$AH"$P/api/v1/statuses -d 'status=only for followers&visibility=private'| j "print(d['uri'])")
until_true 30'[ "$(m_status_by_uri "$alice_on_m" "$fo_uri" | j "print(d and d[\"visibility\"])")" = "private" ]'&& ok "a followers-only post reaches Mastodon as private"|| ko "followers-only post missing or widened on Mastodon"
["$(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json'"$fo_uri")"="404"]&& ok "the followers-only post is not served unsigned"|| ko "followers-only post served unsigned"
echo" replies"
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/statuses" -d "status=@alice_masto@privapub.test replying from Mastodon&in_reply_to_id=$a_post_on_m&visibility=public"
until_true 30'curl -s -H "$AH" "$P/api/v1/notifications" | j "print(any(n[\"type\"]==\"mention\" for n in d))" | grep -q True'&& ok "mastouser's reply notifies alice_masto"|| ko "reply did not notify"
until_true 15'[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$a_post_id/context" | j "print(len(d[\"descendants\"]))")" -ge 1 ]'&& ok "the reply threads under alice_masto's post"|| ko "reply not threaded on PrivaPub"
curl -s -o /dev/null -X POST -H "$AH"$P/api/v1/statuses -d "status=@mastouser@mastodon.test replying from PrivaPub&in_reply_to_id=$m_post_on_p&visibility=public"
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_post/context" | j "print(len(d[\"descendants\"]))")" -ge 1 ]'&& ok "alice_masto's reply threads under mastouser's post"|| ko "outbound reply not threaded on Mastodon"
echo" likes and boosts"
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/statuses/$m_post_on_p/favourite"
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/statuses/$m_post_on_p/reblog"
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_post" | j "print(d[\"favourites_count\"])")" = "1" ]'&& ok "alice_masto's like counts on Mastodon"|| ko "like not counted on Mastodon"
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_post" | j "print(d[\"reblogs_count\"])")" = "1" ]'&& ok "alice_masto's boost counts on Mastodon"|| ko "boost not counted on Mastodon"
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/statuses/$m_post_on_p/unfavourite"
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/statuses/$m_post_on_p/unreblog"
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_post" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "0 0" ]'&& ok "alice_masto's unlike and unboost reach Mastodon"|| ko "undo of like or boost not applied on Mastodon"
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/statuses/$a_post_on_m/favourite"
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/statuses/$a_post_on_m/reblog"
until_true 30'[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$a_post_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "1 1" ]'&& ok "mastouser's like and boost count on PrivaPub"|| ko "like or boost not counted on PrivaPub"
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/statuses/$a_post_on_m/unfavourite"
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/statuses/$a_post_on_m/unreblog"
until_true 30'[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$a_post_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "0 0" ]'&& ok "mastouser's unlike and unboost reach PrivaPub"|| ko "undo of like or boost not applied on PrivaPub"
echo" direct messages"
curl -s -o /dev/null -X POST -H "$AH"$P/api/v1/statuses -d 'status=@mastouser@mastodon.test a secret for Mastodon&visibility=direct'
until_true 30'mcurl -H "$MH" "$M/api/v1/conversations" | grep -q "a secret for Mastodon"'&& ok "alice_masto's DM reaches mastouser"|| ko "DM missing on Mastodon"
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/statuses" -d 'status=@alice_masto@privapub.test a secret for PrivaPub&visibility=direct'
until_true 30'curl -s -H "$AH" "$P/api/v1/conversations" | grep -q "a secret for PrivaPub"'&& ok "mastouser's DM reaches alice_masto"|| ko "DM missing on PrivaPub"
! curl -s "$P/api/v1/timelines/public"| grep -q "a secret for PrivaPub"&& ok "the DM is not on PrivaPub's public timeline"|| ko "DM leaked to the public timeline"
echo" polls"
a_poll_uri=$(curl -s -X POST -H "$AH"$P/api/v1/statuses -d 'status=cats or dogs&visibility=public&poll[options][]=cats&poll[options][]=dogs&poll[expires_in]=3600'| j "print(d['uri'])")
until_true 30'[ "$(m_status_by_uri "$alice_on_m" "$a_poll_uri" | j "print(len(d[\"poll\"][\"options\"]))")" = "2" ]'&& ok "alice_masto's poll reaches Mastodon as a poll"|| ko "poll missing on Mastodon"
until_true 30'[ "$(curl -s -H "$AH" "$P/api/v1/accounts/$a_poll_id/statuses" | j "print(next(s[\"poll\"][\"options\"][1][\"votes_count\"] for s in d if s[\"uri\"]==\"$a_poll_uri\"))")" = "1" ]'&& ok "mastouser's vote counts on PrivaPub"|| ko "vote not counted on PrivaPub"
m_poll=$(mcurl -X POST -H "$MH""$M/api/v1/statuses" -d 'status=tea or coffee, Mastodon asks&visibility=public&poll[options][]=tea&poll[options][]=coffee&poll[expires_in]=3600'| j "print(d['id'])")
until_true 30'curl -s -H "$AH" "$P/api/v1/timelines/home" | j "print(any(s[\"poll\"] and \"Mastodon asks\" in s[\"content\"] for s in d))" | grep -q True'&& ok "mastouser's poll reaches PrivaPub as a poll"|| ko "poll missing on PrivaPub"
m_poll_on_p=$(curl -s -H "$AH""$P/api/v1/timelines/home"| j "print(next(s['poll']['id'] for s in d if s['poll'] and 'Mastodon asks' in s['content']))")
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/polls/$m_poll_on_p/votes" -d 'choices[]=0'
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_poll" | j "print(d[\"poll\"][\"options\"][0][\"votes_count\"])")" = "1" ]'&& ok "alice_masto's vote counts on Mastodon"|| ko "vote not counted on Mastodon"
echo" quotes"
q_target=$(mcurl -X POST -H "$MH""$M/api/v1/statuses" -d 'status=quote me if you like&visibility=public'| j "print(d['id'])")
until_true 30'curl -s -H "$AH" "$P/api/v1/timelines/home" | grep -q "quote me if you like"'||true
q_target_on_p=$(curl -s -H "$AH""$P/api/v1/timelines/home"| j "print(next(s['id'] for s in d if 'quote me if you like' in s['content']))")
until_true 30'[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$a_quote" | j "print((d.get(\"quote\") or {}).get(\"state\"))")" = "accepted" ]'&& ok "Mastodon approves alice_masto's quote (FEP-044f)"|| ko "quote of a Mastodon post not approved"
m_quote=$(mcurl -X POST -H "$MH""$M/api/v1/statuses" -d "status=quoting PrivaPub&visibility=public"ed_status_id=$a_post_on_m"| j "print(d['id'])")
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_quote" | j "print((d.get(\"quote\") or {}).get(\"state\"))")" = "accepted" ]'&& ok "PrivaPub approves mastouser's quote (FEP-044f)"|| ko "quote of a PrivaPub post not approved on Mastodon"
until_true 30'[ "$(m_status_by_uri "$alice_on_m" "$a_media_uri" | j "print(d[\"media_attachments\"][0][\"description\"])")" = "a red square" ]'&& ok "an image with alt text reaches Mastodon"|| ko "image or alt text missing on Mastodon"
m_media=$(mcurl -X POST -H "$MH""$M/api/v2/media" -F "file=@$work/red.png;type=image/png" -F 'description=a red square from Mastodon'| j "print(d['id'])")
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/statuses" -d "status=a picture from Mastodon&visibility=public&media_ids[]=$m_media"
until_true 30'curl -s -H "$AH" "$P/api/v1/timelines/home" | j "print(any(\"a picture from Mastodon\" 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 Mastodon\" for s in d))" | grep -q True'\
&& ok "an image with alt text from Mastodon arrives through our proxy"|| ko "Mastodon's image missing, unproxied or without alt text"
echo" edits and deletes"
curl -s -o /dev/null -X PUT -H "$AH""$P/api/v1/statuses/$a_post_id" -d 'status=Hello Mastodon from PrivaPub, edited'
until_true 30'mcurl -H "$MH" "$M/api/v1/statuses/$a_post_on_m" | grep -q "edited"'&& ok "alice_masto's edit reaches Mastodon"|| ko "edit not applied on Mastodon"
mcurl -o /dev/null -X PUT -H "$MH""$M/api/v1/statuses/$m_post" -d 'status=Hello PrivaPub from Mastodon, edited'
until_true 30'[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$m_post_on_p/history" | j "print(len(d))")" = "2" ]'&& ok "mastouser's edit reaches PrivaPub with its history"|| ko "Mastodon's edit not applied on PrivaPub"
until_true 30'[ "$(mcurl -o /dev/null -w "%{http_code}" -H "$MH" "$M/api/v1/statuses/$cw_on_m")" = "404" ]'&& ok "alice_masto's delete reaches Mastodon"|| ko "delete not applied on Mastodon"
["$(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json'"$cw_uri")"="410"]&& ok "the deleted post answers 410"|| ko "deleted post does not answer 410"
curl -s -o /dev/null -X POST -H "$BH""$P/api/v1/accounts/$masto_on_p_b/follow"
until_true 30'mcurl -H "$MH" "$M/api/v1/follow_requests" | j "print(any(a[\"acct\"]==\"bob_masto@privapub.test\" for a in d))" | grep -q True'&& ok "bob_masto's follow waits as a request on locked mastouser"|| ko "follow request missing on Mastodon"
bob_on_m=$(mcurl -H "$MH""$M/api/v1/follow_requests"| j "print(next(a['id'] for a in d if a['acct']=='bob_masto@privapub.test'))")
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/follow_requests/$bob_on_m/authorize"
until_true 30'[ "$(curl -s -H "$BH" "$P/api/v1/accounts/relationships?id[]=$masto_on_p_b" | j "print(d[0][\"following\"])")" = "True" ]'&& ok "mastouser's approval reaches PrivaPub"|| ko "approval not applied on PrivaPub"
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/accounts/$circle_on_m/follow"
until_true 15'[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$circle_on_m" | j "print(d[0][\"requested\"])")" = "True" ]'&& ok "mastouser's request to join a circle waits for its owner"|| ko "joining the circle was not held for approval"
# Mastodon 4.7 names its actors by number (https://mastodon.test/ap/users/<id>), so the pending request says who asked
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$circle_on_m" | j "print(d[0][\"following\"])")" = "True" ]'&& ok "the owner's approval makes mastouser a circle member"|| ko "circle approval did not reach Mastodon"
-d "{\"avatarId\":\"$alice_id\",\"text\":\"only the circle sees this\",\"groupId\":\"$circle\"}")
circle_uri=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'print(db.Post.findOne({Text:/only the circle sees this/},{ObjectURI:1}).ObjectURI)')
# Mastodon 4.7 learns whose personal inbox a delivery reached only from /users/<name>/inbox, never from the numeric
# /ap/users/<id>/inbox it now advertises, and keeps a post naming no local account only for that recipient
# (InboxesController#account_required?, Create#addresses_local_accounts?). A circle post names only the circle.
["$(mcurl -H "Authorization: Bearer $OT""$M/api/v1/accounts/$alice_on_m_o/statuses?limit=40"| j "print(any(s['uri']=='$circle_uri' for s in d))")"="False"]\
&& ok "another Mastodon user does not see the circle post"|| ko "the circle post leaked to a non-member"
["$(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" reports"
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/reports" -d "account_id=$masto_on_p&comment=pasture-report-$circle_name&forward=true"
until_true 30'mcurl -H "$MH" "$M/api/v1/admin/reports" | grep -q "pasture-report-$circle_name"'&& ok "a report reaches Mastodon's moderators"|| ko "report missing on Mastodon"
["$(mcurl -H "$MH""$M/api/v1/admin/reports"| j "print(next((r['account']['username'] for r in d if 'pasture-report-$circle_name' in r['comment']), ''))")" !="alice_masto"]\
&& ok "the report does not come from the reporting persona"|| ko "the report names the reporting persona"
echo" blocks and unfollows"
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/accounts/$masto_on_p/unfollow"
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$alice_on_m" | j "print(d[0][\"followed_by\"])")" = "False" ]'&& ok "alice_masto's unfollow reaches Mastodon"|| ko "unfollow not applied on Mastodon"
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/accounts/$masto_on_p/block"
until_true 30'[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$alice_on_m" | j "print(d[0][\"blocked_by\"])")" = "True" ]'&& ok "alice_masto's block reaches Mastodon"|| ko "block not applied on Mastodon"
curl -s -o /dev/null -X POST -H "$AH""$P/api/v1/accounts/$masto_on_p/unblock"
mcurl -o /dev/null -X POST -H "$MH""$M/api/v1/accounts/$alice_on_m/block"
sleep 5; xf "Mastodon's block is enforced on PrivaPub (inbound Block is P7)"