80f01d4813
- ``tests/test_preferences_api.py`` no longer captures the auth API key at module-import time. The new ``client`` fixture resolves it inside its body and bakes the Bearer header into ``TestClient.headers``, so the e2e conftest swapping the global config singleton during collection cannot leave the test holding a stale 401-bound header. Same proven pattern as ``test_audio_processing_templates_api.py``. - ``.gitignore`` now anchors ``/server/src/data/`` defensively. If the server is launched from ``server/src/`` (uncommon but possible during ad-hoc debugging), its relative ``data/`` resolves there. Templates now live in SQLite (``capture_templates`` / ``pattern_templates`` / ``postprocessing_templates`` tables); any stale ``*.json`` that lands in that directory is a runtime export and must not be committed. - Three such stale exports were untracked at the start of the pre-merge audit and have been deleted from the working tree. - ``TODO.md`` flips the shutdown-action checklist to done and notes that real-hardware verification (WLED + serial after Ctrl+C) is still pending.
137 lines
4.4 KiB
Python
137 lines
4.4 KiB
Python
"""Tests for /api/v1/preferences/dashboard-layout endpoints."""
|
|
|
|
import pytest
|
|
|
|
from ledgrab.config import get_config
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def client():
|
|
"""TestClient with auth header read at fixture-build time.
|
|
|
|
The auth API key is resolved here (not at module import) so any
|
|
config-singleton mutation that happens during pytest collection —
|
|
notably ``server/tests/e2e/conftest.py`` reassigning the global
|
|
config to a different test key during collection of e2e tests —
|
|
cannot leave us holding a stale Bearer header that yields 401.
|
|
"""
|
|
from fastapi.testclient import TestClient
|
|
|
|
from ledgrab.main import app
|
|
|
|
api_key = next(iter(get_config().auth.api_keys.values()), "")
|
|
with TestClient(app, raise_server_exceptions=False) as c:
|
|
if api_key:
|
|
c.headers["Authorization"] = f"Bearer {api_key}"
|
|
yield c
|
|
|
|
|
|
def _minimal_layout() -> dict:
|
|
return {
|
|
"version": 1,
|
|
"sections": [
|
|
{
|
|
"key": "perf",
|
|
"visible": True,
|
|
"collapsedDefault": False,
|
|
"density": "comfortable",
|
|
"options": {},
|
|
},
|
|
],
|
|
"perfCells": [
|
|
{
|
|
"key": "cpu",
|
|
"visible": True,
|
|
"mode": "inherit",
|
|
"span": 1,
|
|
"window": 120,
|
|
"yScale": "auto",
|
|
"precision": 1,
|
|
"showSubtitle": True,
|
|
"showRefLine": True,
|
|
},
|
|
],
|
|
"global": {
|
|
"width": "full",
|
|
"accent": "target",
|
|
"animations": "full",
|
|
"emptyState": "hide",
|
|
"toolbarPosition": "top",
|
|
"autoCollapseRunningEmpty": False,
|
|
"showTutorial": True,
|
|
"perfMode": "both",
|
|
"pollMs": 1000,
|
|
},
|
|
}
|
|
|
|
|
|
def test_get_dashboard_layout_default_empty(client):
|
|
"""When no layout has been saved, GET returns an empty object."""
|
|
# Clear first so this test is order-independent.
|
|
client.delete("/api/v1/preferences/dashboard-layout")
|
|
resp = client.get("/api/v1/preferences/dashboard-layout")
|
|
assert resp.status_code == 200
|
|
assert resp.json() == {}
|
|
|
|
|
|
def test_put_then_get_dashboard_layout(client):
|
|
"""PUT a layout, GET it back unchanged."""
|
|
layout = _minimal_layout()
|
|
put = client.put("/api/v1/preferences/dashboard-layout", json=layout)
|
|
assert put.status_code == 200
|
|
assert put.json() == {"ok": True}
|
|
|
|
got = client.get("/api/v1/preferences/dashboard-layout")
|
|
assert got.status_code == 200
|
|
body = got.json()
|
|
assert body["version"] == 1
|
|
assert body["sections"][0]["key"] == "perf"
|
|
assert body["perfCells"][0]["key"] == "cpu"
|
|
assert body["global"]["perfMode"] == "both"
|
|
|
|
|
|
def test_put_rejects_missing_version(client):
|
|
"""Body without numeric version field is rejected with 422."""
|
|
bad = {"sections": []}
|
|
resp = client.put("/api/v1/preferences/dashboard-layout", json=bad)
|
|
assert resp.status_code == 422
|
|
|
|
|
|
def test_put_rejects_non_object(client):
|
|
"""Bare arrays / strings / numbers are rejected by FastAPI body validation."""
|
|
resp = client.put(
|
|
"/api/v1/preferences/dashboard-layout",
|
|
json=["not", "an", "object"],
|
|
)
|
|
assert resp.status_code in (400, 422)
|
|
|
|
|
|
def test_delete_clears_layout(client):
|
|
"""DELETE wipes the saved layout so subsequent GET returns empty."""
|
|
client.put("/api/v1/preferences/dashboard-layout", json=_minimal_layout())
|
|
deleted = client.delete("/api/v1/preferences/dashboard-layout")
|
|
assert deleted.status_code == 200
|
|
after = client.get("/api/v1/preferences/dashboard-layout")
|
|
assert after.status_code == 200
|
|
assert after.json() == {}
|
|
|
|
|
|
def test_layout_round_trip_preserves_unknown_fields(client):
|
|
"""Frontend may add new keys (e.g. v1.1 sections) — backend must
|
|
pass them through verbatim, not strip them."""
|
|
layout = _minimal_layout()
|
|
layout["futureField"] = {"foo": "bar"}
|
|
layout["sections"].append(
|
|
{
|
|
"key": "audio-meters",
|
|
"visible": True,
|
|
"collapsedDefault": False,
|
|
"density": "comfortable",
|
|
"options": {"sensitivity": 0.7},
|
|
}
|
|
)
|
|
client.put("/api/v1/preferences/dashboard-layout", json=layout)
|
|
got = client.get("/api/v1/preferences/dashboard-layout").json()
|
|
assert got["futureField"] == {"foo": "bar"}
|
|
assert any(s["key"] == "audio-meters" for s in got["sections"])
|