diff --git a/backend/tests/test_2464c_photos_session_release.py b/backend/tests/test_2464c_photos_session_release.py index ce7afe9a..10b2a9db 100644 --- a/backend/tests/test_2464c_photos_session_release.py +++ b/backend/tests/test_2464c_photos_session_release.py @@ -114,3 +114,55 @@ def test_missing_row_still_404(_session_with_photo_row) -> None: with pytest.raises(HTTPException) as exc: photos.get_photo(db=_session_with_photo_row, obj_id=999, file_id="nope", size="thumb") assert exc.value.status_code == 404 + + +def test_session_usable_after_close_for_thumb_update( + _session_with_photo_row, tmp_path: Path, monkeypatch +) -> None: + """Сессия переиспользуется ПОСЛЕ close(): UPDATE thumb_path + commit доезжают. + + Это самый рискованный участок #2928: `db.close()` стоит выше по коду, а ниже + сессия ещё раз работает с БД. Поведение штатное (`close()` возвращает + соединение в пул, следующий execute берёт новое и открывает свою транзакцию), + но на проде эта ветка сегодня не исполняется НИ РАЗУ: замер 19.08.2026 — + 0 строк `domrf_kn_photos` с непустым local_path и пустым thumb_path (все 1 889 + закешированных уже с миниатюрами). То есть первый же ленивый фетч новой + фотографии пойдёт по коду, который не проверял никто. + + Дополнительно фиксируем, что генерация миниатюры (медленная работа) идёт уже + без открытой транзакции — то же свойство, что и для внешнего фетча. + """ + from app.api.v1 import photos + + original = tmp_path / "orig.jpg" + original.write_bytes(b"jpeg") + _session_with_photo_row.execute( + text("UPDATE domrf_kn_photos SET local_path = :p"), {"p": str(original)} + ) + _session_with_photo_row.commit() + + generated = tmp_path / "orig.webp" + seen: dict[str, bool] = {} + + def _fake_make_thumbnail(src: Path): + seen["in_transaction"] = _session_with_photo_row.in_transaction() + generated.write_bytes(b"webp") + return generated + + monkeypatch.setattr(photos, "make_thumbnail", _fake_make_thumbnail) + + resp = photos.get_photo(db=_session_with_photo_row, obj_id=1, file_id="f1", size="thumb") + + assert ( + seen.get("in_transaction") is False + ), "миниатюра генерируется при открытой транзакции — соединение пула занято" + assert getattr(resp, "path", None) == str(generated) + + # Главное: запись ПОСЛЕ close() действительно доехала до БД — читаем ОТДЕЛЬНЫМ + # соединением. Через ту же сессию проверять нельзя: она видит собственную + # незакоммиченную транзакцию, и пропажа commit() прошла бы незамеченной. + with _session_with_photo_row.get_bind().connect() as fresh: + stored = fresh.execute( + text("SELECT thumb_path FROM domrf_kn_photos WHERE obj_id = 1") + ).scalar_one() + assert stored == str(generated), "UPDATE после close() не закоммичен"