gendesign/backend/tests/services/site_finder/test_okn_lookup.py
bot-backend 01360e9c3d
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
fix(site_finder): SAVEPOINT-isolate gate-lookup db.execute swallows (#2464 Wave 2) (#2468)
2026-07-08 06:46:03 +00:00

107 lines
3.9 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.

"""Тесты 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