|
| 1 | +""" |
| 2 | +Integration tests for the geospatial track service. |
| 3 | +
|
| 4 | +Requires a running PostGIS instance. Set DATABASE_URL to point at it, or the |
| 5 | +whole module is skipped. The sample flight CSV is copied to a temp path for |
| 6 | +each test session so the ingest deduplication key is unique and does not |
| 7 | +collide with any manually-ingested data. |
| 8 | +
|
| 9 | +Run with: |
| 10 | + DATABASE_URL=postgresql://geo:geopassword@localhost:5432/flightlog \ |
| 11 | + .venv/bin/pytest tests/test_geo.py -v |
| 12 | +""" |
| 13 | + |
| 14 | +import csv |
| 15 | +import os |
| 16 | +import shutil |
| 17 | +from pathlib import Path |
| 18 | + |
| 19 | +import pytest |
| 20 | +from fastapi.testclient import TestClient |
| 21 | + |
| 22 | +from geo.ingest import ingest |
| 23 | + |
| 24 | +DATABASE_URL = os.environ.get("DATABASE_URL", "") |
| 25 | +DATA_CSV = Path(__file__).parent.parent / "data" / "sample_flight.csv" |
| 26 | + |
| 27 | +pytestmark = pytest.mark.skipif( |
| 28 | + not DATABASE_URL, |
| 29 | + reason="DATABASE_URL not set — start PostGIS with docker compose -f docker-compose.geo.yml up", |
| 30 | +) |
| 31 | + |
| 32 | + |
| 33 | +@pytest.fixture(scope="module") |
| 34 | +def csv_path(tmp_path_factory): |
| 35 | + """A temp copy of the sample CSV so the source_file key is test-session-unique.""" |
| 36 | + dest = tmp_path_factory.mktemp("geo") / "test_flight.csv" |
| 37 | + shutil.copy(DATA_CSV, dest) |
| 38 | + return dest |
| 39 | + |
| 40 | + |
| 41 | +@pytest.fixture(scope="module") |
| 42 | +def expected_points(csv_path): |
| 43 | + """Count data rows in the CSV (excluding header).""" |
| 44 | + with open(csv_path, newline="") as fh: |
| 45 | + return sum(1 for _ in csv.DictReader(fh)) |
| 46 | + |
| 47 | + |
| 48 | +@pytest.fixture(scope="module") |
| 49 | +def flight_id(csv_path): |
| 50 | + return ingest(str(csv_path), DATABASE_URL) |
| 51 | + |
| 52 | + |
| 53 | +@pytest.fixture(scope="module") |
| 54 | +def client(): |
| 55 | + os.environ["DATABASE_URL"] = DATABASE_URL |
| 56 | + # Import after setting the env var so asyncpg pool creation can read it. |
| 57 | + from geo.api import app # noqa: PLC0415 |
| 58 | + |
| 59 | + with TestClient(app) as c: |
| 60 | + yield c |
| 61 | + |
| 62 | + |
| 63 | +# --- ingest --- |
| 64 | + |
| 65 | + |
| 66 | +def test_ingest_returns_positive_id(flight_id): |
| 67 | + assert isinstance(flight_id, int) |
| 68 | + assert flight_id > 0 |
| 69 | + |
| 70 | + |
| 71 | +def test_ingest_idempotent(csv_path, flight_id): |
| 72 | + """Running ingest twice on the same file must return the same id, not raise.""" |
| 73 | + second_id = ingest(str(csv_path), DATABASE_URL) |
| 74 | + assert second_id == flight_id |
| 75 | + |
| 76 | + |
| 77 | +# --- API --- |
| 78 | + |
| 79 | + |
| 80 | +def test_health(client): |
| 81 | + resp = client.get("/health") |
| 82 | + assert resp.status_code == 200 |
| 83 | + assert resp.json() == {"status": "ok"} |
| 84 | + |
| 85 | + |
| 86 | +def test_track_is_geojson_feature(client, flight_id): |
| 87 | + resp = client.get(f"/flights/{flight_id}/track") |
| 88 | + assert resp.status_code == 200 |
| 89 | + body = resp.json() |
| 90 | + assert body["type"] == "Feature" |
| 91 | + assert body["properties"]["flight_id"] == flight_id |
| 92 | + |
| 93 | + |
| 94 | +def test_track_geometry_is_linestring(client, flight_id): |
| 95 | + body = client.get(f"/flights/{flight_id}/track").json() |
| 96 | + assert body["geometry"]["type"] == "LineString" |
| 97 | + |
| 98 | + |
| 99 | +def test_track_point_count(client, flight_id, expected_points): |
| 100 | + """Coordinate count must match CSV row count — no duplicates, no drops.""" |
| 101 | + body = client.get(f"/flights/{flight_id}/track").json() |
| 102 | + coords = body["geometry"]["coordinates"] |
| 103 | + assert len(coords) == expected_points |
| 104 | + |
| 105 | + |
| 106 | +def test_track_coordinates_are_3d(client, flight_id): |
| 107 | + """Each coordinate must be [lon, lat, alt] — three values (LineStringZ).""" |
| 108 | + body = client.get(f"/flights/{flight_id}/track").json() |
| 109 | + coords = body["geometry"]["coordinates"] |
| 110 | + assert all(len(c) == 3 for c in coords) |
| 111 | + |
| 112 | + |
| 113 | +def test_track_unknown_flight_returns_404(client): |
| 114 | + resp = client.get("/flights/999999/track") |
| 115 | + assert resp.status_code == 404 |
0 commit comments