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>
682 lines
22 KiB
Python
682 lines
22 KiB
Python
import json
|
|
import time
|
|
import uuid
|
|
from typing import Any, Dict
|
|
|
|
import pytest
|
|
|
|
from api_server import ( # noqa: E402
|
|
DummyBridge,
|
|
bridge_sent_envelope,
|
|
device_body_from_envelope,
|
|
)
|
|
|
|
|
|
def _json(resp) -> Dict[str, Any]:
|
|
# Many endpoints already set Content-Type; but be tolerant for now.
|
|
return resp.json() # pragma: no cover
|
|
|
|
|
|
def _find_id_by_field(list_resp_json: Dict[str, Any], field: str, value: str) -> str:
|
|
for obj_id, data in list_resp_json.items():
|
|
if isinstance(data, dict) and data.get(field) == value:
|
|
return str(obj_id)
|
|
raise AssertionError(f"Could not find id for {field}={value!r}")
|
|
|
|
|
|
def _create_and_apply_profile(c, base_url: str) -> str:
|
|
"""Sequences/scenes/presets need an active profile in session."""
|
|
unique_profile_name = f"pytest-profile-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(f"{base_url}/profiles", json={"name": unique_profile_name})
|
|
assert resp.status_code == 201
|
|
profile_id = next(iter(resp.json().keys()))
|
|
resp = c.post(f"{base_url}/profiles/{profile_id}/apply")
|
|
assert resp.status_code == 200
|
|
return str(profile_id)
|
|
|
|
|
|
def test_main_routes(server):
|
|
c = server["client"]
|
|
base_url: str = server["base_url"]
|
|
|
|
resp = c.get(f"{base_url}/")
|
|
assert resp.status_code == 200
|
|
assert "LED Controller" in resp.text
|
|
|
|
resp = c.get(f"{base_url}/favicon.ico")
|
|
assert resp.status_code == 204
|
|
|
|
resp = c.get(f"{base_url}/static/style.css")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.get(f"{base_url}/settings/page")
|
|
assert resp.status_code == 200
|
|
assert "LED Controller" in resp.text
|
|
|
|
with c.websocket_connect("/ws") as ws:
|
|
ws.send_text('{"v":"1","select":["off"]}')
|
|
|
|
|
|
def test_settings_controller(server):
|
|
c = server["client"]
|
|
base_url: str = server["base_url"]
|
|
|
|
resp = c.get(f"{base_url}/settings")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert isinstance(data, dict)
|
|
assert "wifi_channel" in data
|
|
|
|
resp = c.get(f"{base_url}/settings/wifi/ap")
|
|
assert resp.status_code == 200
|
|
ap = resp.json()
|
|
assert "saved_ssid" in ap
|
|
assert "active" in ap
|
|
|
|
unique_ssid = f"pytest-ssid-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(
|
|
f"{base_url}/settings/wifi/ap",
|
|
json={"ssid": unique_ssid, "password": "secret", "channel": 1},
|
|
)
|
|
assert resp.status_code == 200
|
|
msg = resp.json()
|
|
assert msg["ssid"] == unique_ssid
|
|
assert msg["channel"] == 1
|
|
|
|
resp = c.post(
|
|
f"{base_url}/settings/wifi/ap",
|
|
json={"ssid": "bad-ssid", "password": "secret", "channel": 12},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
resp = c.put(f"{base_url}/settings", json={"wifi_channel": 11})
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.put(f"{base_url}/settings", json={"wifi_channel": 12})
|
|
assert resp.status_code == 400
|
|
|
|
resp = c.put(f"{base_url}/settings", json={"global_brightness": 42})
|
|
assert resp.status_code == 200
|
|
resp = c.get(f"{base_url}/settings")
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("global_brightness") == 42
|
|
|
|
resp = c.put(
|
|
f"{base_url}/settings",
|
|
json={"sequence_switch_wait": "downbeat"},
|
|
)
|
|
assert resp.status_code == 200
|
|
resp = c.get(f"{base_url}/settings")
|
|
assert resp.json().get("sequence_switch_wait") == "downbeat"
|
|
|
|
resp = c.put(f"{base_url}/settings", json={"global_brightness": 300})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_profiles_presets_zones_endpoints(server, monkeypatch):
|
|
c = server["client"]
|
|
base_url: str = server["base_url"]
|
|
bridge: DummyBridge = server["bridge"]
|
|
|
|
import controllers.device as device_ctl
|
|
|
|
monkeypatch.setattr(device_ctl, "IDENTIFY_OFF_DELAY_S", 0.05)
|
|
|
|
unique_profile_name = f"pytest-profile-{uuid.uuid4().hex[:8]}"
|
|
|
|
resp = c.post(f"{base_url}/profiles", json={"name": unique_profile_name})
|
|
assert resp.status_code == 201
|
|
created = resp.json()
|
|
assert isinstance(created, dict)
|
|
profile_id = next(iter(created.keys()))
|
|
|
|
resp = c.post(f"{base_url}/profiles/{profile_id}/apply")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.get(f"{base_url}/profiles/current")
|
|
assert resp.status_code == 200
|
|
current = resp.json()
|
|
assert str(current["id"]) == str(profile_id)
|
|
|
|
# Presets CRUD (scoped to current profile session).
|
|
resp = c.get(f"{base_url}/presets")
|
|
assert resp.status_code == 200
|
|
presets = resp.json()
|
|
assert isinstance(presets, dict)
|
|
assert presets # seeded presets should exist
|
|
|
|
first_preset_id = next(iter(presets.keys()))
|
|
resp = c.get(f"{base_url}/presets/{first_preset_id}")
|
|
assert resp.status_code == 200
|
|
assert resp.json() # dict
|
|
|
|
unique_preset_name = f"pytest-preset-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(
|
|
f"{base_url}/presets",
|
|
json={
|
|
"name": unique_preset_name,
|
|
"pattern": "on",
|
|
"colors": ["#ff0000"],
|
|
"brightness": 123,
|
|
"delay": 100,
|
|
},
|
|
)
|
|
assert resp.status_code == 201
|
|
created_preset = resp.json()
|
|
new_preset_id = next(iter(created_preset.keys()))
|
|
assert created_preset[new_preset_id]["profile_id"] == str(profile_id)
|
|
|
|
resp = c.put(
|
|
f"{base_url}/presets/{new_preset_id}",
|
|
json={"brightness": 77},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["brightness"] == 77
|
|
|
|
bridge.sent.clear()
|
|
resp = c.post(
|
|
f"{base_url}/presets/send",
|
|
json={"preset_ids": [new_preset_id], "save": False},
|
|
)
|
|
assert resp.status_code == 200
|
|
sent_result = resp.json()
|
|
assert sent_result["presets_sent"] >= 1
|
|
assert len(bridge.sent) >= 1
|
|
|
|
resp = c.delete(f"{base_url}/presets/{new_preset_id}")
|
|
assert resp.status_code == 200
|
|
resp = c.get(f"{base_url}/presets/{new_preset_id}")
|
|
assert resp.status_code == 404
|
|
|
|
# Zones CRUD (scoped to current profile session).
|
|
unique_zone_name = f"pytest-zone-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(
|
|
f"{base_url}/zones",
|
|
json={"name": unique_zone_name, "names": ["1", "2"]},
|
|
)
|
|
assert resp.status_code == 201
|
|
created_zones = resp.json()
|
|
zone_id = next(iter(created_zones.keys()))
|
|
|
|
resp = c.get(f"{base_url}/zones/{zone_id}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == unique_zone_name
|
|
|
|
resp = c.post(f"{base_url}/zones/{zone_id}/set-current")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.get(f"{base_url}/zones/current")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["zone_id"] == str(zone_id)
|
|
|
|
resp = c.put(
|
|
f"{base_url}/zones/{zone_id}",
|
|
json={"name": f"{unique_zone_name}-updated", "names": ["3"]},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["names"] == ["3"]
|
|
|
|
resp = c.post(f"{base_url}/zones/{zone_id}/clone", json={"name": "pytest-zone-clone"})
|
|
assert resp.status_code == 201
|
|
clone_payload = resp.json()
|
|
clone_id = next(iter(clone_payload.keys()))
|
|
|
|
resp = c.get(f"{base_url}/zones/{clone_id}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.delete(f"{base_url}/zones/{clone_id}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.delete(f"{base_url}/zones/{zone_id}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.get(f"{base_url}/profiles/{profile_id}/export")
|
|
assert resp.status_code == 200
|
|
bundle = resp.json()
|
|
assert bundle.get("kind") == "profile"
|
|
assert isinstance(bundle.get("presets"), dict)
|
|
|
|
import_name = f"pytest-imported-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(
|
|
f"{base_url}/profiles/import",
|
|
json={"bundle": bundle, "name": import_name, "apply": False},
|
|
)
|
|
assert resp.status_code == 201
|
|
imported_profile_id = resp.json().get("id") or next(
|
|
k for k in resp.json().keys() if k != "id"
|
|
)
|
|
resp = c.delete(f"{base_url}/profiles/{imported_profile_id}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.get(f"{base_url}/presets/{first_preset_id}/export")
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("kind") == "preset"
|
|
resp = c.post(
|
|
f"{base_url}/presets/import",
|
|
json={"bundle": resp.json()},
|
|
)
|
|
assert resp.status_code == 201
|
|
imported_preset_id = next(iter(resp.json().keys()))
|
|
resp = c.delete(f"{base_url}/presets/{imported_preset_id}")
|
|
assert resp.status_code == 200
|
|
|
|
# Profile clone + update endpoints.
|
|
clone_name = f"pytest-profile-clone-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(f"{base_url}/profiles/{profile_id}/clone", json={"name": clone_name})
|
|
assert resp.status_code == 201
|
|
cloned = resp.json()
|
|
clone_profile_id = next(iter(cloned.keys()))
|
|
|
|
resp = c.post(f"{base_url}/profiles/{clone_profile_id}/apply")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.put(
|
|
f"{base_url}/profiles/current",
|
|
json={"name": f"{clone_name}-updated"},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.put(
|
|
f"{base_url}/profiles/{clone_profile_id}",
|
|
json={"name": f"{clone_name}-updated-2"},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.delete(f"{base_url}/profiles/{clone_profile_id}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.delete(f"{base_url}/profiles/{profile_id}")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_groups_sequences_scenes_palettes_patterns_endpoints(server):
|
|
c = server["client"]
|
|
base_url: str = server["base_url"]
|
|
bridge: DummyBridge = server["bridge"]
|
|
|
|
_create_and_apply_profile(c, base_url)
|
|
|
|
# Groups.
|
|
unique_group_name = f"pytest-group-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(f"{base_url}/groups", json={"name": unique_group_name})
|
|
assert resp.status_code == 201
|
|
groups_list = c.get(f"{base_url}/groups").json()
|
|
group_id = _find_id_by_field(groups_list, "name", unique_group_name)
|
|
|
|
resp = c.get(f"{base_url}/groups/{group_id}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == unique_group_name
|
|
|
|
resp = c.put(f"{base_url}/groups/{group_id}", json={"brightness": 10})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["brightness"] == 10
|
|
|
|
resp = c.delete(f"{base_url}/groups/{group_id}")
|
|
assert resp.status_code == 200
|
|
|
|
# Sequences.
|
|
unique_seq_name = f"pytest-seq-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(
|
|
f"{base_url}/sequences",
|
|
json={
|
|
"name": unique_seq_name,
|
|
"steps": [{"preset_id": "1", "group_ids": []}],
|
|
},
|
|
)
|
|
assert resp.status_code == 201
|
|
sequences_list = c.get(f"{base_url}/sequences").json()
|
|
seq_id = _find_id_by_field(sequences_list, "name", unique_seq_name)
|
|
|
|
resp = c.get(f"{base_url}/sequences/{seq_id}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.put(f"{base_url}/sequences/{seq_id}", json={"step_duration_ms": 1234})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["step_duration_ms"] == 1234
|
|
|
|
resp = c.delete(f"{base_url}/sequences/{seq_id}")
|
|
assert resp.status_code == 200
|
|
|
|
# Scenes.
|
|
unique_scene_name = f"pytest-scene-{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(f"{base_url}/scenes", json={"name": unique_scene_name})
|
|
assert resp.status_code == 201
|
|
scenes_list = c.get(f"{base_url}/scenes").json()
|
|
scene_id = _find_id_by_field(scenes_list, "name", unique_scene_name)
|
|
|
|
resp = c.get(f"{base_url}/scenes/{scene_id}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.put(f"{base_url}/scenes/{scene_id}", json={"name": unique_scene_name + "-updated"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"].endswith("-updated")
|
|
|
|
resp = c.delete(f"{base_url}/scenes/{scene_id}")
|
|
assert resp.status_code == 200
|
|
|
|
# Palettes.
|
|
colors = ["#112233", "#445566"]
|
|
resp = c.post(f"{base_url}/palettes", json={"colors": colors})
|
|
assert resp.status_code == 201
|
|
palette_payload = resp.json()
|
|
palette_id = str(palette_payload["id"])
|
|
|
|
resp = c.get(f"{base_url}/palettes/{palette_id}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["id"] == palette_id
|
|
|
|
resp = c.put(f"{base_url}/palettes/{palette_id}", json={"colors": ["#000000"]})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["colors"] == ["#000000"]
|
|
|
|
resp = c.delete(f"{base_url}/palettes/{palette_id}")
|
|
assert resp.status_code == 200
|
|
|
|
# Devices (LED driver registry).
|
|
resp = c.get(f"{base_url}/devices")
|
|
assert resp.status_code == 200
|
|
assert resp.json() == {}
|
|
|
|
resp = c.post(f"{base_url}/devices", json={})
|
|
assert resp.status_code == 400
|
|
|
|
resp = c.post(
|
|
f"{base_url}/devices",
|
|
json={"name": "pytest-dev", "address": "aa:bb:cc:dd:ee:ff"},
|
|
)
|
|
assert resp.status_code == 201
|
|
dev_map = resp.json()
|
|
dev_id = next(iter(dev_map.keys()))
|
|
assert dev_id == "aabbccddeeff"
|
|
assert dev_map[dev_id]["name"] == "pytest-dev"
|
|
assert dev_map[dev_id]["id"] == dev_id
|
|
assert dev_map[dev_id]["type"] == "led"
|
|
assert dev_map[dev_id]["transport"] == "espnow"
|
|
assert dev_map[dev_id]["address"] == "aabbccddeeff"
|
|
|
|
resp = c.get(f"{base_url}/devices/{dev_id}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "pytest-dev"
|
|
assert resp.json()["type"] == "led"
|
|
assert resp.json().get("connected") is None
|
|
|
|
resp = c.get(f"{base_url}/devices")
|
|
assert resp.status_code == 200
|
|
assert resp.json()[dev_id].get("connected") is None
|
|
|
|
bridge.sent.clear()
|
|
resp = c.post(f"{base_url}/devices/{dev_id}/identify")
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("message")
|
|
assert len(bridge.sent) >= 1
|
|
first = bridge_sent_envelope(bridge, 0)
|
|
assert first["v"] == "1"
|
|
first_body = device_body_from_envelope(first, dev_id)
|
|
assert first_body["p"]["__identify"]["p"] == "blink"
|
|
assert first_body["p"]["__identify"]["d"] == 50
|
|
assert first_body["s"] == ["__identify"]
|
|
deadline = time.monotonic() + 2.0
|
|
while len(bridge.sent) < 2 and time.monotonic() < deadline:
|
|
time.sleep(0.02)
|
|
assert len(bridge.sent) >= 2
|
|
second = bridge_sent_envelope(bridge, 1)
|
|
second_body = device_body_from_envelope(second, dev_id)
|
|
assert second_body["s"] == ["off"]
|
|
|
|
resp = c.post(
|
|
f"{base_url}/devices",
|
|
json={
|
|
"name": "pytest-wifi",
|
|
"type": "led",
|
|
"transport": "wifi",
|
|
"address": "192.168.50.10",
|
|
"mac": "102030405060",
|
|
},
|
|
)
|
|
assert resp.status_code == 201
|
|
wid = "102030405060"
|
|
assert wid in resp.json()
|
|
assert resp.json()[wid]["transport"] == "wifi"
|
|
assert resp.json()[wid]["address"] == "192.168.50.10"
|
|
|
|
resp = c.get(f"{base_url}/devices/{wid}")
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("connected") is None
|
|
|
|
resp = c.post(
|
|
f"{base_url}/devices",
|
|
json={
|
|
"name": "pytest-wifi",
|
|
"transport": "wifi",
|
|
"address": "192.168.50.11",
|
|
"mac": "102030405061",
|
|
},
|
|
)
|
|
assert resp.status_code == 201
|
|
wid2 = "102030405061"
|
|
assert wid2 in resp.json()
|
|
assert resp.json()[wid2]["name"] == "pytest-wifi"
|
|
|
|
resp = c.post(
|
|
f"{base_url}/devices",
|
|
json={
|
|
"name": "pytest-wifi-dupmac",
|
|
"transport": "wifi",
|
|
"address": "192.168.50.99",
|
|
"mac": "102030405060",
|
|
},
|
|
)
|
|
assert resp.status_code == 409
|
|
|
|
resp = c.post(
|
|
f"{base_url}/devices",
|
|
json={"name": "no-mac-wifi", "transport": "wifi", "address": "192.168.50.12"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
resp = c.post(
|
|
f"{base_url}/devices",
|
|
json={"name": "bad-tr", "transport": "serial"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
resp = c.put(f"{base_url}/devices/{dev_id}", json={"name": " "})
|
|
assert resp.status_code == 400
|
|
|
|
resp = c.put(f"{base_url}/devices/{dev_id}", json={"name": "renamed"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "renamed"
|
|
|
|
resp = c.put(f"{base_url}/devices/{wid}", json={"name": "renamed"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "renamed"
|
|
|
|
resp = c.delete(f"{base_url}/devices/{wid2}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.delete(f"{base_url}/devices/{wid}")
|
|
assert resp.status_code == 200
|
|
|
|
resp = c.delete(f"{base_url}/devices/{dev_id}")
|
|
assert resp.status_code == 200
|
|
|
|
# Patterns.
|
|
resp = c.get(f"{base_url}/patterns/definitions")
|
|
assert resp.status_code == 200
|
|
definitions = resp.json()
|
|
assert isinstance(definitions, dict)
|
|
assert "colour_cycle" in definitions
|
|
cc_mode = definitions["colour_cycle"].get("mode")
|
|
assert isinstance(cc_mode, dict)
|
|
assert "0" in cc_mode and "1" in cc_mode
|
|
assert "blink" in definitions
|
|
blink_mode = definitions["blink"].get("mode")
|
|
assert not isinstance(blink_mode, dict) or len(blink_mode) < 2
|
|
|
|
pattern_id = f"pytest_pattern_{uuid.uuid4().hex[:8]}"
|
|
resp = c.post(
|
|
f"{base_url}/patterns",
|
|
json={"name": pattern_id, "data": {"foo": "bar"}},
|
|
)
|
|
assert resp.status_code == 201
|
|
assert resp.json()["foo"] == "bar"
|
|
|
|
resp = c.get(f"{base_url}/patterns")
|
|
assert resp.status_code == 200
|
|
patterns_list = resp.json()
|
|
assert isinstance(patterns_list, dict)
|
|
# Runtime list merges repo ``db/pattern.json`` + driver ``.py`` names; test DB
|
|
# entries are still exposed on GET /patterns/<id> after POST.
|
|
assert "blink" in patterns_list or len(patterns_list) >= 1
|
|
|
|
resp = c.get(f"{base_url}/patterns/{pattern_id}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["foo"] == "bar"
|
|
|
|
resp = c.put(f"{base_url}/patterns/{pattern_id}", json={"baz": 1})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["baz"] == 1
|
|
|
|
resp = c.delete(f"{base_url}/patterns/{pattern_id}")
|
|
assert resp.status_code == 200
|
|
|
|
# on/off are firmware-only in presets.py — no OTA file; API rejects serve/send/upload/driver.
|
|
resp = c.get(f"{base_url}/patterns/ota/file/on.py")
|
|
assert resp.status_code == 400
|
|
assert "error" in resp.json()
|
|
|
|
resp = c.post(f"{base_url}/patterns/off/send", json={})
|
|
assert resp.status_code == 400
|
|
assert "error" in resp.json()
|
|
|
|
resp = c.post(
|
|
f"{base_url}/patterns/upload",
|
|
json={"name": "on.py", "code": "class On:\n def run(self, p):\n yield\n"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
resp = c.post(
|
|
f"{base_url}/patterns/driver",
|
|
json={
|
|
"name": "off",
|
|
"code": "class Off:\n def run(self, p):\n yield\n",
|
|
},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_audio_api(server):
|
|
c = server["client"]
|
|
base_url = server["base_url"]
|
|
|
|
resp = c.get(f"{base_url}/api/audio/status")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert "status" in body
|
|
assert "audio_run" in body["status"]
|
|
|
|
resp = c.get(f"{base_url}/api/audio/devices")
|
|
assert resp.status_code == 200
|
|
assert "devices" in resp.json()
|
|
|
|
resp = c.put(
|
|
f"{base_url}/api/audio/device",
|
|
json={"device_select": "default", "device_override": ""},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("ok") is True
|
|
|
|
resp = c.post(f"{base_url}/api/audio/reset")
|
|
assert resp.status_code == 409
|
|
|
|
resp = c.post(f"{base_url}/api/audio/stop")
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("ok") is True
|
|
|
|
|
|
def test_bridge_settings_api(server, monkeypatch):
|
|
c = server["client"]
|
|
base_url = server["base_url"]
|
|
|
|
import controllers.wifi_bridge as wifi_bridge_ctl # noqa: E402
|
|
|
|
monkeypatch.setattr(wifi_bridge_ctl, "nmcli_available", lambda: True)
|
|
monkeypatch.setattr(
|
|
wifi_bridge_ctl,
|
|
"list_wifi_interfaces",
|
|
lambda: [{"device": "wlan0", "type": "wifi", "state": "connected"}],
|
|
)
|
|
|
|
async def _fake_scan(device):
|
|
_ = device
|
|
return [{"ssid": "bridge-test", "signal": 80}]
|
|
|
|
monkeypatch.setattr(wifi_bridge_ctl, "scan_wifi", _fake_scan)
|
|
|
|
resp = c.get(f"{base_url}/settings/wifi/interfaces")
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("ok") is True
|
|
assert resp.json()["interfaces"][0]["device"] == "wlan0"
|
|
|
|
resp = c.get(f"{base_url}/settings/wifi/scan", params={"device": "wlan0"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["networks"][0]["ssid"] == "bridge-test"
|
|
|
|
resp = c.get(f"{base_url}/settings/wifi/bridges")
|
|
assert resp.status_code == 200
|
|
payload = resp.json()
|
|
assert payload.get("ok") is True
|
|
assert "bridge_transport" in payload
|
|
assert "bridges" in payload
|
|
|
|
resp = c.put(
|
|
f"{base_url}/settings/wifi/bridges",
|
|
json={
|
|
"bridge_transport": "serial",
|
|
"bridge_serial_port": "/dev/ttyUSB0",
|
|
"bridges": [],
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("ok") is True
|
|
|
|
resp = c.get(f"{base_url}/settings/wifi/bridges")
|
|
assert resp.json().get("bridge_transport") == "serial"
|
|
|
|
|
|
def test_group_identify(server, monkeypatch):
|
|
c = server["client"]
|
|
base_url = server["base_url"]
|
|
bridge: DummyBridge = server["bridge"]
|
|
|
|
import controllers.device as device_ctl # noqa: E402
|
|
|
|
monkeypatch.setattr(device_ctl, "IDENTIFY_OFF_DELAY_S", 0.05)
|
|
|
|
_create_and_apply_profile(c, base_url)
|
|
|
|
resp = c.post(f"{base_url}/groups", json={"name": "pytest-identify-group"})
|
|
assert resp.status_code == 201
|
|
groups_list = c.get(f"{base_url}/groups").json()
|
|
group_id = _find_id_by_field(groups_list, "name", "pytest-identify-group")
|
|
|
|
resp = c.post(
|
|
f"{base_url}/devices",
|
|
json={"name": "identify-dev", "address": "aabbccddeeff"},
|
|
)
|
|
assert resp.status_code == 201
|
|
dev_id = "aabbccddeeff"
|
|
|
|
resp = c.put(
|
|
f"{base_url}/groups/{group_id}",
|
|
json={"devices": [dev_id]},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
bridge.sent.clear()
|
|
resp = c.post(f"{base_url}/groups/{group_id}/identify")
|
|
assert resp.status_code == 200
|
|
assert resp.json().get("sent", 0) >= 1
|
|
assert len(bridge.sent) >= 1
|
|
|