aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_node_link_banner.py
blob: eef29bfba5a98b849cd9ac1a1eccf788b88200b8 (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
"""
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