Files
SocialPub/tools/pasture/scenarios/instance.sh
T
thepraandClaude Opus 5.5 70d1ffe9a2
Build / Build (push) Successful in 1m3s
R5 the server's own settings: instance, rules, announcements, relays, media limits, the staff's log
A singleton ServerSettings (title, descriptions, contact, thumbnail, privacy policy, terms, rules, relays, media
overrides, PublishActivity), cached by InstanceSettings and read by the instance API, NodeInfo, the instance actor, the
web pages and the mailer; its defaults are the old literals, so nothing changes until an administrator edits.
/clientapi/admin/settings (and /thumbnail), /announcements, /relays for administrators; /clientapi/moderator/log, the
staff's log (AdminAction, written by domain blocks, backups, settings and the CLI; no IP, never a root paired with its
personas). The Mastodon API's rules, extended description, privacy policy, terms of service, domain blocks (Published,
Obfuscate), announcements (dismiss, reactions, streaming) and activity (only when published). Reports carry rule ids.
Media overrides clamped to hard caps. Relays named by the settings reconcile next to the configured ones.

Verified: interop.sh instance accounts relay with mastodon, relay, aoderelay and mailpit, 119 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
2026-10-17 04:02:47 +02:00

227 lines
23 KiB
Bash

# The server's own settings (completion program R5), edited as the administrator through /clientapi/admin: what the
# instance API (v1 and v2), NodeInfo, the instance actor, the OAuth sign-in page and the instance's rules, description,
# privacy policy and terms say; published domain blocks (and an unpublished one that stays out); a report naming rules
# reaching the moderators with their texts; an announcement listed, dismissed by one persona, reacted to and streamed;
# media limits from the settings (a 1 KB quota refusing an upload, then reset); the thumbnail; a relay added through the
# settings and removed (the configured ones can't be); the weekly activity once published; the staff's log listing all
# of it, without an address. It puts every setting back as it found it at the end, so other scenarios see the usual
# instance. Needs the relay and aoderelay peers (batch A); mastodon is not used.
. "$here/peers/relay.sh"
api() { local jwt=$1 method=$2 path=$3; shift 3; curl -s -X "$method" -H "Authorization: Bearer $jwt" -H 'Content-Type: application/json' "$P$path" "$@"; }
code_of() { local jwt=$1 method=$2 path=$3; shift 3; curl -s -o /dev/null -w '%{http_code}' -X "$method" -H "Authorization: Bearer $jwt" -H 'Content-Type: application/json' "$P$path" "$@"; }
# settings_form <view json> <python statements changing f>: the settings form made from a settings view, then changed
settings_form() { python3 -c "
import json,sys
v=json.loads(sys.argv[1])
f={k: v.get(k) for k in ['title','shortDescription','description','contactEmail','contactAccountId','privacyPolicy','termsOfService','termsEffectiveAt','publishActivity']}
f['rules']=[{'id': r['id'], 'text': r['text'], 'hint': r.get('hint')} for r in v.get('rules', []) if not r.get('isDeleted')]
m=v.get('media') or {}
f['media']={k: m.get(k) for k in ['quotaBytesPerRoot','maxImageBytes','maxVideoBytes','maxSeconds']}
$2
print(json.dumps(f))" "$1"; }
relay_state() { podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'var s=db.RelaySubscription.findOne({Configured:"'"$1"'"}); print(s ? s.State : "none")'; }
# noise_png <path> <side>: a PNG of random pixels, which compresses to more than a kilobyte
noise_png() { python3 -c "
import os,struct,zlib
w=h=$2
raw=b''.join(b'\x00'+os.urandom(3*w) for _ in range(h))
png=b'\x89PNG\r\n\x1a\n'+b''.join(struct.pack('>I',len(c))+t+c+struct.pack('>I',zlib.crc32(t+c)&0xffffffff) for t,c in [(b'IHDR',struct.pack('>IIBBBBB',w,h,8,2,0,0,0)),(b'IDAT',zlib.compress(raw)),(b'IEND',b'')])
open('$1','wb').write(png)"; }
echo "instance"
run=$(date +%s)
privapub_root >/dev/null # the pasture's root, made if this runs first
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'])")
[ -n "$ADMIN" ] && ok "the pasture's root signs in as the administrator" || { ko "no administrator's login"; return 1; }
AT=$(privapub_token alice_instance); BT=$(privapub_token bob_instance)
AH="Authorization: Bearer $AT"; BH="Authorization: Bearer $BT"
alice_id=$(curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
bob_id=$(curl -s -H "$BH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
[ -n "$alice_id" ] && [ -n "$bob_id" ] && ok "two personas for the scenario" || { ko "no personas for the scenario"; return 1; }
before=$(api "$ADMIN" GET /clientapi/admin/settings)
[ -n "$(echo "$before" | j "print(d['title'])")" ] && ok "the administrator reads the settings" || { ko "the settings could not be read"; return 1; }
[ "$(code_of "$AT" GET /clientapi/admin/settings)" != 200 ] && ok "a persona's token reads no settings" || ko "a persona's token read the settings"
echo " the server described"
title="Pasture Commons $run"
form=$(settings_form "$before" "
f.update(title='$title', shortDescription='A test field for $run.', description='Welcome to **the pasture** $run.<script>alert(1)</script>',
contactEmail='staff@privapub.test', contactAccountId='$alice_id', privacyPolicy='We keep *little* about you ($run).',
termsOfService='Be kind ($run).', termsEffectiveAt='2026-10-01T00:00:00Z')
f['rules']=[{'text': 'No spam $run', 'hint': 'Ads included'}, {'text': 'Be kind $run'}]")
saved=$(api "$ADMIN" POST /clientapi/admin/settings -d "$form")
r1=$(echo "$saved" | j "print([r['id'] for r in d['rules'] if r['text'] == 'No spam $run'][0])")
r2=$(echo "$saved" | j "print([r['id'] for r in d['rules'] if r['text'] == 'Be kind $run'][0])")
[ -n "$r1" ] && [ -n "$r2" ] && [ "$(echo "$saved" | j "print(d['title'], d['contactAccount']['userName'])")" = "$title alice_instance" ] \
&& ok "the settings are saved, the rules given their ids" || ko "the settings were not saved: $(echo "$saved" | head -c 300)"
v1=$(curl -s "$P/api/v1/instance")
[ "$(echo "$v1" | j "print(d['title'] == '$title' and d['short_description'] == 'A test field for $run.' and d['email'] == 'staff@privapub.test' and d['contact_account']['id'] == '$alice_id' and [r['text'] for r in d['rules']] == ['No spam $run', 'Be kind $run'])")" = True ] \
&& ok "/api/v1/instance says so" || ko "/api/v1/instance: $(echo "$v1" | j "print(d['title'], d['email'], d['rules'])")"
v2=$(curl -s "$P/api/v2/instance")
[ "$(echo "$v2" | j "print(d['title'] == '$title' and d['description'] == 'A test field for $run.' and d['contact']['email'] == 'staff@privapub.test' and d['contact']['account']['username'] == 'alice_instance' and [r['id'] for r in d['rules']] == ['$r1', '$r2'] and d['rules'][0]['hint'] == 'Ads included')")" = True ] \
&& ok "/api/v2/instance says so" || ko "/api/v2/instance: $(echo "$v2" | j "print(d['title'], d['contact'], d['rules'])")"
[ "$(curl -s "$P/nodeinfo/2.1" | j "print(d['metadata']['nodeName'] == '$title' and d['metadata']['nodeDescription'] == 'A test field for $run.')")" = True ] \
&& ok "NodeInfo names the server and describes it" || ko "NodeInfo: $(curl -s "$P/nodeinfo/2.1" | j "print(d['metadata'])")"
actor=$(pfetch -H 'Accept: application/activity+json' https://privapub.test/peasants/privapub)
[ "$(echo "$actor" | j "print(d['name'] == '$title' and 'A test field for $run.' in d['summary'])")" = True ] \
&& ok "the instance actor carries the name and the short description" || ko "the instance actor: $(echo "$actor" | j "print(d['name'], d['summary'])")"
[ "$(curl -s "$P/api/v1/instance/rules" | j "print([(r['id'], r['text']) for r in d] == [('$r1', 'No spam $run'), ('$r2', 'Be kind $run')])")" = True ] \
&& ok "/instance/rules lists them in order" || ko "/instance/rules: $(curl -s "$P/api/v1/instance/rules")"
ext=$(curl -s "$P/api/v1/instance/extended_description")
[ "$(echo "$ext" | j "print('<strong>the pasture</strong>' in d['content'] and '<script' not in d['content'] and d['updated_at'][:10] == '$(date -u +%F)')")" = True ] \
&& ok "the extended description is rendered from Markdown, sanitized, dated today" || ko "the extended description: $ext"
[ "$(curl -s "$P/api/v1/instance/privacy_policy" | j "print('<em>little</em>' in d['content'] and d['updated_at'][:10] == '$(date -u +%F)')")" = True ] \
&& ok "the privacy policy is rendered and dated" || ko "the privacy policy: $(curl -s "$P/api/v1/instance/privacy_policy")"
tos=$(curl -s "$P/api/v1/instance/terms_of_service")
[ "$(echo "$tos" | j "print(d['effective_date'] == '2026-10-01' and d['effective'] is True and 'Be kind ($run)' in d['content'] and d['succeeded_by'] is None)")" = True ] \
&& ok "the terms of service answer in Mastodon 4.4's shape" || ko "the terms of service: $tos"
[ "$(curl -s -o /dev/null -w '%{http_code}' "$P/api/v1/instance/terms_of_service/2026-10-01")" = 200 ] \
&& [ "$(curl -s -o /dev/null -w '%{http_code}' "$P/api/v1/instance/terms_of_service/2020-01-01")" = 404 ] \
&& ok "the terms answer for their date and no other" || ko "the terms by date answer wrongly"
curl -s "$P/oauth/login" | grep -q "Sign in to $title" && ok "the OAuth sign-in page names the server" || ko "the OAuth sign-in page does not name the server"
[ "$(code_of "$ADMIN" POST /clientapi/admin/settings -d "$(settings_form "$saved" "f['contactAccountId']='0123456789abcdef01234567'")")" = 422 ] \
&& ok "a contact that is no persona here is refused" || ko "a contact that is no persona was taken"
[ "$(code_of "$ADMIN" POST /clientapi/admin/settings -d "$(settings_form "$saved" "f['title']='x'*65")")" = 400 ] \
&& ok "a title over 64 characters is refused" || ko "a title over 64 characters was taken"
edited=$(api "$ADMIN" POST /clientapi/admin/settings -d "$(settings_form "$saved" "f['rules']=[{'id': '$r1', 'text': 'No spam at all $run'}, {'text': 'Third $run'}]")")
[ "$(echo "$edited" | j "print([(r['id'] == '$r1', r['text'], r['isDeleted']) for r in d['rules']] == [(True, 'No spam at all $run', False), (False, 'Third $run', False), (False, 'Be kind $run', True)])")" = True ] \
&& ok "an edited rule keeps its id, a new one gets one, one left out is kept deleted" || ko "the rules after an edit: $(echo "$edited" | j "print(d['rules'])")"
[ "$(curl -s "$P/api/v1/instance/rules" | j "print('$r2' not in [r['id'] for r in d] and len(d) == 2)")" = True ] \
&& ok "a deleted rule is no longer listed" || ko "a deleted rule is still listed"
echo " public domain blocks"
shown="blocked-$run.example"; quiet="quiet-$run.example"
api "$ADMIN" POST /clientapi/admin/domainblocks/insert -d "{\"domain\":\"$shown\",\"suspend\":true,\"publicComment\":\"spam $run\",\"published\":true,\"obfuscate\":true}" >/dev/null
api "$ADMIN" POST /clientapi/admin/domainblocks/insert -d "{\"domain\":\"$quiet\",\"suspend\":false,\"publicComment\":\"quiet $run\"}" >/dev/null
obfuscated=$(python3 -c "
d='$shown'; v=len(d)//4
print(''.join('*' if v < i < len(d)-v and c != '.' else c for i,c in enumerate(d)))")
digest=$(printf '%s' "$shown" | sha256sum | cut -d' ' -f1)
blocks=$(curl -s "$P/api/v1/instance/domain_blocks")
[ "$(echo "$blocks" | j "print([b for b in d if b['digest'] == '$digest'] == [{'domain': '$obfuscated', 'digest': '$digest', 'severity': 'suspend', 'comment': 'spam $run'}])")" = True ] \
&& ok "a published block is listed, its name obfuscated, with its digest" || ko "the published block: $blocks"
[ "$(echo "$blocks" | j "print(not any('quiet $run' == b['comment'] for b in d))")" = True ] && ok "an unpublished block stays out" || ko "an unpublished block is listed"
api "$ADMIN" POST "/clientapi/admin/domainblocks/delete?domain=$shown" >/dev/null
api "$ADMIN" POST "/clientapi/admin/domainblocks/delete?domain=$quiet" >/dev/null
[ "$(curl -s "$P/api/v1/instance/domain_blocks" | j "print(any(b['digest'] == '$digest' for b in d))")" = False ] && ok "and leaves the list when deleted" || ko "a deleted block is still listed"
echo " rules in reports"
report=$(curl -s -X POST -H "$AH" "$P/api/v1/reports" -d "account_id=$bob_id" -d "rule_ids[]=$r1" -d "rule_ids[]=$r2" -d "rule_ids[]=no-such-rule" --data-urlencode "comment=rules $run")
report_id=$(echo "$report" | j "print(d['id'])")
[ "$(echo "$report" | j "print(sorted(d['rule_ids']) == sorted(['$r1', '$r2']) and d['category'] == 'violation')")" = True ] \
&& ok "a report keeps the rules that exist (a deleted one too) and becomes a violation" || ko "the report: $report"
[ "$(api "$ADMIN" GET "/clientapi/moderator/reports?resolved=false" | j "print([sorted(r['rules']) for r in d if r['id'] == '$report_id'] == [sorted(['No spam at all $run', 'Be kind $run'])])")" = True ] \
&& ok "the moderators read the rules' texts" || ko "the moderators do not see the rules: $(api "$ADMIN" GET "/clientapi/moderator/reports?resolved=false" | head -c 400)"
[ "$(code_of "$ADMIN" POST "/clientapi/moderator/reports/$report_id/resolve")" = 200 ] && ok "and resolve it" || ko "the report could not be resolved"
echo " announcements"
curl -sN --max-time 12 -H "$BH" "$P/api/v1/streaming/user" > "$work/sse" 2>/dev/null &
sse=$!
sleep 1
announcement=$(api "$ADMIN" POST /clientapi/admin/announcements/insert -d "{\"text\":\"The pasture is *open* $run\",\"isPublished\":true}")
aid=$(echo "$announcement" | j "print(d['id'])")
[ -n "$aid" ] && [ "$(echo "$announcement" | j "print(d['isShown'] and '<em>open</em>' in d['html'])")" = True ] && ok "the administrator publishes an announcement" || ko "no announcement: $announcement"
[ "$(curl -s -H "$AH" "$P/api/v1/announcements" | j "print([(a['read'], '<em>open</em>' in a['content'], a['reactions']) for a in d if a['id'] == '$aid'] == [(False, True, [])])")" = True ] \
&& ok "a persona reads it, unread" || ko "the persona's announcements: $(curl -s -H "$AH" "$P/api/v1/announcements")"
[ "$(curl -s "$P/api/v1/announcements" | j "print([('read' in a) for a in d if a['id'] == '$aid'] == [False])")" = True ] \
&& ok "an anonymous reader gets it without a read state" || ko "the anonymous announcements: $(curl -s "$P/api/v1/announcements")"
[ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$AH" "$P/api/v1/announcements/$aid/dismiss")" = 200 ] \
&& [ "$(curl -s -H "$AH" "$P/api/v1/announcements" | j "print([a['read'] for a in d if a['id'] == '$aid'])")" = "[True]" ] \
&& [ "$(curl -s -H "$BH" "$P/api/v1/announcements" | j "print([a['read'] for a in d if a['id'] == '$aid'])")" = "[False]" ] \
&& ok "dismissed by one persona, still unread for the other" || ko "dismissing is not per persona"
thumbs=$(python3 -c "import urllib.parse; print(urllib.parse.quote('👍'))")
curl -s -o /dev/null -X PUT -H "$AH" "$P/api/v1/announcements/$aid/reactions/$thumbs"
[ "$(curl -s -H "$AH" "$P/api/v1/announcements" | j "print([a['reactions'] for a in d if a['id'] == '$aid'] == [[{'name': '👍', 'count': 1, 'me': True}]])")" = True ] \
&& [ "$(curl -s -H "$BH" "$P/api/v1/announcements" | j "print([a['reactions'][0]['me'] for a in d if a['id'] == '$aid'])")" = "[False]" ] \
&& ok "a reaction is counted, and known as its own to the persona alone" || ko "the reaction: $(curl -s -H "$AH" "$P/api/v1/announcements")"
[ "$(curl -s -o /dev/null -w '%{http_code}' -X PUT -H "$AH" "$P/api/v1/announcements/$aid/reactions/abc")" = 422 ] \
&& ok "a reaction that is no emoji is refused (422)" || ko "a reaction that is no emoji was taken"
api "$ADMIN" POST /clientapi/admin/announcements/update -d "{\"id\":\"$aid\",\"text\":\"The pasture is *open*, edited $run\",\"isPublished\":true}" >/dev/null
curl -s -o /dev/null -X DELETE -H "$AH" "$P/api/v1/announcements/$aid/reactions/$thumbs"
[ "$(curl -s -H "$AH" "$P/api/v1/announcements" | j "print([a['reactions'] for a in d if a['id'] == '$aid'])")" = "[[]]" ] && ok "and taken back" || ko "the reaction stayed"
api "$ADMIN" POST "/clientapi/admin/announcements/delete?id=$aid" >/dev/null
[ "$(curl -s -H "$AH" "$P/api/v1/announcements" | j "print(any(a['id'] == '$aid' for a in d))")" = False ] && ok "a deleted announcement is gone" || ko "a deleted announcement is still listed"
wait "$sse" 2>/dev/null
grep -q '^event: announcement$' "$work/sse" && grep -q "edited $run" "$work/sse" && ok "the user stream tells it made and edited" || ko "no announcement event on the user stream"
grep -q '^event: announcement.reaction$' "$work/sse" && ok "the user stream tells its reactions" || ko "no announcement.reaction event"
grep -A1 '^event: announcement.delete$' "$work/sse" | grep -q "data: $aid" && ok "the user stream tells it deleted" || ko "no announcement.delete event"
echo " media limits"
noise_png "$work/noise.png" 64
limited=$(api "$ADMIN" POST /clientapi/admin/settings -d "$(settings_form "$edited" "f['media']['maxImageBytes']=2097152")")
[ "$(curl -s "$P/api/v2/instance" | j "print(d['configuration']['media_attachments']['image_size_limit'])")" = 2097152 ] \
&& ok "an image limit from the settings is what the instance API advertises" || ko "the image limit did not reach the instance API"
quota=$(api "$ADMIN" POST /clientapi/admin/settings -d "$(settings_form "$limited" "f['media']['quotaBytesPerRoot']=1024")")
[ "$(echo "$quota" | j "print(d['media']['effectiveQuotaBytesPerRoot'])")" = 1024 ] && ok "a quota of 1 KB takes effect" || ko "the quota: $(echo "$quota" | j "print(d['media'])")"
[ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$AH" "$P/api/v2/media" -F "file=@$work/noise.png;type=image/png")" = 422 ] \
&& ok "an upload over it is refused (422)" || ko "an upload over the quota was taken"
reset=$(api "$ADMIN" POST /clientapi/admin/settings -d "$(settings_form "$quota" "f['media']={}")")
[ "$(echo "$reset" | j "print(d['media']['effectiveQuotaBytesPerRoot'] == d['media']['configuredQuotaBytesPerRoot'] and d['media']['effectiveMaxImageBytes'] == d['media']['configuredMaxImageBytes'])")" = True ] \
&& [ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$AH" "$P/api/v2/media" -F "file=@$work/noise.png;type=image/png")" = 200 ] \
&& ok "reset, the configured limits are back and the upload goes through" || ko "the limits did not reset"
echo " the thumbnail"
noise_png "$work/thumb.png" 96
thumb=$(curl -s -X POST -H "Authorization: Bearer $ADMIN" -F "file=@$work/thumb.png;type=image/png" "$P/clientapi/admin/settings/thumbnail")
turl=$(echo "$thumb" | j "print(d['thumbnailUrl'])")
[ "$(echo "$thumb" | j "print(d['thumbnailIsDefault'])")" = False ] && [ "$(curl -s "$P/api/v1/instance" | j "print(d['thumbnail'])")" = "$turl" ] \
&& [ "$(curl -s "$P/api/v2/instance" | j "print(d['thumbnail']['url'])")" = "$turl" ] \
&& ok "an uploaded thumbnail is the instance's" || ko "the thumbnail: $(echo "$thumb" | head -c 300)"
[ "$(pfetch -o /dev/null -w '%{http_code} %{content_type}' "$turl")" = "200 image/jpeg" ] && ok "served, re-encoded as a JPEG" || ko "the thumbnail is not served"
[ "$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval "print(db.MediaAttachment.countDocuments({ServerUse: 'thumbnail', TrashedAt: null}))")" = 1 ] \
&& ok "its row is held by the server" || ko "the thumbnail's row is not held"
[ "$(api "$ADMIN" DELETE /clientapi/admin/settings/thumbnail | j "print(d['thumbnailIsDefault'])")" = True ] \
&& [ "$(curl -s "$P/api/v1/instance" | j "print(d['thumbnail'].endswith('/media/missing-header.png'))")" = True ] \
&& [ "$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval "print(db.MediaAttachment.countDocuments({ServerUse: 'thumbnail', TrashedAt: null}))")" = 0 ] \
&& ok "removed, the default picture is back and the old one trashed" || ko "the thumbnail could not be removed"
echo " relays from the settings"
relays=$(api "$ADMIN" GET /clientapi/admin/relays)
[ "$(echo "$relays" | j "print(sorted(r['address'] for r in d if r['source'] == 'configuration') == ['https://aoderelay.test/actor', 'https://relay.test/actor'])")" = True ] \
&& ok "the configured relays are listed as the configuration's" || ko "the relays: $relays"
[ "$(code_of "$ADMIN" POST "/clientapi/admin/relays/delete?address=https://relay.test/actor")" = 409 ] \
&& ok "a configured relay can't be removed from the page (409)" || ko "a configured relay was removed from the page"
extra="https://relay.test/inbox"
[ "$(code_of "$ADMIN" POST /clientapi/admin/relays/insert -d "{\"address\":\"$extra\"}")" = 202 ] && ok "a relay is added through the settings" || ko "the relay was not added"
until_true 30 '[ "$(relay_state "$extra")" = 1 ]' && ok "and subscribed to, accepted" \
|| { [ "$(relay_state "$extra")" = 0 ] && ok "and subscribed to (requested; the relay answers a second subscription of the server's slowly)" || ko "the relay added was never asked ($(relay_state "$extra"))"; }
[ "$(api "$ADMIN" GET /clientapi/admin/relays | j "print([r['source'] for r in d if r['address'] == '$extra'])")" = "['settings']" ] \
&& ok "listed as the settings'" || ko "the added relay is not listed as the settings'"
[ "$(code_of "$ADMIN" POST "/clientapi/admin/relays/delete?address=$extra")" = 202 ] && until_true 15 '[ "$(relay_state "$extra")" = none ]' \
&& ok "removed, its subscription goes" || ko "the added relay could not be removed"
sleep 3
[ "$(relay_state https://relay.test/actor)" = 1 ] && relay_subscribers | grep -qx privapub.test \
&& ok "and the relay's configured subscription stays (no Undo for an actor still named)" || ko "removing the added address ended the configured subscription"
echo " activity"
[ "$(curl -s "$P/api/v1/instance/activity")" = "[]" ] && ok "the weekly activity is not published by default" || ko "the activity was published unasked"
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/statuses" -d "status=a post this week $run&visibility=public"
api "$ADMIN" POST /clientapi/admin/settings -d "$(settings_form "$reset" "f['publishActivity']=True")" >/dev/null
[ "$(curl -s "$P/api/v1/instance/activity" | j "print(len(d) == 12 and int(d[0]['statuses']) >= 1 and d[0]['logins'] == '0' and d[0]['registrations'].isdigit() and int(d[0]['week']) > int(d[1]['week']))")" = True ] \
&& ok "published, twelve weeks, this one first, counting its posts" || ko "the activity: $(curl -s "$P/api/v1/instance/activity" | head -c 300)"
echo " the staff's log"
log=$(api "$ADMIN" GET "/clientapi/moderator/log?limit=100")
missing=$(echo "$log" | j "
seen={i['action'] for i in d['items']}
print(' '.join(sorted({'root.promote','settings.update','rule.insert','rule.update','rule.delete','domain_block.insert','domain_block.delete','report.resolve','announcement.insert','announcement.update','announcement.delete','settings.thumbnail','relay.insert','relay.delete'} - seen)))")
[ -z "$missing" ] && [ -n "$log" ] && ok "it lists every act of the scenario" || ko "the log misses: $missing"
[ "$(echo "$log" | j "print(all(i.get('by') in ('$ROOT_USER', 'cli') for i in d['items']))")" = True ] \
&& [ "$(echo "$log" | j "print(any(i['action'] == 'root.promote' and i.get('by') == 'cli' for i in d['items']))")" = True ] \
&& ok "each by the administrator's login, or the command line's" || ko "the log names someone else"
echo "$log" | grep -qE '127\.0\.0\.1|11\.42\.|::1' && ko "the log holds an address" || ok "and holds no address"
page=$(api "$ADMIN" GET "/clientapi/moderator/log?limit=2")
next=$(echo "$page" | j "print(d['next'])")
first=$(echo "$page" | j "print(' '.join(i['id'] for i in d['items']))")
[ -n "$next" ] && [ "$(api "$ADMIN" GET "/clientapi/moderator/log?limit=2&before=$next" | j "print(len(d['items']) == 2 and not set(i['id'] for i in d['items']) & set('$first'.split()) and all(i['id'] < '$next' for i in d['items']))")" = True ] \
&& ok "paged newest first" || ko "the log does not page"
echo " back to the defaults"
restored=$(api "$ADMIN" POST /clientapi/admin/settings -d "$(settings_form "$before" "pass")")
[ "$(curl -s "$P/api/v1/instance" | j "print(d['title'] == '$(echo "$before" | j "print(d['title'])")' and d['rules'] == [] and d['contact_account'] is None)")" = True ] \
&& [ "$(curl -s -o /dev/null -w '%{http_code}' "$P/api/v1/instance/terms_of_service")" = 404 ] \
&& [ "$(curl -s "$P/api/v1/instance/activity")" = "[]" ] \
&& [ "$(curl -s "$P/nodeinfo/2.1" | j "print(d['metadata']['nodeName'])")" = "$(echo "$before" | j "print(d['title'])")" ] \
&& ok "every setting is as the scenario found it" || ko "the settings were not put back: $(echo "$restored" | head -c 300)"