diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_node_link_banner.py | 93 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_node_page_pairing.py | 2 |
2 files changed, 94 insertions, 1 deletions
diff --git a/packages/meshbay-hub/tests/test_node_link_banner.py b/packages/meshbay-hub/tests/test_node_link_banner.py new file mode 100644 index 0000000..eef29bf --- /dev/null +++ b/packages/meshbay-hub/tests/test_node_link_banner.py @@ -0,0 +1,93 @@ +""" +A node that cannot serve the signed-in account says so, and the account can +let go of a node from anywhere. + +Signing in on a desktop links this machine's node to the account (main.js +`ensureNode`), but never over a key already linked -- that would cut off the +user's other machine -- nor a node set up for another account. Found on a real +install (2026-10-10): the node read "Running" while the hub refused it in a +loop, and nothing said why. And the only way to unlink was the linked +machine's own Node page, of no use once that machine is gone. +""" + +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" +NODE_PAGE = STATIC / "node-page.js" +PROFILE_PAGE = STATIC / "profile-page.js" + + +def _src(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +@pytest.mark.skipif(shutil.which("node") is None, reason="node is not available") +def test_the_problem_is_worked_out_from_the_node_and_the_hub(tmp_path): + m = re.search(r"^export function nodeLinkProblem\(.*?^\}", _src(NODE_PAGE), re.M | re.S) + assert m, "node-page.js no longer has nodeLinkProblem" + mine = {"username": "alice", "hub_url": "https://hub.example/", "pk_node_ed25519": "K1"} + cases = [ + [mine, "K1", "alice", "https://hub.example"], # linked here: fine + [mine, None, "alice", "https://hub.example"], # nothing linked: sign-in links it + [mine, "K2", "alice", "https://hub.example"], # another node's key + [{**mine, "username": "bob"}, "K1", "alice", "https://hub.example"], + [mine, "K1", "alice", "https://other.example"], # set up for another hub + [mine, "K2", "alice", ""], # browser: same-origin hub + [None, "K2", "alice", "https://hub.example"], # node not answering + ] + script = tmp_path / "case.js" + script.write_text( + m.group(0).replace("export ", "") + + f"\nconsole.log(JSON.stringify({json.dumps(cases)}" + ".map(c => nodeLinkProblem(...c))));", + encoding="utf-8") + out = subprocess.run(["node", str(script)], capture_output=True, encoding="utf-8", check=True) + assert json.loads(out.stdout) == [ + None, None, "other_node", "other_account", "other_account", "other_node", None] + + +def test_the_node_page_shows_the_banner_and_offers_the_link(): + src = _src(NODE_PAGE) + page = src.split("export function NodePage(", 1)[1] + assert "<${NodeLinkBanner} info=${nodeInfo} token=${token} username=${username}" in page + banner = src.split("function NodeLinkBanner(", 1)[1].split("\nfunction ", 1)[0] + # Asked of the hub, not remembered from sign-in. + assert "hubFetch(`/v1/users/${encodeURIComponent(username)}/pubkeys`" in banner + assert "nodeLinkProblem(info, linkedKey, username, HUB)" in banner + take_over = banner.split("const takeOver = async () => {", 1)[1].split("\n };", 1)[0] + # Replacing another machine's link is asked first, then this node's key + # replaces it directly: node:start left a node answering "running" alone, + # and a node signed in before the account was linked elsewhere does + # (found clicking it on a real install). For another account node:start + # asks itself before switching, and takeOver gets past "running". + assert "problem === 'other_node' && !await ask(t('node.link_here_confirm'))" in take_over + assert "method: 'PUT', token, body: { pk_node_ed25519: nodeKey }" in take_over + assert "platform.node.start({ hubUrl: HUB, username, token, takeOver: true })" in take_over + for key in ("node.link_other_node", "node.link_other_account", "node.link_here", + "node.link_take_over", "node.link_linking"): + assert f"'{key}'" in banner, key + + +def test_node_start_switches_a_running_node_when_asked_to_take_it_over(): + main_js = (Path(__file__).resolve().parents[3] / "packages" / "meshbay-client" / "src" + / "main.js").read_text(encoding="utf-8") + start = main_js.split("handle('node:start', async (_e, opts) => {", 1)[1] + start = start.split("\n });\n", 1)[0] + assert "already.status === 'running' && !(opts && opts.takeOver)" in start + + +def test_the_profile_page_can_unlink_the_node(): + src = _src(PROFILE_PAGE) + unlink = src.split("const unlinkNodeKey = useCallback(async () => {", 1)[1].split("}, [", 1)[0] + assert "if (!await ask(t('settings.node_key_unlink_confirm'))) return;" in unlink + assert "hubFetch('/v1/users/me/node_key', { method: 'DELETE', token: user.token })" in unlink + assert "setCurrentNodeKey(null)" in unlink + # Offered only where there is a key to let go of. + shown = src.split("${currentNodeKey && html`", 1)[1].split("`}", 1)[0] + assert "onClick=${unlinkNodeKey}" in shown diff --git a/packages/meshbay-hub/tests/test_node_page_pairing.py b/packages/meshbay-hub/tests/test_node_page_pairing.py index c716e0a..ac77356 100644 --- a/packages/meshbay-hub/tests/test_node_page_pairing.py +++ b/packages/meshbay-hub/tests/test_node_page_pairing.py @@ -56,7 +56,7 @@ def test_pair_this_browser_queues_the_code_instead_of_claiming_success(): pair = src.split("const doPairOperator = useCallback(", 1)[1].split("}, [refresh]);", 1)[0] assert "session.pendingJoinCode = result.code;" in pair assert "setOperatorPaired(true)" not in pair - assert "import { HUB, session } from './hub-client.js';" in src + assert re.search(r"import \{ HUB, session(, \w+)* \} from './hub-client.js';", src) def test_no_pairing_banner_for_a_node_with_no_group_yet(): |