gendesign/backend/tests/workers/test_planning_harvest.py
lekss361 9e4348629b
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m36s
Deploy / build-worker (push) Successful in 2m20s
Deploy / deploy (push) Successful in 1m14s
feat(sf): ППТ/ПМТ WFS ingest → planning_projects (future-supply, #1104)
Миграция 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>
2026-06-06 20:25:23 +00:00

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