Files
glucose-dashboard/tests/test_nightscout_client.py
T
willemjanandClaude Sonnet 5 3ef9a7666a add reporting foundation: multipage nav, auth gate, date-range fetch
First increment of the clinical reporting feature (see
docs/requirements-reporting.md and docs/design-reporting.md): moves the
existing live view to pages/1_Live.py, adds a password gate since the
dashboard URL will be shared with the diabetes team, adds
fetch_entries_range/fetch_treatments_range for multi-day report queries,
and adds reporting.py with the core clinical calculations (GMI, %CV,
5-band time-in-range, hypo/hyper event detection per the Battelino et al.
2019 persistence rule). No report pages wired up yet — that's next.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-26 20:00:48 +02:00

137 lines
4.8 KiB
Python

from datetime import date, datetime, timedelta, timezone
import pytest
import nightscout_client as ns
class FakeResponse:
def __init__(self, payload):
self._payload = payload
def raise_for_status(self):
pass
def json(self):
return self._payload
def test_mgdl_to_mmol_converts_correctly():
assert ns.mgdl_to_mmol(126.0) == pytest.approx(7.0, abs=0.05)
def test_base_url_requires_nightscout_url(monkeypatch):
monkeypatch.delenv("NIGHTSCOUT_URL", raising=False)
with pytest.raises(ns.NightscoutConfigError):
ns._base_url()
def test_base_url_strips_trailing_slash(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com/")
assert ns._base_url() == "https://example.com"
def test_auth_prefers_token_over_api_secret(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_TOKEN", "my-token")
monkeypatch.setenv("NIGHTSCOUT_API_SECRET", "my-secret")
headers, params = ns._auth()
assert params == {"token": "my-token"}
assert headers == {}
def test_auth_hashes_api_secret(monkeypatch):
monkeypatch.delenv("NIGHTSCOUT_TOKEN", raising=False)
monkeypatch.setenv("NIGHTSCOUT_API_SECRET", "my-secret")
headers, params = ns._auth()
assert params == {}
assert headers["API-SECRET"] != "my-secret" # nooit in plaintext
assert len(headers["API-SECRET"]) == 40 # sha1 hexdigest
def test_fetch_entries_converts_and_filters_by_time(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
monkeypatch.delenv("NIGHTSCOUT_TOKEN", raising=False)
monkeypatch.delenv("NIGHTSCOUT_API_SECRET", raising=False)
now = datetime.now(timezone.utc)
recent = now - timedelta(hours=1)
too_old = now - timedelta(hours=48)
payload = [
{"date": int(recent.timestamp() * 1000), "sgv": 126},
{"date": int(too_old.timestamp() * 1000), "sgv": 90},
]
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse(payload))
result = ns.fetch_entries(hours=24)
assert len(result) == 1
assert result.iloc[0]["mmol"] == pytest.approx(7.0, abs=0.05)
def test_fetch_entries_handles_empty_response(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse([]))
result = ns.fetch_entries(hours=24)
assert result.empty
assert list(result.columns) == ["timestamp", "mmol"]
def test_fetch_treatments_fills_missing_columns(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
now = datetime.now(timezone.utc)
payload = [
{"created_at": now.isoformat(), "eventType": "Site Change"},
]
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse(payload))
result = ns.fetch_treatments(hours=24)
assert len(result) == 1
assert result.iloc[0]["eventType"] == "Site Change"
assert pd_isna(result.iloc[0]["insulin"])
assert pd_isna(result.iloc[0]["carbs"])
def pd_isna(value):
import pandas as pd
return pd.isna(value)
def test_fetch_entries_range_filters_to_date_boundaries(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
monkeypatch.delenv("NIGHTSCOUT_TOKEN", raising=False)
monkeypatch.delenv("NIGHTSCOUT_API_SECRET", raising=False)
payload = [
{"date": int(datetime(2026, 1, 15, 12, 0, tzinfo=timezone.utc).timestamp() * 1000), "sgv": 126},
{"date": int(datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc).timestamp() * 1000), "sgv": 90},
{"date": int(datetime(2026, 2, 1, 12, 0, tzinfo=timezone.utc).timestamp() * 1000), "sgv": 100},
]
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse(payload))
result = ns.fetch_entries_range(date(2026, 1, 1), date(2026, 1, 31))
assert len(result) == 2 # de 1 feb-meting valt buiten het bereik
assert list(result.columns) == ["timestamp", "mmol"]
def test_fetch_entries_range_handles_empty_response(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse([]))
result = ns.fetch_entries_range(date(2026, 1, 1), date(2026, 1, 31))
assert result.empty
def test_fetch_treatments_range_filters_to_date_boundaries(monkeypatch):
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
payload = [
{"created_at": "2026-01-15T12:00:00Z", "eventType": "Meal Bolus", "insulin": 4.0, "carbs": 40},
{"created_at": "2026-02-01T12:00:00Z", "eventType": "Meal Bolus", "insulin": 3.0, "carbs": 30},
]
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse(payload))
result = ns.fetch_treatments_range(date(2026, 1, 1), date(2026, 1, 31))
assert len(result) == 1
assert result.iloc[0]["insulin"] == pytest.approx(4.0)