""" The Android shell's security contract, pinned by reading its source. The same treatment `test_desktop_shell.py` gives the Electron application, for the same reason: an emulator or a phone is what proves the shell runs, and the suite has neither. What this proves is that the properties the design depends on (docs/MESHBAY_DESIGN.md §8.2, as the Android plan restates them) are in the source, and it fails when one is removed. The JVM unit tests run too when an Android SDK is present. """ import os import re import shutil import subprocess from pathlib import Path import pytest PACKAGES = Path(__file__).resolve().parents[2] ANDROID = PACKAGES / "meshbay-android" APP = ANDROID / "app" SRC = APP / "src" / "main" / "kotlin" / "org" / "meshbay" / "client" SHIM = APP / "src" / "main" / "assets" / "bridge" / "meshbay-bridge.js" CLIENT = PACKAGES / "meshbay-client" pytestmark = pytest.mark.skipif(not ANDROID.exists(), reason="android sources not present") def _read(path: Path) -> str: return path.read_text(encoding="utf-8") def _kotlin() -> str: return "\n".join(_read(p) for p in sorted(SRC.rglob("*.kt"))) def _strip_js_comments(source: str) -> str: source = re.sub(r"/\*.*?\*/", "", source, flags=re.S) return re.sub(r"(?m)//.*$", "", source) # ── The page comes from the package ────────────────────────────────────────── def test_the_interface_is_copied_from_its_single_source_and_never_committed(): build = _read(APP / "build.gradle.kts") assert '"../meshbay-hub/src/meshbay_hub/static"' in build # The desktop application's page: the hub's carries a /a// prefix # that would point back at the hub. assert '"../meshbay-client/scripts/index.html"' in build assert "deleteRecursively()" in build, "a stale file could survive a rebuild" assert "addGeneratedSourceDirectory" in build assert "build/" in _read(ANDROID / ".gitignore") assert not (APP / "src" / "main" / "assets" / "ui").exists(), \ "a copy of the interface is in the source tree, which is how a fork begins" def test_the_webview_loads_the_package_and_nothing_else(): activity = _read(SRC / "MainActivity.kt") loads = re.findall(r"\.loadUrl\(([^)]*)\)", activity) assert loads and set(loads) == {"UiAssets.START"}, loads assert "loadDataWithBaseURL" not in activity and "loadData(" not in activity # The hub never becomes the document origin; a link out leaves the app. assert "shouldOverrideUrlLoading" in activity and "openExternally" in activity def test_the_policy_is_the_desktop_policy_sent_as_a_header(): """One interface, one policy for the packaged builds — and the desktop's is already held to the hub's by test_the_two_policies_stay_in_step.""" def directives(text: str, start: str, end: str) -> dict[str, str]: body = text.split(start, 1)[1].split(end, 1)[0] out = {} for d in re.findall(r"[`\"]([a-z-]+(?: [^`\"]*)?)[`\"]", body): d = d.replace("${RECAPTCHA_SRC}", "$RECAPTCHA_SRC") out[d.split()[0]] = d return out desktop = directives(_read(CLIENT / "src" / "main.js"), "const CSP = [", "].join") kotlin = _read(SRC / "shell" / "UiAssets.kt") android = directives(kotlin, "val CSP = listOf(", ").joinToString") assert desktop and android == desktop, set(android.items()) ^ set(desktop.items()) assets = _read(SRC / "shell" / "UiAssets.kt") assert '"Content-Security-Policy" to CSP' in assets recaptcha = 'RECAPTCHA_SRC = "https://www.google.com https://www.gstatic.com"' assert recaptcha in assets markup = re.sub(r"", "", _read(CLIENT / "scripts" / "index.html"), flags=re.S) assert "Content-Security-Policy" not in markup def test_the_asset_handler_cannot_be_walked_out_of(): assets = _read(SRC / "shell" / "UiAssets.kt") assert '".."' in assets and 'val asset = "ui/"' in assets def test_plain_http_is_refused_except_to_loopback(): hub = _read(SRC / "hub" / "HubClient.kt") assert "The hub address must be https" in hub assert 'Regex("^http://(localhost|127\\\\.)")' in hub config = _read(APP / "src" / "main" / "res" / "xml" / "network_security_config.xml") assert ']*>([^<]+)", config) assert set(allowed) == {"localhost", "127.0.0.1"} def test_the_page_reaches_the_signed_in_hub_only(): hub = _read(SRC / "hub" / "HubClient.kt") assert "Refused: not this hub" in hub and "sameOrigin(target, hub)" in hub assert ".followRedirects(false)" in hub, "a redirect would carry the token elsewhere" # ── The bridge ────────────────────────────────────────────────────────────── def test_no_javascript_interface_is_ever_added(): """addJavascriptInterface injects into every frame of every origin.""" for path in APP.rglob("*.kt"): assert "addJavascriptInterface" not in _read(path), path def test_every_message_is_checked_for_our_top_level_document(): bridge = _read(SRC / "bridge" / "Bridge.kt") check = bridge.split("override fun onPostMessage", 1)[1].split("work.execute", 1)[0] assert "!isMainFrame" in check and "UiAssets.ORIGIN" in check activity = _read(SRC / "MainActivity.kt") assert "addWebMessageListener(web, Bridge.PORT, setOf(UiAssets.ORIGIN)" in activity assert re.search(r"addDocumentStartJavaScript\(web, .*setOf\(UiAssets\.ORIGIN\)\)", activity) def _shim_channels() -> set[str]: return set(re.findall(r"call\('([\w:-]+)'", _strip_js_comments(_read(SHIM)))) def test_the_shim_offers_desktop_channels_and_native_answers_each(): preload_js = _read(CLIENT / "src" / "preload.js") preload = set(re.findall(r"ipcRenderer\.invoke\('([\w:-]+)'", preload_js)) channels_kt = _read(SRC / "bridge" / "Channels.kt") native = set(re.findall(r'^\s*"([\w:-]+)" ->', channels_kt, flags=re.M)) shim = _shim_channels() assert shim, "no channel found in the shim" assert shim <= preload, f"channels the desktop does not have: {shim - preload}" assert shim == native, f"shim and native disagree: {shim ^ native}" def test_what_a_phone_does_not_have_is_absent_not_refusing(): """platform.js decides what to show from whether an object exists (`platform.node.available`, `platform.folder.available`, …): an object that only refused would put screens on the page that fail when used.""" shim = _strip_js_comments(_read(SHIM)) exposed = shim.split("const meshbay = {", 1)[1].split("\n };", 1)[0] for absent in ("node:", "rootPicker:", "minimizeToTray:", "setTrayLabels:"): assert absent not in exposed, absent assert "nodeAdmin: false" in exposed and "localFolders: false" in exposed def test_the_bridge_is_frozen_and_the_port_hidden(): shim = _strip_js_comments(_read(SHIM)) assert "delete window.meshbayNative" in shim assert "window.top !== window" in shim assert "writable: false, configurable: false" in shim assert "Object.freeze" in shim def test_file_system_access_is_removed_from_the_page(): """The WebView exposes it (spike S-1) and cannot back it with anything.""" shim = _strip_js_comments(_read(SHIM)) for name in ("showDirectoryPicker", "showSaveFilePicker", "showOpenFilePicker"): assert f"'{name}'" in shim, name def test_the_hub_address_is_injected_not_fetched(): """platform.hubBase() is called while modules load, before anything can await.""" assert "HUB_BASE" in _strip_js_comments(_read(SHIM)) assert "const HUB_BASE = ${JSONObject.quote(hub.base)}" in _read(SRC / "MainActivity.kt") def test_the_node_setup_welcome_needs_a_node(): """A phone has a bridge and no node: with no groups yet it must see the invitations and the join link, not a node wizard it cannot finish.""" from spa_source import STATIC app = _read(STATIC / "app.js") home = app.split("function HomePage(", 1)[1].split("\n}\n", 1)[0] gate = home.split("<${SetupWelcome}", 1)[0] assert "platform.capabilities.nodeAdmin" in gate assert "platform.isNative" not in gate # ── The window ────────────────────────────────────────────────────────────── def test_nothing_is_granted_and_video_may_go_fullscreen(): activity = _read(SRC / "MainActivity.kt") assert "onPermissionRequest(request: PermissionRequest) = request.deny()" in activity # Without a custom view, requestFullscreen() never settles (CLAUDE.md). assert "override fun onShowCustomView" in activity assert "override fun onHideCustomView" in activity def test_no_backup_carries_the_keys_away(): manifest = _read(APP / "src" / "main" / "AndroidManifest.xml") assert 'android:allowBackup="false"' in manifest assert 'android:dataExtractionRules="@xml/data_extraction_rules"' in manifest def test_the_gradle_distribution_is_pinned_by_checksum(): props = _read(ANDROID / "gradle" / "wrapper" / "gradle-wrapper.properties") assert re.search(r"^distributionSha256Sum=[0-9a-f]{64}$", props, flags=re.M) def test_the_version_is_the_packages_version(): """All packages share one version (CLAUDE.md); this one reads it rather than writing it a second time.""" build = _read(APP / "build.gradle.kts") assert 'rootDir.resolve("../meshbay-client/package.json")' in build assert not re.search(r'versionName = "\d', build) @pytest.mark.skipif(not os.environ.get("ANDROID_HOME") or shutil.which("java") is None, reason="no Android SDK in the environment") def test_the_jvm_unit_tests_pass(): result = subprocess.run(["./gradlew", "--no-daemon", "-q", "testDebugUnitTest"], cwd=ANDROID, capture_output=True, text=True, timeout=900) assert result.returncode == 0, result.stdout[-3000:] + result.stderr[-3000:]