From 35a7764db3f58a93c32206cb3ce74bb2f03967e7 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 23 Sep 2026 18:05:14 +0200 Subject: feat(hub): open, create and join invitation links in the interface #/invite takes the link out of the address on load and keeps it in the tab through registration and sign-in; joining is one click, only the ticket goes to the hub, and the code goes only to the node the link names once it has signed its challenge. Members tab gains "Invite by link" (shared e-mail box, pending list, cancel both halves); home page takes a pasted link. Browser probe drives the real app, signed out and in. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-hub/tests/test_invite_link_flow.py | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/meshbay-hub/tests/test_invite_link_flow.py (limited to 'packages/meshbay-hub/tests/test_invite_link_flow.py') diff --git a/packages/meshbay-hub/tests/test_invite_link_flow.py b/packages/meshbay-hub/tests/test_invite_link_flow.py new file mode 100644 index 0000000..42f461c --- /dev/null +++ b/packages/meshbay-hub/tests/test_invite_link_flow.py @@ -0,0 +1,61 @@ +""" +An invitation link, opened in the real application (harness/invite_link_probe.py). + +The functions behind a link are tested one by one in +`test_invite_link_client.py`; this is where they meet the router, the sign-in +state and the hub. Two readers: one with no account, who must be sent to +register with the invitation kept, and one signed in, who must be shown it, +join with one click and land on the group. For both, the code never appears in +a request to the hub — it is the node's, and the hub is only handed the ticket. +""" + +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +HARNESS = Path(__file__).parent / "harness" / "invite_link_probe.py" + + +@pytest.fixture(scope="module") +def cases(): + if shutil.which("google-chrome") is None: + pytest.skip("Chrome is not available") + proc = subprocess.run([sys.executable, str(HARNESS)], + capture_output=True, text=True, timeout=180) + assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}" + out = {c["case"]: c for c in json.loads(proc.stdout)} + for c in out.values(): + assert "error" not in c, c["error"] + return out + + +@pytest.mark.parametrize("case", ["signed_out", "signed_in"]) +def test_the_code_is_out_of_the_address_and_kept_in_the_tab(cases, case): + c = cases[case] + assert c["hash_after_load"] == "#/invite" + assert c["pending"] and c["pending"]["c"] == "K7P2-9WQX" + + +@pytest.mark.parametrize("case", ["signed_out", "signed_in"]) +def test_the_code_never_reaches_the_hub(cases, case): + assert cases[case]["code_in_a_hub_request"] is False + + +def test_a_reader_with_no_account_is_sent_to_register_with_the_invitation_kept(cases): + c = cases["signed_out"] + assert c["register_link"] and c["hash_after_click"] == "#/register" + assert c["pending_after_click"] is True + assert not any("/invite-links/" in u for u in c["hub_calls"]), ( + "nothing about the invitation is asked of the hub before sign-in") + + +def test_a_signed_in_reader_joins_with_one_click_and_lands_on_the_group(cases): + c = cases["signed_in"] + assert "the-owner" in c["text_after_load"] and "Some Group" in c["text_after_load"] + assert c["join_button"] + assert c["redeem_bodies"] == ['{"ticket":"AbCdEfGhIjKlMnOpQr-_12"}'] + assert c["hash_after_click"] == "#/group/0f8fad5b-d9cb-469f-a165-70867728950e" -- cgit v1.2.3