"""Each driver against its own server, before any federation is trusted: two accounts of the platform make, like, boost, reply to, vote on, edit and delete objects, and the database view (`stored`) must agree with what was done and with the API. A failing selftest means the driver is wrong, not the federation.""" import time import traceback from dialects import driver from dialects.base import Account, PostSpec, Unsupported import state PASSWORD = "Town-Selftest-Pass-7!" def wait(fn, tries=20, delay=1.0): for _ in range(tries): value = fn() if value: return value time.sleep(delay) return fn() class Result: def __init__(self, peer): self.peer = peer self.passed = 0 self.failed = 0 self.skipped = 0 def check(self, cond, what): if cond: self.passed += 1 print(f" ok {what}") else: self.failed += 1 print(f" FAIL {what}") def skip(self, what): self.skipped += 1 print(f" n/a {what}") def accounts(peer): root = "st_root" if peer == "privapub" else None # Mastodon reserves names that contain its own, so the platform goes in as its two-letter code import gen short = gen.SHORT.get(peer, peer[:2]) return [Account(peer, f"st_a_{short}", PASSWORD, name="Selftest A", bio="selftest", root=root, fields=[("site", "pasture")], avatar_seed=f"st-a-{peer}"), Account(peer, f"st_b_{short}", PASSWORD, name="Selftest B", root=root)] def run(peers): total_failed = 0 for peer in peers: print(peer) r = Result(peer) try: if peer == "lemmy": lemmy(r) else: microblog(peer, r) except Exception as e: r.check(False, f"selftest stopped: {e!r}") traceback.print_exc() print(f" {r.passed} passed, {r.failed} failed, {r.skipped} not applicable") total_failed += r.failed return 1 if total_failed else 0 def microblog(peer, r): d = driver(peer) a, b = state.provision(peer, accounts(peer)) r.check(a.token and b.token, "both accounts signed in") try: d.update_profile(a, a.account) r.check(True, "profile updated (name, bio, fields, avatar)") except Unsupported as e: r.skip(str(e)) made = d.post(a, PostSpec(text=f"selftest post on {peer} (st1)", tags=["townselftest"])) row = wait(lambda: d.stored([made.uri])[made.uri].exists and d.stored([made.uri])[made.uri]) r.check(row and row.exists, f"the post is in the database under its uri {made.uri}") r.check(row and str(row.local_id) == str(made.local_id), f"database id {row and row.local_id} is the API id {made.local_id}") r.check(row and row.visibility == "public", f"stored visibility public (got {row and row.visibility})") d.like(b, made.uri) r.check(wait(lambda: d.stored([made.uri])[made.uri].likes == 1), "a like counts once") try: d.boost(b, made.uri) r.check(wait(lambda: d.stored([made.uri])[made.uri].boosts == 1), "a boost counts once") except Unsupported as e: r.skip(str(e)) reply = d.post(b, PostSpec(text=f"selftest reply on {peer} (st2)", reply_to_uri=made.uri)) rrow = wait(lambda: d.stored([reply.uri])[reply.uri].exists and d.stored([reply.uri])[reply.uri]) r.check(rrow and rrow.parent_uri == made.uri, f"the reply names its parent (got {rrow and rrow.parent_uri})") r.check(wait(lambda: (d.stored([made.uri])[made.uri].replies or 0) >= 1), "the parent counts the reply") poll = d.post(a, PostSpec(text=f"selftest poll on {peer} (st3)", poll={"options": ["tea", "coffee"], "expires_in": 86400})) d.vote(b, poll.uri, [1]) r.check(wait(lambda: (d.stored([poll.uri])[poll.uri].votes or [None, None])[1] == 1), f"a vote counts on the second option (got {d.stored([poll.uri])[poll.uri].votes})") private = d.post(a, PostSpec(text=f"selftest followers-only on {peer} (st4)", visibility="followers")) prow = d.stored([private.uri])[private.uri] r.check(prow.visibility == "followers", f"stored visibility followers (got {prow.visibility})") seen = d.seen(b, [made.uri, private.uri]) r.check(seen[made.uri], "a non-follower sees the public post") r.check(not seen[private.uri], "a non-follower does not see the followers-only post") try: d.edit(a, made.uri, PostSpec(text=f"selftest post on {peer}, edited (st1)")) r.check(wait(lambda: d.stored([made.uri])[made.uri].edited), "the edit is recorded") except Unsupported as e: r.skip(str(e)) d.delete(b, reply.uri) gone = wait(lambda: (lambda x: not x.exists or x.deleted)(d.stored([reply.uri])[reply.uri])) r.check(gone, "a deleted reply is gone or tombstoned") try: d.react(b, made.uri, "🎉") r.check(wait(lambda: (d.stored([made.uri])[made.uri].reactions or {}).get("🎉", 0) >= 1), "a reaction is stored") except Unsupported as e: r.skip(str(e)) def lemmy(r): d = driver("lemmy") a, b = state.provision("lemmy", accounts("lemmy")) r.check(a.token and b.token, "both accounts signed in") name = f"stc{int(time.time())}" c = d.community(a, name, "Selftest community") thread = d.post(a, PostSpec(text="selftest thread body (st1)", title="Selftest thread", group=c["id"])) row = d.stored([thread.uri])[thread.uri] r.check(row.exists and row.local_id == thread.local_id, f"the thread is in the database ({row.local_id})") d.like(b, thread.uri) r.check(wait(lambda: d.stored([thread.uri])[thread.uri].likes >= 2), "an upvote counts (with the author's own)") comment = d.post(b, PostSpec(text="selftest comment (st2)", reply_to_uri=thread.uri)) crow = d.stored([comment.uri])[comment.uri] r.check(crow.parent_uri == thread.uri, f"the comment names its thread (got {crow.parent_uri})") reply = d.post(a, PostSpec(text="selftest comment reply (st3)", reply_to_uri=comment.uri)) rrow = d.stored([reply.uri])[reply.uri] r.check(rrow.parent_uri == comment.uri, f"a nested comment names its parent comment (got {rrow.parent_uri})") d.downvote(a, comment.uri) r.check(wait(lambda: (d.stored([comment.uri])[comment.uri].reactions or {}).get("down") == 1), "a downvote counts") d.edit(a, thread.uri, PostSpec(text="selftest thread body, edited (st1)")) r.check(wait(lambda: d.stored([thread.uri])[thread.uri].edited), "the edit is recorded") pm = d.post(a, PostSpec(text="selftest private message (st4)", visibility="direct", mentions=[f"{b.account.username}@lemmy.test"])) r.check(d.stored([pm.uri])[pm.uri].exists, "a private message is in the database") d.delete(a, reply.uri) r.check(wait(lambda: d.stored([reply.uri])[reply.uri].deleted), "a deleted comment is marked deleted")