"""Tests for geofence haversine, enter/exit detection, and persistence.""" from unittest.mock import MagicMock, patch import pytest class TestHaversineDistance: """Test haversine_distance accuracy.""" def test_same_point_zero_distance(self): from utils.geofence import haversine_distance assert haversine_distance(51.5, -0.1, 51.5, -0.1) == 0.0 def test_known_distance_london_paris(self): from utils.geofence import haversine_distance # London to Paris ~340km dist = haversine_distance(51.5074, -0.1278, 48.8566, 2.3522) assert 340_000 < dist < 345_000 def test_short_distance(self): from utils.geofence import haversine_distance # Two points ~111m apart (0.001 degrees latitude at equator) dist = haversine_distance(0.0, 0.0, 0.001, 0.0) assert 100 < dist < 120 def test_antipodal_distance(self): from utils.geofence import haversine_distance # North pole to south pole ~20015km dist = haversine_distance(90.0, 0.0, -90.0, 0.0) assert 20_000_000 < dist < 20_050_000 class TestGeofenceManager: """Test enter/exit detection logic.""" @pytest.fixture(autouse=True) def _setup(self): """Provide a fresh GeofenceManager with mocked DB.""" from utils.geofence import GeofenceManager with patch("utils.geofence._ensure_table"), patch("utils.geofence.get_db") as mock_db: # Mock the context manager mock_conn = MagicMock() mock_db.return_value.__enter__ = MagicMock(return_value=mock_conn) mock_db.return_value.__exit__ = MagicMock(return_value=False) self.manager = GeofenceManager() # Override list_zones to return test data self._zones = [] self.manager.list_zones = lambda: self._zones def test_no_zones_returns_empty(self): events = self.manager.check_position("TEST1", "aircraft", 51.5, -0.1) assert events == [] def test_enter_event(self): self._zones = [ { "id": 1, "name": "London", "lat": 51.5074, "lon": -0.1278, "radius_m": 10000, "alert_on": "enter_exit", } ] # First position inside zone events = self.manager.check_position("AC1", "aircraft", 51.5074, -0.1278) assert len(events) == 1 assert events[0]["type"] == "geofence_enter" assert events[0]["zone_name"] == "London" def test_no_duplicate_enter(self): self._zones = [ { "id": 1, "name": "London", "lat": 51.5074, "lon": -0.1278, "radius_m": 10000, "alert_on": "enter_exit", } ] # First enter self.manager.check_position("AC1", "aircraft", 51.5074, -0.1278) # Second check still inside - should not fire enter again events = self.manager.check_position("AC1", "aircraft", 51.508, -0.128) assert len(events) == 0 def test_exit_event(self): self._zones = [ { "id": 1, "name": "London", "lat": 51.5074, "lon": -0.1278, "radius_m": 1000, "alert_on": "enter_exit", } ] # Enter self.manager.check_position("AC1", "aircraft", 51.5074, -0.1278) # Exit (far away) events = self.manager.check_position("AC1", "aircraft", 52.0, 0.0) assert len(events) == 1 assert events[0]["type"] == "geofence_exit" def test_enter_only_mode(self): self._zones = [ { "id": 1, "name": "London", "lat": 51.5074, "lon": -0.1278, "radius_m": 1000, "alert_on": "enter", } ] # Enter events = self.manager.check_position("AC1", "aircraft", 51.5074, -0.1278) assert len(events) == 1 assert events[0]["type"] == "geofence_enter" # Exit should not fire events = self.manager.check_position("AC1", "aircraft", 52.0, 0.0) assert len(events) == 0 def test_metadata_included_in_event(self): self._zones = [ { "id": 1, "name": "Zone", "lat": 0.0, "lon": 0.0, "radius_m": 100000, "alert_on": "enter_exit", } ] events = self.manager.check_position( "AC1", "aircraft", 0.0, 0.0, metadata={"callsign": "TEST01", "altitude": 35000} ) assert events[0]["callsign"] == "TEST01" assert events[0]["altitude"] == 35000