summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/tests/test_membership.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 00:56:04 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 00:56:04 +0200
commit6f68bb6c1970e26de1f66c073ea5d7fecd21840d (patch)
tree859c74db5819f1d49046e71a53e12afe5eb4193a /packages/meshbay-cli/tests/test_membership.py
parent16d92c96bde3a65fc54a93b4662df30cafc831e4 (diff)
downloadmeshbay-6f68bb6c1970e26de1f66c073ea5d7fecd21840d.tar.gz
feat(cli): invitations, join, decline, leave, devices, browser access
join accepts an invitation or redeems a link pasted as sent, then has the node admit a key pair made here, with the invitation's code (a link's code goes only to the node it names; one for a node that is away is kept for the next connection). decline, leave and devices revoke ask first, and refuse without --yes where there is no terminal. devices approve countersigns the one waiting request a code matches, none when several do. browser-access is the application's setting, kept per machine and never enabled from the hub's mirror: on leaves a copy sealed under M on each node, off withdraws it, unset touches nothing. A key pair made for a first contact is now kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/tests/test_membership.py')
-rw-r--r--packages/meshbay-cli/tests/test_membership.py78
1 files changed, 78 insertions, 0 deletions
diff --git a/packages/meshbay-cli/tests/test_membership.py b/packages/meshbay-cli/tests/test_membership.py
new file mode 100644
index 0000000..30fe680
--- /dev/null
+++ b/packages/meshbay-cli/tests/test_membership.py
@@ -0,0 +1,78 @@
+"""
+The pieces `join`, `devices` and `browser-access` stand on, held to the
+application's own rules: the invitation link's shape (`static/invite-link.js`),
+how a typed code is folded (`crypto.js` `normalizeCode`), and which bundle on a
+node is current enough to be left alone.
+"""
+
+import base64
+
+import pytest
+from meshbay_cli.client.devices import find_device, normalize_code
+from meshbay_cli.client.invite import parse_link
+from meshbay_cli.client.keys import bundle_is_current, open_bundle, seal_bundle
+from meshbay_cli.errors import NOT_FOUND, USAGE, CliError
+
+GROUP = "0f8fad5b-d9cb-469f-a165-70867728950e"
+TICKET = "AbCdEfGhIjKlMnOpQr-_12"
+NODE_URLSAFE = base64.urlsafe_b64encode(bytes(range(250, 256)) * 5 + b"\xfe\xff").decode()
+
+
+def _link(**over) -> str:
+ q = {"v": "1", "g": GROUP, "t": TICKET, "n": NODE_URLSAFE.rstrip("="), "c": "k7p2-9wqx",
+ **over}
+ return "https://hub.test/#/invite?" + "&".join(f"{k}={v}" for k, v in q.items())
+
+
+def test_a_link_gives_its_hub_group_ticket_node_and_code():
+ link = parse_link(_link())
+ assert (link.origin, link.group_id, link.ticket, link.code) == (
+ "https://hub.test", GROUP, TICKET, "K7P2-9WQX")
+ # The node key as the hub lists nodes: standard base64, padded.
+ assert base64.b64decode(link.node_pk) == base64.urlsafe_b64decode(NODE_URLSAFE)
+ assert "+" in link.node_pk or "/" in link.node_pk
+ # Only the fragment, as somebody may paste it.
+ assert parse_link(_link().split("#", 1)[1]).origin == ""
+
+
+@pytest.mark.parametrize("bad", [{"v": "2"}, {"g": "not-a-group"}, {"t": "short"},
+ {"n": "x" * 42}, {"c": "K7P2-9WQ"}, {"c": "K7P2+9WQX"}])
+def test_a_malformed_link_is_no_link(bad):
+ assert parse_link(_link(**bad)) is None
+
+
+def test_text_without_a_link_is_no_link():
+ assert parse_link("Family@alice") is None
+
+
+def test_a_code_is_folded_as_the_application_folds_it():
+ assert normalize_code("k7p2-9wqx") == "K7P29WQX"
+ assert normalize_code(" ilou 0O1V ") == "110V001V"
+
+
+def test_a_device_is_named_by_an_unambiguous_prefix():
+ devices = [{"pk_ed25519": "ABCDEFGH1234"}, {"pk_ed25519": "ABCDEFGH5678"}]
+ assert find_device(devices, "ABCDEFGH5")["pk_ed25519"] == "ABCDEFGH5678"
+ with pytest.raises(CliError) as e:
+ find_device(devices, "ABCDEFGH")
+ assert e.value.code == USAGE
+ with pytest.raises(CliError) as e:
+ find_device(devices, "ABCD")
+ assert e.value.code == USAGE
+ with pytest.raises(CliError) as e:
+ find_device(devices, "ZZZZZZZZ")
+ assert e.value.code == NOT_FOUND
+
+
+def test_a_bundle_sealed_here_opens_and_is_current_only_under_its_own_key():
+ keys = {"skEd": base64.b64encode(b"ed").decode(), "skX": base64.b64encode(b"x").decode()}
+ m, other = b"\x01" * 32, b"\x02" * 32
+ sealed = seal_bundle(keys, master=m, pepper_version=1, user_id="u", node_pk="pk")
+ assert base64.b64decode(sealed)[:5] == b"MBK3\x01"
+ assert open_bundle(sealed, master=m, legacy=None, user_id="u", node_pk="pk") == keys
+ assert bundle_is_current(sealed, master=m, user_id="u", node_pk="pk")
+ # Sealed under an earlier passphrase, or for another node: sealed again.
+ assert not bundle_is_current(sealed, master=other, user_id="u", node_pk="pk")
+ assert not bundle_is_current(sealed, master=m, user_id="u", node_pk="pk2")
+ legacy = base64.b64encode(b"MBK2" + b"\x00" * 40).decode()
+ assert not bundle_is_current(legacy, master=m, user_id="u", node_pk="pk")