Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local>
189 lines
6.9 KiB
Python
189 lines
6.9 KiB
Python
"""Тесты 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
|