aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/node-page.js16
-rw-r--r--packages/meshbay-hub/tests/test_node_page_pairing.py48
2 files changed, 60 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
index a59c100..7fd7ab1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
@@ -13,6 +13,14 @@ import { HUB } from './hub-client.js';
// (platform.node.call), not over MNP/WebRTC. The MNP protocol types remain
// for potential future browser-side use.
+// true / false from a node that has read its roster, null from one that has
+// not yet (it publishes the roster only once it has signed in to the hub) --
+// which is not the same as having no operator.
+export function pairedFrom(result) {
+ const v = result && result.operator_paired;
+ return v === true || v === false ? v : null;
+}
+
function NodeServicePanel({ onChanged, token, username }) {
const [info, setInfo] = useState(null);
const [busy, setBusy] = useState('');
@@ -202,7 +210,7 @@ export function NodePage({ groups, token, username }) {
const [editIce, setEditIce] = useState(null);
const [savingIce, setSavingIce] = useState(false);
const [iceInput, setIceInput] = useState('');
- const [operatorPaired, setOperatorPaired] = useState(false);
+ const [operatorPaired, setOperatorPaired] = useState(null);
const [pairBusy, setPairBusy] = useState(false);
const [pairStatus, setPairStatus] = useState('');
const [nodeInfo, setNodeInfo] = useState(null);
@@ -231,7 +239,7 @@ export function NodePage({ groups, token, username }) {
}
const result = await nodeCall('GET', '/api/groups');
setNodeGroups(result.groups || []);
- setOperatorPaired(!!result.operator_paired);
+ setOperatorPaired(pairedFrom(result));
setNodeSettings(result.settings || null);
try { setNodeInfo(await nodeCall('GET', '/api/status')); } catch {}
setStatus('connected');
@@ -253,7 +261,7 @@ export function NodePage({ groups, token, username }) {
try {
const result = await nodeCall('GET', '/api/groups');
setNodeGroups(result.groups || []);
- setOperatorPaired(!!result.operator_paired);
+ setOperatorPaired(pairedFrom(result));
setNodeSettings(result.settings || null);
try { setNodeInfo(await nodeCall('GET', '/api/status')); } catch {}
} catch {}
@@ -711,7 +719,7 @@ export function NodePage({ groups, token, username }) {
</div>
<${NodeServicePanel} onChanged=${fetchStatus} token=${token} username=${username} />
${actionMsg && html`<div class="node-message">${actionMsg}</div>`}
- ${!operatorPaired && html`
+ ${operatorPaired === false && html`
<div class="node-pair-banner">
<p>${t('node.pair_needed')}</p>
<button class="btn btn-primary btn-small"
diff --git a/packages/meshbay-hub/tests/test_node_page_pairing.py b/packages/meshbay-hub/tests/test_node_page_pairing.py
new file mode 100644
index 0000000..435488e
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_node_page_pairing.py
@@ -0,0 +1,48 @@
+"""
+The Node page's "No operator paired" banner.
+
+A node publishes its roster only once it has signed in to the hub, and until
+then it has no way to know who is paired. It used to answer `false` in that
+window and the page took `!operator_paired` for "not paired" -- so a node stuck
+waiting for its account told its operator to pair again, about a pairing that
+was intact, and sent them after the wrong problem.
+"""
+
+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"
+
+
+def _source() -> str:
+ return NODE_PAGE.read_text(encoding="utf-8")
+
+
+@pytest.mark.skipif(shutil.which("node") is None, reason="node is not available")
+def test_paired_is_unknown_unless_the_node_says_true_or_false(tmp_path):
+ m = re.search(r"^export function pairedFrom\(.*?^\}", _source(), re.M | re.S)
+ assert m, "node-page.js no longer has pairedFrom"
+ script = tmp_path / "case.js"
+ cases = [{"operator_paired": True}, {"operator_paired": False},
+ {"operator_paired": None}, {}, None, {"operator_paired": "yes"}]
+ script.write_text(m.group(0).replace("export ", "")
+ + f"\nconsole.log(JSON.stringify({json.dumps(cases)}.map(pairedFrom)));",
+ encoding="utf-8")
+ out = subprocess.run(["node", str(script)], capture_output=True, encoding="utf-8", check=True)
+ assert json.loads(out.stdout) == [True, False, None, None, None, None]
+
+
+def test_the_banner_needs_an_explicit_false():
+ src = _source()
+ banner = src.index('class="node-pair-banner"')
+ guard = src.rindex("${", 0, banner)
+ assert src[guard:banner].startswith("${operatorPaired === false &&"), (
+ "the pairing banner must not show for a node that has not read its roster")
+ assert "useState(null)" in src.split("const [operatorPaired", 1)[1].split("\n", 1)[0]
+ assert "setOperatorPaired(!!" not in src