"""Unit-тесты для ET-011 GPX-builder (`src/api/gps_tracks/export.py`).
Покрывает test-plan: UT-01, UT-02, UT-03, UT-05.
"""
from __future__ import annotations
import os
import xml.etree.ElementTree as ET
import pytest
from lxml import etree as lxml_et
from src.api.gps_tracks.export import build_gpx
GPX_NS = "http://www.topografix.com/GPX/1/1"
GPX = "{%s}" % GPX_NS
_FIXTURES_DIR = os.path.join(
os.path.dirname(__file__), "..", "fixtures", "gpx-1.1"
)
_GPX_XSD_PATH = os.path.abspath(os.path.join(_FIXTURES_DIR, "gpx.xsd"))
@pytest.fixture(scope="module")
def gpx_schema() -> lxml_et.XMLSchema:
"""Загружает GPX 1.1 XSD-схему (см. tests/fixtures/gpx-1.1/gpx.xsd)."""
if not os.path.exists(_GPX_XSD_PATH):
pytest.skip(f"GPX XSD not found at {_GPX_XSD_PATH}")
return lxml_et.XMLSchema(lxml_et.parse(_GPX_XSD_PATH))
def _validate_gpx(xml_str: str, schema: lxml_et.XMLSchema) -> None:
"""Валидирует GPX-строку по schema; падает с диагностикой при ошибке."""
doc = lxml_et.fromstring(xml_str.encode("utf-8"))
schema.assertValid(doc)
# ─── UT-01: корректная структура GPX 1.1 ──────────────────────────────────
def test_ut01_build_gpx_basic_structure():
"""UT-01: 5 точек, name/description/external_urls — все элементы на месте."""
xml_str = build_gpx(
track_id=1,
name="Test trail",
description="A short description",
activity_type="enduro",
user="testuser",
created_at="2024-05-12T10:00:00Z",
sources=["osm"],
external_urls=["https://www.openstreetmap.org/way/1"],
coords=[
(37.60, 55.74),
(37.61, 55.75),
(37.62, 55.76),
(37.63, 55.77),
(37.64, 55.78),
],
)
# ET-парсинг — используем тот же ElementTree namespace
root = ET.fromstring(xml_str)
assert root.tag == f"{GPX}gpx"
assert root.attrib["version"] == "1.1"
assert root.attrib["creator"] == "Enduro Trails"
metadata = root.find(f"{GPX}metadata")
assert metadata is not None
name_el = metadata.find(f"{GPX}name")
assert name_el is not None and name_el.text == "Test trail"
link_el = metadata.find(f"{GPX}link")
assert link_el is not None
assert link_el.attrib["href"] == "https://www.openstreetmap.org/way/1"
trks = root.findall(f"{GPX}trk")
assert len(trks) == 1
trk = trks[0]
segs = trk.findall(f"{GPX}trkseg")
assert len(segs) == 1
pts = segs[0].findall(f"{GPX}trkpt")
assert len(pts) == 5
for pt in pts:
# lat/lon — float-парсебельные
lat = float(pt.attrib["lat"])
lon = float(pt.attrib["lon"])
assert -90 <= lat <= 90
assert -180 <= lon <= 180
def test_ut01_metadata_link_text_includes_source():
"""UT-01: text = 'Источник: '."""
xml_str = build_gpx(
track_id=1,
name="x",
description=None,
activity_type=None,
user=None,
created_at=None,
sources=["osm"],
external_urls=["https://www.openstreetmap.org/way/42"],
coords=[(0.0, 0.0), (1.0, 1.0)],
)
root = ET.fromstring(xml_str)
link = root.find(f"{GPX}metadata/{GPX}link")
text_el = link.find(f"{GPX}text")
assert text_el is not None
assert text_el.text == "Источник: osm"
def test_ut01_osm_copyright_present():
"""UT-01 / AC-10: для OSM-источника присутствует с OSM license."""
xml_str = build_gpx(
track_id=1,
name="osm track",
description=None,
activity_type=None,
user=None,
created_at=None,
sources=["osm"],
external_urls=["https://www.openstreetmap.org/way/123"],
coords=[(0.0, 0.0), (1.0, 1.0)],
)
root = ET.fromstring(xml_str)
cr = root.find(f"{GPX}metadata/{GPX}copyright")
assert cr is not None
assert cr.attrib["author"] == "Enduro Trails"
lic = cr.find(f"{GPX}license")
assert lic is not None
assert lic.text == "https://www.openstreetmap.org/copyright"
# ─── UT-02: пустые / NULL поля ────────────────────────────────────────────
def test_ut02_empty_fields_no_elements():
"""UT-02: ,