Some checks failed
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m26s
Deploy / deploy (push) Has been cancelled
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
Новый ekb_geoportal_client.py — WFS-клиент муниципального геопортала ЕКБ (Geometa GeoServer, public). features_at_point (CQL INTERSECTS), features_in_bbox (CQL BBOX), zone_index_at (crosswalk координата→индекс ПЗЗ). EPSG:4326, OGC WFS GetFeature (не WMS — reproject-баг). Единый источник землепользование/КРТ/ППТ/ЗОУИТ для ЕКБ. 7 тестов. Foundation #1067 C7; wiring в C8/D9. Refs #1067. Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local>
120 lines
5 KiB
Python
120 lines
5 KiB
Python
"""Тесты 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:")
|