"""Tests for /api/v1/preferences/notifications endpoints.""" import pytest from ledgrab.config import get_config @pytest.fixture(scope="module") def client(): """TestClient with auth header — same pattern as test_preferences_api.py.""" 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 _full_prefs() -> dict: return { "channels": { "device_online": "snack", "device_offline": "both", "device_discovered": "os", "device_lost": "none", }, "background_discovery_enabled": True, "startup_grace_sec": 15, "flap_debounce_sec": 7, } def test_get_returns_defaults_when_unset(client): """When no prefs have been saved, GET returns the documented defaults.""" # Wipe the stored row to a falsy value so this test is independent of # any prior test in the suite that may have PUT a customised matrix. # `load_notification_preferences` falls back to schema defaults when # the stored value is empty / falsy. from ledgrab.api.dependencies import get_database db = get_database() db.set_setting("notification_preferences", {}) resp = client.get("/api/v1/preferences/notifications") assert resp.status_code == 200 body = resp.json() assert body["background_discovery_enabled"] is True assert body["startup_grace_sec"] == 10 assert body["flap_debounce_sec"] == 5 # Default channel matrix assert body["channels"]["device_online"] == "snack" assert body["channels"]["device_offline"] == "both" assert body["channels"]["device_discovered"] == "snack" assert body["channels"]["device_lost"] == "none" def test_put_then_get_round_trips(client): """PUT a payload, GET it back unchanged.""" payload = _full_prefs() put = client.put("/api/v1/preferences/notifications", json=payload) assert put.status_code == 200 assert put.json()["startup_grace_sec"] == 15 got = client.get("/api/v1/preferences/notifications") assert got.status_code == 200 assert got.json() == payload def test_put_rejects_invalid_channel(client): """A bogus channel value (e.g. 'siren') is rejected by Pydantic.""" bad = _full_prefs() bad["channels"]["device_offline"] = "siren" resp = client.put("/api/v1/preferences/notifications", json=bad) assert resp.status_code == 422 def test_put_rejects_grace_out_of_range(client): """startup_grace_sec is clamped to [0, 300].""" bad = _full_prefs() bad["startup_grace_sec"] = -5 assert client.put("/api/v1/preferences/notifications", json=bad).status_code == 422 bad["startup_grace_sec"] = 9999 assert client.put("/api/v1/preferences/notifications", json=bad).status_code == 422 def test_put_rejects_debounce_out_of_range(client): """flap_debounce_sec is clamped to [0, 60].""" bad = _full_prefs() bad["flap_debounce_sec"] = 999 assert client.put("/api/v1/preferences/notifications", json=bad).status_code == 422 def test_partial_payload_uses_defaults_for_omitted_channels(client): """Pydantic fills in default channels when the matrix is partial. The frontend may want to PUT only what changed; the backend should fill in the default channel matrix for omitted rows so we don't silently lose user preferences via partial-write. """ partial = {"background_discovery_enabled": False} resp = client.put("/api/v1/preferences/notifications", json=partial) assert resp.status_code == 200 body = resp.json() assert body["background_discovery_enabled"] is False # Defaulted matrix is present assert body["channels"]["device_offline"] == "both" def test_corrupt_stored_value_falls_back_to_defaults(client): """If something stomps on the stored row, the GET handler must return defaults instead of 500. Mirrors how load_shutdown_action treats corrupt input.""" # Stuff garbage into the underlying setting via the same Database # the route uses, then verify GET still works. from ledgrab.api.dependencies import get_database db = get_database() db.set_setting("notification_preferences", {"channels": "totally-wrong"}) resp = client.get("/api/v1/preferences/notifications") assert resp.status_code == 200 # Defaults restored assert resp.json()["channels"]["device_offline"] == "both"