From d3ad243c4ae3a273f623bd5fc631e3266aa4d0e4 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 30 Sep 2026 11:49:56 +0200 Subject: fix: only the owner decides who hosts a group, and nobody is made a member unasked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hub: a node may host a group only if its account owns it or the owner approved that node (new `group_hosts`). Membership was the ceiling, and every member holds the group key, so any member's node could register as a host and be the one clients kept. A node claiming a group it may not host is recorded as a request; the owner is notified once and approves or refuses it (GET/POST/DELETE /v1/groups/{id}/hosts[/{node_id}]), which takes effect on a connected node at once. - hub: an owner adding a username creates an invitation (new `group_invitations`), accepted or declined by the invitee (/v1/groups/invitations, /{id}/invitation/accept|decline). Until then the group is not listed, not dialled, not searched and not in any token. Invitation links, open joins and group creation still make members directly: they are the account's own act. - hub: the MNP token names only the group it is minted for (group_id is now required), so a node operator no longer learns a member's other groups. - SPA: invitations on the home page; invited people and host requests in the group's settings; the transport sends group_id. Ten catalogues. - Browser probes for both screens, run in Chrome and Firefox. - Design §5.2, §7.2, §7.3, AV32, AV33; protocol §6.3; user guide. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-hub/tests/test_invitation_ui.py | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/meshbay-hub/tests/test_invitation_ui.py (limited to 'packages/meshbay-hub/tests/test_invitation_ui.py') diff --git a/packages/meshbay-hub/tests/test_invitation_ui.py b/packages/meshbay-hub/tests/test_invitation_ui.py new file mode 100644 index 0000000..99538e9 --- /dev/null +++ b/packages/meshbay-hub/tests/test_invitation_ui.py @@ -0,0 +1,66 @@ +""" +The two answers a person now gives, in the real application, in both engines. + +An invitee answers an invitation on the home page (harness/invitation_probe.py); +a group owner answers a node that asked to host the group, and sees who was +invited and has not answered (harness/group_hosts_probe.py). The hub side is +`test_invitations_and_hosts.py`; this is where it meets the interface. +""" + +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +HARNESS = Path(__file__).parent / "harness" +BINARY = {"chrome": "google-chrome", "firefox": "firefox"} + + +def _run(probe: str, engine: str): + if shutil.which(BINARY[engine]) is None: + pytest.skip(f"{engine} is not available") + proc = subprocess.run([sys.executable, str(HARNESS / probe), "--engine", engine], + capture_output=True, text=True, timeout=240) + assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}" + return json.loads(proc.stdout) + + +@pytest.fixture(scope="module", params=["chrome", "firefox"]) +def invitation(request): + return {c["case"]: c for c in _run("invitation_probe.py", request.param)} + + +@pytest.fixture(scope="module", params=["chrome", "firefox"]) +def hosts(request): + return _run("group_hosts_probe.py", request.param) + + +def test_an_invitation_is_listed_with_its_two_answers(invitation): + for c in invitation.values(): + assert "error" not in c, c["error"] + assert c["listed"] and c["buttons"] == 2 + + +def test_accepting_joins_and_shows_the_group(invitation): + c = invitation["accept"] + assert c["answer_call"] == [ + "POST /v1/groups/0f8fad5b-d9cb-469f-a165-70867728950e/invitation/accept"] + assert c["group_card"] and not c["invitation_still_shown"] + + +def test_declining_leaves_no_group(invitation): + c = invitation["decline"] + assert c["answer_call"][0].endswith("/invitation/decline") + assert not c["group_card"] and not c["invitation_still_shown"] + + +def test_the_owner_sees_the_invited_and_approves_a_host(hosts): + assert "error" not in hosts, hosts.get("error") + assert hosts["invited_row"] + assert hosts["host_rows"] == 2 + assert hosts["buttons_on_request"] == 2 # approve and refuse + assert hosts["buttons_on_approved"] == 1 # refuse only + assert hosts["decision"] == ["POST /v1/groups/g1/hosts/n-asking"] -- cgit v1.2.3