test: four-rung suite that asserts on decoded pixels, not log lines
130 tests, ~20s, 94% coverage. Four rungs, each proving something the one below it cannot: registry admission, fan-out, signal authorization and teardown, no I/O wire camelCase round-trip, malformed frames, WebSocket lifecycle e2e real capture -> encode -> SRTP -> decode -> painted frames realdisplay the actual X11 capture backend on a private Xvfb display The e2e rung drives Chromium's fake media device, which produces a genuine moving 1280x720 monitor stream, so the assertion is that a watcher's <video> has a size and that its pixels change over time -- a frozen or never-started peer connection fails the suite, which a "signal arrived" assertion would not have caught. Findings encoded as flags and comments, because each cost a debugging cycle: the headless shell has no media stack at all (use channel="chromium"); mediaDevices is undefined off a secure context (serve over 127.0.0.1); and --use-fake-ui-for-media-stream breaks real X11 capture by approving the request without ever selecting a source, so the realdisplay suite selects the source explicitly instead. The realdisplay fixture strips WAYLAND_DISPLAY and DBUS_SESSION_BUS_ADDRESS from the browser environment. Without that, a headed Chromium reaches xdg-desktop-portal, mirrors the developer's actual desktop and raises a permission dialog on their real screen -- which is exactly what happened before this was isolated. Skipped unless MIRROR_REALDISPLAY=1. Also guards the frontend's structure: scripts must parse as ES modules, every id a script looks up must exist in its page, ids must not collide across pages that share one stylesheet, and nothing may load from a CDN. LLM-Generated: true
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
"""HTTP surface + signaling socket tests. No browser required."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
STATIC = Path(__file__).resolve().parent.parent / "src/mirror/static"
|
||||
EXTERNAL_REF = re.compile(r"""(?:src|href)\s*=\s*["']https?://""", re.IGNORECASE)
|
||||
|
||||
|
||||
def handshake(socket, role, name=""):
|
||||
socket.send_json({"type": "hello", "role": role, "name": name})
|
||||
message = socket.receive_json()
|
||||
assert message["type"] == "welcome", message
|
||||
return message["peerId"]
|
||||
|
||||
|
||||
def wait_for(socket, kind):
|
||||
"""Read frames until one has ``type == kind``.
|
||||
|
||||
The roster legitimately pushes ``stream_updated`` frames whenever the
|
||||
audience changes, so tests must not assume a one-message-per-action cadence.
|
||||
Any ``error`` frame seen on the way is a failure worth surfacing.
|
||||
"""
|
||||
seen = []
|
||||
while True:
|
||||
message = socket.receive_json()
|
||||
if message.get("type") == kind:
|
||||
return message
|
||||
assert message.get("type") != "error", (
|
||||
f"unexpected error while waiting for {kind}: {message}"
|
||||
)
|
||||
seen.append(message)
|
||||
assert len(seen) < 25, f"never saw {kind!r}; got {seen}"
|
||||
|
||||
|
||||
class TestPages:
|
||||
@pytest.mark.parametrize(
|
||||
("path", "marker"),
|
||||
[
|
||||
("/", "Watch everything"),
|
||||
("/mirror", "Share my screen"),
|
||||
("/share", "Share my screen"),
|
||||
("/watch", "tile-template"),
|
||||
],
|
||||
)
|
||||
def test_pages_render(self, client, path, marker):
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"].startswith("text/html")
|
||||
assert marker in response.text
|
||||
|
||||
def test_pages_are_not_cacheable(self, client):
|
||||
assert client.get("/watch").headers["cache-control"] == "no-store"
|
||||
|
||||
def test_the_two_pages_use_the_media_apis_they_claim(self):
|
||||
share = (STATIC / "share.js").read_text()
|
||||
watch = (STATIC / "watch.js").read_text()
|
||||
assert "getDisplayMedia" in share
|
||||
assert "RTCPeerConnection" in share and "RTCPeerConnection" in watch
|
||||
assert "new WebSocket" in (STATIC / "net.js").read_text()
|
||||
|
||||
def test_nothing_is_loaded_from_a_cdn(self):
|
||||
"""The no-CDN rule is a hard product requirement, so assert it."""
|
||||
offenders = []
|
||||
for file in sorted(STATIC.iterdir()):
|
||||
text = file.read_text()
|
||||
if EXTERNAL_REF.search(text):
|
||||
offenders.append(file.name)
|
||||
if "@import" in text or "fonts.googleapis" in text:
|
||||
offenders.append(f"{file.name} (@import)")
|
||||
assert offenders == []
|
||||
|
||||
@pytest.mark.parametrize("asset", ["app.css", "net.js", "share.js", "watch.js"])
|
||||
def test_static_assets_are_served(self, client, asset):
|
||||
assert client.get(f"/static/{asset}").status_code == 200
|
||||
|
||||
def test_unknown_page_is_a_404(self, client):
|
||||
assert client.get("/nope").status_code == 404
|
||||
|
||||
|
||||
class TestClientScripts:
|
||||
"""Guards for the hand-written ES modules: they must parse, and the ids they
|
||||
reach for must exist. Both of these have already bitten once."""
|
||||
|
||||
NODE = shutil.which("node")
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is not installed")
|
||||
@pytest.mark.parametrize("script", ["net.js", "share.js", "watch.js"])
|
||||
def test_scripts_parse_as_es_modules(self, script, tmp_path):
|
||||
copy = tmp_path / "subject.mjs"
|
||||
copy.write_text((STATIC / script).read_text())
|
||||
result = subprocess.run([self.NODE, "--check", str(copy)], capture_output=True, text=True)
|
||||
assert result.returncode == 0, f"{script}: {result.stderr}"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("script", "page"),
|
||||
[("share.js", "mirror.html"), ("watch.js", "watch.html")],
|
||||
)
|
||||
def test_every_element_the_script_reaches_for_exists(self, script, page):
|
||||
html = (STATIC / page).read_text()
|
||||
wanted = set(re.findall(r"""\$\(\"#([A-Za-z0-9_-]+)\"\)""", (STATIC / script).read_text()))
|
||||
missing = {name for name in wanted if f'id="{name}"' not in html}
|
||||
assert not missing, f"{script} looks up {sorted(missing)} which {page} does not render"
|
||||
|
||||
def test_both_pages_load_the_stylesheet_and_their_module(self):
|
||||
for page, script in (("mirror.html", "share.js"), ("watch.html", "watch.js")):
|
||||
html = (STATIC / page).read_text()
|
||||
assert 'href="/static/app.css"' in html
|
||||
assert f'src="/static/{script}"' in html
|
||||
assert 'type="module"' in html
|
||||
|
||||
def test_the_two_pages_share_no_element_ids_but_the_shared_chrome(self):
|
||||
"""Both pages consume one stylesheet, so a duplicated id means a colliding rule.
|
||||
|
||||
This is not hypothetical: a `#stage` on both pages once made the share
|
||||
page's preview inherit `display: none` from the watch page's overlay.
|
||||
"""
|
||||
shared = {"net", "notice", "toast"}
|
||||
ids = {
|
||||
page: set(re.findall(r'id="([A-Za-z0-9_-]+)"', (STATIC / page).read_text()))
|
||||
for page in ("mirror.html", "watch.html", "index.html")
|
||||
}
|
||||
assert ids["mirror.html"] & ids["watch.html"] == shared
|
||||
assert ids["index.html"] & ids["mirror.html"] <= shared | {"livebar"}
|
||||
for page, found in ids.items():
|
||||
assert len(found) == len(set(found)), f"{page} declares an id twice"
|
||||
|
||||
def test_videos_are_muted_and_inline_so_autoplay_is_not_blocked(self):
|
||||
for page in ("mirror.html", "watch.html"):
|
||||
for tag in re.findall(r"<video[^>]*>", (STATIC / page).read_text()):
|
||||
assert "muted" in tag, f"{page}: {tag} would be blocked by autoplay policy"
|
||||
assert "playsinline" in tag, f"{page}: {tag} would hijack iOS into fullscreen"
|
||||
|
||||
|
||||
class TestStylesheet:
|
||||
def test_hidden_always_means_gone(self):
|
||||
"""Every component that sets its own `display` outranks the UA's `[hidden]`.
|
||||
|
||||
Without this rule the share page shows the LIVE badge and the Stop button
|
||||
while nothing is being shared, and every wall tile offers an Exit button
|
||||
it is not on stage for.
|
||||
"""
|
||||
css = (STATIC / "app.css").read_text()
|
||||
assert re.search(r"\[hidden\]\s*\{[^}]*display:\s*none\s*!important", css), (
|
||||
"app.css must declare a global `[hidden] { display: none !important }`"
|
||||
)
|
||||
# Components that set `display` and are toggled with `hidden` in JS.
|
||||
for selector in (".btn", ".livetag", ".icon-btn", ".toast", ".net"):
|
||||
assert selector in css, f"{selector} is toggled by `hidden`; it lives in app.css"
|
||||
|
||||
def test_no_rule_reintroduces_a_conflicting_display_for_hidden(self):
|
||||
"""Every rule whose selector mentions `[hidden]` must actually hide."""
|
||||
css = re.sub(r"/\*.*?\*/", "", (STATIC / "app.css").read_text(), flags=re.S)
|
||||
offenders = [
|
||||
selector.strip()
|
||||
for selector, body in re.findall(r"([^{}]+)\{([^{}]*)\}", css)
|
||||
if "[hidden]" in selector and not re.search(r"display\s*:\s*none", body)
|
||||
]
|
||||
assert offenders == []
|
||||
|
||||
|
||||
class TestJsonApi:
|
||||
def test_config_advertises_the_media_defaults(self, client):
|
||||
body = client.get("/api/config").json()
|
||||
assert body["iceServers"] == []
|
||||
assert body["maxBitrateBps"] == 2_000_000
|
||||
assert "version" in body
|
||||
|
||||
def test_config_is_a_whitelist(self, client):
|
||||
body = client.get("/api/config").json()
|
||||
assert set(body) == {"version", "iceServers", "maxBitrateBps", "pingIntervalMs"}
|
||||
|
||||
def test_streams_starts_empty(self, client):
|
||||
body = client.get("/api/streams").json()
|
||||
assert body["mirrors"] == []
|
||||
assert body["stats"]["mirrors"] == 0
|
||||
|
||||
def test_healthz(self, client):
|
||||
assert client.get("/healthz").json()["status"] == "ok"
|
||||
|
||||
|
||||
class TestSignalingSocket:
|
||||
def test_hello_must_come_first(self, client):
|
||||
with client.websocket_connect("/ws") as socket:
|
||||
socket.send_json({"type": "publish", "label": "sneaky"})
|
||||
message = socket.receive_json()
|
||||
assert message["type"] == "error"
|
||||
assert message["code"] == "hello_required"
|
||||
assert message["fatal"] is True
|
||||
|
||||
def test_malformed_json_is_rejected(self, client):
|
||||
with client.websocket_connect("/ws") as socket:
|
||||
socket.send_text("{not json")
|
||||
assert socket.receive_json()["code"] == "bad_json"
|
||||
|
||||
def test_a_non_object_frame_is_rejected(self, client):
|
||||
with client.websocket_connect("/ws") as socket:
|
||||
socket.send_text("[1, 2, 3]")
|
||||
assert socket.receive_json()["code"] == "bad_json"
|
||||
|
||||
def test_an_unknown_role_is_refused(self, client):
|
||||
with client.websocket_connect("/ws") as socket:
|
||||
socket.send_json({"type": "hello", "role": "spectator"})
|
||||
assert socket.receive_json()["code"] == "bad_role"
|
||||
|
||||
def test_unknown_message_type_is_reported_but_survivable(self, client):
|
||||
with client.websocket_connect("/ws") as socket:
|
||||
handshake(socket, "viewer", "V")
|
||||
socket.send_json({"type": "telepathy"})
|
||||
assert socket.receive_json()["code"] == "bad_message"
|
||||
socket.send_json({"type": "ping"})
|
||||
assert socket.receive_json()["type"] == "pong"
|
||||
|
||||
def test_ping_roundtrips(self, client):
|
||||
with client.websocket_connect("/ws") as socket:
|
||||
handshake(socket, "viewer")
|
||||
socket.send_json({"type": "ping"})
|
||||
assert socket.receive_json() == {"type": "pong"}
|
||||
|
||||
def test_an_oversized_frame_is_refused(self, client):
|
||||
with client.websocket_connect("/ws") as socket:
|
||||
handshake(socket, "viewer")
|
||||
socket.send_text(json.dumps({"type": "ping", "padding": "x" * 70_000}))
|
||||
assert socket.receive_json()["code"] == "too_large"
|
||||
|
||||
def test_roster_flows_from_sharer_to_viewer(self, client):
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
sharer_id = handshake(sharer, "sharer", "Linux Box")
|
||||
viewer_id = handshake(viewer, "viewer", "Phone")
|
||||
|
||||
sharer.send_json({"type": "publish", "label": "Terminal", "width": 1280, "height": 720})
|
||||
added = wait_for(viewer, "stream_added")["stream"]
|
||||
assert added["id"] == sharer_id
|
||||
assert added["label"] == "Terminal"
|
||||
assert added["name"] == "Linux Box"
|
||||
assert added["width"] == 1280
|
||||
|
||||
viewer.send_json({"type": "subscribe", "streamId": sharer_id})
|
||||
joined = wait_for(sharer, "peer_joined")
|
||||
assert joined["viewerId"] == viewer_id
|
||||
assert joined["name"] == "Phone"
|
||||
|
||||
sharer.send_json({"type": "signal", "to": viewer_id, "payload": {"sdp": {"s": "o"}}})
|
||||
relayed = wait_for(viewer, "signal")
|
||||
assert relayed["from"] == sharer_id
|
||||
assert relayed["payload"] == {"sdp": {"s": "o"}}
|
||||
|
||||
viewer.send_json({"type": "signal", "to": sharer_id, "payload": {"c": "ice"}})
|
||||
assert wait_for(sharer, "signal")["payload"] == {"c": "ice"}
|
||||
|
||||
def test_capture_metadata_survives_the_wire(self, client):
|
||||
"""Regression: `displaySurface` once arrived as "" because the model expected
|
||||
snake_case and unknown keys were being ignored."""
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
handshake(sharer, "sharer", "A")
|
||||
handshake(viewer, "viewer")
|
||||
sharer.send_json(
|
||||
{
|
||||
"type": "publish",
|
||||
"label": "proj.gl — zsh",
|
||||
"displaySurface": "window",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"audio": True,
|
||||
}
|
||||
)
|
||||
stream = wait_for(viewer, "stream_added")["stream"]
|
||||
assert stream["displaySurface"] == "window"
|
||||
assert stream["audio"] is True
|
||||
assert (stream["width"], stream["height"]) == (1920, 1080)
|
||||
assert client.get("/api/streams").json()["mirrors"][0]["displaySurface"] == "window"
|
||||
|
||||
def test_both_viewers_of_one_mirror_see_their_own_leg(self, client):
|
||||
with (
|
||||
client.websocket_connect("/ws") as sharer,
|
||||
client.websocket_connect("/ws") as one,
|
||||
client.websocket_connect("/ws") as two,
|
||||
):
|
||||
sharer_id = handshake(sharer, "sharer", "A")
|
||||
first = handshake(one, "viewer", "one")
|
||||
second = handshake(two, "viewer", "two")
|
||||
sharer.send_json({"type": "publish", "label": "Screen"})
|
||||
wait_for(one, "stream_added")
|
||||
wait_for(two, "stream_added")
|
||||
|
||||
one.send_json({"type": "subscribe", "streamId": sharer_id})
|
||||
two.send_json({"type": "subscribe", "streamId": sharer_id})
|
||||
assert wait_for(sharer, "peer_joined")["viewerId"] == first
|
||||
assert wait_for(sharer, "peer_joined")["viewerId"] == second
|
||||
|
||||
sharer.send_json({"type": "signal", "to": first, "payload": {"n": 1}})
|
||||
sharer.send_json({"type": "signal", "to": second, "payload": {"n": 2}})
|
||||
assert wait_for(one, "signal")["payload"] == {"n": 1}
|
||||
assert wait_for(two, "signal")["payload"] == {"n": 2}
|
||||
|
||||
def test_a_mirror_disappears_for_viewers_when_the_sharer_leaves(self, client):
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
sharer_id = handshake(sharer, "sharer", "A")
|
||||
handshake(viewer, "viewer", "V")
|
||||
sharer.send_json({"type": "publish", "label": "Screen"})
|
||||
wait_for(viewer, "stream_added")
|
||||
viewer.send_json({"type": "subscribe", "streamId": sharer_id})
|
||||
wait_for(sharer, "peer_joined")
|
||||
|
||||
sharer.close()
|
||||
assert wait_for(viewer, "stream_removed")["streamId"] == sharer_id
|
||||
|
||||
def test_unpublish_removes_the_mirror(self, client):
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
sharer_id = handshake(sharer, "sharer")
|
||||
handshake(viewer, "viewer")
|
||||
sharer.send_json({"type": "publish", "label": "Screen"})
|
||||
wait_for(viewer, "stream_added")
|
||||
sharer.send_json({"type": "unpublish"})
|
||||
assert wait_for(viewer, "stream_removed")["streamId"] == sharer_id
|
||||
assert client.get("/api/streams").json()["mirrors"] == []
|
||||
|
||||
def test_signalling_a_peer_that_never_subscribed_is_refused(self, client):
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
sharer_id = handshake(sharer, "sharer")
|
||||
viewer_id = handshake(viewer, "viewer")
|
||||
sharer.send_json({"type": "publish", "label": "Screen"})
|
||||
assert wait_for(viewer, "stream_added")["stream"]["sharerId"] == sharer_id
|
||||
# The viewer knows the mirror exists but has not subscribed to it.
|
||||
sharer.send_json({"type": "signal", "to": viewer_id, "payload": {}})
|
||||
assert wait_for(sharer, "error")["code"] == "not_subscribed"
|
||||
|
||||
def test_a_viewer_cannot_reach_a_sharer_it_never_subscribed_to(self, client):
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
sharer_id = handshake(sharer, "sharer", "A")
|
||||
handshake(viewer, "viewer")
|
||||
sharer.send_json({"type": "publish", "label": "Screen"})
|
||||
wait_for(viewer, "stream_added")
|
||||
viewer.send_json({"type": "signal", "to": sharer_id, "payload": {}})
|
||||
assert wait_for(viewer, "error")["code"] == "not_subscribed"
|
||||
|
||||
def test_subscribing_to_a_mirror_that_is_not_there_reports_cleanly(self, client):
|
||||
with client.websocket_connect("/ws") as viewer:
|
||||
handshake(viewer, "viewer")
|
||||
viewer.send_json({"type": "subscribe", "streamId": "p404"})
|
||||
message = wait_for(viewer, "error")
|
||||
assert message["code"] == "unknown_stream"
|
||||
assert message["fatal"] is False
|
||||
|
||||
def test_editing_the_display_name_reaches_viewers(self, client):
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
sharer_id = handshake(sharer, "sharer", "Old name")
|
||||
handshake(viewer, "viewer")
|
||||
sharer.send_json({"type": "publish", "label": "Screen"})
|
||||
wait_for(viewer, "stream_added")
|
||||
sharer.send_json({"type": "hello", "role": "sharer", "name": "Design Laptop"})
|
||||
assert wait_for(viewer, "stream_updated")["stream"]["name"] == "Design Laptop"
|
||||
assert client.get("/api/streams").json()["mirrors"][0]["sharerId"] == sharer_id
|
||||
|
||||
def test_stats_track_the_fan_out(self, client):
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
sharer_id = handshake(sharer, "sharer", "A")
|
||||
handshake(viewer, "viewer", "V")
|
||||
sharer.send_json({"type": "publish", "label": "Screen"})
|
||||
wait_for(viewer, "stream_added")
|
||||
viewer.send_json({"type": "subscribe", "streamId": sharer_id})
|
||||
wait_for(sharer, "peer_joined")
|
||||
stats = client.get("/api/streams").json()["stats"]
|
||||
assert stats["mirrors"] == 1
|
||||
assert stats["watching"] == 1
|
||||
assert stats["connections"] == 2
|
||||
|
||||
def test_one_peer_cannot_impersonate_another(self, client):
|
||||
"""`to` is addressed by peer id; a viewer must not be able to spoof a sharer."""
|
||||
with client.websocket_connect("/ws") as sharer, client.websocket_connect("/ws") as viewer:
|
||||
sharer_id = handshake(sharer, "sharer", "A")
|
||||
handshake(viewer, "viewer", "V")
|
||||
sharer.send_json({"type": "publish", "label": "Screen"})
|
||||
wait_for(viewer, "stream_added")
|
||||
viewer.send_json({"type": "publish", "label": "forged"})
|
||||
assert wait_for(viewer, "error")["code"] == "not_sharer"
|
||||
assert client.get("/api/streams").json()["stats"]["mirrors"] == 1
|
||||
assert sharer_id and viewer
|
||||
Reference in New Issue
Block a user