Files

404 lines
16 KiB
Python

from __future__ import annotations
from fastapi.testclient import TestClient
from lumaops_backend.connectors.base import DeviceState, RGBColor
from lumaops_backend.connectors.mock import MockOpenRGBAdapter
from lumaops_backend.main import create_app
from lumaops_backend.services.commands import CommandService
def test_direct_mode_without_explicit_color_preserves_last_nonzero_color() -> None:
color = RGBColor(red=86, green=96, blue=255)
effective = CommandService._effective_state(
{
"desired_state": DeviceState(power=True, colors=[color]).model_dump(mode="json"),
"modes": [{"index": 0, "name": "Direct"}],
},
DeviceState(power=True, mode_index=0),
)
assert effective.colors == [color]
assert effective.mode_index == 0
def test_main_device_command_flow(client: TestClient) -> None:
discovery = client.post("/api/v1/discovery")
assert discovery.status_code == 200
assert discovery.json()["found_count"] == 2
devices_response = client.get("/api/v1/devices")
assert devices_response.status_code == 200
devices = devices_response.json()["items"]
assert len(devices) == 2
device = next(item for item in devices if item["fingerprint"] == "mock-mainboard")
request = {"state": {"power": True, "colors": [{"red": 20, "green": 40, "blue": 60}]}}
command = client.post(
f"/api/v1/devices/{device['id']}/state",
json=request,
headers={"Idempotency-Key": "main-flow-1"},
)
assert command.status_code == 200
assert command.json()["status"] == "succeeded"
duplicate = client.post(
f"/api/v1/devices/{device['id']}/state",
json=request,
headers={"Idempotency-Key": "main-flow-1"},
)
assert duplicate.json()["id"] == command.json()["id"]
detail = client.get(f"/api/v1/devices/{device['id']}").json()
assert detail["state"]["colors"][0] == {"red": 20, "green": 40, "blue": 60}
resized = client.put(
f"/api/v1/devices/{device['id']}/zones/1",
json={"led_count": 24},
)
assert resized.status_code == 200
assert resized.json()["zones"][1]["led_count"] == 24
assert resized.json()["led_count"] == 32
zone_color = {"red": 10, "green": 80, "blue": 160}
zone_command = client.post(
f"/api/v1/devices/{device['id']}/zones/1/state",
json={"state": {"colors": [zone_color]}},
)
assert zone_command.status_code == 200
assert zone_command.json()["zone_index"] == 1
assert zone_command.json()["led_count"] == 120
assert client.get(f"/api/v1/devices/{device['id']}").json()["zones"][1]["led_count"] == 120
commands = client.get("/api/v1/commands").json()
audit = client.get("/api/v1/audit").json()
assert commands["total"] == 2
assert audit["total"] >= 1
assert any(item["action"] == "device.resize_zone" for item in audit["items"])
def test_desired_state_survives_connector_restart_and_power_cycle(client: TestClient) -> None:
client.post("/api/v1/discovery")
device = next(
item
for item in client.get("/api/v1/devices").json()["items"]
if item["fingerprint"] == "mock-mainboard"
)
target = f"/api/v1/devices/{device['id']}"
color = {"red": 81, "green": 109, "blue": 245}
applied = client.post(
f"{target}/state",
json={"state": {"power": True, "colors": [color]}},
)
assert applied.status_code == 200
detail = client.get(target).json()
assert detail["desired_state"]["colors"][0] == color
connector = client.app.state.context.registry.get("openrgb-mock")
assert isinstance(connector, MockOpenRGBAdapter)
connector._devices["mock-mainboard"].state = DeviceState( # noqa: SLF001
power=False,
brightness=72,
colors=[RGBColor(red=0, green=0, blue=0)] * 12,
mode="Direct",
mode_index=0,
speed=5,
)
commands_before = client.get("/api/v1/commands").json()["total"]
discovery = client.post("/api/v1/discovery")
assert discovery.status_code == 200
restored = client.get(target).json()
assert restored["state"]["power"] is True
assert restored["state"]["colors"]
assert all(entry == color for entry in restored["state"]["colors"])
commands_after = client.get("/api/v1/commands").json()
assert commands_after["total"] == commands_before + 1
assert commands_after["items"][0]["actor"] == "system-restore"
assert client.post("/api/v1/discovery").status_code == 200
assert client.get("/api/v1/commands").json()["total"] == commands_after["total"]
assert client.post(f"{target}/state", json={"state": {"power": False}}).status_code == 200
powered_off = client.get(target).json()
assert powered_off["state"]["power"] is False
assert powered_off["desired_state"]["colors"]
assert all(entry == color for entry in powered_off["desired_state"]["colors"])
assert client.post(f"{target}/state", json={"state": {"power": True}}).status_code == 200
powered_on = client.get(target).json()
assert powered_on["state"]["power"] is True
assert powered_on["state"]["colors"]
assert all(entry == color for entry in powered_on["state"]["colors"])
def test_explicit_colorless_effect_is_not_replaced_by_power_restore(
client: TestClient,
) -> None:
device = client.get("/api/v1/devices").json()["items"][0]
client.post(
f"/api/v1/devices/{device['id']}/state",
json={"state": {"power": True, "colors": [{"red": 24, "green": 48, "blue": 96}]}},
)
effect = client.post(
f"/api/v1/devices/{device['id']}/state",
json={"state": {"power": True, "mode_index": 2}},
)
assert effect.status_code == 200
assert effect.json()["state"]["mode_index"] == 2
def test_startup_reconciles_desired_state_once_before_ready(settings) -> None:
color = {"red": 24, "green": 96, "blue": 192}
device_id = ""
with TestClient(create_app(settings)) as first_client:
device = next(
item
for item in first_client.get("/api/v1/devices").json()["items"]
if item["fingerprint"] == "mock-mainboard"
)
device_id = device["id"]
applied = first_client.post(
f"/api/v1/devices/{device_id}/state",
json={"state": {"power": True, "colors": [color]}},
)
assert applied.status_code == 200
with TestClient(create_app(settings)) as restarted_client:
assert restarted_client.get("/health/ready").json() == {"status": "ready"}
restored = restarted_client.get(f"/api/v1/devices/{device_id}").json()
assert restored["state"]["colors"]
assert all(entry == color for entry in restored["state"]["colors"])
commands = restarted_client.get("/api/v1/commands?limit=100").json()["items"]
restores = [
command
for command in commands
if command["actor"] == "system-restore" and command["target_id"] == device_id
]
assert len(restores) == 1
assert restores[0]["status"] == "succeeded"
def test_rooms_groups_and_scenes(client: TestClient) -> None:
client.post("/api/v1/discovery")
devices = client.get("/api/v1/devices").json()["items"]
room = client.post("/api/v1/rooms", json={"name": "Werkplek", "sort_order": 1})
assert room.status_code == 201
first = devices[0]
assert (
client.patch(
f"/api/v1/devices/{first['id']}", json={"room_id": room.json()["id"]}
).status_code
== 200
)
group = client.post(
"/api/v1/groups",
json={"name": "Bureau", "device_ids": [item["id"] for item in devices]},
)
assert group.status_code == 201
tagged = client.patch(f"/api/v1/devices/{devices[1]['id']}", json={"tags": ["bureau", "rgb"]})
assert tagged.status_code == 200
assert tagged.json()["tags"] == ["bureau", "rgb"]
dynamic_group = client.post(
"/api/v1/groups",
json={"name": "Met tag", "dynamic_query": {"tags": ["bureau"], "match": "all"}},
)
assert dynamic_group.status_code == 201
assert [item["id"] for item in dynamic_group.json()["devices"]] == [devices[1]["id"]]
assert client.get("/api/v1/tags").json()[0]["device_count"] == 1
scene = client.post(
"/api/v1/scenes",
json={
"name": "Focus",
"favorite": True,
"items": [
{
"target_type": "group",
"target_id": group.json()["id"],
"state": {"power": True, "colors": [{"red": 42, "green": 80, "blue": 180}]},
}
],
},
)
assert scene.status_code == 201
preview = client.get(f"/api/v1/scenes/{scene.json()['id']}/preview").json()
assert preview["device_count"] == 2
applied = client.post(
f"/api/v1/scenes/{scene.json()['id']}/apply", json={"rollback_on_failure": True}
)
assert applied.status_code == 200
assert applied.json()["status"] == "succeeded"
duplicate = client.post(f"/api/v1/scenes/{scene.json()['id']}/duplicate", json={})
assert duplicate.status_code == 201
assert duplicate.json()["name"] == "Focus (kopie)"
assert len(duplicate.json()["items"]) == 1
exported = client.get(f"/api/v1/scenes/{scene.json()['id']}/export")
assert exported.status_code == 200
assert exported.json()["format"] == "lumaops-scene-v1"
imported = client.post("/api/v1/scenes/import", json=exported.json())
assert imported.status_code == 201
assert imported.json()["name"] == "Focus"
assert client.get("/api/v1/scenes").json()["total"] == 3
def test_physical_dram_family_groups_modules_and_supports_group_commands(
client: TestClient,
) -> None:
devices = client.get("/api/v1/devices").json()["items"]
device_ids = [device["id"] for device in devices]
with client.app.state.context.database.connection() as conn:
conn.execute(
"UPDATE devices SET device_type='dram', name='Corsair Vengeance RGB Pro SL DDR4', "
"vendor='Corsair', model='Corsair DRAM RGB Device', state_json=(SELECT state_json "
"FROM devices ORDER BY controller_index LIMIT 1) WHERE id IN (?, ?)",
tuple(device_ids),
)
groups = client.get("/api/v1/device-groups")
assert groups.status_code == 200
assert len(groups.json()) == 1
group = groups.json()[0]
assert group["device_type"] == "dram"
assert group["module_count"] == 2
assert group["mixed"] is False
assert [device["controller_index"] for device in group["devices"]] == [0, 1]
color = {"red": 34, "green": 102, "blue": 204}
applied = client.post(
f"/api/v1/device-groups/{group['id']}/state",
json={"state": {"power": True, "colors": [color]}},
)
assert applied.status_code == 200
assert applied.json()["status"] == "succeeded"
assert [result["status"] for result in applied.json()["results"]] == [
"succeeded",
"succeeded",
]
for device_id in device_ids:
detail = client.get(f"/api/v1/devices/{device_id}").json()
assert detail["desired_state"]["colors"][0] == color
def test_setup_health_backup_and_diagnostics(client: TestClient) -> None:
setup = client.post("/api/v1/setup/inspect")
assert setup.status_code == 200
assert "storage" in setup.json()
complete = client.post(
"/api/v1/setup/complete",
json={"appdata_confirmed": True, "backup_location_confirmed": True},
)
assert complete.json()["completed"] is True
assert client.get("/health/live").status_code == 200
assert client.get("/health/ready").status_code == 200
health = client.get("/api/v1/health")
assert health.status_code == 200
backup = client.post("/api/v1/backups")
assert backup.status_code == 201
assert backup.json()["name"].endswith(".db")
diagnostic = client.get("/api/v1/diagnostics/export")
assert diagnostic.status_code == 200
assert diagnostic.content.startswith(b"PK")
def test_setup_complete_accepts_legacy_backup_field(client: TestClient) -> None:
complete = client.post(
"/api/v1/setup/complete",
json={"appdata_confirmed": True, "backup_confirmed": True},
)
assert complete.status_code == 200
assert complete.json()["completed"] is True
def test_resource_lifecycle_group_capabilities_and_automation_history(
client: TestClient,
) -> None:
client.post("/api/v1/discovery")
devices = client.get("/api/v1/devices").json()["items"]
room = client.post("/api/v1/rooms", json={"name": "Studio", "description": "Boven"}).json()
updated_room = client.put(
f"/api/v1/rooms/{room['id']}",
json={"name": "Werkstudio", "description": "Bovenverdieping"},
)
assert updated_room.status_code == 200
assert updated_room.json()["description"] == "Bovenverdieping"
client.patch(f"/api/v1/devices/{devices[0]['id']}", json={"room_id": room["id"]})
assert client.delete(f"/api/v1/rooms/{room['id']}").status_code == 204
assert client.get(f"/api/v1/devices/{devices[0]['id']}").json()["room_id"] is None
group = client.post(
"/api/v1/groups",
json={"name": "Alles", "device_ids": [device["id"] for device in devices]},
).json()
group_result = client.post(
f"/api/v1/groups/{group['id']}/state",
json={"state": {"speed": 3}},
)
assert group_result.status_code == 200
assert {item["status"] for item in group_result.json()["results"]} == {
"succeeded",
"skipped",
}
scene = client.post("/api/v1/scenes", json={"name": "Leeg", "items": []}).json()
automation = client.post(
"/api/v1/automations",
json={
"name": "Avond",
"description": "Dagelijkse rustige scène",
"trigger": {"type": "time", "at": "20:00", "weekdays": [0, 2, 4]},
"actions": [{"type": "scene", "scene_id": scene["id"]}],
"timezone": "Europe/Brussels",
"cooldown_seconds": 0,
"conflict_key": "woonkamer",
},
)
assert automation.status_code == 201
assert automation.json()["description"] == "Dagelijkse rustige scène"
run = client.post(f"/api/v1/automations/{automation.json()['id']}/run")
assert run.status_code == 200
assert run.json()["status"] == "succeeded"
history = client.get(f"/api/v1/automations/{automation.json()['id']}/runs").json()
assert history["total"] == 1
assert history["items"][0]["status"] == "succeeded"
audit = client.get("/api/v1/audit?limit=500").json()["items"]
assert any(item["action"] == "api.create_room" for item in audit)
assert any(item["action"] == "api.create_automation" for item in audit)
def test_automation_requests_reject_invalid_schedules_and_actions(client: TestClient) -> None:
base = {
"name": "Ongeldig",
"description": "Validatiecontrole",
"trigger": {"type": "time", "at": "20:00", "weekdays": [0]},
"actions": [{"type": "scene", "scene_id": "scene-id"}],
"timezone": "Europe/Brussels",
}
invalid_time = client.post(
"/api/v1/automations",
json={**base, "trigger": {"type": "time", "at": "25:90", "weekdays": [0]}},
)
assert invalid_time.status_code == 422
assert invalid_time.json()["error"]["code"] == "validation_error"
missing_scene = client.post(
"/api/v1/automations",
json={**base, "actions": [{"type": "scene", "scene_id": None}]},
)
assert missing_scene.status_code == 422
invalid_timezone = client.post(
"/api/v1/automations",
json={**base, "timezone": "Mars/Olympus_Mons"},
)
assert invalid_timezone.status_code == 422