"""Тесты parcel_krt_requisites (#1060) — КРТ-реквизиты из ekb_krt_sites + planning_projects. DB/сеть не дёргаются: мокаем db.execute через FakeDB. Проверяем: - exact-матч → matched=True, confidence='exact', реквизиты заполнены; - planning overlap без матча → matched=False, confidence='none', без застройщика; - пустой WKT → []; - OperationalError (таблица не задеплоена) → graceful []; - дубликаты строк (один pp_id × krt_id) → дедублируются. """ from __future__ import annotations from decimal import Decimal from typing import Any from sqlalchemy.exc import OperationalError from app.services.site_finder.krt_lookup import parcel_krt_requisites class _MappingRow(dict): # type: ignore[type-arg] """Имитирует sqlalchemy MappingResult row (dict-like).""" class _Result: def __init__(self, rows: list[dict[str, Any]]) -> None: self._rows = [_MappingRow(r) for r in rows] def mappings(self) -> _Result: return self def all(self) -> list[_MappingRow]: return self._rows class _FakeDB: """Мок SQLAlchemy Session: первый вызов execute → first_rows; второй → second_rows.""" def __init__( self, first_rows: list[dict[str, Any]] | None = None, second_rows: list[dict[str, Any]] | None = None, raise_first: Exception | None = None, raise_second: Exception | None = None, ) -> None: self._calls = 0 self._first = first_rows or [] self._second = second_rows or [] self._raise_first = raise_first self._raise_second = raise_second def execute(self, sql: Any, params: dict[str, Any] | None = None) -> _Result: self._calls += 1 if self._calls == 1: if self._raise_first: raise self._raise_first return _Result(self._first) if self._raise_second: raise self._raise_second return _Result(self._second) _WKT = "POLYGON((60.5 56.8, 60.6 56.8, 60.6 56.9, 60.5 56.9, 60.5 56.8))" # Типичная строка exact-матча (planning_projects ⋈ ekb_krt_sites) _EXACT_ROW = { "pp_id": 1166, "pp_full_name": ( " ПАГЕ № 1970 от 16.08.2024\nПроект межевания территории в районе улицы Летчиков" ), "dmd_actual_year": 2024, "doc_status_name": "Действующий", "krt_id": 7, "site_name": "2. КРТ жилой застройки в районе ул. Летчиков", "developer_name": "ООО СЗ Летчиков", "contract_number": "2-7/21.7-14", "auction_final_price": Decimal("5000000"), "gradpotential_sqm": Decimal("25000"), "area_ha": Decimal("1.50"), "krt_kind": "жилой застройки", } def test_exact_match_returns_requisites() -> None: """Exact-матч по ПАГЕ-номер+год → matched=True, реквизиты застройщика заполнены.""" db = _FakeDB(first_rows=[_EXACT_ROW]) result = parcel_krt_requisites(db, _WKT) assert len(result) == 1 r = result[0] assert r["matched"] is True assert r["confidence"] == "exact" assert r["krt_geom_ref"] == 1166 assert r["site_name"] == "2. КРТ жилой застройки в районе ул. Летчиков" assert r["developer_name"] == "ООО СЗ Летчиков" assert r["contract_number"] == "2-7/21.7-14" assert r["auction_final_price"] == 5_000_000.0 assert r["gradpotential_sqm"] == 25_000.0 assert r["area_ha"] == 1.50 def test_exact_match_deduplicates_pp_krt_pair() -> None: """Дублирующиеся строки (pp_id × krt_id) дедублируются → один результат.""" db = _FakeDB(first_rows=[_EXACT_ROW, _EXACT_ROW]) result = parcel_krt_requisites(db, _WKT) assert len(result) == 1 def test_no_match_returns_geom_only() -> None: """Первый запрос пуст (нет exact-матча) → второй запрос → matched=False без реквизитов.""" no_match_row = { "pp_id": 999, "pp_full_name": " ПАГЕ № 4033 от 27.12.2022\n" "Проект планировки территории в районе улицы Краснофлотцев", "dmd_actual_year": 2022, "doc_status_name": "Действующий", } db = _FakeDB(first_rows=[], second_rows=[no_match_row]) result = parcel_krt_requisites(db, _WKT) assert len(result) == 1 r = result[0] assert r["matched"] is False assert r["confidence"] == "none" assert r["krt_geom_ref"] == 999 assert r["developer_name"] is None assert r["auction_final_price"] is None assert r["gradpotential_sqm"] is None assert r["site_name"] is None def test_empty_wkt_returns_empty_list() -> None: """parcel_wkt=None → пустой список без вызова DB.""" db = _FakeDB(first_rows=[_EXACT_ROW]) assert parcel_krt_requisites(db, None) == [] assert db._calls == 0 # DB не вызывалась def test_empty_string_wkt_returns_empty_list() -> None: """parcel_wkt='' → пустой список.""" assert parcel_krt_requisites(_FakeDB(), "") == [] def test_operational_error_first_query_returns_empty() -> None: """OperationalError на первом запросе (таблица не задеплоена) → [].""" db = _FakeDB(raise_first=OperationalError("stmt", {}, Exception("relation does not exist"))) result = parcel_krt_requisites(db, _WKT) assert result == [] def test_operational_error_second_query_returns_empty() -> None: """OperationalError на втором запросе (no-match fallback) → [].""" db = _FakeDB( first_rows=[], raise_second=OperationalError("stmt", {}, Exception("relation does not exist")), ) result = parcel_krt_requisites(db, _WKT) assert result == [] def test_none_decimal_fields_become_none() -> None: """NULL Decimal-поля → None в результате, не падают при float().""" row = {**_EXACT_ROW, "auction_final_price": None, "gradpotential_sqm": None, "area_ha": None} db = _FakeDB(first_rows=[row]) result = parcel_krt_requisites(db, _WKT) assert result[0]["auction_final_price"] is None assert result[0]["gradpotential_sqm"] is None assert result[0]["area_ha"] is None def test_no_overlap_at_all_returns_empty() -> None: """Оба запроса возвращают пустые списки → [].""" db = _FakeDB(first_rows=[], second_rows=[]) result = parcel_krt_requisites(db, _WKT) assert result == []