|
1 | 1 | import json |
2 | 2 | import threading |
3 | 3 | from contextlib import contextmanager |
| 4 | +from io import BytesIO |
4 | 5 | from urllib.error import HTTPError |
5 | 6 | from urllib.request import Request, urlopen |
6 | 7 |
|
@@ -52,6 +53,7 @@ def test_owner_chat_csrf_exact_origin_queue_and_persistent_reply(tmp_path): |
52 | 53 | with gateway(tmp_path) as (server, base): |
53 | 54 | code, headers, body = request(base, "/chat") |
54 | 55 | assert code == 200 and b"Awaiting saved result" in body |
| 56 | + assert remote_access.NAVIGATION in body |
55 | 57 | assert "script-src 'sha256-" in headers["Content-Security-Policy"] |
56 | 58 | assert server.server_address[0] == "127.0.0.1" |
57 | 59 | _, _, body = request(base, "/api/chat") |
@@ -90,6 +92,52 @@ def test_owner_chat_csrf_exact_origin_queue_and_persistent_reply(tmp_path): |
90 | 92 | assert json.loads(body)["requests"][0]["response"] == reply |
91 | 93 |
|
92 | 94 |
|
| 95 | +@pytest.mark.parametrize( |
| 96 | + "opening", |
| 97 | + [ |
| 98 | + b"<body>", |
| 99 | + b'<body class="dashboard">', |
| 100 | + b"<BODY class='dashboard'>", |
| 101 | + b'<body data-label="a > b">', |
| 102 | + ], |
| 103 | +) |
| 104 | +def test_dashboard_navigation_preserves_renderer_and_body_attributes(opening): |
| 105 | + source = b"<!doctype html><html>" + opening + b"<script>render()</script></body></html>" |
| 106 | + result = remote_access.dashboard_navigation(source) |
| 107 | + assert opening + remote_access.NAVIGATION in result |
| 108 | + assert result.replace(remote_access.NAVIGATION, b"", 1) == source |
| 109 | + assert remote_access.dashboard_navigation(result) == result |
| 110 | + |
| 111 | + |
| 112 | +def test_dashboard_without_body_cannot_silently_omit_navigation(): |
| 113 | + with pytest.raises(ValueError, match="no body element"): |
| 114 | + remote_access.dashboard_navigation(b"<html><p>Missing body</p></html>") |
| 115 | + |
| 116 | + |
| 117 | +def test_gateway_dashboard_injects_mobile_links_and_preserves_upstream_policy( |
| 118 | + tmp_path, monkeypatch |
| 119 | +): |
| 120 | + source = b'<html><body class="dashboard"><script>render()</script></body></html>' |
| 121 | + policy = "default-src 'none'; script-src 'sha256-existing'; style-src 'unsafe-inline'" |
| 122 | + calls = [] |
| 123 | + |
| 124 | + def upstream(req, timeout): |
| 125 | + calls.append((req.full_url, timeout)) |
| 126 | + response = BytesIO(source) |
| 127 | + response.headers = {"Content-Type": "text/html", "Content-Security-Policy": policy} |
| 128 | + return response |
| 129 | + |
| 130 | + monkeypatch.setattr(remote_access, "urlopen", upstream) |
| 131 | + with gateway(tmp_path) as (_, base): |
| 132 | + code, headers, body = request(base, "/") |
| 133 | + assert code == 200 |
| 134 | + assert headers["Content-Security-Policy"] == policy |
| 135 | + assert body.replace(remote_access.NAVIGATION, b"", 1) == source |
| 136 | + assert b'href="/chat"' in body and b'href="/"' in body |
| 137 | + assert b"min-height:44px" in body |
| 138 | + assert calls == [(CONFIG["dashboard"] + "/", 5)] |
| 139 | + |
| 140 | + |
93 | 141 | @pytest.mark.parametrize( |
94 | 142 | "route", |
95 | 143 | [ |
|
0 commit comments