aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_node_link_banner.py93
-rw-r--r--packages/meshbay-hub/tests/test_node_page_pairing.py2
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():