fix(#261): custom_pois — regression tests для db.commit() в create/update/delete (P0 persistence) #262
1 changed files with 141 additions and 1 deletions
|
|
@ -10,9 +10,11 @@
|
|||
7. Scoring integration: custom POI в 500м с weight=+2 увеличивает score
|
||||
8. No X-Session-Id → auto-generated UUID в response header
|
||||
9. Scoring absent when no X-Session-Id header
|
||||
10. Service-level: db.commit() вызывается в create/update/delete (#261 regression guard)
|
||||
|
||||
Стратегия mock: сервисные функции патчим через unittest.mock.patch,
|
||||
DB — через dependency_overrides (аналогично test_analyze_inline_weights.py).
|
||||
Service commit tests (#261): MagicMock db + assert db.commit.assert_called_once().
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -26,7 +28,7 @@ import pytest
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
from app.schemas.custom_poi import CustomPoiOut
|
||||
from app.schemas.custom_poi import CustomPoiCreate, CustomPoiOut, CustomPoiUpdate
|
||||
|
||||
# ── Константы ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -453,3 +455,141 @@ def test_analyze_custom_poi_negative_weight_decreases_score() -> None:
|
|||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
_stop_analyze_patches()
|
||||
|
||||
|
||||
# ── Service commit regression tests (#261) ────────────────────────────────────
|
||||
#
|
||||
# Проверяем что service-функции вызывают db.commit() после каждой мутации.
|
||||
# Если db.commit() убрать из сервиса — тест упадёт на assert_called_once().
|
||||
#
|
||||
# Паттерн "два сеанса": write_db отдельный от последующего чтения.
|
||||
# Эмулирует независимый SELECT после HTTP request close:
|
||||
# get_db.finally → db.close() без commit → rollback pending tx,
|
||||
# поэтому commit должен быть явным внутри каждой service-функции.
|
||||
|
||||
|
||||
def _make_row_data(
|
||||
poi_id: int = 1,
|
||||
user_id: str = _SESSION,
|
||||
name: str = "Тест",
|
||||
weight: float = 1.5,
|
||||
lon: float = 60.605,
|
||||
lat: float = 56.838,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"id": poi_id,
|
||||
"user_id": user_id,
|
||||
"parcel_cad": None,
|
||||
"name": name,
|
||||
"category": "park",
|
||||
"weight": weight,
|
||||
"lon": lon,
|
||||
"lat": lat,
|
||||
"notes": None,
|
||||
"created_at": _TS,
|
||||
"updated_at": _TS,
|
||||
}
|
||||
|
||||
|
||||
def _make_db_with_row(row_data: dict[str, Any] | None) -> MagicMock:
|
||||
"""Вернуть MagicMock-сессию где execute().mappings().first() → row_data mapping."""
|
||||
db = MagicMock()
|
||||
row_mock: MagicMock | None = None
|
||||
if row_data is not None:
|
||||
row_mock = MagicMock()
|
||||
row_mock.__getitem__ = lambda self, k: row_data[k]
|
||||
row_mock.get = lambda k, default=None: row_data.get(k, default)
|
||||
|
||||
mapping_mock = MagicMock()
|
||||
mapping_mock.first.return_value = row_mock
|
||||
|
||||
exec_mock = MagicMock()
|
||||
exec_mock.mappings.return_value = mapping_mock
|
||||
exec_mock.first.return_value = row_mock # для DELETE RETURNING id (не mappings)
|
||||
|
||||
db.execute.return_value = exec_mock
|
||||
return db
|
||||
|
||||
|
||||
def test_service_create_custom_poi_commits() -> None:
|
||||
"""create_custom_poi вызывает db.commit() — regression guard #261.
|
||||
|
||||
Если commit убрать: row существует только в рамках незакоммиченной транзакции,
|
||||
при db.close() (get_db.finally) соединение вернётся в пул без commit → rollback.
|
||||
"""
|
||||
from app.services.site_finder.custom_pois import create_custom_poi
|
||||
|
||||
db = _make_db_with_row(_make_row_data())
|
||||
payload = CustomPoiCreate(name="Тест", weight=1.5, lon=60.605, lat=56.838)
|
||||
|
||||
result = create_custom_poi(db, _SESSION, payload)
|
||||
|
||||
db.commit.assert_called_once()
|
||||
assert result.id == 1
|
||||
assert result.user_id == _SESSION
|
||||
assert result.weight == pytest.approx(1.5)
|
||||
|
||||
|
||||
def test_service_delete_custom_poi_commits_when_found() -> None:
|
||||
"""delete_custom_poi вызывает db.commit() когда запись найдена — regression guard #261."""
|
||||
from app.services.site_finder.custom_pois import delete_custom_poi
|
||||
|
||||
db = _make_db_with_row(_make_row_data())
|
||||
db.execute.return_value.first.return_value = MagicMock() # RETURNING id truthy
|
||||
|
||||
deleted = delete_custom_poi(db, poi_id=1, user_id=_SESSION)
|
||||
|
||||
db.commit.assert_called_once()
|
||||
assert deleted is True
|
||||
|
||||
|
||||
def test_service_delete_custom_poi_no_commit_when_not_found() -> None:
|
||||
"""delete_custom_poi НЕ вызывает db.commit() если запись не найдена."""
|
||||
from app.services.site_finder.custom_pois import delete_custom_poi
|
||||
|
||||
db = _make_db_with_row(None)
|
||||
db.execute.return_value.first.return_value = None # RETURNING id пусто
|
||||
|
||||
deleted = delete_custom_poi(db, poi_id=999, user_id=_SESSION)
|
||||
|
||||
db.commit.assert_not_called()
|
||||
assert deleted is False
|
||||
|
||||
|
||||
def test_service_update_custom_poi_commits_when_fields_given() -> None:
|
||||
"""update_custom_poi вызывает db.commit() при наличии полей для обновления — regression #261."""
|
||||
from app.services.site_finder.custom_pois import update_custom_poi
|
||||
|
||||
db = _make_db_with_row(_make_row_data(weight=2.0))
|
||||
payload = CustomPoiUpdate(weight=2.0)
|
||||
|
||||
result = update_custom_poi(db, poi_id=1, user_id=_SESSION, payload=payload)
|
||||
|
||||
db.commit.assert_called_once()
|
||||
assert result is not None
|
||||
|
||||
|
||||
def test_service_update_custom_poi_no_commit_when_payload_empty() -> None:
|
||||
"""update_custom_poi НЕ вызывает db.commit() если payload не содержит изменяемых полей."""
|
||||
from app.services.site_finder.custom_pois import update_custom_poi
|
||||
|
||||
db = _make_db_with_row(_make_row_data())
|
||||
# Все поля None → sets = ["updated_at = NOW()"], len == 1 → UPDATE не выполняется
|
||||
payload = CustomPoiUpdate()
|
||||
|
||||
update_custom_poi(db, poi_id=1, user_id=_SESSION, payload=payload)
|
||||
|
||||
db.commit.assert_not_called()
|
||||
|
||||
|
||||
def test_service_update_custom_poi_no_commit_when_not_found() -> None:
|
||||
"""update_custom_poi НЕ вызывает db.commit() если POI не найдена."""
|
||||
from app.services.site_finder.custom_pois import update_custom_poi
|
||||
|
||||
db = _make_db_with_row(None) # get_custom_poi вернёт None (POI не найдена)
|
||||
payload = CustomPoiUpdate(weight=3.0)
|
||||
|
||||
result = update_custom_poi(db, poi_id=999, user_id=_SESSION, payload=payload)
|
||||
|
||||
db.commit.assert_not_called()
|
||||
assert result is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue