Replace the Microdot-only entrypoint with a CombinedASGI app that handles FastAPI routes (audio API, websocket, dev live-reload) while delegating the rest to Microdot. Suppress noisy /__dev/ access logs during live-reload polling. Co-authored-by: Cursor <cursoragent@cursor.com>
168 lines
5.3 KiB
Python
168 lines
5.3 KiB
Python
"""Shared FastAPI test server fixture for API endpoint tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import builtins
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional
|
|
|
|
import pytest
|
|
from starlette.testclient import TestClient
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
SRC_PATH = PROJECT_ROOT / "src"
|
|
LIB_PATH = PROJECT_ROOT / "lib"
|
|
|
|
for p in (str(PROJECT_ROOT), str(LIB_PATH), str(SRC_PATH)):
|
|
if p in sys.path:
|
|
sys.path.remove(p)
|
|
sys.path.insert(0, p)
|
|
|
|
|
|
class DummyBridge:
|
|
def __init__(self):
|
|
self.sent: list[tuple[Any, Optional[str]]] = []
|
|
|
|
async def send(self, data: Any, addr: Optional[str] = None):
|
|
if isinstance(data, dict):
|
|
from util.bridge_envelope import ( # noqa: E402
|
|
BROADCAST_MAC,
|
|
build_devices_envelope,
|
|
format_mac_key,
|
|
is_broadcast_mac,
|
|
normalize_mac_key,
|
|
)
|
|
from util.v1_wire import compact_envelope # noqa: E402
|
|
|
|
if data.get("v") == "1" and ("devices" in data or "dv" in data):
|
|
data = compact_envelope(data)
|
|
elif addr is not None:
|
|
s = str(addr).strip().lower()
|
|
if is_broadcast_mac(s):
|
|
mac_key = BROADCAST_MAC
|
|
else:
|
|
h = normalize_mac_key(s)
|
|
mac_key = format_mac_key(h) if h else None
|
|
if mac_key:
|
|
body = {k: v for k, v in data.items() if k != "v"}
|
|
data = build_devices_envelope({mac_key: body})
|
|
else:
|
|
data = json.dumps(data, separators=(",", ":"))
|
|
else:
|
|
data = json.dumps(data, separators=(",", ":"))
|
|
elif isinstance(data, (bytes, bytearray)):
|
|
data = bytes(data).decode(errors="ignore")
|
|
self.sent.append((data, addr))
|
|
return True
|
|
|
|
|
|
def bridge_sent_envelope(bridge: DummyBridge, index: int) -> Dict[str, Any]:
|
|
data, _addr = bridge.sent[index]
|
|
if isinstance(data, dict):
|
|
return data
|
|
return json.loads(data)
|
|
|
|
|
|
def device_body_from_envelope(envelope: Dict[str, Any], mac: str) -> Dict[str, Any]:
|
|
from util.bridge_envelope import format_mac_key, normalize_mac_key # noqa: E402
|
|
|
|
devs = envelope.get("dv") or envelope.get("devices") or {}
|
|
key = format_mac_key(normalize_mac_key(mac))
|
|
return devs[key]
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def server(monkeypatch, tmp_path_factory):
|
|
"""In-process FastAPI app with isolated db/settings."""
|
|
tmp_root = tmp_path_factory.mktemp("endpoint-tests")
|
|
tmp_db_dir = tmp_root / "db"
|
|
tmp_settings_file = tmp_root / "settings.json"
|
|
|
|
for p in (str(SRC_PATH), str(LIB_PATH), str(PROJECT_ROOT)):
|
|
if p in sys.path:
|
|
sys.path.remove(p)
|
|
sys.path.insert(0, p)
|
|
|
|
import settings as settings_mod # noqa: E402
|
|
|
|
settings_mod.Settings.SETTINGS_FILE = str(tmp_settings_file)
|
|
|
|
import models.model as model_mod # noqa: E402
|
|
|
|
monkeypatch.setattr(model_mod, "_db_dir", lambda: str(tmp_db_dir))
|
|
|
|
import models.preset as models_preset # noqa: E402
|
|
import models.profile as models_profile # noqa: E402
|
|
import models.group as models_group # noqa: E402
|
|
import models.zone as models_zone # noqa: E402
|
|
import models.pallet as models_pallet # noqa: E402
|
|
import models.scene as models_scene # noqa: E402
|
|
import models.pattern as models_pattern # noqa: E402
|
|
import models.sequence as models_sequence # noqa: E402
|
|
import models.device as models_device # noqa: E402
|
|
|
|
for cls in (
|
|
models_preset.Preset,
|
|
models_profile.Profile,
|
|
models_group.Group,
|
|
models_zone.Zone,
|
|
models_pallet.Palette,
|
|
models_scene.Scene,
|
|
models_pattern.Pattern,
|
|
models_sequence.Sequence,
|
|
models_device.Device,
|
|
):
|
|
if hasattr(cls, "_instance"):
|
|
delattr(cls, "_instance")
|
|
|
|
orig_open = builtins.open
|
|
|
|
def patched_open(file, *args, **kwargs):
|
|
if isinstance(file, str):
|
|
if file in {"db/pattern.json", "pattern.json", "/db/pattern.json"}:
|
|
file = str(PROJECT_ROOT / "db" / "pattern.json")
|
|
return orig_open(file, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "open", patched_open)
|
|
|
|
old_cwd = os.getcwd()
|
|
os.chdir(str(SRC_PATH))
|
|
|
|
dummy_bridge = DummyBridge()
|
|
|
|
try:
|
|
for mod_name in (
|
|
"controllers.preset",
|
|
"controllers.profile",
|
|
"controllers.group",
|
|
"controllers.sequence",
|
|
"controllers.zone",
|
|
"controllers.palette",
|
|
"controllers.scene",
|
|
"controllers.pattern",
|
|
"controllers.settings",
|
|
"controllers.device",
|
|
"controllers.wifi_bridge",
|
|
"fastapi_app",
|
|
"app_factory",
|
|
):
|
|
sys.modules.pop(mod_name, None)
|
|
|
|
from models.transport import set_bridge # noqa: E402
|
|
from fastapi_app import create_application # noqa: E402
|
|
|
|
set_bridge(dummy_bridge)
|
|
app = create_application(test_mode=True)
|
|
|
|
with TestClient(app, raise_server_exceptions=True) as client:
|
|
yield {
|
|
"base_url": "",
|
|
"client": client,
|
|
"bridge": dummy_bridge,
|
|
}
|
|
finally:
|
|
os.chdir(old_cwd)
|