diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-15 00:56:04 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-15 00:56:04 +0200 |
| commit | 6f68bb6c1970e26de1f66c073ea5d7fecd21840d (patch) | |
| tree | 859c74db5819f1d49046e71a53e12afe5eb4193a /packages/meshbay-cli/tests/test_membership.py | |
| parent | 16d92c96bde3a65fc54a93b4662df30cafc831e4 (diff) | |
| download | meshbay-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.py | 78 |
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") |