gendesign/backend/tests/test_ekb_geoportal_client.py
lekss361 4e41f9be73
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
feat(sf): EKB geoportal WFS client (#1083)
Новый 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>
2026-06-06 18:37:09 +00:00

120 lines
5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты 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:")