fix(gps-tracks): normalise GeoJSON props, add health fields, OSM meta fetch, z-order fix
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 4s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 3s
CI / test (pull_request) Failing after 4s
CI / build (pull_request) Has been skipped

Refs: ET-008

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 12:42:36 +00:00
parent 37190049db
commit edbe9a3044
5 changed files with 255 additions and 1 deletions

View File

@@ -349,3 +349,54 @@ async def test_cache_clear_endpoint(db_with_tracks):
assert resp.status_code == 200
data = resp.json()
assert data["cleared"] is True
# ─── F-01/F-02: GeoJSON normalised properties ─────────────────────────────────
@pytest.mark.asyncio
async def test_f01_f02_geojson_normalised_properties(db_with_tracks):
"""F-01/F-02: GeoJSON features carry activity/source (MVT-compatible) and length_km."""
app = _make_test_app(db_with_tracks)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
resp = await client.get(
"/api/gps-tracks",
params={"bbox": "37.5,55.7,37.9,55.9"},
)
assert resp.status_code == 200
data = resp.json()
assert len(data["features"]) > 0
for feature in data["features"]:
props = feature["properties"]
# F-01: MVT-compatible aliases
assert "activity" in props, "activity field missing (F-01)"
assert "source" in props, "source field missing (F-01)"
assert isinstance(props["source"], str), "source must be str (F-01)"
assert props["activity"] == props["activity_type"], "activity must equal activity_type"
# F-02: length in km
assert "length_km" in props, "length_km missing (F-02)"
assert isinstance(props["length_km"], float), "length_km must be float"
if props["length_m"]:
assert abs(props["length_km"] - props["length_m"] / 1000) < 0.01
# ─── F-04: health extended fields ─────────────────────────────────────────────
@pytest.mark.asyncio
async def test_f04_health_extended_fields(db_with_tracks):
"""F-04: /health returns db_size_mb, tracks_by_source, tile_cache_size."""
app = _make_test_app(db_with_tracks)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
resp = await client.get("/api/gps-tracks/health")
assert resp.status_code == 200
data = resp.json()
# db_size_mb
assert "db_size_mb" in data, "db_size_mb missing (F-04)"
assert isinstance(data["db_size_mb"], (int, float))
assert data["db_size_mb"] >= 0
# tracks_by_source
assert "tracks_by_source" in data, "tracks_by_source missing (F-04)"
assert isinstance(data["tracks_by_source"], dict)
# tile_cache_size
assert "tile_cache_size" in data, "tile_cache_size missing (F-04)"
assert isinstance(data["tile_cache_size"], int)
assert data["tile_cache_size"] >= 0

View File

@@ -180,3 +180,69 @@ def test_u44_multiple_tracks_in_gpx():
ids = {t.external_id for t in tracks}
assert "111" in ids
assert "222" in ids
# ─── U-45: _parse_gpx_meta_response ──────────────────────────────────────────
def test_u45_meta_response_with_known_tag():
"""U-45: _parse_gpx_meta_response extracts activity via MAPPING."""
from src.api.gps_tracks.sources.osm import _parse_gpx_meta_response
content = b"""<?xml version="1.0"?>
<osm version="0.6">
<gpx_file id="123" name="my_ride.gpx" user="alice">
<description>Weekend ride</description>
<tag>enduro</tag>
<tag>motorcycle</tag>
</gpx_file>
</osm>"""
meta = _parse_gpx_meta_response(content)
assert meta is not None
assert meta["activity_type"] == "enduro"
assert meta["name"] == "my_ride.gpx"
assert meta["user"] == "alice"
assert meta["description"] == "Weekend ride"
def test_u45_meta_response_unknown_tag_returns_none_activity():
"""U-45: unknown tag → activity_type is None."""
from src.api.gps_tracks.sources.osm import _parse_gpx_meta_response
content = b"""<?xml version="1.0"?>
<osm version="0.6">
<gpx_file id="99" name="trip.gpx" user="bob">
<tag>unknown-sport</tag>
</gpx_file>
</osm>"""
meta = _parse_gpx_meta_response(content)
assert meta is not None
assert meta["activity_type"] is None
def test_u45_meta_response_motorcycle_maps_to_moto():
"""U-45: 'motorcycle' tag maps to 'moto' via MAPPING."""
from src.api.gps_tracks.sources.osm import _parse_gpx_meta_response
content = b"""<?xml version="1.0"?>
<osm version="0.6">
<gpx_file id="77" name="ride.gpx" user="carl">
<tag>motorcycle</tag>
</gpx_file>
</osm>"""
meta = _parse_gpx_meta_response(content)
assert meta["activity_type"] == "moto"
def test_u45_meta_response_invalid_xml_returns_none():
"""U-45: malformed XML returns None."""
from src.api.gps_tracks.sources.osm import _parse_gpx_meta_response
meta = _parse_gpx_meta_response(b"not xml at all <<<")
assert meta is None
def test_u45_meta_response_no_gpx_file_element():
"""U-45: valid XML but no gpx_file element → result has all None values."""
from src.api.gps_tracks.sources.osm import _parse_gpx_meta_response
content = b"""<?xml version="1.0"?><osm version="0.6"></osm>"""
meta = _parse_gpx_meta_response(content)
# Function should return the dict with None values, not None itself
assert meta is not None
assert meta["activity_type"] is None
assert meta["name"] is None