"""Тесты parcel_okn_objects (#1066, #1159) — ОКН-объекты рядом с участком. БД не дёргается: фейковая сессия с .execute().fetchall() (позиционные колонки). Проверяем happy path, graceful на отсутствие WKT/таблицы, и SAVEPOINT-регрессию (#2464 cluster A): db.execute обёрнут в begin_nested, сбой не отравляет сессию. """ from __future__ import annotations import contextlib from typing import Any from sqlalchemy.exc import OperationalError from app.services.site_finder.okn_lookup import parcel_okn_objects _WKT = "POLYGON((60.5 56.8, 60.6 56.8, 60.6 56.9, 60.5 56.9, 60.5 56.8))" _OKN_ROW = ( "66-001", # source_id "федеральный", # category 42, # distance_m "Дом купца Иванова", # name "ул. Ленина, 1", # address ) class _FakeDB: """Фейковая сессия: execute().fetchall() (позиционные строки) + begin_nested().""" def __init__(self, rows: list[tuple[Any, ...]] | 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) -> Any: if isinstance(self._rows, Exception): raise self._rows class _Result: def __init__(self, rows: list[tuple[Any, ...]]) -> None: self._rows = rows def fetchall(self) -> list[tuple[Any, ...]]: return self._rows return _Result(self._rows) def test_returns_okn_objects() -> None: """Один ОКН-объект в радиусе → проброс полей.""" db = _FakeDB([_OKN_ROW]) out = parcel_okn_objects(db, _WKT) assert len(out) == 1 r = out[0] assert r["source_id"] == "66-001" assert r["category"] == "федеральный" assert r["distance_m"] == 42 assert r["name"] == "Дом купца Иванова" assert r["address"] == "ул. Ленина, 1" def test_empty_wkt_returns_empty() -> None: """parcel_wkt=None → [] без вызова DB.""" db = _FakeDB([_OKN_ROW]) assert parcel_okn_objects(db, None) == [] assert db.begin_nested_calls == 0 def test_no_objects_in_radius_returns_empty() -> None: """Нет объектов в радиусе → [].""" assert parcel_okn_objects(_FakeDB([]), _WKT) == [] def test_missing_table_is_graceful() -> None: """okn_objects ещё не задеплоена (OperationalError) → [] (analyze не падает).""" db = _FakeDB(OperationalError("stmt", {}, Exception("relation okn_objects does not exist"))) assert parcel_okn_objects(db, _WKT) == [] def test_unexpected_exception_is_graceful() -> None: """Любая неожиданная ошибка БД → [] (analyze не падает).""" db = _FakeDB(RuntimeError("connection refused")) assert parcel_okn_objects(db, _WKT) == [] # ── #2464 cluster A: SAVEPOINT-регрессия (session-poisoning) ──────────────────── def test_uses_savepoint_and_session_reusable_after_failure() -> None: """db.execute обёрнут в begin_nested (SAVEPOINT) — сбой не отравляет сессию: follow-up вызов на ТОЙ ЖЕ db-сессии успевает (не "current transaction is aborted").""" db = _FakeDB(RuntimeError("connection refused")) result = parcel_okn_objects(db, _WKT) assert result == [] assert db.begin_nested_calls == 1 db._rows = [_OKN_ROW] result2 = parcel_okn_objects(db, _WKT) assert len(result2) == 1 assert result2[0]["source_id"] == "66-001" assert db.begin_nested_calls == 2