""" The browser's half of an invitation link (docs/MESHBAY_DESIGN.md §3.4). Three properties, each run against the shipped code rather than restated: - **one shape.** The hub writes a link when it mails one (`invite_url`), the page writes one when it shows one (`buildInviteLink`), and the page reads both (`parseInvite`). A disagreement is a link that opens on nothing. - **the code leaves the address at once, and the tab keeps it.** Run in node against a stand-in `window`: `captureFromLocation` rewrites the address and stores what it read, and a malformed link is cleaned out without being kept. - **the code goes to the node the link names, and to no other.** The transport's `_linkJoinRefusal` is what stops it; this runs it. The rest are read from the source, which is the evidence there is for them: the hub is never handed the code except when the inviter ticked the mail box, the capture is the first thing `app.js` loads, and signing out forgets the invitation. """ import base64 import json import re import shutil import subprocess from pathlib import Path import pytest from meshbay_hub import mail as mail_mod from meshbay_hub.api import invite_links from spa_source import transport_source STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" LINK_JS = STATIC / "invite-link.js" pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node is not available") GROUP = "0f8fad5b-d9cb-469f-a165-70867728950e" TICKET = "AbCdEfGhIjKlMnOpQr-_12" NODE_PK_STD = base64.b64encode(bytes(range(32))).decode() # has '+', '/', '=' CODE = "K7P2-9WQX" def _module_body() -> str: """invite-link.js with its import, its exports and its load-time capture removed — the functions as shipped, runnable against a stand-in window.""" src = LINK_JS.read_text(encoding="utf-8") src = re.sub(r"^import .*?;\n", "", src, flags=re.M) src = src.replace("export function", "function") tail = "\ncaptureFromLocation();\nwindow.addEventListener('hashchange', captureFromLocation);\n" assert src.endswith(tail), "invite-link.js no longer ends with its load-time capture" return src[: -len(tail)] def _run(tmp_path, script: str): harness = tmp_path / "h.js" harness.write_text(script) out = subprocess.run(["node", str(harness)], capture_output=True, text=True, timeout=60) assert out.returncode == 0, out.stderr return json.loads(out.stdout) _WINDOW = r""" const store = new Map(); globalThis.sessionStorage = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k), }; const replaced = []; globalThis.window = { location: { hash: '', pathname: '/', search: '' }, history: { replaceState: (_s, _t, url) => replaced.push(url) }, addEventListener() {}, }; const platform = { hubOrigin: () => 'https://hub.example' }; """ def test_one_shape_between_the_hub_and_the_page(tmp_path, monkeypatch): monkeypatch.setattr(mail_mod, "_hub_url", "https://hub.example") n = NODE_PK_STD.replace("+", "-").replace("/", "_").rstrip("=") from_hub = invite_links.invite_url(GROUP, TICKET, n, CODE) got = _run(tmp_path, _WINDOW + _module_body() + f""" const fields = {{ g: '{GROUP}', t: '{TICKET}', n: nodePkForLink('{NODE_PK_STD}'), c: '{CODE}' }}; process.stdout.write(JSON.stringify({{ parsed: parseInvite({json.dumps(from_hub)}), built: buildInviteLink('https://hub.example', fields), back: nodePkFromLink(fields.n), lower: parseInvite({json.dumps(from_hub.replace(CODE, CODE.lower()))}), }})); """) assert got["parsed"] == {"g": GROUP, "t": TICKET, "n": n, "c": CODE} assert got["built"] == from_hub assert got["back"] == NODE_PK_STD, "the key the transport compares must come back exact" assert got["lower"]["c"] == CODE def test_the_cli_writes_the_same_link(monkeypatch): """`meshbay-node member invite --link` builds its link on the node, from the hub address it was configured with; it must be the hub's own shape.""" from meshbay_node.ops import _invite_url monkeypatch.setattr(mail_mod, "_hub_url", "https://hub.example") n = NODE_PK_STD.replace("+", "-").replace("/", "_").rstrip("=") assert (_invite_url("https://hub.example/", GROUP, TICKET, NODE_PK_STD, CODE) == invite_links.invite_url(GROUP, TICKET, n, CODE)) @pytest.mark.parametrize("tamper", [ lambda u: u.replace("v=1", "v=2"), lambda u: u.replace(CODE, "K7P2-9WQ"), lambda u: u.replace(CODE, "K7P2-9WQX