Files
planeMapper/tests/test_fetcher.py
Matt Edholm 6208134a1c Implement story 2.1: aircraft data model and fetcher
Add HttpFetcher and FileFixtureFetcher with shared _parse_aircraft helper,
DUMP1090_URL constant, realistic fixture data, and full test coverage for
all acceptance criteria (AC1–AC5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:03:20 -04:00

125 lines
3.5 KiB
Python

from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
import requests
from planemapper.fetcher import FileFixtureFetcher, HttpFetcher
_FIXTURES = Path(__file__).parent / "fixtures" / "aircraft_sample.json"
_FULL_RESPONSE = {
"aircraft": [
{
"hex": "4ca7f2",
"lat": 53.3498,
"lon": -6.2603,
"flight": "EIN123 ",
"altitude": 12000,
"category": "A3",
"track": 270.0,
"mlat": [],
}
]
}
_DEFAULTS_RESPONSE = {
"aircraft": [
{
"hex": "aabbcc",
"lat": 51.0,
"lon": -1.0,
}
]
}
_MLAT_RESPONSE = {
"aircraft": [
{
"hex": "dddddd",
"lat": 52.0,
"lon": -2.0,
"mlat": ["lat", "lon"],
}
]
}
def _mock_get(payload: dict) -> MagicMock:
mock_resp = MagicMock()
mock_resp.json.return_value = payload
mock_resp.raise_for_status = MagicMock()
return mock_resp
def test_http_fetcher_full_response() -> None:
with patch("planemapper.fetcher.requests.get", return_value=_mock_get(_FULL_RESPONSE)):
aircraft = HttpFetcher().fetch()
assert len(aircraft) == 1
a = aircraft[0]
assert a.icao == "4ca7f2"
assert a.lat == 53.3498
assert a.lon == -6.2603
assert a.callsign == "EIN123"
assert a.altitude_ft == 12000
assert a.category == "A3"
assert a.heading == 270.0
assert a.is_mlat is False
assert a.is_stale is False
def test_http_fetcher_missing_fields_use_defaults() -> None:
with patch("planemapper.fetcher.requests.get", return_value=_mock_get(_DEFAULTS_RESPONSE)):
aircraft = HttpFetcher().fetch()
assert len(aircraft) == 1
a = aircraft[0]
assert a.callsign == ""
assert a.altitude_ft == 0
assert a.category == ""
assert a.is_mlat is False
def test_http_fetcher_altitude_ground_string() -> None:
payload = {"aircraft": [{"hex": "aaa", "lat": 51.0, "lon": -1.0, "altitude": "ground"}]}
with patch("planemapper.fetcher.requests.get", return_value=_mock_get(payload)):
aircraft = HttpFetcher().fetch()
assert aircraft[0].altitude_ft == 0
def test_http_fetcher_skips_entries_without_position() -> None:
payload = {"aircraft": [{"hex": "nopos"}, {"hex": "haspos", "lat": 51.0, "lon": -1.0}]}
with patch("planemapper.fetcher.requests.get", return_value=_mock_get(payload)):
aircraft = HttpFetcher().fetch()
assert len(aircraft) == 1
assert aircraft[0].icao == "haspos"
def test_http_fetcher_propagates_timeout() -> None:
with patch("planemapper.fetcher.requests.get", side_effect=requests.Timeout):
with pytest.raises(requests.Timeout):
HttpFetcher().fetch()
def test_http_fetcher_mlat_flag() -> None:
with patch("planemapper.fetcher.requests.get", return_value=_mock_get(_MLAT_RESPONSE)):
aircraft = HttpFetcher().fetch()
assert aircraft[0].is_mlat is True
def test_file_fixture_fetcher_returns_aircraft() -> None:
aircraft = FileFixtureFetcher(_FIXTURES).fetch()
assert len(aircraft) == 4
def test_file_fixture_fetcher_no_network_call() -> None:
with patch("planemapper.fetcher.requests.get") as mock_get:
FileFixtureFetcher(_FIXTURES).fetch()
mock_get.assert_not_called()
def test_file_fixture_fetcher_mlat_aircraft() -> None:
aircraft = FileFixtureFetcher(_FIXTURES).fetch()
mlat_aircraft = [a for a in aircraft if a.is_mlat]
assert len(mlat_aircraft) == 1
assert mlat_aircraft[0].icao == "4003c3"