summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/tests/test_roster.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 02:06:35 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 02:06:35 +0200
commit419af660eeb8a4403e37a63c54a15586d04d5a8e (patch)
treeb0c0cdd831a5fb2864d5bedf581681aa0ec86023 /packages/meshbay-cli/tests/test_roster.py
parent47583c5e405f3011a526df73cc9485c46598f214 (diff)
downloadmeshbay-419af660eeb8a4403e37a63c54a15586d04d5a8e.tar.gz
feat(cli): chat — Tier 2 check on the signing device, interactive room
Each message's signing device is checked against the group's roster as the page checks it (a port of transport-roster.js, held to it by the parity test): chains of countersignatures walked from each account's roots, the keys accepted per account and node kept in the store, the roster read again once before a key is called changed. Only "changed" is said, under the message; --json carries every verdict. chat with no group opens the interactive chat: the groups, then one group's conversation IRC-style with pushed messages arriving live and a line to write in; Esc or /part goes back to the groups, /quit or Ctrl+Q quits. Like the bare command, it opens on the sign-in screen when nothing is stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/tests/test_roster.py')
-rw-r--r--packages/meshbay-cli/tests/test_roster.py100
1 files changed, 100 insertions, 0 deletions
diff --git a/packages/meshbay-cli/tests/test_roster.py b/packages/meshbay-cli/tests/test_roster.py
new file mode 100644
index 0000000..cdaf90b
--- /dev/null
+++ b/packages/meshbay-cli/tests/test_roster.py
@@ -0,0 +1,100 @@
+"""
+Tier 2 for chat (docs/MESHBAY_DESIGN.md §3.3): whose device signed a message,
+checked against the group's roster and the keys this machine accepted before —
+through `main()`, against the in-memory node, with real countersignatures.
+"""
+
+import json
+
+import pytest
+from fake_hub import PASSPHRASE, USERNAME, FakeHub
+from fake_node import FakeNode
+from meshbay_cli import __main__ as cli
+from meshbay_cli.client.keys import Identity
+from meshbay_cli.client.roster import RosterCheck, judge, verify_roster
+from meshbay_cli.commands import chat, context
+
+
+def _new() -> Identity:
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
+ return Identity(Ed25519PrivateKey.generate(), X25519PrivateKey.generate())
+
+
+@pytest.fixture
+def node(monkeypatch, capsys):
+ hub, fake = FakeHub(), FakeNode()
+ monkeypatch.setattr(context, "Hub", lambda url: hub)
+
+ async def open_group(account, group):
+ return fake.session()
+ monkeypatch.setattr(chat, "open_group", open_group)
+ assert cli.main(["--hub", hub.url, "-u", USERNAME, "--password", PASSPHRASE,
+ "login"]) == 0
+ capsys.readouterr()
+ return fake
+
+
+def _last(capsys, n=1) -> list[dict]:
+ assert cli.main(["--json", "chat", f"-{n}", "Family"]) == 0
+ return json.loads(capsys.readouterr().out)
+
+
+def test_each_verdict_on_a_message_signing_device(node, capsys):
+ phone = node.say("from bob's phone")
+ assert _last(capsys)[0]["trust"] == "first" # first sight: pinned
+ assert _last(capsys)[0]["trust"] == "pinned"
+
+ # A second device, countersigned by the first: no notice.
+ laptop = _new()
+ node.list_device("bob-test", laptop, countersigned_by=phone)
+ node.say("from bob's laptop", device=laptop)
+ assert _last(capsys)[0]["trust"] == "linked"
+
+ # Named as countersigned and carrying no proof, as a lying node would list it.
+ planted = _new()
+ node.list_device("bob-test", planted, countersigned_by=phone, proof=False)
+ node.say("hello, it's bob", device=planted)
+ assert _last(capsys)[0]["trust"] == "changed"
+ # Not in the roster at all.
+ node.say("hello again", device=_new())
+ assert _last(capsys)[0]["trust"] == "changed"
+
+ # Said in plain text under the message, and nowhere else.
+ assert cli.main(["chat", "-5", "Family"]) == 0
+ out = capsys.readouterr().out.splitlines()
+ assert sum("key you have not seen before" in line for line in out) == 2
+ assert out[-1].lstrip().startswith("! This account")
+ assert out[-3].lstrip().startswith("! This account")
+
+ # Another account, first sight.
+ node.say("hi from carol", sender="carol-test")
+ assert _last(capsys)[0]["trust"] == "first"
+
+
+async def test_a_device_listed_after_the_roster_was_read_is_not_a_substitution(node):
+ """The roster is read once per connection; a member's new device added since
+ is read again before "changed" is said."""
+ from meshbay_cli.client.account import Account
+ from meshbay_cli.client.store import Store
+ store = Store()
+ account = Account.open(context.Hub("x"), None, lambda: PASSPHRASE, store)
+ session = node.session()
+ phone = node.say("one")
+ check = RosterCheck(account, session)
+ assert await check.status("u-bob-test", phone.pk_ed) == "first"
+ tablet = _new()
+ node.list_device("bob-test", tablet, countersigned_by=phone) # after the read
+ assert await check.status("u-bob-test", tablet.pk_ed) == "linked"
+
+
+def test_a_root_is_a_device_naming_no_countersigner():
+ a, b = _new(), _new()
+ rows = [{"user_id": "u", "pk_ed25519": a.pk_ed, "pk_x25519": a.pk_x,
+ "added_by_pk": ""},
+ {"user_id": "u", "pk_ed25519": b.pk_ed, "pk_x25519": b.pk_x,
+ "added_by_pk": a.pk_ed, "add_sig": "", "add_nonce": "", "add_ts": 0}]
+ got = verify_roster({"devices": rows, "node_pk": "pk"}, "pk")["u"]
+ assert got.verified == [a.pk_ed] and got.unevidenced == [b.pk_ed]
+ assert judge([a.pk_ed], got, b.pk_ed) == ("changed", None)
+ assert judge(None, got, b.pk_ed)[0] == "first"