gendesign/backend/tests/services/site_finder/test_connection_capacity_savepoint.py
bot-backend 39dd63333e
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 3m29s
Deploy / build-worker (push) Successful in 4m48s
Deploy / deploy (push) Successful in 1m43s
fix(site_finder): SAVEPOINT-isolate DB errors across cluster A (#2464) (#2465)
2026-07-07 12:50:11 +00:00

138 lines
6.8 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.

"""SAVEPOINT-регрессия для connection_capacity_lookup.py (#2464 cluster A finding 2).
gas/heat/gas-outlet query helpers (_query_gas_city_grs, _query_gas_outlet_counts,
_query_gas_outlet_points, _query_heat_latest) когда-то ловили db.execute-сбой в
bare ``except Exception`` без SAVEPOINT/rollback — на реальном Postgres это отравляло
db shared с caller (GET .../connection-capacity, Depends(get_db)): следующий db.execute
в этом же запросе падал с "current transaction is aborted". Сиблинг
``_query_nearby_network_zones`` (в этом же файле) уже был правильным (db.begin_nested())
— эти тесты проверяют, что остальные функции приведены в соответствие.
MagicMock-сессия не эмулирует реальный aborted-transaction Postgres — тесты
проверяют (1) graceful fallback при сбое, (2) что begin_nested() реально вызван
вокруг db.execute (SAVEPOINT-обёртка присутствует в коде), (3) что db остаётся
usable для следующего вызова на том же mock-сессии (call_count растёт, ошибка не
проглатывает control flow).
"""
from __future__ import annotations
import os
from unittest.mock import MagicMock
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
from app.services.site_finder.connection_capacity_lookup import (
_query_gas_city_grs,
_query_gas_outlet_counts,
_query_gas_outlet_points,
_query_heat_latest,
)
_WKT = "POLYGON((60.8 56.7, 60.9 56.7, 60.9 56.8, 60.8 56.8, 60.8 56.7))"
def _mock_db_raising_once(final_rows: list[dict] | None = None) -> MagicMock:
"""db.execute кидает на 1-м вызове, на 2-м успешно отдаёт final_rows."""
db = MagicMock()
ok_result = MagicMock()
ok_result.mappings.return_value.all.return_value = final_rows or []
ok_result.mappings.return_value.first.return_value = (final_rows or [None])[0]
db.execute.side_effect = [RuntimeError("simulated DB failure"), ok_result]
return db
class TestGasCityGrsSavepoint:
def test_query_failure_degrades_gracefully(self) -> None:
db = MagicMock()
db.execute.side_effect = RuntimeError("gas_grs_capacity недоступна")
out = _query_gas_city_grs(db)
assert out["city_grs"] == []
assert out["total_free_th_m3_h"] is None
def test_uses_begin_nested_savepoint(self) -> None:
"""Фикс: query обёрнут в db.begin_nested(), не голый db.execute."""
db = MagicMock()
db.execute.return_value.mappings.return_value.all.return_value = []
db.execute.return_value.mappings.return_value.first.return_value = None
_query_gas_city_grs(db)
# 2 execute на функцию: _query_gas_outlet_counts (city-level счётчики) +
# сам gas_grs_capacity query — оба теперь под begin_nested.
assert db.begin_nested.call_count >= 1
def test_session_usable_after_failure(self) -> None:
"""db.execute сбоит один раз (city_grs), но db остаётся usable для
следующего (не связанного) вызова на этой же сессии."""
db = _mock_db_raising_once()
# outlet_counts вызывается ПЕРВЫМ внутри _query_gas_city_grs — заставим его
# тоже отдать пустой результат нормально, а основной query — кинуть.
db.execute.side_effect = [
MagicMock(
mappings=MagicMock(return_value=MagicMock(first=lambda: None))
), # outlet_counts
RuntimeError("main query failed"), # gas_grs_capacity query
]
out = _query_gas_city_grs(db)
assert out["city_grs"] == []
# Следующий вызов на том же db.execute отрабатывает (session не отравлена).
next_result = MagicMock()
next_result.mappings.return_value.all.return_value = [{"marker": "ok"}]
db.execute.side_effect = None
db.execute.return_value = next_result
assert db.execute("SELECT 1").mappings().all() == [{"marker": "ok"}]
class TestGasOutletCountsSavepoint:
def test_query_failure_degrades_to_zeros(self) -> None:
db = MagicMock()
db.execute.side_effect = RuntimeError("gas_grs_outlet_points недоступна")
out = _query_gas_outlet_counts(db)
assert out == {"outlets_total": 0, "outlets_deficit": 0, "outlets_needs_calc": 0}
def test_uses_begin_nested_savepoint(self) -> None:
db = MagicMock()
db.execute.return_value.mappings.return_value.first.return_value = None
_query_gas_outlet_counts(db)
assert db.begin_nested.call_count == 1
class TestGasOutletPointsSavepoint:
def test_query_failure_degrades_to_empty_list(self) -> None:
db = MagicMock()
db.execute.side_effect = RuntimeError("gas_grs_outlet_points (радиус) недоступна")
assert _query_gas_outlet_points(db, _WKT) == []
def test_uses_begin_nested_savepoint(self) -> None:
db = MagicMock()
db.execute.return_value.mappings.return_value.all.return_value = []
_query_gas_outlet_points(db, _WKT)
assert db.begin_nested.call_count == 1
class TestHeatLatestSavepoint:
def test_query_failure_degrades_gracefully(self) -> None:
db = MagicMock()
db.execute.side_effect = RuntimeError("heat_system_reserves недоступна")
out = _query_heat_latest(db)
assert out == {"systems": [], "total_reserve_gcal_h": None}
def test_uses_begin_nested_savepoint(self) -> None:
db = MagicMock()
db.execute.return_value.mappings.return_value.all.return_value = []
_query_heat_latest(db)
assert db.begin_nested.call_count == 1
def test_session_usable_after_failure(self) -> None:
"""execute сбоит один раз, db остаётся usable для следующего вызова —
имитирует heat → gas_outlet_points → nearby_network_zones в
get_connection_capacity() на одной shared Session."""
db = MagicMock()
db.execute.side_effect = RuntimeError("boom")
out = _query_heat_latest(db)
assert out["systems"] == []
db.execute.side_effect = None
ok_result = MagicMock()
ok_result.mappings.return_value.all.return_value = [{"marker": "next-query-ok"}]
db.execute.return_value = ok_result
assert db.execute("SELECT 1").mappings().all() == [{"marker": "next-query-ok"}]