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