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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
|
"""
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/<hash>/ 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 '<base-config cleartextTrafficPermitted="false"' in config
allowed = re.findall(r"<domain[^>]*>([^<]+)</domain>", 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))
native = set()
for path in (SRC / "bridge" / "Channels.kt", SRC / "bridge" / "KeyChannels.kt",
SRC / "cast" / "CastChannels.kt"):
native |= set(re.findall(r'^\s*"([\w:-]+)" ->', _read(path), 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:]
|