83 lines
3.6 KiB
Python
83 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import http.client
|
|
import json
|
|
import threading
|
|
import unittest
|
|
|
|
from Brain.yovision_brain.domain import Point, Zone, zone_from_payload
|
|
from Brain.yovision_brain.server import DemoHTTPServer, DemoHandler, index_path, parse_bind
|
|
|
|
|
|
class FakeEngine:
|
|
def __init__(self) -> None:
|
|
self.zone = Zone("zone-demo-01", "测试区域", 1, (Point(0.1, 0.1), Point(0.9, 0.1), Point(0.5, 0.9)))
|
|
|
|
def state(self) -> dict[str, object]:
|
|
return {"source": {"label": "safe", "connected": True}, "events": [], "zone": {"version": self.zone.version}}
|
|
|
|
def frame_jpeg(self) -> bytes:
|
|
return b"\xff\xd8safe-jpeg\xff\xd9"
|
|
|
|
def update_zone(self, payload: object) -> Zone:
|
|
self.zone = zone_from_payload(payload, self.zone)
|
|
return self.zone
|
|
|
|
|
|
class BindTests(unittest.TestCase):
|
|
def test_only_explicit_loopback_is_allowed(self) -> None:
|
|
self.assertEqual(parse_bind("127.0.0.1:8090"), ("127.0.0.1", 8090))
|
|
self.assertEqual(parse_bind("localhost:8090"), ("localhost", 8090))
|
|
with self.assertRaisesRegex(ValueError, "loopback"):
|
|
parse_bind("0.0.0.0:8090")
|
|
with self.assertRaisesRegex(ValueError, "loopback"):
|
|
parse_bind("192.168.1.10:8090")
|
|
|
|
|
|
class HTTPTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
html = index_path().read_text(encoding="utf-8")
|
|
self.server = DemoHTTPServer(("127.0.0.1", 0), FakeEngine(), "test-token", html)
|
|
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
|
self.thread.start()
|
|
self.connection = http.client.HTTPConnection("127.0.0.1", self.server.server_port, timeout=2)
|
|
|
|
def tearDown(self) -> None:
|
|
self.connection.close()
|
|
self.server.shutdown()
|
|
self.server.server_close()
|
|
self.thread.join(timeout=2)
|
|
|
|
def test_index_substitutes_token_and_sets_security_headers(self) -> None:
|
|
self.connection.request("GET", "/brain-demo")
|
|
response = self.connection.getresponse()
|
|
body = response.read().decode("utf-8")
|
|
self.assertEqual(response.status, 200)
|
|
self.assertIn('content="test-token"', body)
|
|
self.assertNotIn("__BRAIN_DEMO_TOKEN__", body)
|
|
self.assertEqual(response.getheader("X-Frame-Options"), "DENY")
|
|
self.assertIn("default-src 'self'", response.getheader("Content-Security-Policy"))
|
|
|
|
def test_zone_write_requires_token_and_rejects_unknown_fields(self) -> None:
|
|
body = json.dumps({"name": "新区域", "points": [{"x": 0.1, "y": 0.1}, {"x": 0.9, "y": 0.1}, {"x": 0.5, "y": 0.9}]})
|
|
self.connection.request("PUT", "/api/v1/zones/active", body=body, headers={"Content-Type": "application/json"})
|
|
forbidden = self.connection.getresponse()
|
|
forbidden.read()
|
|
self.assertEqual(forbidden.status, 403)
|
|
|
|
self.connection.request("PUT", "/api/v1/zones/active", body=body, headers={"Content-Type": "application/json", "X-Brain-Demo-Token": "test-token"})
|
|
accepted = self.connection.getresponse()
|
|
payload = json.loads(accepted.read())
|
|
self.assertEqual(accepted.status, 200)
|
|
self.assertEqual(payload["version"], 2)
|
|
|
|
invalid = json.dumps({"name": "bad", "points": [], "tenant_id": "leak"})
|
|
self.connection.request("PUT", "/api/v1/zones/active", body=invalid, headers={"Content-Type": "application/json", "X-Brain-Demo-Token": "test-token"})
|
|
rejected = self.connection.getresponse()
|
|
self.assertEqual(rejected.status, 400)
|
|
self.assertEqual(json.loads(rejected.read())["code"], "invalid_zone")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|