"""Тесты riasurt_lookup (#108) — is_in_aglomeration_but_not_ekb + parcel_riasurt_gate. БД мокается фейк-сессией, возвращающей заранее заданные mapping-строки. Проверяем агломерация-гейтинг (ЕКБ-сити исключён), topic-бакеты, graceful при ошибке БД. """ from __future__ import annotations import contextlib from typing import Any from sqlalchemy.exc import ProgrammingError from app.services.site_finder import riasurt_lookup from app.services.site_finder.riasurt_lookup import ( is_in_aglomeration_but_not_ekb, parcel_riasurt_gate, ) _WKT = "POLYGON((60 56,60.1 56,60.1 56.1,60 56.1,60 56))" def test_is_in_aglomeration_excludes_ekb_city() -> None: """ЕКБ-сити (66:41) → False.""" assert is_in_aglomeration_but_not_ekb("66:41:0204016:10") is False assert is_in_aglomeration_but_not_ekb("66:41") is False def test_is_in_aglomeration_includes_outskirts() -> None: """Окраины (66:35 Берёзовский, 66:62 В.Пышма, 66:25 Сысерть) → True.""" assert is_in_aglomeration_but_not_ekb("66:35:0101001:5") is True assert is_in_aglomeration_but_not_ekb("66:62:1234567:1") is True assert is_in_aglomeration_but_not_ekb("66:25:0000001:99") is True def test_is_in_aglomeration_rejects_non_sverdl_and_empty() -> None: """Не-Свердл (не 66:*) и пусто → False.""" assert is_in_aglomeration_but_not_ekb("50:21:0000001:1") is False assert is_in_aglomeration_but_not_ekb(None) is False assert is_in_aglomeration_but_not_ekb("") is False class _Row(dict): """mapping-строка: поддерживает r["key"].""" class _Result: def __init__(self, rows: list[_Row]) -> None: self._rows = rows def mappings(self) -> _Result: return self def all(self) -> list[_Row]: return self._rows class _FakeDB: def __init__(self, rows: list[_Row] | Exception) -> None: self._rows = rows self.begin_nested_calls = 0 @contextlib.contextmanager def begin_nested(self) -> Any: """Фейковый SAVEPOINT — не глотает исключение из execute (как реальный).""" self.begin_nested_calls += 1 yield def execute(self, sql: Any, params: dict[str, Any] | None = None) -> _Result: if isinstance(self._rows, Exception): raise self._rows return _Result(self._rows) def test_gate_not_applicable_for_ekb_city() -> None: """ЕКБ-сити → applicable=False, БД не дёргается.""" db = _FakeDB([]) gate = parcel_riasurt_gate(db, _WKT, "66:41:0204016:10") # type: ignore[arg-type] assert gate["applicable"] is False assert gate["overlaps"] == [] def test_gate_buckets_topics() -> None: """Пересечения раскладываются по topic-бакетам.""" rows = [ _Row( source_layer_id=845274, layer_topic="territorial_zone", mo_name="Берёзовский", obshnz="Ж-1", description="Зона застройки", ), _Row( source_layer_id=846381, layer_topic="functional_zone", mo_name="Берёзовский", obshnz=None, description="Жилая функц.зона", ), _Row( source_layer_id=844759, layer_topic="red_lines", mo_name="Берёзовский", obshnz=None, description=None, ), _Row( source_layer_id=846365, layer_topic="szz", mo_name="Берёзовский", obshnz=None, description=None, ), _Row( source_layer_id=845425, layer_topic="flood_zone", mo_name="Берёзовский", obshnz=None, description=None, ), _Row( source_layer_id=844478, layer_topic="krt", mo_name="Берёзовский", obshnz=None, description=None, ), ] db = _FakeDB(rows) gate = parcel_riasurt_gate(db, _WKT, "66:35:0101001:5") # type: ignore[arg-type] assert gate["applicable"] is True assert gate["tier"] == "Зона застройки" assert gate["func_zone"] == "Жилая функц.зона" assert gate["red_lines"] is True assert gate["szz"] is True assert gate["szo"] is False assert gate["flood_zone"] is True assert gate["krt"] is True assert len(gate["overlaps"]) == 6 def test_gate_graceful_on_db_error() -> None: """Ошибка БД (pre-migration) → пустой gate, не падаем.""" db = _FakeDB(ProgrammingError("stmt", {}, Exception("no table"))) gate = parcel_riasurt_gate(db, _WKT, "66:35:0101001:5") # type: ignore[arg-type] assert gate == riasurt_lookup.RIASURT_EMPTY_GATE def test_gate_empty_wkt_returns_empty() -> None: """Нет WKT → пустой gate.""" db = _FakeDB([]) gate = parcel_riasurt_gate(db, None, "66:35:0101001:5") # type: ignore[arg-type] assert gate["applicable"] is False def test_gate_uses_savepoint_and_session_reusable_after_failure() -> None: """#2464 cluster A: db.execute обёрнут в begin_nested (SAVEPOINT) — сбой не отравляет сессию. Follow-up вызов на ТОЙ ЖЕ db-инстансе успевает.""" db = _FakeDB(ProgrammingError("stmt", {}, Exception("no table"))) gate = parcel_riasurt_gate(db, _WKT, "66:35:0101001:5") # type: ignore[arg-type] assert gate == riasurt_lookup.RIASURT_EMPTY_GATE assert db.begin_nested_calls == 1 # follow-up на той же сессии (таблица теперь "доступна"). db._rows = [] gate2 = parcel_riasurt_gate(db, _WKT, "66:35:0101001:5") # type: ignore[arg-type] assert gate2["applicable"] is True assert db.begin_nested_calls == 2