Some checks failed
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
107 lines
3.9 KiB
Python
107 lines
3.9 KiB
Python
"""Тесты 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
|