"""Unit tests for the health data service and its endpoints.""" import pytest from services import health as health_svc DAYS = [ {"date": "2026-08-20", "steps": 6500, "heart_rate": 70, "hrv": 45, "sleep_duration": 6, "sleep_quality": 80, "stress": 55, "calories": 260}, {"date": "2026-08-21", "steps": 9000, "heart_rate": 62, "hrv": 46, "sleep_duration": 8, "sleep_quality": 79, "stress": 40, "calories": 360}, {"date": "2026-08-22", "steps": 7500, "heart_rate": 68, "hrv": 47, "sleep_duration": 7, "sleep_quality": 78, "stress": 48, "calories": 300}, ] class TestGetSummary: def test_empty_for_new_user(self, db, user): assert health_svc.get_summary(user["id"]) == [] def test_returns_all_rows_ordered_by_date(self, seed_health, user): seed_health(DAYS) rows = health_svc.get_summary(user["id"]) assert [r["date"] for r in rows] == [ "2026-08-20", "2026-08-21", "2026-08-22" ] def test_maps_to_camel_case(self, seed_health, user): seed_health(DAYS) row = health_svc.get_summary(user["id"])[0] assert row["heartRate"] == 70 assert row["heartRateVariability"] == 45 assert row["caloriesBurned"] == 260 assert row["sleep"] == {"duration": 6, "quality": 80} def test_sleep_is_none_when_absent(self, seed_health, user): seed_health([{"date": "2026-08-20", "steps": 100}]) assert health_svc.get_summary(user["id"])[0]["sleep"] is None def test_start_date_filter_is_inclusive(self, seed_health, user): seed_health(DAYS) rows = health_svc.get_summary(user["id"], start="2026-08-21") assert [r["date"] for r in rows] == ["2026-08-21", "2026-08-22"] def test_end_date_filter_is_inclusive(self, seed_health, user): seed_health(DAYS) rows = health_svc.get_summary(user["id"], end="2026-08-21") assert [r["date"] for r in rows] == ["2026-08-20", "2026-08-21"] def test_both_bounds(self, seed_health, user): seed_health(DAYS) rows = health_svc.get_summary(user["id"], "2026-08-21", "2026-08-21") assert len(rows) == 1 def test_range_with_no_matches(self, seed_health, user): seed_health(DAYS) assert health_svc.get_summary(user["id"], "2027-01-01") == [] def test_scoped_to_the_requesting_user(self, seed_health, user, db): seed_health(DAYS) db.execute( "INSERT INTO users (id, email, garmin_email, garmin_password_hash) " "VALUES (?,?,?,?)", ["other-user", "other@example.com", "o@example.com", "x"], ) db.execute( "INSERT INTO health_data (id, user_id, date, steps) VALUES (?,?,?,?)", ["other-1", "other-user", "2026-08-20", 99999], ) rows = health_svc.get_summary(user["id"]) assert all(r["steps"] != 99999 for r in rows) assert len(rows) == 3 class TestMetricReads: """Metric endpoints must drop rows where that metric is NULL.""" def test_steps_excludes_nulls(self, seed_health, user): seed_health(DAYS + [{"date": "2026-08-23", "heart_rate": 60}]) rows = health_svc.get_steps(user["id"]) assert len(rows) == 3 assert all(r["steps"] is not None for r in rows) def test_heart_rate_excludes_nulls(self, seed_health, user): seed_health(DAYS + [{"date": "2026-08-23", "steps": 100}]) rows = health_svc.get_heart_rate(user["id"]) assert len(rows) == 3 def test_sleep_excludes_nulls(self, seed_health, user): seed_health(DAYS + [{"date": "2026-08-23", "steps": 100}]) rows = health_svc.get_sleep(user["id"]) assert len(rows) == 3 assert rows[0] == {"date": "2026-08-20", "duration": 6, "quality": 80} class TestUpsert: def test_insert_creates_row(self, db, user): health_svc.upsert_health_daily( user["id"], {"date": "2026-08-20", "steps": 5000} ) rows = health_svc.get_summary(user["id"]) assert len(rows) == 1 and rows[0]["steps"] == 5000 def test_second_upsert_updates_instead_of_duplicating(self, db, user): health_svc.upsert_health_daily( user["id"], {"date": "2026-08-20", "steps": 5000} ) health_svc.upsert_health_daily( user["id"], {"date": "2026-08-20", "steps": 8000} ) rows = health_svc.get_summary(user["id"]) assert len(rows) == 1, "re-syncing a day must not duplicate it" assert rows[0]["steps"] == 8000 def test_upsert_is_deterministic_by_user_and_date(self, db, user): a = health_svc.upsert_health_daily(user["id"], {"date": "2026-08-20"}) b = health_svc.upsert_health_daily(user["id"], {"date": "2026-08-20"}) assert a == b def test_missing_metrics_stored_as_null(self, db, user): health_svc.upsert_health_daily( user["id"], {"date": "2026-08-20", "steps": 100} ) row = health_svc.get_summary(user["id"])[0] assert row["heartRate"] is None assert row["sleep"] is None class TestActivities: def test_insert_and_read_back(self, db, user): health_svc.insert_activity( user["id"], { "activityType": "running", "startTime": "2026-08-20T07:00:00", "endTime": "2026-08-20T07:30:00", "duration": 1800, "distance": 5.0, "calories": 320, "heartRateAverage": 140, "heartRateMax": 165, }, ) rows = health_svc.get_activities(user["id"]) assert len(rows) == 1 assert rows[0]["activity_type"] == "running" assert rows[0]["distance"] == 5.0 def test_ids_are_unique_per_insert(self, db, user): base = { "activityType": "running", "startTime": "2026-08-20T07:00:00", "endTime": "2026-08-20T07:30:00", } assert health_svc.insert_activity(user["id"], base) != health_svc.insert_activity( user["id"], base ) class TestEndpoints: @pytest.mark.parametrize( "path", ["/api/health/summary", "/api/health/steps", "/api/health/heart-rate", "/api/health/sleep", "/api/health/activities"], ) def test_require_authentication(self, client, path): assert client.get(path).status_code == 401 @pytest.mark.parametrize( "path", ["/api/health/summary", "/api/health/steps", "/api/health/heart-rate", "/api/health/sleep", "/api/health/activities"], ) def test_return_json_list_when_authenticated(self, client, auth, path): r = client.get(path, headers=auth) assert r.status_code == 200 assert isinstance(r.get_json(), list) def test_summary_reflects_seeded_data(self, client, auth, seed_health): seed_health(DAYS) body = client.get("/api/health/summary", headers=auth).get_json() assert len(body) == 3 def test_query_params_are_applied(self, client, auth, seed_health): seed_health(DAYS) body = client.get( "/api/health/summary?startDate=2026-08-22", headers=auth ).get_json() assert len(body) == 1