jurix-api / tests /test_core /test_database_engines.py
rahim237cm's picture
Claude Opus 5
test: couverture des comportements qui etaient silencieusement casses
cd71f3c
Raw History Blame Contribute Delete
3.46 kB
"""
Tests structurels sur les moteurs de base.
Onze appels a create_engine() etaient dissemines dans le code, un par fonction
ou par instance, et aucun n'appelait jamais dispose() : chaque loi traitee et
chaque question posee au RAG laissaient un pool de connexions ouvert jusqu'a
l'arret du processus. Ces tests encodent la regle plutot que de la documenter.
"""
import ast
import pathlib
import pytest
APP_ROOT = pathlib.Path(__file__).resolve().parents[2] / "app"
ALLOWED = {APP_ROOT / "core" / "database.py"}
def _modules_calling_create_engine():
offenders = []
for path in APP_ROOT.rglob("*.py"):
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
name = getattr(func, "id", None) or getattr(func, "attr", None)
if name in {"create_engine", "create_async_engine"} and path not in ALLOWED:
offenders.append(f"{path.relative_to(APP_ROOT.parent)}:{node.lineno}")
return offenders
def test_no_module_builds_its_own_engine():
"""Un seul endroit a le droit de creer un moteur."""
offenders = _modules_calling_create_engine()
assert offenders == [], (
"Moteur cree hors de app/core/database.py : "
+ ", ".join(offenders)
+ ". Utiliser SyncSessionLocal() ou AsyncSessionLocal()."
)
def test_sync_session_factory_is_shared():
"""Toutes les sessions synchrones partagent le meme moteur."""
from app.core.database import SyncSessionLocal, sync_engine
with SyncSessionLocal() as first, SyncSessionLocal() as second:
assert first.get_bind() is second.get_bind()
assert first.get_bind() is sync_engine
def test_embedding_service_borrows_the_shared_factory():
"""Le service d'embeddings ne possede plus de moteur en propre."""
from app.core.database import SyncSessionLocal
from app.services.embedding_service import EmbeddingService
service = EmbeddingService(use_cache=True)
assert service._sync_session_factory is SyncSessionLocal
assert not hasattr(service, "_sync_engine")
def test_sync_engine_points_at_the_test_database():
"""
Garde-fou : le moteur synchrone lit settings.DATABASE_URL, soit la base de
DEVELOPPEMENT si la surcharge de conftest n'a pas pris. Les tests du
pipeline y ecriraient alors de vraies donnees.
"""
from app.core.database import sync_engine
from tests.conftest import TEST_DATABASE_URL
expected_port = int(TEST_DATABASE_URL.rsplit(":", 1)[1].split("/")[0])
assert sync_engine.url.port == expected_port
@pytest.mark.asyncio
async def test_close_db_disposes_both_engines(monkeypatch):
"""close_db() doit fermer le moteur async ET le moteur synchrone."""
from app.core import database
disposed = {"async": False, "sync": False}
class _FakeAsyncEngine:
async def dispose(self):
disposed["async"] = True
class _FakeSyncEngine:
def dispose(self):
disposed["sync"] = True
# Les moteurs sont remplaces en entier : AsyncEngine.dispose est en lecture
# seule, on ne peut pas la substituer attribut par attribut.
monkeypatch.setattr(database, "engine", _FakeAsyncEngine())
monkeypatch.setattr(database, "sync_engine", _FakeSyncEngine())
await database.close_db()
assert disposed == {"async": True, "sync": True}