summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/tests/test_cli_actions.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_cli_actions.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_cli_actions.py')
-rw-r--r--packages/meshbay-cli/tests/test_cli_actions.py192
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}"