aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-23 22:25:28 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-24 16:45:37 +0200
commit328b01a2dd545d70a078db8df1e91b02d65bfc9c (patch)
tree9af69d617d9482ba4256f3d7334c9dd6cde0d5e2
parent92ea222b7aca6a3eb5f04330f0af6755e6e434e3 (diff)
downloadmeshbay-328b01a2dd545d70a078db8df1e91b02d65bfc9c.tar.gz
test: read the WebRTC transport's source as a set of files
Source-reading tests take their text from node_source (node) and node_tree (hub): webrtc_server.py plus anything under transport/webrtc/, so a check for something's absence keeps reading the code it guards if that code moves. test_node_source_scope holds the boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--packages/meshbay-hub/tests/node_tree.py54
-rw-r--r--packages/meshbay-hub/tests/test_app_settings_plugin.py17
-rw-r--r--packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py4
-rw-r--r--packages/meshbay-hub/tests/test_video_buffer_ceiling.py12
-rw-r--r--packages/meshbay-hub/tests/test_video_seek.py9
-rw-r--r--packages/meshbay-hub/tests/test_video_stream_switch.py7
-rw-r--r--packages/meshbay-node/tests/node_source.py52
-rw-r--r--packages/meshbay-node/tests/test_app_directories.py7
-rw-r--r--packages/meshbay-node/tests/test_chat_encryption.py4
-rw-r--r--packages/meshbay-node/tests/test_disk_io_off_loop.py31
-rw-r--r--packages/meshbay-node/tests/test_hwaccel.py5
-rw-r--r--packages/meshbay-node/tests/test_node_source_scope.py35
-rw-r--r--packages/meshbay-node/tests/test_roster_pairing.py9
-rw-r--r--packages/meshbay-node/tests/test_security_regressions.py25
-rw-r--r--packages/meshbay-node/tests/test_task_lifetime.py51
-rw-r--r--packages/meshbay-node/tests/test_transport_wire_parity.py30
-rw-r--r--packages/meshbay-node/tests/test_upload_size_cap.py4
17 files changed, 231 insertions, 125 deletions
diff --git a/packages/meshbay-hub/tests/node_tree.py b/packages/meshbay-hub/tests/node_tree.py
new file mode 100644
index 0000000..8704492
--- /dev/null
+++ b/packages/meshbay-hub/tests/node_tree.py
@@ -0,0 +1,54 @@
+"""
+The node's WebRTC transport, as source text, for the hub tests that check a
+client contract against what the node actually sends.
+
+Read from the tree rather than imported, so these tests keep working in a
+checkout without the node installed. The file set is derived — the transport
+is `webrtc_server.py` and whatever has been split out of it under
+`transport/webrtc/` — so that code moving between those files does not leave a
+test reading a file its subject has left. The node suite's `node_source.py`
+draws the same line and holds it with `test_node_source_scope.py`.
+"""
+
+import ast
+from pathlib import Path
+
+# parents[2] is `packages/`: the tests live at packages/meshbay-hub/tests/. A
+# wrong index here does not fail anything — every test using it skips, which
+# is worse than not having them: a green run that measured nothing. That
+# happened once.
+TRANSPORT = (Path(__file__).resolve().parents[2] / "meshbay-node" / "src"
+ / "meshbay_node" / "transport")
+SERVER = TRANSPORT / "webrtc_server.py"
+
+
+def available() -> bool:
+ return SERVER.exists()
+
+
+def webrtc_files() -> list[Path]:
+ files = [SERVER]
+ package = TRANSPORT / "webrtc"
+ if package.is_dir():
+ files += sorted(p for p in package.rglob("*.py")
+ if "__pycache__" not in p.parts)
+ return files
+
+
+def webrtc_source() -> str:
+ return "\n".join(p.read_text(encoding="utf-8") for p in webrtc_files())
+
+
+def method(name: str) -> str:
+ """The text of one method of a class in the transport, found by name."""
+ found = []
+ for path in webrtc_files():
+ text = path.read_text(encoding="utf-8")
+ for node in ast.walk(ast.parse(text)):
+ if isinstance(node, ast.ClassDef):
+ for member in node.body:
+ if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)) \
+ and member.name == name:
+ found.append(ast.get_source_segment(text, member, padded=True))
+ assert len(found) == 1, f"{name}: expected one definition, found {len(found)}"
+ return found[0]
diff --git a/packages/meshbay-hub/tests/test_app_settings_plugin.py b/packages/meshbay-hub/tests/test_app_settings_plugin.py
index 027fee6..a1ecec7 100644
--- a/packages/meshbay-hub/tests/test_app_settings_plugin.py
+++ b/packages/meshbay-hub/tests/test_app_settings_plugin.py
@@ -15,6 +15,7 @@ fault this refactor's new import graph could otherwise reintroduce.
import re
from pathlib import Path
+import node_tree
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
@@ -24,12 +25,6 @@ GROUP_PAGE = STATIC / "group-page.js"
SETTINGS_UI = STATIC / "settings-ui.js"
FOLDER_TREE = STATIC / "folder-tree.js"
TRANSPORT = STATIC / "transport.js"
-# parents[2] is `packages/` — the tests live at
-# packages/meshbay-hub/tests/, so [0] is tests, [1] the package, [2] packages.
-# Got this wrong once and the two cross-package checks below skipped silently,
-# which is worse than not having them: a green run that measured nothing.
-NODE_SERVER = (Path(__file__).resolve().parents[2] / "meshbay-node" / "src"
- / "meshbay_node" / "transport" / "webrtc_server.py")
PANES = ["chat-app-settings.js", "video-app-settings.js",
"music-app-settings.js", "photos-app-settings.js"]
@@ -89,11 +84,10 @@ def test_every_registered_app_has_a_key_the_node_would_accept():
key here that `ALLOWED_APPS` does not have is an app whose settings are
refused by the node with no clue why.
"""
- node = NODE_SERVER
- if not node.exists():
+ if not node_tree.available():
pytest.skip("the node package is not in this checkout")
m = re.search(r"ALLOWED_APPS = frozenset\(\{([^}]*)\}\)",
- node.read_text(encoding="utf-8"))
+ node_tree.webrtc_source())
assert m, "ALLOWED_APPS moved"
allowed = set(re.findall(r"'([^']+)'|\"([^\"]+)\"", m.group(1)))
allowed = {a or b for a, b in allowed}
@@ -207,9 +201,8 @@ def test_the_directory_op_is_signed_and_names_its_app():
assert "admin_challenge" in body and "_authorizeAdminOp" in body
assert "${appKey}:${clean.join(',')}" in body
- node = NODE_SERVER
- if node.exists():
- assert 'f"{app}:{\',\'.join(clean)}"' in node.read_text(encoding="utf-8"), (
+ if node_tree.available():
+ assert 'f"{app}:{\',\'.join(clean)}"' in node_tree.webrtc_source(), (
"the node builds a different subject than the client signs")
diff --git a/packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py b/packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py
index 451b57b..7fc9d8e 100644
--- a/packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py
+++ b/packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py
@@ -31,6 +31,7 @@ the code less app-specific rather than more:
import re
from pathlib import Path
+import node_tree
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
@@ -88,8 +89,7 @@ def test_the_node_names_it_once_and_only_in_the_allow_list():
demonstrate anything. That entry plus the client's registry line is the
whole cost.
"""
- source = (NODE_SRC / "transport" / "webrtc_server.py").read_text(
- encoding="utf-8")
+ source = node_tree.webrtc_source()
code = re.sub(r"^\s*#.*$", "", source, flags=re.M)
hits = [ln for ln in code.splitlines() if "helloworld" in ln.lower()]
assert len(hits) == 1, f"expected one mention, got: {hits}"
diff --git a/packages/meshbay-hub/tests/test_video_buffer_ceiling.py b/packages/meshbay-hub/tests/test_video_buffer_ceiling.py
index 7dd9c81..a488976 100644
--- a/packages/meshbay-hub/tests/test_video_buffer_ceiling.py
+++ b/packages/meshbay-hub/tests/test_video_buffer_ceiling.py
@@ -54,12 +54,11 @@ import shutil
import subprocess
from pathlib import Path
+import node_tree
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
APP = STATIC / "video-player.js"
-NODE_SERVER = (Path(__file__).resolve().parents[2] / "meshbay-node" / "src"
- / "meshbay_node" / "transport" / "webrtc_server.py")
pytestmark = pytest.mark.skipif(
shutil.which("node") is None or not APP.exists(),
@@ -390,20 +389,17 @@ def test_a_buffered_viewer_still_tells_the_node_it_is_there(app):
"the node's stall timeout ends a film that is merely paused")
-@pytest.mark.skipif(not NODE_SERVER.exists(), reason="node sources unavailable")
+@pytest.mark.skipif(not node_tree.available(), reason="node sources unavailable")
def test_the_node_ends_a_stream_on_silence_not_on_stinginess():
"""The other half of the keepalive: the node has to honour it."""
- text = NODE_SERVER.read_text()
- i = text.index("async def _await_stream_credit")
- body = text[i:text.index("\n async def ", i + 1)]
+ body = node_tree.method("_await_stream_credit")
assert "self._stream_heard_at" in body, (
"the stall budget still accumulates over the whole wait, so a keepalive "
"that grants no credit cannot keep a paused film alive")
assert "waited += STREAM_CREDIT_POLL" not in body, (
"the budget still accumulates over the whole wait rather than being "
"measured from the last thing the peer said")
- grant = text[text.index("def _grant_stream_credit"):]
- grant = grant[:grant.index("\n def ", 1)]
+ grant = node_tree.method("_grant_stream_credit")
assert "self._stream_heard_at = time.monotonic()" in grant, (
"n=0 does not refresh the timeout, so the keepalive is a no-op")
diff --git a/packages/meshbay-hub/tests/test_video_seek.py b/packages/meshbay-hub/tests/test_video_seek.py
index 85e774d..665970d 100644
--- a/packages/meshbay-hub/tests/test_video_seek.py
+++ b/packages/meshbay-hub/tests/test_video_seek.py
@@ -33,13 +33,12 @@ import re
import shutil
from pathlib import Path
+import node_tree
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
APP = STATIC / "video-player.js"
TRANSPORT = STATIC / "transport.js"
-NODE_SERVER = (Path(__file__).resolve().parents[2] / "meshbay-node" / "src"
- / "meshbay_node" / "transport" / "webrtc_server.py")
pytestmark = pytest.mark.skipif(not APP.exists(), reason="SPA sources unavailable")
@@ -59,11 +58,9 @@ def _player(app: str) -> str:
@pytest.fixture(scope="module")
def stream_fn():
- if not NODE_SERVER.exists():
+ if not node_tree.available():
pytest.skip("node sources unavailable")
- text = NODE_SERVER.read_text()
- i = text.index("async def _stream_video_inner")
- return text[i:text.index("\n async def ", i + 1)]
+ return node_tree.method("_stream_video_inner")
def test_the_seek_is_an_index_lookup_not_a_decode(stream_fn):
diff --git a/packages/meshbay-hub/tests/test_video_stream_switch.py b/packages/meshbay-hub/tests/test_video_stream_switch.py
index 80d9421..7859057 100644
--- a/packages/meshbay-hub/tests/test_video_stream_switch.py
+++ b/packages/meshbay-hub/tests/test_video_stream_switch.py
@@ -32,6 +32,7 @@ import shutil
import subprocess
from pathlib import Path
+import node_tree
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
@@ -149,11 +150,9 @@ def test_stream_messages_are_matched_to_the_file_they_belong_to(app, handler):
def test_the_node_actually_stamps_those_messages():
"""The guard above is worth nothing if the field is not sent."""
- server = (Path(__file__).resolve().parents[2] / "meshbay-node" / "src"
- / "meshbay_node" / "transport" / "webrtc_server.py")
- if not server.exists():
+ if not node_tree.available():
pytest.skip("the node sources are not available")
- text = server.read_text()
+ text = node_tree.webrtc_source()
for const in ("STREAM_INIT", "STREAM_DATA", "STREAM_END"):
i = text.index(f"MNP.{const}")
block = text[i:i + 400]
diff --git a/packages/meshbay-node/tests/node_source.py b/packages/meshbay-node/tests/node_source.py
new file mode 100644
index 0000000..df63773
--- /dev/null
+++ b/packages/meshbay-node/tests/node_source.py
@@ -0,0 +1,52 @@
+"""
+The node's source text, for the tests that have to read it.
+
+A test that reads source to prove something is *absent* — no bare
+`ensure_future`, no exception text sent to a peer, no GEK unwrapped over MNP —
+goes on passing when the code it guards moves to another file: it simply stops
+looking at that code. So every such test takes its text from here, and the
+file sets below are derived from the tree rather than listed, so that a module
+added later is read without anybody having to remember it.
+"""
+
+import inspect
+import textwrap
+from pathlib import Path
+
+SRC = Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
+TRANSPORT = SRC / "transport"
+
+# The WebRTC transport is `webrtc_server.py` and whatever has been split out of
+# it into `transport/webrtc/`. The other modules beside them are not part of it
+# and are not held to its rules by these tests: QUIC is a transport of its own
+# and not at parity (docs/MESHBAY_DESIGN.md §15.3), and `wire.py`, `tls_cert.py`
+# and the ICE helpers serve both or neither.
+WEBRTC_PACKAGE = TRANSPORT / "webrtc"
+
+
+def webrtc_files() -> list[Path]:
+ """Every module of the WebRTC transport, however it is split up."""
+ files = [TRANSPORT / "webrtc_server.py"]
+ if WEBRTC_PACKAGE.is_dir():
+ files += sorted(p for p in WEBRTC_PACKAGE.rglob("*.py")
+ if "__pycache__" not in p.parts)
+ return files
+
+
+def webrtc_source() -> str:
+ return "\n".join(p.read_text(encoding="utf-8") for p in webrtc_files())
+
+
+def session_source() -> str:
+ """The body of WebRTCPeerSession, including every class it is built from."""
+ from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+
+ return "\n".join(inspect.getsource(k) for k in WebRTCPeerSession.__mro__
+ if k.__module__.startswith("meshbay_node."))
+
+
+def session_method(name: str) -> str:
+ """One method of WebRTCPeerSession, wherever it is defined."""
+ from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+
+ return textwrap.dedent(inspect.getsource(getattr(WebRTCPeerSession, name)))
diff --git a/packages/meshbay-node/tests/test_app_directories.py b/packages/meshbay-node/tests/test_app_directories.py
index 955a769..1b8d1e0 100644
--- a/packages/meshbay-node/tests/test_app_directories.py
+++ b/packages/meshbay-node/tests/test_app_directories.py
@@ -32,6 +32,7 @@ from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roots import RootSet
from meshbay_node.roster import Roster
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+from node_source import session_method
from conftest import one_root
@@ -423,11 +424,7 @@ async def test_the_ack_has_no_list_of_applications_of_its_own():
"""
from meshbay_node.daemon import NodeDaemon
- source = Path(WebRTCPeerSession.__module__.replace(".", "/"))
- text = (Path(__file__).resolve().parents[2] / "meshbay-node" / "src"
- / f"{source}.py").read_text(encoding="utf-8")
- body = text[text.index("def _app_directories_ack"):]
- body = body[:body.index("\n def ", 1)]
+ body = session_method("_app_directories_ack")
for app in NodeDaemon.APP_DIR_KEYS:
assert app not in body, (
f"the ack names {app!r}; it must read the context's own keys")
diff --git a/packages/meshbay-node/tests/test_chat_encryption.py b/packages/meshbay-node/tests/test_chat_encryption.py
index e286306..da8f5dd 100644
--- a/packages/meshbay-node/tests/test_chat_encryption.py
+++ b/packages/meshbay-node/tests/test_chat_encryption.py
@@ -30,6 +30,7 @@ from meshbay_node.chat import FORMAT_SEALED_V1, ChatStore
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roster import open_roster
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+from node_source import webrtc_source
from conftest import one_root
@@ -258,8 +259,7 @@ async def test_there_is_no_setting_that_re_enables_plaintext(node):
assert session.sent[-1]["type"] == "error"
assert await node["chat"].message_count() == 0
- source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
+ source = webrtc_source()
assert 'get("chat_encrypted"' not in source, (
"nothing may read a chat_encrypted setting — there is no switch")
diff --git a/packages/meshbay-node/tests/test_disk_io_off_loop.py b/packages/meshbay-node/tests/test_disk_io_off_loop.py
index ceaf565..f98eceb 100644
--- a/packages/meshbay-node/tests/test_disk_io_off_loop.py
+++ b/packages/meshbay-node/tests/test_disk_io_off_loop.py
@@ -36,6 +36,7 @@ from meshbay_node.roots import Root, RootSet
from meshbay_node.transfers import LeaselessReads
from meshbay_node.transport import webrtc_server
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+from node_source import webrtc_files
from conftest import one_root, sealed_upload
@@ -238,9 +239,10 @@ def test_no_handler_touches_the_disk_on_the_loop():
The whole class, not the calls that were fixed.
Every measured test above exercises a handler that exists today; a new one
- that stats a root inline would pass all of them. So this walks the module's
- syntax tree instead and fails on any filesystem call outside the few
- functions written to be run through `off_disk`.
+ that stats a root inline would pass all of them. So this walks the syntax
+ tree of every module of the WebRTC transport instead and fails on any
+ filesystem call outside the few functions written to be run through
+ `off_disk`.
`entry_abs_path` and `safe_subdir` are in the list because both are
`Path.resolve()` underneath, and a resolve is syscalls whatever it is
@@ -274,14 +276,17 @@ def test_no_handler_touches_the_disk_on_the_loop():
found.append(f"{owner} calls {name}() at line {child.lineno}")
visit(child, owner)
- tree = ast.parse(Path(webrtc_server.__file__).read_text())
- for node in tree.body:
- if isinstance(node, ast.ClassDef):
- for member in node.body:
- if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)):
- visit(member, member.name)
- elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
- visit(node, node.name)
+ for path in webrtc_files():
+ tree = ast.parse(path.read_text(encoding="utf-8"))
+ before = len(found)
+ for node in tree.body:
+ if isinstance(node, ast.ClassDef):
+ for member in node.body:
+ if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ visit(member, member.name)
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ visit(node, node.name)
+ found[before:] = [f"{path.name}: {f}" for f in found[before:]]
assert not found, (
"filesystem calls made from the event loop:\n "
@@ -362,8 +367,8 @@ def test_the_scratch_read_is_only_ever_reached_on_a_thread():
handler. Passed to `asyncio.to_thread` it appears in the syntax tree as a
name; called inline it appears as a call, which is what this refuses.
"""
- tree = ast.parse(Path(webrtc_server.__file__).read_text())
- direct = [n.lineno for n in ast.walk(tree)
+ direct = [f"{path.name}:{n.lineno}" for path in webrtc_files()
+ for n in ast.walk(ast.parse(path.read_text(encoding="utf-8")))
if isinstance(n, ast.Call)
and isinstance(n.func, ast.Name)
and n.func.id == "_read_scratch_capped"]
diff --git a/packages/meshbay-node/tests/test_hwaccel.py b/packages/meshbay-node/tests/test_hwaccel.py
index a79d784..0e6c6ab 100644
--- a/packages/meshbay-node/tests/test_hwaccel.py
+++ b/packages/meshbay-node/tests/test_hwaccel.py
@@ -15,11 +15,10 @@ real file.
import re
import sys
-from pathlib import Path
import pytest
from meshbay_node import hwaccel
-from meshbay_node.transport import webrtc_server
+from node_source import webrtc_source
from conftest import needs_subprocess
@@ -94,7 +93,7 @@ def test_every_encoder_produces_what_the_node_announces():
and every mode is checked against it. Changing the arguments of one encoder
without the other, or changing either without the announced string, fails.
"""
- source = Path(webrtc_server.__file__).read_text(encoding="utf-8")
+ source = webrtc_source()
announced = set(re.findall(r"avc1\.([0-9a-f]{6})", source))
assert announced == {"640029"}, (
"the streaming path announces a codec string this test does not know "
diff --git a/packages/meshbay-node/tests/test_node_source_scope.py b/packages/meshbay-node/tests/test_node_source_scope.py
new file mode 100644
index 0000000..e5a86ff
--- /dev/null
+++ b/packages/meshbay-node/tests/test_node_source_scope.py
@@ -0,0 +1,35 @@
+"""
+The file set `node_source` hands to the source-reading tests has to contain
+the code those tests are about.
+
+It is derived from the tree — `webrtc_server.py` and everything under
+`transport/webrtc/` — so that a module split out later is read without anybody
+adding it to a list. That only holds while the code actually lives there: a
+class the session is built from, put anywhere else, would leave every
+"nothing here does X" test in this suite silently not reading it.
+"""
+
+import inspect
+from pathlib import Path
+
+from meshbay_node.transport.webrtc_server import WebRTCPeerSession, WebRTCTransport
+from node_source import TRANSPORT, webrtc_files
+
+
+def test_every_class_the_transport_is_built_from_is_in_scope():
+ scope = {p.resolve() for p in webrtc_files()}
+ for cls in (WebRTCPeerSession, WebRTCTransport):
+ for klass in cls.__mro__:
+ if not klass.__module__.startswith("meshbay_node."):
+ continue
+ where = Path(inspect.getsourcefile(klass)).resolve()
+ assert where in scope, (
+ f"{klass.__qualname__} ({cls.__name__}) is defined in "
+ f"{where.relative_to(TRANSPORT.parent)}, which the source-reading "
+ f"tests do not read — move it under transport/webrtc/")
+
+
+def test_the_scope_is_not_empty():
+ files = webrtc_files()
+ assert TRANSPORT / "webrtc_server.py" in files
+ assert all(p.exists() for p in files)
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py
index 91191c1..038f12c 100644
--- a/packages/meshbay-node/tests/test_roster_pairing.py
+++ b/packages/meshbay-node/tests/test_roster_pairing.py
@@ -23,6 +23,7 @@ from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR, join_transcript
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roster import Roster, hash_code, normalize_code
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+from node_source import session_method, webrtc_source
from conftest import one_root
@@ -568,9 +569,7 @@ def test_challenge_carries_node_pk_in_source():
Belt and braces for the above: the field must be in the message the node
builds, whatever the surrounding handshake does.
"""
- source = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "transport"
- / "webrtc_server.py").read_text(encoding="utf-8")
+ source = webrtc_source()
challenge = source[source.find("MNP.HANDSHAKE_CHALLENGE,"):]
challenge = challenge[:challenge.find("})")]
assert "node_pk" in challenge, (
@@ -899,9 +898,7 @@ def test_admin_authority_is_never_fetched_from_the_hub():
"""
src = Path(__file__).parent.parent / "src" / "meshbay_node"
- verifier = (src / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
- body = verifier[verifier.index("async def _verify_admin_sig"):]
- body = body[:body.index("\n def ", 1)]
+ body = session_method("_verify_admin_sig")
assert "operator_pks" in body, "the roster is where authority comes from"
# Past the docstring: it names what was removed on purpose, so a reader knows
# not to put it back. What must not reappear is code.
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py
index 7010523..7ace21b 100644
--- a/packages/meshbay-node/tests/test_security_regressions.py
+++ b/packages/meshbay-node/tests/test_security_regressions.py
@@ -18,6 +18,7 @@ from meshbay_common.crypto import generate_gek
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roots import RootSet
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+from node_source import TRANSPORT, webrtc_files, webrtc_source
from conftest import one_root, opened_ack, sealed_upload
@@ -461,9 +462,7 @@ def test_no_member_can_hand_the_node_key_material(tmp_path):
"key material over MNP (C5b)"
)
- source = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "transport"
- / "webrtc_server.py").read_text(encoding="utf-8")
+ source = webrtc_source()
assert "_do_gek_bundle_store" not in source
assert "_admin_exec_bundle_store" not in source
@@ -496,8 +495,7 @@ def test_gek_auto_activation_is_gone():
Since the operator's X25519 public key is public, any member could hand the
node a GEK of their choosing. Nothing arriving over MNP may set a live GEK.
"""
- source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
+ source = webrtc_source()
assert "_try_activate_gek" not in source
assert 'unwrap_gek_aes' not in source, (
"the MNP path must not unwrap a GEK — activation is local-admin only"
@@ -709,8 +707,7 @@ def test_peer_errors_do_not_leak_internals():
client needs to know why it was refused, and those strings are authored for
that purpose. The check targets the generic `except Exception as e` path.
"""
- source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
+ source = webrtc_source()
assert '"detail": str(e)' not in source, (
"generic exception text relayed to peer — use a fixed message"
)
@@ -759,9 +756,10 @@ def test_no_transport_ships_media_outside_the_aead():
"the constant outliving the handlers is how a deleted endpoint keeps "
"looking like part of the wire contract")
- root = Path(__file__).parent.parent / "src" / "meshbay_node" / "transport"
- for name in ("webrtc_server.py", "quic_server.py", "quic_client.py"):
- source = (root / name).read_text(encoding="utf-8")
+ quic = [TRANSPORT / "quic_server.py", TRANSPORT / "quic_client.py"]
+ for path in webrtc_files() + quic:
+ name = path.name
+ source = path.read_text(encoding="utf-8")
# Word boundaries: `_stream_segments` and `STREAM_SEGMENT_SIZE` belong
# to the live `stream_data` path, which is encrypted and stays.
assert not re.search(r"\bstream_seg\b", source), (
@@ -780,11 +778,10 @@ def test_ffmpeg_never_blocks_the_event_loop():
"""
import ast
- source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
- tree = ast.parse(source)
+ source = webrtc_source()
blocking = [
- node for node in ast.walk(tree)
+ node for path in webrtc_files()
+ for node in ast.walk(ast.parse(path.read_text(encoding="utf-8")))
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "run"
diff --git a/packages/meshbay-node/tests/test_task_lifetime.py b/packages/meshbay-node/tests/test_task_lifetime.py
index 133c030..a8445fa 100644
--- a/packages/meshbay-node/tests/test_task_lifetime.py
+++ b/packages/meshbay-node/tests/test_task_lifetime.py
@@ -25,33 +25,28 @@ file.
"""
import re
-from pathlib import Path
import pytest
-
-SERVER = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py")
+from node_source import TRANSPORT, session_method, session_source, webrtc_source
pytestmark = pytest.mark.skipif(
- not SERVER.exists(), reason="the node sources are not available")
+ not TRANSPORT.exists(), reason="the node sources are not available")
@pytest.fixture(scope="module")
def source():
- return SERVER.read_text(encoding="utf-8")
+ return webrtc_source()
@pytest.fixture(scope="module")
-def session(source):
- """The body of WebRTCPeerSession."""
- i = source.index("class WebRTCPeerSession")
- return source[i:source.index("\nclass WebRTCTransport")]
+def session():
+ """The body of WebRTCPeerSession, and of every class it is built from."""
+ return session_source()
def test_the_session_keeps_a_reference_to_what_it_starts(session):
assert "self._tasks: set[asyncio.Task] = set()" in session
- spawn = session[session.index("def _spawn("):]
- spawn = spawn[:spawn.index("\n def ", 1)]
+ spawn = session_method("_spawn")
assert "self._tasks.add(task)" in spawn, "the reference is what keeps it alive"
assert "self._tasks.discard(" in spawn and "add_done_callback(" in spawn, (
"without this the set grows for the life of the session")
@@ -72,8 +67,7 @@ def test_nothing_in_the_session_is_fired_and_forgotten(session):
def test_the_stream_still_takes_a_slot_for_its_whole_life(session):
"""The leak is only interesting because the slot is held this way."""
- body = session[session.index("async def _stream_video(self"):]
- body = body[:body.index("\n async def ", 1)]
+ body = session_method("_stream_video")
assert "async with sem:" in body
@@ -83,13 +77,11 @@ def test_closing_a_session_releases_its_tasks(session):
The cancelling itself lives in shutdown_tasks, which the state handler also
uses; close() is the variant that additionally shuts the peer connection.
"""
- close = session[session.index("async def close(self)"):]
- close = close[:close.index("\n\n")] if "\n\n" in close else close
+ close = session_method("close")
assert "shutdown_tasks()" in close
assert "self._pc.close()" in close
- fn = session[session.index("async def shutdown_tasks(self)"):]
- fn = fn[:fn.index("\n async def ", 1)]
+ fn = session_method("shutdown_tasks")
assert "_stop_stream()" in fn
assert "task.cancel()" in fn
assert "gather" in fn, "cancelling without awaiting does not run the exits"
@@ -129,8 +121,7 @@ def test_a_new_request_retires_the_previous_stream(session):
assert "self._spawn(self._replace_stream(msg))" in session, (
"the stream request must go through the path that retires the old one")
- fn = session[session.index("async def _replace_stream(self"):]
- fn = fn[:fn.index("\n async def ", 1)]
+ fn = session_method("_replace_stream")
assert "self._stop_stream()" in fn
assert "await asyncio.wait_for(asyncio.shield(prev)" in fn, (
"the slot comes back when the old task exits its `async with sem` — "
@@ -173,8 +164,7 @@ def test_losing_the_peer_stops_its_work(source):
def test_shutdown_is_separate_from_closing_the_connection(session):
"""The state handler runs while aiortc is already tearing pc down."""
- fn = session[session.index("async def shutdown_tasks(self)"):]
- fn = fn[:fn.index("\n async def ", 1)]
+ fn = session_method("shutdown_tasks")
assert "self._pc.close()" not in fn, (
"calling pc.close() from the state handler re-enters the teardown")
assert "task.cancel()" in fn and "gather" in fn
@@ -186,8 +176,7 @@ def test_a_dead_channel_is_noticed_while_waiting_not_after(source):
Waiting the whole budget on a channel that is already shut is the slot
being held for nothing, which is what the log above shows.
"""
- fn = source[source.index("async def _await_stream_credit"):]
- fn = fn[:fn.index("\n async def ", 1)]
+ fn = session_method("_await_stream_credit")
before = fn[:fn.index("wait_for")]
assert 'readyState != "open"' in before, (
"the channel must be checked before the wait, not only after it")
@@ -221,8 +210,7 @@ def test_the_pipes_are_drained_before_waiting_on_ffmpeg(source):
Which is the reported failure exactly: one video fine, the next hanging,
the one after refused.
"""
- fn = source[source.index("async def _stream_video_inner"):]
- fn = fn[:fn.index("\n def _send(")]
+ fn = session_method("_stream_video_inner")
finally_block = fn[fn.index("finally:"):]
assert "proc.kill()" in finally_block
@@ -234,8 +222,7 @@ def test_the_pipes_are_drained_before_waiting_on_ffmpeg(source):
def test_releasing_the_slot_does_not_depend_on_ffmpeg_behaving(source):
"""SIGKILL has already been sent; the OS will reap it either way."""
- fn = source[source.index("async def _stream_video_inner"):]
- fn = fn[:fn.index("\n def _send(")]
+ fn = session_method("_stream_video_inner")
tail = fn[fn.index("wait_for(proc.wait()"):]
assert "except Exception" in tail, (
"a timeout waiting for ffmpeg must not stop the slot coming back")
@@ -253,13 +240,7 @@ def test_chunks_wait_for_room_on_the_channel(session):
work through the rest — which is what "stuck at 1 MB" looks like, one chunk
being exactly one megabyte.
"""
- start = session.index("async def _do_file_request")
- # Up to whatever the next member is. This used to end at
- # "\n def _do_stream_segment" — a neighbour removed in MNP 2.0 — and an
- # `index()` on a name that no longer exists fails the test for a reason
- # that has nothing to do with what it is about.
- nxt = re.search(r"\n (?:@|(?:async )?def )", session[start:])
- fn = session[start:start + nxt.start()] if nxt else session[start:]
+ fn = session_method("_do_file_request")
assert "DOWNLOAD_BUFFER_HIGH" in fn, "the send buffer has to be watched"
assert "await asyncio.sleep" in fn, "waiting for room is the point"
assert 'readyState != "open"' in fn, (
diff --git a/packages/meshbay-node/tests/test_transport_wire_parity.py b/packages/meshbay-node/tests/test_transport_wire_parity.py
index 6e74b28..cd31fb4 100644
--- a/packages/meshbay-node/tests/test_transport_wire_parity.py
+++ b/packages/meshbay-node/tests/test_transport_wire_parity.py
@@ -23,6 +23,7 @@ from meshbay_common.protocol import MNP, file_chunk_plaintext, file_chunk_wire
from meshbay_node.indexer import DirectoryIndexer
from meshbay_node.transport import quic_server, webrtc_server
from meshbay_node.transport.wire import index_sync_message
+from node_source import webrtc_source
from conftest import one_root
@@ -41,23 +42,27 @@ def shared_dir(tmp_path):
return d
+def _transports():
+ """Each transport's whole source: WebRTC is several modules, QUIC is one."""
+ return [("the WebRTC transport", webrtc_source()),
+ (quic_server.__name__, inspect.getsource(quic_server))]
+
+
def test_both_transports_use_the_one_chunk_encoder():
"""Neither server may encrypt a chunk itself."""
- for module in (webrtc_server, quic_server):
- source = inspect.getsource(module)
- assert "file_chunk_wire" in source, f"{module.__name__} bypasses the shared encoder"
+ for name, source in _transports():
+ assert "file_chunk_wire" in source, f"{name} bypasses the shared encoder"
assert "encrypt_chunk_aes(" not in source, (
- f"{module.__name__} encrypts a chunk on its own — that is how the two "
+ f"{name} encrypts a chunk on its own — that is how the two "
f"copies diverged the first time")
assert "chunk_key_aes(" not in source, (
- f"{module.__name__} derives a chunk key on its own")
+ f"{name} derives a chunk key on its own")
def test_both_transports_use_the_one_index_builder():
- for module in (webrtc_server, quic_server):
- source = inspect.getsource(module)
+ for name, source in _transports():
assert "index_sync_message" in source, (
- f"{module.__name__} builds index_sync itself")
+ f"{name} builds index_sync itself")
assert "index_b64" not in inspect.getsource(quic_server), (
"QUIC is serializing the index again — that was the fork")
@@ -68,13 +73,12 @@ def test_neither_transport_seals_by_hand():
A server reaching for AESGCM or HKDF directly is a second envelope waiting to
disagree with the first about a nonce length, an info string or an AAD.
"""
- for module in (webrtc_server, quic_server):
- source = inspect.getsource(module)
- assert "seal(" in source, f"{module.__name__} sends an unsealed ack"
+ for name, source in _transports():
+ assert "seal(" in source, f"{name} sends an unsealed ack"
assert "AESGCM(" not in source, (
- f"{module.__name__} builds its own AEAD instead of using groupbox")
+ f"{name} builds its own AEAD instead of using groupbox")
assert "HKDF(" not in source, (
- f"{module.__name__} derives its own subkey instead of using groupbox")
+ f"{name} derives its own subkey instead of using groupbox")
def test_the_daemon_does_not_build_an_index_message_itself():
diff --git a/packages/meshbay-node/tests/test_upload_size_cap.py b/packages/meshbay-node/tests/test_upload_size_cap.py
index cc04eba..dbb14d9 100644
--- a/packages/meshbay-node/tests/test_upload_size_cap.py
+++ b/packages/meshbay-node/tests/test_upload_size_cap.py
@@ -25,6 +25,7 @@ from meshbay_node.transport.webrtc_server import (
WebRTCPeerSession,
WebRTCTransport,
)
+from node_source import webrtc_source
def _cfg(tmp_path: Path, body: str):
@@ -115,8 +116,7 @@ def test_the_handler_reads_it_rather_than_the_constant():
is already in flight — which it does only if the handler asks the context
each time instead of closing over the constant.
"""
- src = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
- / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
+ src = webrtc_source()
i = src.index("Upload exceeds size limit")
check = src[src.rindex("if state.bytes", 0, i):i]
assert "_max_upload_bytes()" in check, (