Files
ledgrab/server/tests/test_preferences_api.py
T
alexei.dolgolyov 80f01d4813 chore: harden test isolation, gitignore stale src/data, mark shutdown action done
- ``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.
2026-04-25 15:11:39 +03:00

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"])