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_cli_actions.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_cli_actions.py')
| -rw-r--r-- | packages/meshbay-cli/tests/test_cli_actions.py | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/packages/meshbay-cli/tests/test_cli_actions.py b/packages/meshbay-cli/tests/test_cli_actions.py index b88d93c..4fffc4e 100644 --- a/packages/meshbay-cli/tests/test_cli_actions.py +++ b/packages/meshbay-cli/tests/test_cli_actions.py @@ -233,6 +233,198 @@ def test_groups_lists_each_group_with_its_state(hub, capsys): assert rows[0]["path"] == "Club%2FPhotos@bob-test" +class _Terminal(io.StringIO): + """stdin at a terminal, with the answers a person types.""" + + def isatty(self): + return True + + +def _signed_in(hub, capsys): + assert main("--hub", hub.url, "-u", USERNAME, "--password", PASSPHRASE, "login") == 0 + capsys.readouterr() + + +CLUB = {"id": "g2", "name": "Club/Photos", "owner_username": "bob-test", + "node_online": True, "visibility": "private", "is_admin": False, "hosted": True} + + +@pytest.fixture +def joined(monkeypatch): + """`join`'s node half, recorded: the code and the node each try was given.""" + from meshbay_cli.client.session import NodeRefusal + from meshbay_cli.commands import join + tries = [] + state = {"needs_code": False, "away": False} + + async def open_group(account, group, *, join_code=None, node_pk=None): + tries.append({"group": group["id"], "code": join_code, "node_pk": node_pk}) + if state["away"]: + from meshbay_cli.errors import NODE, CliError + raise CliError(NODE, "No node serving it is online.") + if state["needs_code"] and not join_code: + raise NodeRefusal("this node has not admitted you yet", "code_required") + return FakeSession() + monkeypatch.setattr(join, "open_group", open_group) + return tries, state + + +def test_an_invitation_is_listed_joined_and_named_in_groups(hub, joined, capsys): + tries, _ = joined + hub.pending.append({**CLUB, "invited_by": "bob-test"}) + _signed_in(hub, capsys) + + assert main("groups") == 0 + assert "1 invitation waiting for your answer" in capsys.readouterr().err + assert main("invitations") == 0 + header, line = capsys.readouterr().out.splitlines() + assert header.split() == ["INVITED", "BY", "DATE", "GROUP"] + assert line.startswith("bob-test") and line.endswith("Club%2FPhotos@bob-test") + + assert main("join", "Club%2FPhotos") == 0 + out = capsys.readouterr() + assert "Joined Club%2FPhotos@bob-test" in out.out + assert tries == [{"group": "g2", "code": None, "node_pk": None}] + # The hub keeps no word on browser access: said once, nothing done. + assert "meshbay-cli browser-access on" in out.err + assert [g["id"] for g in hub.groups] == ["g1", "g2"] and not hub.pending + + assert main("invitations") == 0 + assert "No invitation is waiting" in capsys.readouterr().err + assert main("join", "Nothing") == 3 + + +def test_a_node_that_asks_for_a_code_gets_it_from_the_option_or_the_terminal( + hub, joined, monkeypatch, capsys): + tries, state = joined + state["needs_code"] = True + hub.pending.append({**CLUB, "invited_by": "bob-test"}) + _signed_in(hub, capsys) + + monkeypatch.setattr("sys.stdin.isatty", lambda: False) + assert main("join", "Club%2FPhotos") == 5 # a member now, not admitted + err = capsys.readouterr().err + assert "join Club%2FPhotos@bob-test --code CODE" in err + + assert main("join", "Club%2FPhotos", "--code", "k7p2-9wqx") == 0 + assert tries[-1]["code"] == "K7P2-9WQX" + + monkeypatch.setattr("sys.stdin", _Terminal("ab12-cd34\n")) + assert main("join", "Club%2FPhotos@bob-test") == 0 + assert tries[-1]["code"] == "AB12-CD34" + + +def test_an_invitation_link_goes_to_its_hub_and_its_code_to_its_node(hub, joined, capsys): + tries, state = joined + ticket, node = "AbCdEfGhIjKlMnOpQr-_12", "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8" + link = (f"{hub.url}/#/invite?v=1&g=0f8fad5b-d9cb-469f-a165-70867728950e&t={ticket}" + f"&n={node}&c=k7p2-9wqx") + hub.links[ticket] = {**CLUB, "id": "0f8fad5b-d9cb-469f-a165-70867728950e"} + _signed_in(hub, capsys) + + assert main("join", link.replace(hub.url, "https://elsewhere.test")) == 1 + assert "--hub https://elsewhere.test" in capsys.readouterr().err + + state["away"] = True # redeemed, but the node is not there + assert main("join", link) == 5 + assert "The code is kept" in capsys.readouterr().err + assert tries[-1] == {"group": "0f8fad5b-d9cb-469f-a165-70867728950e", + "code": "K7P2-9WQX", + "node_pk": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="} + from meshbay_cli.client.store import Store + store = Store() + store.load() + assert store.secrets["join_codes"]["0f8fad5b-d9cb-469f-a165-70867728950e"] == { + "code": "K7P2-9WQX", "node_pk": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="} + + assert main("join", link) == 3 # spent: the hub says so + assert "no longer works" in capsys.readouterr().err + + +def test_declining_and_leaving_ask_first(hub, monkeypatch, capsys): + hub.pending.append({**CLUB, "invited_by": "bob-test"}) + hub.groups.append({**CLUB, "id": "g3", "name": "Choir"}) + _signed_in(hub, capsys) + + monkeypatch.setattr("sys.stdin.isatty", lambda: False) + assert main("decline", "Club%2FPhotos") == 1 # no terminal, no --yes + assert "pass --yes" in capsys.readouterr().err and hub.pending + assert main("leave", "Choir") == 1 + assert any(g["id"] == "g3" for g in hub.groups) + assert main("leave", "Family") == 8 # the owner, before any question + assert "You own" in capsys.readouterr().err + + monkeypatch.setattr("sys.stdin", _Terminal("n\n")) + assert main("leave", "Choir") == 1 + assert any(g["id"] == "g3" for g in hub.groups) + monkeypatch.setattr("sys.stdin", _Terminal("y\n")) + assert main("leave", "Choir") == 0 + assert "Left Choir@bob-test" in capsys.readouterr().out + assert not any(g["id"] == "g3" for g in hub.groups) + + assert main("decline", "--yes", "Club%2FPhotos") == 0 + assert not hub.pending and not any(g["id"] == "g2" for g in hub.groups) + + +def test_devices_are_listed_approved_by_code_and_revoked(node, monkeypatch, capsys): + from meshbay_cli.client.groups import open_group # noqa: F401 (patched below) + from meshbay_cli.commands import devices + + async def open_group_(account, group): + return node.session() + monkeypatch.setattr(devices, "open_group", open_group_) + + other = node.request_device("ZX4P-7QRT") + assert main("devices", "list", "Family") == 0 + out = capsys.readouterr() + assert out.out.splitlines()[1].startswith(f"* {node.identity.pk_ed[:16]}") + assert "1 device waiting for approval" in out.err + + assert main("devices", "approve", "Family", "zx4p-7qrz") == 3 # no request has it + assert main("devices", "approve", "Family", "ZX4P 7QRT") == 0 + assert other.pk_ed in [d["pk_ed25519"] for d in node.devices] + + # The same code filed twice: neither is approved. + node.request_device("AAAA-BBBB") + node.request_device("AAAA-BBBB") + assert main("devices", "approve", "Family", "AAAA-BBBB") == 3 + assert "none was approved" in capsys.readouterr().err + assert len(node.devices) == 2 + + monkeypatch.setattr("sys.stdin.isatty", lambda: False) + assert main("devices", "revoke", "Family", node.identity.pk_ed[:12], "--yes") == 8 + assert main("devices", "revoke", "Family", other.pk_ed[:12]) == 1 # not confirmed + assert main("devices", "revoke", "Family", other.pk_ed[:12], "--yes") == 0 + assert [d["pk_ed25519"] for d in node.devices] == [node.identity.pk_ed] + assert main("devices", "approve", "Family") == 1 + + +def test_browser_access_is_set_here_and_mirrored_never_taken_from_the_hub( + hub, joined, capsys): + hub.prefs["browser_access"] = "on" # anyone with a session can write it + _signed_in(hub, capsys) + assert main("--json", "browser-access") == 0 + assert json.loads(capsys.readouterr().out) == {"this_machine": None, "hub": "on"} + from meshbay_cli.client.store import Store + store = Store() + store.load() + assert "browser_access" not in store.state + + assert main("browser-access", "off") == 0 + assert hub.prefs["browser_access"] == "off" + store.load() + assert store.state["browser_access"] == "off" + # Set here: `join` has nothing to point out. + hub.pending.append({**CLUB, "invited_by": "bob-test"}) + assert main("join", "Club%2FPhotos") == 0 + assert "browser-access" not in capsys.readouterr().err + + # Kept through a new sign-in of the same account. + _signed_in(hub, capsys) + store.load() + assert store.state["browser_access"] == "off" + + def test_every_action_is_run_here(): """Runs last in this module: every action the parser knows was run above.""" assert RUN == set(cli.ACTIONS), f"never run: {set(cli.ACTIONS) - RUN}" |