diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 12:57:58 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 12:57:58 +0200 |
| commit | 2c6921aa2c35ffd41b6c453e6700574ef631ba2c (patch) | |
| tree | 01c766e13607e4f957900bfd36b4f722e8c8b3c5 /packages/meshbay-hub/tests/test_desktop_shell.py | |
| parent | 8a4651e9d223de856ff085b329801998f95db138 (diff) | |
| download | meshbay-2c6921aa2c35ffd41b6c453e6700574ef631ba2c.tar.gz | |
fix(client): the page names node operations, and the app confirms what widens the node
node:call is replaced by named operations with checked arguments; hosting a
group, sharing an unpicked folder, key rotation, denylist clearing and a change
of node account are confirmed by a native dialog. Every channel checks its
sender, secrets:get/set/clear are gone, node:start writes the app's own hub.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_desktop_shell.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_desktop_shell.py | 81 |
1 files changed, 78 insertions, 3 deletions
diff --git a/packages/meshbay-hub/tests/test_desktop_shell.py b/packages/meshbay-hub/tests/test_desktop_shell.py index b9b3319..732ba07 100644 --- a/packages/meshbay-hub/tests/test_desktop_shell.py +++ b/packages/meshbay-hub/tests/test_desktop_shell.py @@ -18,7 +18,7 @@ import re from pathlib import Path import pytest -from spa_source import transport_files +from spa_source import STATIC, transport_files CLIENT = Path(__file__).resolve().parents[2] / "meshbay-client" MAIN = CLIENT / "src" / "main.js" @@ -378,6 +378,81 @@ def test_plain_http_is_refused_except_to_loopback(): assert "127\\." in source and "localhost" in source +def test_every_channel_answers_only_the_packaged_page(): + """ + A channel registered straight on `ipcMain` would answer any frame that got + hold of a bridge; `handle()` checks the sender first. So no channel may be + registered any other way, and every one the preload names is registered. + """ + source = _main() + wrapper = source.split("function handle(channel, fn) {", 1)[1].split("\n}\n", 1)[0] + assert "fromOurPage(event)" in wrapper + assert source.count("ipcMain.handle(") == 1, "a channel skips the sender check" + registered = set(re.findall(r"\n handle\('([\w:-]+)'", source)) + invoked = set(re.findall(r"ipcRenderer\.invoke\('([\w:-]+)'", _preload())) + assert invoked <= registered, f"the preload names unregistered channels: {invoked - registered}" + + +def test_the_page_cannot_read_or_replace_the_secret_store(): + """It holds the device's hub key (§8.2), which the page is never handed.""" + for channel in ("secrets:get", "secrets:set", "secrets:clear"): + assert channel not in _main() and channel not in _preload(), channel + + +def _node_ops() -> dict[str, str]: + """Each operation of `NODE_OPS` in main.js, with its source.""" + block = _main().split("const NODE_OPS = {", 1)[1].split("\n };\n", 1)[0] + parts = re.split(r"\n (\w+):", "\n" + block) + return dict(zip(parts[1::2], parts[2::2])) + + +def test_the_page_names_node_operations_not_routes(): + """ + The page used to hand the main process a method and a path, which made the + whole loopback API the page's. Every operation the interface calls exists, + and none exists that it does not call — an unused one is surface. + """ + assert "node:call" not in _main() and "node:call" not in _preload() + used: set[str] = set() + for path in STATIC.glob("*.js"): + used |= set(re.findall(r"(?:node\.op|nodeOp)\('(\w+)'", path.read_text(encoding="utf-8"))) + defined = set(_node_ops()) + assert used, "no node operation found in the interface" + assert used <= defined, f"called but not defined: {used - defined}" + assert defined <= used, f"defined but never called: {defined - used}" + + +@pytest.mark.parametrize("op", ["attachGroup", "addRoot", "initGek", "clearDenylist"]) +def test_what_widens_the_node_is_confirmed_natively(op): + """Sharing a folder, hosting a group, replacing the key, re-admitting a + revoked subject: asked by a dialog the main process draws, which a script in + the page cannot answer. A folder chosen in the native picker is its own + confirmation.""" + body = _node_ops()[op] + assert "confirmOrRefuse(" in body or "confirmFolder(" in body + + +def test_the_native_dialogs_are_worded_in_every_language(): + """The words come from the interface's catalogues; a key missing from one is + a dialog that shows its key.""" + keys = set(re.findall(r"(?:confirmOrRefuse|nativeText)\('([\w.]+)'", _main())) + assert "native.declined" in keys and "dialog.ok" in keys + for catalogue in (STATIC / "locales").glob("*.js"): + text = catalogue.read_text(encoding="utf-8") + missing = [k for k in keys if f"'{k}':" not in text] + assert not missing, f"{catalogue.name} lacks {missing}" + + +def test_the_node_is_never_pointed_at_a_hub_the_page_names(): + """`node:start` writes the hub this application is signed in to, and a + username that cannot break out of a TOML string.""" + source = _main() + assert "opts.hubUrl" not in source + provision = source.split("async function provisionFromRequest(opts) {", 1)[1] + provision = provision.split("\n }\n", 1)[0] + assert "config.hubBase" in provision and "USERNAME_RE.test(username)" in provision + + # ── One interface, one source ─────────────────────────────────────────────── def test_the_interface_is_copied_not_forked(): @@ -420,7 +495,7 @@ def test_automatic_saving_never_opens_a_dialog_for_want_of_a_folder(): system Downloads folder is the answer when there is no other. """ source = _main() - begin = source.split("ipcMain.handle('save:begin'", 1)[1].split("ipcMain.handle", 1)[0] + begin = source.split("handle('save:begin'", 1)[1].split("\n handle(", 1)[0] assert "defaultDownloadDir()" in begin, ( "the automatic path has no destination when no folder was chosen") assert "app.getPath('downloads')" in source @@ -430,7 +505,7 @@ def test_a_chosen_folder_that_has_gone_is_not_silently_replaced(): """Someone who picked an external drive should be told it is not there, not find the film in their home directory a week later.""" source = _main() - begin = source.split("ipcMain.handle('save:begin'", 1)[1].split("ipcMain.handle", 1)[0] + begin = source.split("handle('save:begin'", 1)[1].split("\n handle(", 1)[0] assert "config.downloadDir && !chosen" in begin, ( "a chosen-but-missing folder falls through to the default instead of asking") assert "showSaveDialog" in begin |