"""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}"