aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_invitation_ui.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-30 11:49:56 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-30 11:49:56 +0200
commitd3ad243c4ae3a273f623bd5fc631e3266aa4d0e4 (patch)
tree95ff1252c80a71e93c5098822d31b835572d8b52 /packages/meshbay-hub/tests/test_invitation_ui.py
parent69554fac7eba6eef7eb8a1c0111c5b92e7f21256 (diff)
downloadmeshbay-d3ad243c4ae3a273f623bd5fc631e3266aa4d0e4.tar.gz
fix: only the owner decides who hosts a group, and nobody is made a member unasked
- 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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_invitation_ui.py')
-rw-r--r--packages/meshbay-hub/tests/test_invitation_ui.py66
1 files changed, 66 insertions, 0 deletions
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"]