"""Тесты gisogd_so_client (#1058) — каталог-резолв + WFS fetch. Сеть не дёргается: мокаем _http_get_json на module-level. """ from __future__ import annotations from typing import Any from app.services.scrapers import gisogd_so_client from app.services.scrapers.gisogd_so_client import ( _LAYER_FALLBACK, _find_layer_in_tree, fetch_functional_zones, resolve_functional_zone_layer, ) # ── _find_layer_in_tree ─────────────────────────────────────────────────────── _LAYER_KNOWN = "agate_ekbgo:agate_ekbgo_geo_urban_10_functional_zone_f6c5926" _LAYER_ABC = "agate_ekbgo:agate_ekbgo_geo_urban_10_functional_zone_abc123" _LAYER_XYZ = "agate_ekbgo:agate_ekbgo_geo_urban_10_functional_zone_xyz789" def test_find_layer_direct_match() -> None: """Узел с name == 'Функциональные зоны' → возвращает value.""" node = {"name": "Функциональные зоны", "value": _LAYER_KNOWN} result = _find_layer_in_tree(node) assert result == _LAYER_KNOWN def test_find_layer_in_nested_children() -> None: """Вложенный узел в children → резолвится рекурсивно.""" catalog = { "name": "root", "children": [ { "name": "Зонирование", "children": [ {"name": "Функциональные зоны", "value": _LAYER_ABC}, ], }, ], } result = _find_layer_in_tree(catalog) assert result == _LAYER_ABC def test_find_layer_not_found_returns_none() -> None: """Узел не найден → None.""" catalog = {"name": "Другой слой", "value": "some:layer", "children": []} result = _find_layer_in_tree(catalog) assert result is None def test_find_layer_non_dict_node_returns_none() -> None: """Нода не dict → None (безопасный обход).""" assert _find_layer_in_tree(None) is None # type: ignore[arg-type] assert _find_layer_in_tree("string") is None # type: ignore[arg-type] assert _find_layer_in_tree([]) is None # type: ignore[arg-type] # ── resolve_functional_zone_layer ───────────────────────────────────────────── def test_resolve_layer_from_catalog(monkeypatch: Any) -> None: """Каталог доступен → корректный layer из дерева.""" catalog = { "name": "root", "children": [ {"name": "Функциональные зоны", "value": _LAYER_XYZ}, ], } monkeypatch.setattr(gisogd_so_client, "_http_get_json", lambda *a, **kw: catalog) result = resolve_functional_zone_layer() assert result == _LAYER_XYZ def test_resolve_layer_fallback_on_catalog_error(monkeypatch: Any) -> None: """Каталог недоступен → fallback-хардкод.""" def _raise(*a: Any, **kw: Any) -> None: raise Exception("connection refused") monkeypatch.setattr(gisogd_so_client, "_http_get_json", _raise) result = resolve_functional_zone_layer() assert result == _LAYER_FALLBACK def test_resolve_layer_fallback_node_not_found(monkeypatch: Any) -> None: """Каталог пришёл но нужного узла нет → fallback-хардкод.""" catalog = { "name": "root", "children": [{"name": "Другой слой", "value": "some:other:layer"}], } monkeypatch.setattr(gisogd_so_client, "_http_get_json", lambda *a, **kw: catalog) result = resolve_functional_zone_layer() assert result == _LAYER_FALLBACK # ── fetch_functional_zones ──────────────────────────────────────────────────── def _make_fc(n: int) -> dict[str, Any]: """FeatureCollection с n фичами.""" return { "type": "FeatureCollection", "features": [ { "geometry": {"type": "Polygon", "coordinates": []}, "properties": {"urban10_globalid": f"uuid-{i}"}, } for i in range(n) ], } def test_fetch_returns_features_single_request(monkeypatch: Any) -> None: """Успешный одиночный WFS-запрос → список фич.""" fc = _make_fc(6436) calls: list[dict[str, Any]] = [] def _mock_get(url: str, params: dict[str, str] | None = None, **kw: Any) -> Any: calls.append({"url": url, "params": params}) return fc monkeypatch.setattr(gisogd_so_client, "_http_get_json", _mock_get) features = fetch_functional_zones("agate_ekbgo:test_layer") assert len(features) == 6436 assert len(calls) == 1 # один запрос assert calls[0]["params"]["typeNames"] == "agate_ekbgo:test_layer" def test_fetch_pagination_on_timeout(monkeypatch: Any) -> None: """Первый запрос — TimeoutException → постраничная загрузка.""" import httpx first_call = True def _mock_get(url: str, params: dict[str, str] | None = None, **kw: Any) -> Any: nonlocal first_call if first_call: first_call = False raise httpx.TimeoutException("timeout") # Постраничные: 3 фичи на первой странице, пустая на второй. start = int((params or {}).get("startIndex", 0)) if start == 0: return _make_fc(3) return {"type": "FeatureCollection", "features": []} monkeypatch.setattr(gisogd_so_client, "_http_get_json", _mock_get) features = fetch_functional_zones("agate_ekbgo:test_layer") assert len(features) == 3 def test_fetch_empty_on_wfs_error(monkeypatch: Any) -> None: """WFS не отвечает → пустой список, без падения.""" def _raise(*a: Any, **kw: Any) -> None: raise Exception("connection error") monkeypatch.setattr(gisogd_so_client, "_http_get_json", _raise) features = fetch_functional_zones("agate_ekbgo:test_layer") assert features == [] def test_fetch_resolves_layer_when_none(monkeypatch: Any) -> None: """type_name=None → вызывается resolve_functional_zone_layer перед WFS.""" resolved: list[str] = [] def _mock_resolve() -> str: resolved.append("called") return "agate_ekbgo:resolved_layer" def _mock_get(url: str, params: dict[str, str] | None = None, **kw: Any) -> Any: return _make_fc(2) monkeypatch.setattr(gisogd_so_client, "resolve_functional_zone_layer", _mock_resolve) monkeypatch.setattr(gisogd_so_client, "_http_get_json", _mock_get) features = fetch_functional_zones(type_name=None) assert resolved == ["called"] assert len(features) == 2