Конфиг-only активация двух новых источников GPS-треков поверх pipeline ET-008. Не вводит новых компонентов, БД-таблиц, endpoint'ов. Config: - config/gps_sources.yaml: enduro_russia enabled=true, base_url исправлен на endurorussia.ru (без дефиса); добавлена запись wikiloc с max_tracks_per_run=50, activity_filter=[motorcycle, enduro]. - config/gps_regions.yaml: wikiloc добавлен в tsfo_plus_chuvashia.sources. Parser: - wikiloc.py: добавлен soft-cap max_tracks_per_run в collect(), извлечение created_at из GPX metadata/первого trkpt — для корректной межисточниковой дедупликации с EnduroRussia. UI (src/web/gps_tracks.js): - GPS_SOURCE_COLORS: добавлен цвет wikiloc (#4363d8). - Дефолтный фильтр sources включает wikiloc. - GPS_SOURCE_ATTRIBUTIONS: маппинг source_id → строка атрибуции; _updateGpsAttribution() подтягивает /api/gps-tracks/health и выставляет attribution с теми источниками, у которых tracks > 0. - _buildGpsFiltersUI: чекбокс «Wikiloc» в #gps-source-grid. Tests: - Fixtures: 7 файлов в tests/fixtures/gps-tracks/. - Unit: 10 UT-ER + 10 UT-WL — парсеры, MAPPING, bbox-фильтр, pagination, 429/403 graceful-stop, rate-limit, max_tracks_per_run. - Integration: IT-ER-01, IT-WL-01, IT-WL-02, IT-DEDUP-01, IT-LIC-01 через scripts.gps_collect.main + httpx.MockTransport. - Contract: 2 CT-ER с маркером @pytest.mark.network (nightly only). - JS: 2 новых теста на наличие wikiloc в SOURCE_COLORS и в фильтрах. Linters/Tests: ruff clean (новые файлы), 166 pytest passed, 24 JS-tests passed. Refs: ET-009 Acceptance: AC-01..AC-08, AC-14..AC-17 (для AC-09..AC-13 — продакшн-прогон) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
"""Contract smoke tests for live endurorussia.ru API (ET-009).
|
||
|
||
Маркер @pytest.mark.network — пропускается в обычном CI.
|
||
Запускается вручную или nightly: `pytest -m network`.
|
||
|
||
Coverage:
|
||
- CT-ER-01: GET /api/tracks?page=0&limit=5 → 200 + items, total
|
||
- CT-ER-02: GET /api/tracks/{first_id}/gpx → 200 + parseable GPX
|
||
"""
|
||
import pytest
|
||
|
||
import defusedxml.ElementTree as ET
|
||
import httpx
|
||
|
||
|
||
BASE_URL = "https://endurorussia.ru"
|
||
USER_AGENT = "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
|
||
|
||
|
||
@pytest.mark.network
|
||
def test_ct_er_01_tracks_list_200_with_items():
|
||
"""CT-ER-01: GET /api/tracks?page=0&limit=5 → 200, JSON с items, total."""
|
||
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
||
with httpx.Client(timeout=30, headers=headers) as client:
|
||
resp = client.get(f"{BASE_URL}/api/tracks?page=0&limit=5")
|
||
|
||
assert resp.status_code == 200, f"got {resp.status_code}: {resp.text[:200]}"
|
||
data = resp.json()
|
||
assert "items" in data
|
||
assert "total" in data
|
||
assert isinstance(data["items"], list)
|
||
assert isinstance(data["total"], int)
|
||
assert len(data["items"]) > 0
|
||
first = data["items"][0]
|
||
assert "id" in first
|
||
assert "name" in first
|
||
|
||
|
||
@pytest.mark.network
|
||
def test_ct_er_02_track_gpx_200_parseable():
|
||
"""CT-ER-02: GET /api/tracks/{first_id}/gpx → 200, валидный GPX."""
|
||
headers = {
|
||
"User-Agent": USER_AGENT,
|
||
"Accept": "application/json",
|
||
}
|
||
with httpx.Client(timeout=30, headers=headers) as client:
|
||
list_resp = client.get(f"{BASE_URL}/api/tracks?page=0&limit=5")
|
||
assert list_resp.status_code == 200
|
||
items = list_resp.json().get("items", [])
|
||
assert len(items) > 0
|
||
first_id = items[0]["id"]
|
||
|
||
gpx_resp = client.get(
|
||
f"{BASE_URL}/api/tracks/{first_id}/gpx",
|
||
headers={**headers, "Accept": "application/gpx+xml,application/xml,*/*"},
|
||
)
|
||
|
||
assert gpx_resp.status_code == 200
|
||
ctype = gpx_resp.headers.get("content-type", "").lower()
|
||
assert "xml" in ctype or "gpx" in ctype, f"content-type: {ctype}"
|
||
|
||
# Парсится без exception
|
||
root = ET.fromstring(gpx_resp.content)
|
||
assert root.tag.endswith("gpx"), f"root tag: {root.tag}"
|