tests: extract _build_session_factory to a shared conftest fixture

The same per-test sqlite-engine setup was duplicated across 14 test
files (~30 lines each). Consolidated into a single async fixture
`db_factory` in tests/conftest.py; tests now take db_factory as a
parameter and use `async with db_factory() as session` directly.

No behaviour change — same function-scope, same in-memory schema
created via Base.metadata.create_all, same app.db._engine /
_session_factory rebinding so module-level helpers see the test
engine. Just ~420 lines of boilerplate removed.
This commit is contained in:
Giorgio Gilestro 2026-05-27 20:50:09 +02:00
parent b13caa4c51
commit dcc2c07111
5 changed files with 167 additions and 250 deletions

View file

@ -5,25 +5,6 @@ from __future__ import annotations
import pytest
def _build_session_factory(tmp_path):
"""Per-test sqlite engine + factory. Mirrors test_referral_conversion.py."""
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app import db as db_mod
from app.db import Base
import app.models # noqa: F401 — registers models on Base.metadata
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/loc.db")
factory = async_sessionmaker(engine, expire_on_commit=False)
db_mod._engine = engine
db_mod._session_factory = factory
async def _setup():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
return engine, factory, _setup
def test_user_has_lang_column_with_default_en():
from sqlalchemy import inspect
@ -55,7 +36,7 @@ def test_strategic_log_translation_model_columns():
assert cols["content_md"].nullable is False
async def test_log_translation_fanout_no_active_non_en_users(tmp_path, monkeypatch):
async def test_log_translation_fanout_no_active_non_en_users(db_factory, monkeypatch):
"""When no users have an active non-en lang, the fan-out makes no
translation calls and no rows are inserted."""
from unittest.mock import AsyncMock
@ -65,8 +46,7 @@ async def test_log_translation_fanout_no_active_non_en_users(tmp_path, monkeypat
from app.models import StrategicLog, StrategicLogTranslation, User
from app.jobs import ai_log_job
_, factory, setup = _build_session_factory(tmp_path)
await setup()
factory = db_factory
fake_translate = AsyncMock()
monkeypatch.setattr(ai_log_job, "translate", fake_translate)
@ -92,7 +72,7 @@ async def test_log_translation_fanout_no_active_non_en_users(tmp_path, monkeypat
assert rows == []
async def test_log_translation_fanout_italian_user(tmp_path, monkeypatch):
async def test_log_translation_fanout_italian_user(db_factory, monkeypatch):
"""One user at lang=it triggers one translation; the row lands with
the right lang and log_id."""
from sqlalchemy import select
@ -102,8 +82,7 @@ async def test_log_translation_fanout_italian_user(tmp_path, monkeypatch):
from app.services.openrouter import LogResult
from app.jobs import ai_log_job
_, factory, setup = _build_session_factory(tmp_path)
await setup()
factory = db_factory
async def _fake_translate(client, text, target_lang):
assert target_lang == "it"
@ -139,7 +118,7 @@ async def test_log_translation_fanout_italian_user(tmp_path, monkeypatch):
assert row.llm_cost_usd == pytest.approx(0.00002)
async def test_log_translation_fanout_per_language_failure_isolated(tmp_path, monkeypatch):
async def test_log_translation_fanout_per_language_failure_isolated(db_factory, monkeypatch):
"""If one language's translation fails, the others (if any) still land
and the job does not raise."""
from sqlalchemy import select
@ -148,8 +127,7 @@ async def test_log_translation_fanout_per_language_failure_isolated(tmp_path, mo
from app.models import StrategicLog, StrategicLogTranslation, User
from app.jobs import ai_log_job
_, factory, setup = _build_session_factory(tmp_path)
await setup()
factory = db_factory
async def _fake_translate(client, text, target_lang):
raise RuntimeError("upstream down")
@ -175,7 +153,7 @@ async def test_log_translation_fanout_per_language_failure_isolated(tmp_path, mo
assert rows == []
async def test_analyse_threads_lang_into_system_prompt(tmp_path, monkeypatch):
async def test_analyse_threads_lang_into_system_prompt(db_factory, monkeypatch):
"""When lang='it', the system prompt sent to call_llm contains
'Respond in Italian.' the LLM does the rest."""
from app.services import portfolio_analysis as pa
@ -191,8 +169,7 @@ async def test_analyse_threads_lang_into_system_prompt(tmp_path, monkeypatch):
)
monkeypatch.setattr(pa, "call_llm", _fake_call_llm)
_, factory, setup = _build_session_factory(tmp_path)
await setup()
factory = db_factory
payload = {
"positions": [{"yahoo_ticker": "AAPL", "qty": 10, "avg_cost": 150.0,
@ -213,7 +190,7 @@ async def test_analyse_threads_lang_into_system_prompt(tmp_path, monkeypatch):
assert "Respond in Italian" in system
async def test_analyse_no_clause_when_lang_is_en(tmp_path, monkeypatch):
async def test_analyse_no_clause_when_lang_is_en(db_factory, monkeypatch):
from app.services import portfolio_analysis as pa
from app.services.openrouter import LogResult
@ -227,8 +204,7 @@ async def test_analyse_no_clause_when_lang_is_en(tmp_path, monkeypatch):
)
monkeypatch.setattr(pa, "call_llm", _fake_call_llm)
_, factory, setup = _build_session_factory(tmp_path)
await setup()
factory = db_factory
payload = {
"positions": [{"yahoo_ticker": "AAPL", "qty": 10, "avg_cost": 150.0,
@ -328,13 +304,12 @@ def test_digest_pick_variant_uses_user_lang():
async def test_patch_language_accepts_active(tmp_path):
async def test_patch_language_accepts_active(db_factory):
"""PATCH /api/settings/language accepts 'en' and 'it' and persists."""
from app.models import User
from app.routers.api import patch_language_prefs, LanguagePrefsIn
_, factory, setup = _build_session_factory(tmp_path)
await setup()
factory = db_factory
async with factory() as session:
session.add(User(id=20, email="u@x", tier="paid", lang="en"))
@ -358,14 +333,13 @@ async def test_patch_language_accepts_active(tmp_path):
assert user.lang == "it"
async def test_patch_language_rejects_wip(tmp_path):
async def test_patch_language_rejects_wip(db_factory):
"""PATCH rejects 'es'/'fr'/'de'/'xx' with 400 — ACTIVE_LANGUAGES gate."""
from fastapi import HTTPException
from app.models import User
from app.routers.api import patch_language_prefs, LanguagePrefsIn
_, factory, setup = _build_session_factory(tmp_path)
await setup()
factory = db_factory
async with factory() as session:
session.add(User(id=21, email="u2@x", tier="paid", lang="en"))