"""Tests for models.py — pure deterministic functions only.""" from __future__ import annotations import math import pytest from models import PatientProfile, geocode_zip, haversine_miles @pytest.fixture def minimal_patient(**kwargs): """Minimal PatientProfile factory — callers only specify fields that vary.""" def _make(**overrides): defaults = dict(disease="ALS", age=45, onset_months=6, zip_code="10001", lat=40.75, lon=-73.99) defaults.update(overrides) return PatientProfile(**defaults) return _make # --------------------------------------------------------------------------- # haversine_miles # --------------------------------------------------------------------------- class TestHaversineMiles: def test_same_point_is_zero(self): assert haversine_miles(42.0, -71.0, 42.0, -71.0) == 0.0 def test_known_distance_boston_nyc(self): # Boston (42.3601, -71.0589) to NYC (40.7128, -74.0060) ≈ 190 miles dist = haversine_miles(42.3601, -71.0589, 40.7128, -74.0060) assert 185 < dist < 195 def test_symmetry(self): d1 = haversine_miles(42.0, -71.0, 34.0, -118.0) d2 = haversine_miles(34.0, -118.0, 42.0, -71.0) assert math.isclose(d1, d2, rel_tol=1e-9) def test_short_distance(self): # Two points ~1 mile apart dist = haversine_miles(42.3370, -71.1061, 42.3514, -71.1061) assert 0.8 < dist < 1.2 def test_returns_float(self): result = haversine_miles(0.0, 0.0, 0.0, 1.0) assert isinstance(result, float) # --------------------------------------------------------------------------- # PatientProfile.summary # --------------------------------------------------------------------------- class TestPatientProfileSummary: def test_summary_contains_disease(self, als_patient): assert "Amyotrophic Lateral Sclerosis" in als_patient.summary() def test_summary_contains_age(self, als_patient): assert "52" in als_patient.summary() def test_summary_contains_onset_months(self, als_patient): assert "18" in als_patient.summary() def test_summary_contains_diagnosis_months(self, als_patient): assert "12" in als_patient.summary() def test_summary_contains_zip(self, als_patient): assert "02115" in als_patient.summary() def test_summary_contains_radius(self, als_patient): assert "100" in als_patient.summary() def test_summary_contains_benchmarks(self, als_patient): text = als_patient.summary() assert "alsfrs_r" in text assert "38" in text def test_summary_no_benchmarks(self, minimal_patient): assert "Benchmarks" not in minimal_patient().summary() def test_summary_phase_labels(self, minimal_patient): text = minimal_patient(phases=["2", "3", "0", "na"]).summary() assert "Phase 2" in text assert "Phase 3" in text assert "Early Phase 1" in text assert "Not Applicable" in text def test_summary_study_type_defaults(self, minimal_patient): text = minimal_patient().summary() assert "Clinical trials" in text assert "Observational" not in text assert "EAP" not in text def test_summary_include_observational(self, minimal_patient): assert "Observational" in minimal_patient(include_observational=True).summary() def test_summary_include_eap(self, minimal_patient): assert "EAP" in minimal_patient(include_eap=True).summary() def test_summary_returns_string(self, als_patient): assert isinstance(als_patient.summary(), str) def test_summary_no_phases(self, minimal_patient): assert "Phases:" not in minimal_patient().summary() # --------------------------------------------------------------------------- # geocode_zip — httpx mocked # --------------------------------------------------------------------------- class TestGeocodeZip: def test_returns_lat_lon_from_response(self, httpx_mock): httpx_mock.add_response(json=[{"lat": "42.3370", "lon": "-71.1061"}]) lat, lon = geocode_zip("02115", "US") assert lat == pytest.approx(42.3370) assert lon == pytest.approx(-71.1061) def test_empty_results_raises_value_error(self, httpx_mock): httpx_mock.add_response(json=[]) with pytest.raises(ValueError, match="Cannot geocode"): geocode_zip("00000", "US")