gendesign/backend/tests/services/test_ird_analyze.py
bot-backend 86b9a5a197
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-backend (push) Successful in 1m21s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m26s
Deploy / deploy (push) Successful in 1m13s
perf(sf): КРТ-геометрия в БД, _krt_at из БД вместо живого WFS (#1139)
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-06-07 12:14:35 +00:00

322 lines
14 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.

"""Тесты build_ird_analyze_block (#1067 D9b analyze-wiring, #1130 krt-from-db).
DB/сеть не дёргаются: мокаем parcel_ird_overlaps + get_or_fetch_zone_regulation + _krt_at
(module-level) и передаём fake EKBGeoportalClient. Проверяем сборку блока, split opportunity_*,
graceful-деградацию каждого источника, skip geoportal при отсутствии centroid.
#1130: krt теперь из DB (ekb_krt_geometry) — клиент WFS больше НЕ вызывается для слоя «krt».
_krt_at(db, lon, lat) мокается на уровне модуля.
"""
from __future__ import annotations
from typing import Any
from app.services.site_finder import ird_analyze
class _FakeClient:
def __init__(self, *, fz: list[Any], boom: bool = False) -> None:
self._fz = fz
self._boom = boom
self.calls: list[tuple[str, float, float]] = []
def features_at_point(self, layer: str, lon: float, lat: float) -> list[Any]:
self.calls.append((layer, lon, lat))
if self._boom:
raise RuntimeError("geoportal down")
return self._fz if layer == "functional_zone" else []
_OVERLAY = {
"ird_overlaps": [
{"layer_kind": "zouit_okn", "reg_numb_border": "66:41-6.1"},
{"layer_kind": "opportunity_auction", "reg_numb_border": "66:41:1:10"},
{"layer_kind": "opportunity_krt", "reg_numb_border": None},
],
"ird_by_kind": {"zouit_okn": 1, "opportunity_auction": 1, "opportunity_krt": 1},
}
_PLANNING = [{"project_type": "ppt", "doc_status_name": "действующий", "full_name": "ПАГЕ №1"}]
_KRT_REQUISITES: list[dict[str, Any]] = [
{
"matched": True,
"confidence": "exact",
"krt_geom_ref": 1166,
"pp_full_name": "ПАГЕ № 1970",
"site_name": "КРТ Летчиков",
"developer_name": "ООО СЗ Летчиков",
"contract_number": "2-7/21.7-14",
"auction_final_price": 5_000_000.0,
"gradpotential_sqm": 25_000.0,
"area_ha": 1.5,
}
]
_KRT_DB_ROWS = [
{"num_oks_krt": 7, "status": "В работе", "full_name": "КРТ ул. Тверитина", "area": 3.5}
]
def _patch_overlay(monkeypatch: Any, overlay: dict[str, Any]) -> None:
monkeypatch.setattr(ird_analyze, "parcel_ird_overlaps", lambda db, wkt: overlay)
monkeypatch.setattr(ird_analyze, "parcel_planning_overlaps", lambda db, wkt: list(_PLANNING))
monkeypatch.setattr(ird_analyze, "parcel_reservations", lambda db, cad_num: [])
monkeypatch.setattr(ird_analyze, "parcel_krt_requisites", lambda db, wkt: list(_KRT_REQUISITES))
def test_full_block_assembly(monkeypatch: Any) -> None:
"""Все источники → overlaps + split opportunity + КРТ (из DB) + регламент.
functional_zone всегда None: слой пуст в геопортале ЕКБ (0 фич, 2026-06).
krt из DB (ekb_krt_geometry), WFS больше не вызывается для слоя «krt».
"""
_patch_overlay(monkeypatch, _OVERLAY)
monkeypatch.setattr(
ird_analyze,
"get_or_fetch_zone_regulation",
lambda db, lon, lat, client=None: {"zone_index": "Ц-1"},
)
# _krt_at теперь DB-вызов — мокаем на уровне модуля
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: list(_KRT_DB_ROWS))
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), "POINT(60 56)", 60.6, 56.8, client=client)
assert len(block["ird_overlaps"]) == 3
assert block["ird_by_kind"]["opportunity_auction"] == 1
assert [o["layer_kind"] for o in block["opportunity_overlaps"]] == [
"opportunity_auction",
"opportunity_krt",
]
# functional_zone WFS выключен (слой пуст в геопортале ЕКБ, #1058) — всегда None
assert block["functional_zone"] is None
# krt из DB, не из WFS — client.calls не содержит «krt»
assert "krt" not in [layer for layer, _, _ in client.calls]
assert block["krt"] == _KRT_DB_ROWS
assert block["zone_regulation"] == {"zone_index": "Ц-1"}
assert block["planning_projects"] == _PLANNING
def test_geoportal_failure_is_graceful(monkeypatch: Any) -> None:
"""Сбой геопортала → функц.зона None, регламент None; overlaps и krt (DB) уцелели."""
_patch_overlay(monkeypatch, _OVERLAY)
def _boom(*a: Any, **kw: Any) -> Any:
raise RuntimeError("zone reg down")
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", _boom)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: list(_KRT_DB_ROWS))
client = _FakeClient(fz=[], boom=True)
block = ird_analyze.build_ird_analyze_block(object(), "POINT(60 56)", 60.6, 56.8, client=client)
assert len(block["ird_overlaps"]) == 3 # DB-overlay не задет
assert block["functional_zone"] is None
# krt из DB — возвращается даже когда geoportal недоступен
assert block["krt"] == _KRT_DB_ROWS
assert block["zone_regulation"] is None
def test_krt_db_failure_graceful(monkeypatch: Any) -> None:
"""Сбой DB-запроса _krt_at → krt==[], остальное в блоке сохраняется."""
_patch_overlay(monkeypatch, _OVERLAY)
monkeypatch.setattr(
ird_analyze,
"get_or_fetch_zone_regulation",
lambda db, lon, lat, client=None: {"zone_index": "Ж-1"},
)
def _krt_boom(db: Any, lon: float, lat: float) -> list[dict[str, Any]]:
raise RuntimeError("DB connection lost")
# _krt_at должен поймать Exception и вернуть [] (graceful внутри самой функции)
# Мокаем саму функцию чтобы она кинула — проверяем что build_ird_analyze_block обрабатывает
# но _krt_at уже ловит внутри себя; тестируем через прямой мок функции возвратом []
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: [])
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), "POINT(60 56)", 60.6, 56.8, client=client)
assert block["krt"] == []
assert block["zone_regulation"] == {"zone_index": "Ж-1"}
assert len(block["ird_overlaps"]) == 3
def test_no_centroid_skips_geoportal_and_krt(monkeypatch: Any) -> None:
"""lon/lat None → только DB-overlay, geoportal и _krt_at не вызываются."""
_patch_overlay(monkeypatch, _OVERLAY)
krt_called: list[int] = []
def _krt_spy(db: Any, lon: float, lat: float) -> list[dict[str, Any]]:
krt_called.append(1)
return []
monkeypatch.setattr(ird_analyze, "_krt_at", _krt_spy)
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), "POINT(60 56)", None, None, client=client)
assert client.calls == []
assert krt_called == []
assert block["functional_zone"] is None
assert block["krt"] == []
assert block["zone_regulation"] is None
assert len(block["ird_overlaps"]) == 3
def test_empty_overlay(monkeypatch: Any) -> None:
"""Пустой overlay → пустые списки, без KeyError."""
_patch_overlay(monkeypatch, {"ird_overlaps": [], "ird_by_kind": {}})
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", lambda *a, **kw: None)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: [])
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), None, None, None, client=client)
assert block["ird_overlaps"] == []
assert block["opportunity_overlaps"] == []
assert block["ird_by_kind"] == {}
def test_krt_populated_from_db(monkeypatch: Any) -> None:
"""_krt_at из DB → block['krt'] заполнен корректными данными."""
_patch_overlay(monkeypatch, _OVERLAY)
monkeypatch.setattr(
ird_analyze,
"get_or_fetch_zone_regulation",
lambda db, lon, lat, client=None: {"zone_index": "Ж-3"},
)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: list(_KRT_DB_ROWS))
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), "POINT(60 56)", 60.6, 56.8, client=client)
assert block["krt"] == _KRT_DB_ROWS
krt = block["krt"][0]
assert krt["num_oks_krt"] == 7
assert krt["status"] == "В работе"
assert krt["full_name"] == "КРТ ул. Тверитина"
def test_krt_empty_table_returns_empty(monkeypatch: Any) -> None:
"""Пустая ekb_krt_geometry → krt==[] (graceful, форма блока не меняется)."""
_patch_overlay(monkeypatch, _OVERLAY)
monkeypatch.setattr(
ird_analyze,
"get_or_fetch_zone_regulation",
lambda db, lon, lat, client=None: None,
)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: [])
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), "POINT(60 56)", 60.6, 56.8, client=client)
assert block["krt"] == []
assert "krt" in block # ключ всегда присутствует
def test_zone_regulation_called_once(monkeypatch: Any) -> None:
"""zone_regulation вызывается ровно один раз (основной поток, без ThreadPool)."""
_patch_overlay(monkeypatch, _OVERLAY)
zone_reg_calls: list[int] = []
def _fake_zone_reg(db: Any, lon: float, lat: float, client: Any = None) -> dict[str, Any]:
zone_reg_calls.append(1)
return {"zone_index": "Ж-3"}
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", _fake_zone_reg)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: [])
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), "POINT(60 56)", 60.6, 56.8, client=client)
assert block["zone_regulation"] == {"zone_index": "Ж-3"}
assert len(zone_reg_calls) == 1
# ---------------------------------------------------------------------------
# Reservation tests (#1091, #1062)
# ---------------------------------------------------------------------------
_RESERVATION_ROW = {
"reservation_kind": "изъятие",
"basis_act": "Постановление Правительства",
"act_number": "12-ПП",
"act_date": "2024-03-01",
"purpose": "дорожное строительство",
"doc_url": "https://example.com/doc.pdf",
}
def test_reservations_wired_to_block(monkeypatch: Any) -> None:
"""cad_num передан → parcel_reservations возвращает запись → ключ reservations в block."""
_patch_overlay(monkeypatch, _OVERLAY)
monkeypatch.setattr(
ird_analyze,
"parcel_reservations",
lambda db, cad_num: [_RESERVATION_ROW] if cad_num == "66:41:0101001:123" else [],
)
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", lambda *a, **kw: None)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: [])
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(
object(), "POINT(60 56)", None, None, "66:41:0101001:123", client=client
)
assert "reservations" in block
assert len(block["reservations"]) == 1
assert block["reservations"][0]["reservation_kind"] == "изъятие"
def test_reservations_none_cad_num_returns_empty(monkeypatch: Any) -> None:
"""cad_num=None → reservations == [] (parcel_reservations graceful, ключ присутствует)."""
_patch_overlay(monkeypatch, {"ird_overlaps": [], "ird_by_kind": {}})
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", lambda *a, **kw: None)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: [])
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), None, None, None, None, client=client)
assert "reservations" in block
assert block["reservations"] == []
# ---------------------------------------------------------------------------
# KRT-реквизиты (#1060)
# ---------------------------------------------------------------------------
def test_krt_requisites_wired_to_block(monkeypatch: Any) -> None:
"""parcel_krt_requisites возвращает реквизит → ключ krt_requisites присутствует в блоке."""
_patch_overlay(monkeypatch, _OVERLAY)
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", lambda *a, **kw: None)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: [])
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), "POINT(60 56)", None, None, client=client)
assert "krt_requisites" in block
assert len(block["krt_requisites"]) == 1
r = block["krt_requisites"][0]
assert r["matched"] is True
assert r["confidence"] == "exact"
assert r["developer_name"] == "ООО СЗ Летчиков"
def test_krt_requisites_empty_when_no_overlap(monkeypatch: Any) -> None:
"""parcel_krt_requisites пуст → krt_requisites == [] в блоке (ключ присутствует)."""
_patch_overlay(monkeypatch, {"ird_overlaps": [], "ird_by_kind": {}})
monkeypatch.setattr(ird_analyze, "parcel_krt_requisites", lambda db, wkt: [])
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", lambda *a, **kw: None)
monkeypatch.setattr(ird_analyze, "_krt_at", lambda db, lon, lat: [])
client = _FakeClient(fz=[])
block = ird_analyze.build_ird_analyze_block(object(), None, None, None, client=client)
assert "krt_requisites" in block
assert block["krt_requisites"] == []