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.
This commit is contained in:
@@ -4,18 +4,25 @@ import pytest
|
||||
|
||||
from ledgrab.config import get_config
|
||||
|
||||
_config = get_config()
|
||||
_api_key = next(iter(_config.auth.api_keys.values()), "")
|
||||
AUTH_HEADERS = {"Authorization": f"Bearer {_api_key}"} if _api_key else {}
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@@ -61,8 +68,8 @@ def _minimal_layout() -> dict:
|
||||
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", headers=AUTH_HEADERS)
|
||||
resp = client.get("/api/v1/preferences/dashboard-layout", headers=AUTH_HEADERS)
|
||||
client.delete("/api/v1/preferences/dashboard-layout")
|
||||
resp = client.get("/api/v1/preferences/dashboard-layout")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {}
|
||||
|
||||
@@ -70,15 +77,11 @@ def test_get_dashboard_layout_default_empty(client):
|
||||
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,
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
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", headers=AUTH_HEADERS)
|
||||
got = client.get("/api/v1/preferences/dashboard-layout")
|
||||
assert got.status_code == 200
|
||||
body = got.json()
|
||||
assert body["version"] == 1
|
||||
@@ -90,11 +93,7 @@ def test_put_then_get_dashboard_layout(client):
|
||||
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,
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
resp = client.put("/api/v1/preferences/dashboard-layout", json=bad)
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
@@ -103,24 +102,16 @@ def test_put_rejects_non_object(client):
|
||||
resp = client.put(
|
||||
"/api/v1/preferences/dashboard-layout",
|
||||
json=["not", "an", "object"],
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
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(),
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
deleted = client.delete(
|
||||
"/api/v1/preferences/dashboard-layout",
|
||||
headers=AUTH_HEADERS,
|
||||
)
|
||||
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", headers=AUTH_HEADERS)
|
||||
after = client.get("/api/v1/preferences/dashboard-layout")
|
||||
assert after.status_code == 200
|
||||
assert after.json() == {}
|
||||
|
||||
@@ -139,7 +130,7 @@ def test_layout_round_trip_preserves_unknown_fields(client):
|
||||
"options": {"sensitivity": 0.7},
|
||||
}
|
||||
)
|
||||
client.put("/api/v1/preferences/dashboard-layout", json=layout, headers=AUTH_HEADERS)
|
||||
got = client.get("/api/v1/preferences/dashboard-layout", headers=AUTH_HEADERS).json()
|
||||
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"])
|
||||
|
||||
Reference in New Issue
Block a user