fix(gps-tracks): normalise GeoJSON props, add health fields, OSM meta fetch, z-order fix
Refs: ET-008 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user