aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_node_link_banner.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_node_link_banner.py')
-rw-r--r--packages/meshbay-hub/tests/test_node_link_banner.py84
1 files changed, 52 insertions, 32 deletions
diff --git a/packages/meshbay-hub/tests/test_node_link_banner.py b/packages/meshbay-hub/tests/test_node_link_banner.py
index eef29bf..b043a5b 100644
--- a/packages/meshbay-hub/tests/test_node_link_banner.py
+++ b/packages/meshbay-hub/tests/test_node_link_banner.py
@@ -1,13 +1,13 @@
"""
A node that cannot serve the signed-in account says so, and the account can
-let go of a node from anywhere.
+let go of any of its nodes 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.
+An account links one key per machine it runs a node on. Signing in on a
+desktop links this machine's node (main.js `ensureNode`) beside the others,
+never 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 -- the Profile page lists every linked node.
"""
import json
@@ -33,13 +33,15 @@ def test_the_problem_is_worked_out_from_the_node_and_the_hub(tmp_path):
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
+ [mine, ["K1"], "alice", "https://hub.example"], # linked here: fine
+ [mine, ["K2", "K1"], "alice", "https://hub.example"], # linked beside another: fine
+ [mine, None, "alice", "https://hub.example"], # hub not answered yet
+ [mine, [], "alice", "https://hub.example"], # nothing linked
+ [mine, ["K2"], "alice", "https://hub.example"], # only another machine linked
+ [{**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(
@@ -49,7 +51,8 @@ def test_the_problem_is_worked_out_from_the_node_and_the_hub(tmp_path):
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]
+ None, None, None, "not_linked", "not_linked", "other_account", "other_account",
+ "not_linked", None]
def test_the_node_page_shows_the_banner_and_offers_the_link():
@@ -58,18 +61,17 @@ def test_the_node_page_shows_the_banner_and_offers_the_link():
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
+ assert "linkedNodeKeys(token)" in banner
+ assert "nodeLinkProblem(info, linkedKeys, 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
+ # This node's key is added beside the account's others, so nothing is cut
+ # off and nothing needs asking. node:start left a node answering "running"
+ # alone (found clicking it on a real install). For another account
+ # node:start asks itself before switching, and takeOver gets past "running".
+ assert "ask(" not 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",
+ for key in ("node.link_not_linked", "node.link_other_account", "node.link_here",
"node.link_take_over", "node.link_linking"):
assert f"'{key}'" in banner, key
@@ -82,12 +84,30 @@ def test_node_start_switches_a_running_node_when_asked_to_take_it_over():
assert "already.status === 'running' && !(opts && opts.takeOver)" in start
-def test_the_profile_page_can_unlink_the_node():
+def test_the_profile_page_lists_every_node_and_unlinks_one():
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
+ assert "linkedNodeKeys(user.token)" in src
+ unlink = src.split("const unlinkNodeKey = useCallback(async (pk) => {", 1)[1]
+ unlink = unlink.split("}, [", 1)[0]
+ assert "await ask(t('settings.node_key_unlink_confirm', { key: shortNodeKey(pk) }))" in unlink
+ # The one named, never the account's whole set.
+ assert "method: 'DELETE', token: user.token, body: { pk_node_ed25519: pk }" in unlink
+ assert "filter(k => k.pk_node_ed25519 !== pk)" in unlink
+ row = src.split("${nodeKeys.map(k => html`", 1)[1].split("</li>`)}", 1)[0]
+ assert "onClick=${() => unlinkNodeKey(k.pk_node_ed25519)}" in row
+ assert "settings.node_keys_this_machine" in row
+
+
+def test_the_profile_page_says_nothing_before_the_hub_answers():
+ src = _src(PROFILE_PAGE)
+ assert "const [nodeKeys, setNodeKeys] = useState(null);" in src
+ assert "${nodeKeys !== null && nodeKeys.length === 0 && html`" in src
+
+
+def test_the_desktop_app_links_beside_other_machines():
+ main_js = (Path(__file__).resolve().parents[3] / "packages" / "meshbay-client" / "src"
+ / "main.js").read_text(encoding="utf-8")
+ ensure = main_js.split("async function ensureNode(opts) {", 1)[1].split("\n }\n", 1)[0]
+ assert "other_node" not in ensure
+ assert "/pubkeys" not in ensure
+ assert "linkNodeKeyAndAwaitRunning(" in ensure