This commit is contained in:
2026-01-14 07:03:38 +01:00
parent ecda149a4b
commit c91c0f1fc9
61 changed files with 4388 additions and 38 deletions

87
tests/db/test_connection.py Executable file
View File

@@ -0,0 +1,87 @@
"""
Tests pour la couche de connexion SQLAlchemy.
"""
from dataclasses import dataclass
import pytest
from sqlalchemy import inspect
from pricewatch.app.db.connection import (
check_db_connection,
get_engine,
get_session,
init_db,
reset_engine,
)
from pricewatch.app.db.models import Product
@dataclass
class FakeDbConfig:
"""Config DB minimale pour tests SQLite."""
url: str
host: str = "sqlite"
port: int = 0
database: str = ":memory:"
@dataclass
class FakeAppConfig:
"""Config App minimale pour tests."""
db: FakeDbConfig
debug: bool = False
@pytest.fixture(autouse=True)
def reset_db_engine():
"""Reset l'engine global entre les tests."""
reset_engine()
yield
reset_engine()
@pytest.fixture
def sqlite_config() -> FakeAppConfig:
"""Config SQLite in-memory pour tests."""
return FakeAppConfig(db=FakeDbConfig(url="sqlite:///:memory:"))
def test_get_engine_sqlite(sqlite_config: FakeAppConfig):
"""Cree un engine SQLite fonctionnel."""
engine = get_engine(sqlite_config)
assert engine.url.get_backend_name() == "sqlite"
def test_init_db_creates_tables(sqlite_config: FakeAppConfig):
"""Init DB cree toutes les tables attendues."""
init_db(sqlite_config)
engine = get_engine(sqlite_config)
inspector = inspect(engine)
tables = set(inspector.get_table_names())
assert "products" in tables
assert "price_history" in tables
assert "product_images" in tables
assert "product_specs" in tables
assert "scraping_logs" in tables
def test_get_session_commit(sqlite_config: FakeAppConfig):
"""La session permet un commit simple."""
init_db(sqlite_config)
with get_session(sqlite_config) as session:
product = Product(source="amazon", reference="B08N5WRWNW", url="https://example.com")
session.add(product)
session.commit()
with get_session(sqlite_config) as session:
assert session.query(Product).count() == 1
def test_check_db_connection(sqlite_config: FakeAppConfig):
"""Le health check DB retourne True en SQLite."""
init_db(sqlite_config)
assert check_db_connection(sqlite_config) is True

89
tests/db/test_models.py Executable file
View File

@@ -0,0 +1,89 @@
"""
Tests pour les modeles SQLAlchemy.
"""
from datetime import datetime
import pytest
from sqlalchemy import create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, sessionmaker
from pricewatch.app.db.models import (
Base,
PriceHistory,
Product,
ProductImage,
ProductSpec,
ScrapingLog,
)
@pytest.fixture
def session() -> Session:
"""Session SQLite in-memory pour tests de modeles."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
try:
yield session
finally:
session.close()
def test_product_relationships(session: Session):
"""Les relations principales fonctionnent (prix, images, specs, logs)."""
product = Product(source="amazon", reference="B08N5WRWNW", url="https://example.com")
price = PriceHistory(
price=199.99,
shipping_cost=0,
stock_status="in_stock",
fetch_method="http",
fetch_status="success",
fetched_at=datetime.utcnow(),
)
image = ProductImage(image_url="https://example.com/image.jpg", position=0)
spec = ProductSpec(spec_key="Couleur", spec_value="Noir")
log = ScrapingLog(
url="https://example.com",
source="amazon",
reference="B08N5WRWNW",
fetch_method="http",
fetch_status="success",
fetched_at=datetime.utcnow(),
duration_ms=1200,
html_size_bytes=2048,
errors={"items": []},
notes={"items": ["OK"]},
)
product.price_history.append(price)
product.images.append(image)
product.specs.append(spec)
product.logs.append(log)
session.add(product)
session.commit()
loaded = session.query(Product).first()
assert loaded is not None
assert len(loaded.price_history) == 1
assert len(loaded.images) == 1
assert len(loaded.specs) == 1
assert len(loaded.logs) == 1
def test_unique_product_constraint(session: Session):
"""La contrainte unique source+reference est respectee."""
product_a = Product(source="amazon", reference="B08N5WRWNW", url="https://example.com/a")
product_b = Product(source="amazon", reference="B08N5WRWNW", url="https://example.com/b")
session.add(product_a)
session.commit()
session.add(product_b)
with pytest.raises(IntegrityError):
session.commit()
session.rollback()

82
tests/db/test_repository.py Executable file
View File

@@ -0,0 +1,82 @@
"""
Tests pour le repository SQLAlchemy.
"""
from datetime import datetime
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from pricewatch.app.core.schema import DebugInfo, DebugStatus, FetchMethod, ProductSnapshot
from pricewatch.app.db.models import Base, Product, ScrapingLog
from pricewatch.app.db.repository import ProductRepository
@pytest.fixture
def session() -> Session:
"""Session SQLite in-memory pour tests repository."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
try:
yield session
finally:
session.close()
engine.dispose()
def _make_snapshot(reference: str | None) -> ProductSnapshot:
return ProductSnapshot(
source="amazon",
url="https://example.com/product",
fetched_at=datetime(2026, 1, 14, 12, 0, 0),
title="Produit test",
price=199.99,
currency="EUR",
shipping_cost=0.0,
reference=reference,
images=["https://example.com/img1.jpg"],
specs={"Couleur": "Noir"},
debug=DebugInfo(
method=FetchMethod.HTTP,
status=DebugStatus.SUCCESS,
errors=["Avertissement"],
notes=["OK"],
),
)
def test_save_snapshot_creates_product(session: Session):
"""Le repository persiste produit + log."""
repo = ProductRepository(session)
snapshot = _make_snapshot(reference="B08N5WRWNW")
product_id = repo.save_snapshot(snapshot)
session.commit()
product = session.query(Product).one()
assert product.id == product_id
assert product.reference == "B08N5WRWNW"
assert len(product.images) == 1
assert len(product.specs) == 1
assert len(product.price_history) == 1
log = session.query(ScrapingLog).one()
assert log.product_id == product_id
assert log.errors == ["Avertissement"]
assert log.notes == ["OK"]
def test_save_snapshot_without_reference(session: Session):
"""Sans reference, le produit n'est pas cree mais le log existe."""
repo = ProductRepository(session)
snapshot = _make_snapshot(reference=None)
product_id = repo.save_snapshot(snapshot)
session.commit()
assert product_id is None
assert session.query(Product).count() == 0
assert session.query(ScrapingLog).count() == 1