summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_node_link_banner.py
blob: b043a5b9608155c5e400c3eea4377c6479becf05 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""
A node that cannot serve the signed-in account says so, and the account can
let go of any of its nodes from anywhere.

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
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, ["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(
        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, None, "not_linked", "not_linked", "other_account", "other_account",
        "not_linked", 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 "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]
    # 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_not_linked", "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_lists_every_node_and_unlinks_one():
    src = _src(PROFILE_PAGE)
    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