"""Тесты EKBGeoportalClient (#1067 C7) — WFS-клиент геопортала ЕКБ. Сеть не дёргается: мокаем module-level _http_get_json, проверяем построение CQL/params и парсинг WFS GeoJSON. Образцы ответов — форма из live-разведки 2026-06-06 (.playwright-mcp/ekb-direct-layers-proof.json). """ from __future__ import annotations from typing import Any import pytest from app.services.scrapers.ekb_geoportal_client import ( WFS_LAYERS, EKBFeature, EKBGeoportalClient, ) def _terzone_fc() -> dict[str, Any]: """WFS FeatureCollection терзоны (форма как у live-ответа Ц-1).""" return { "type": "FeatureCollection", "features": [ { "id": "urban_ter_zone.1", "geometry": {"type": "Polygon", "coordinates": []}, "properties": { "urban_index": "Ц-1", "full_name": "Ц-1 Общественно-деловая зона городского центра", "urban_kind_obj": 42, }, } ], } def test_features_at_point_builds_intersects_cql(monkeypatch: pytest.MonkeyPatch) -> None: """features_at_point резолвит typeName + строит CQL INTERSECTS(geoloc, POINT(lon lat)).""" captured: dict[str, Any] = {} def fake_http(url: str, params: dict[str, str], **kw: Any) -> dict[str, Any]: captured["url"] = url captured["params"] = params return _terzone_fc() monkeypatch.setattr("app.services.scrapers.ekb_geoportal_client._http_get_json", fake_http) feats = EKBGeoportalClient().features_at_point("territorial_zone", lon=60.6107, lat=56.8391) assert len(feats) == 1 assert feats[0].properties["urban_index"] == "Ц-1" assert feats[0].crs == "EPSG:4326" # typeName резолвлен из WFS_LAYERS assert captured["params"]["typeNames"] == "portal:portal_geo_urban_ter_zone_exist" assert captured["params"]["request"] == "GetFeature" assert captured["params"]["outputFormat"] == "application/json" # CQL: X=lon, Y=lat assert captured["params"]["CQL_FILTER"] == "INTERSECTS(geoloc, POINT(60.6107 56.8391))" assert captured["url"].endswith("/api/gis/ows/3/portal/wfs") def test_features_at_point_accepts_raw_type_name(monkeypatch: pytest.MonkeyPatch) -> None: """Сырой typeName (не в WFS_LAYERS) прокидывается as-is.""" captured: dict[str, Any] = {} monkeypatch.setattr( "app.services.scrapers.ekb_geoportal_client._http_get_json", lambda url, params, **kw: captured.update(params) or {"features": []}, ) EKBGeoportalClient().features_at_point("portal:portal_geo_custom_layer", lon=60.0, lat=56.0) assert captured["typeNames"] == "portal:portal_geo_custom_layer" def test_features_in_bbox_builds_bbox_cql(monkeypatch: pytest.MonkeyPatch) -> None: """features_in_bbox строит CQL BBOX с EPSG:4326.""" captured: dict[str, Any] = {} monkeypatch.setattr( "app.services.scrapers.ekb_geoportal_client._http_get_json", lambda url, params, **kw: captured.update(params) or _terzone_fc(), ) EKBGeoportalClient().features_in_bbox("krt", (60.5, 56.7, 60.7, 56.9)) assert captured["typeNames"] == "portal:portal_geo_oks_krt" assert captured["CQL_FILTER"] == "BBOX(geoloc, 60.5, 56.7, 60.7, 56.9, 'EPSG:4326')" def test_zone_index_at_returns_index(monkeypatch: pytest.MonkeyPatch) -> None: """zone_index_at вытаскивает urban_index терзоны в точке (crosswalk #1059).""" monkeypatch.setattr( "app.services.scrapers.ekb_geoportal_client._http_get_json", lambda url, params, **kw: _terzone_fc(), ) assert EKBGeoportalClient().zone_index_at(60.6107, 56.8391) == "Ц-1" def test_zone_index_at_none_when_empty(monkeypatch: pytest.MonkeyPatch) -> None: """Нет терзоны в точке → None (не падает).""" monkeypatch.setattr( "app.services.scrapers.ekb_geoportal_client._http_get_json", lambda url, params, **kw: {"type": "FeatureCollection", "features": []}, ) assert EKBGeoportalClient().zone_index_at(0.0, 0.0) is None def test_ekb_feature_from_raw() -> None: """EKBFeature.from_raw парсит id/geometry/properties; пустые → defaults.""" f = EKBFeature.from_raw({"id": "x.1", "geometry": {"type": "Point"}, "properties": {"k": "v"}}) assert f.feature_id == "x.1" assert f.geometry == {"type": "Point"} assert f.properties == {"k": "v"} empty = EKBFeature.from_raw({}) assert empty.feature_id is None assert empty.geometry is None assert empty.properties == {} def test_wfs_layers_catalog_has_key_layers() -> None: """Ключевые ИРД/supply-слои присутствуют в каталоге.""" for key in ("territorial_zone", "functional_zone", "krt", "ppt", "zouit"): assert key in WFS_LAYERS assert WFS_LAYERS[key].startswith("portal:")