Миграция 134_planning_projects.sql + planning_harvest.py: ингест утверждённых ППТ/ПМТ ЕКБ из геопортал-WFS (один BBOX-запрос на слой, geom уже 4326 → SetSRID без Transform) → planning_projects (project_type/source_key UNIQUE, doc_status/год). SAVEPOINT per-row, beat monthly (1-е 06:00). Зависит от TLS-фикса #1103. Документ-центричная таблица отдельно от ird_overlays. 3 теста. ТЭП/PDF — follow-up. Refs #1085. Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local>
111 lines
3.8 KiB
Python
111 lines
3.8 KiB
Python
"""Тесты harvest_planning_projects (#1085) — геопортал-WFS ППТ/ПМТ → planning_projects.
|
|
|
|
Сеть/БД не дёргаются: мокаем EKBGeoportalClient + SessionLocal, проверяем фетч обоих слоёв,
|
|
project_type-маппинг, skip битых фич, нормализацию года и upsert-параметры.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from typing import Any
|
|
|
|
from app.workers.tasks import planning_harvest
|
|
|
|
|
|
class _Feat:
|
|
def __init__(self, geometry: dict[str, Any] | None, properties: dict[str, Any]) -> None:
|
|
self.geometry = geometry
|
|
self.properties = properties
|
|
|
|
|
|
class _FakeClient:
|
|
"""EKBGeoportalClient-двойник: layer → 1 фича на слой."""
|
|
|
|
def __init__(self, *a: Any, **kw: Any) -> None:
|
|
self.bbox_calls: list[str] = []
|
|
|
|
def features_in_bbox(self, layer: str, bbox: Any) -> list[_Feat]:
|
|
self.bbox_calls.append(layer)
|
|
return [
|
|
_Feat(
|
|
{"type": "Polygon", "coordinates": [[[60, 56], [60.1, 56], [60, 56.1], [60, 56]]]},
|
|
{
|
|
"key": 1000010173321902 if layer == "ppt" else 1000010209297292,
|
|
"full_name": f"ПАГЕ № 1 ({layer})",
|
|
"doc_status": 1,
|
|
"doc_status_name": "действующий",
|
|
"project_name": "Тестовый проект",
|
|
"dmd_actual_year": "2021",
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
class _FakeDB:
|
|
def __init__(self) -> None:
|
|
self.executed: list[tuple[Any, dict[str, Any] | None]] = []
|
|
self.committed = False
|
|
self.closed = False
|
|
|
|
@contextmanager
|
|
def begin_nested(self) -> Any:
|
|
yield
|
|
|
|
def execute(self, sql: Any, params: dict[str, Any] | None = None) -> Any:
|
|
self.executed.append((sql, params))
|
|
return None
|
|
|
|
def commit(self) -> None:
|
|
self.committed = True
|
|
|
|
def close(self) -> None:
|
|
self.closed = True
|
|
|
|
|
|
def _patch(monkeypatch: Any, db: _FakeDB) -> _FakeClient:
|
|
client = _FakeClient()
|
|
monkeypatch.setattr(planning_harvest, "EKBGeoportalClient", lambda *a, **kw: client)
|
|
monkeypatch.setattr(planning_harvest, "SessionLocal", lambda: db)
|
|
return client
|
|
|
|
|
|
def test_harvest_both_layers(monkeypatch: Any) -> None:
|
|
"""ППТ + ПМТ → upsert по фиче на слой; project_type-маппинг; year нормализован в int."""
|
|
db = _FakeDB()
|
|
client = _patch(monkeypatch, db)
|
|
|
|
result = planning_harvest.harvest_planning_projects()
|
|
|
|
assert result == {"features": 2}
|
|
assert sorted(client.bbox_calls) == ["pmt", "ppt"]
|
|
assert len(db.executed) == 2
|
|
types = {params["project_type"] for _, params in db.executed if params}
|
|
assert types == {"ppt", "pmt"}
|
|
_, p0 = db.executed[0]
|
|
assert p0 is not None
|
|
assert p0["dmd_actual_year"] == 2021 # строка "2021" → int
|
|
assert p0["doc_status"] == "1" # приведён к строке
|
|
assert isinstance(p0["geojson"], str)
|
|
|
|
|
|
def test_skips_feature_without_key_or_geom() -> None:
|
|
"""Фича без key / geometry → не пишется (defensive)."""
|
|
db = _FakeDB()
|
|
no_key = _Feat({"type": "Polygon", "coordinates": []}, {"full_name": "x"})
|
|
no_geom = _Feat(None, {"key": 5})
|
|
assert (
|
|
planning_harvest._upsert_feature(db, project_type="ppt", feature=no_key, fetched_at="t")
|
|
is False
|
|
)
|
|
assert (
|
|
planning_harvest._upsert_feature(db, project_type="pmt", feature=no_geom, fetched_at="t")
|
|
is False
|
|
)
|
|
assert db.executed == []
|
|
|
|
|
|
def test_year_normalization() -> None:
|
|
assert planning_harvest._to_int("2021") == 2021
|
|
assert planning_harvest._to_int(2021) == 2021
|
|
assert planning_harvest._to_int(None) is None
|
|
assert planning_harvest._to_int("н/д") is None
|