codex
This commit is contained in:
BIN
tests/db/__pycache__/test_connection.cpython-313-pytest-9.0.2.pyc
Executable file
BIN
tests/db/__pycache__/test_connection.cpython-313-pytest-9.0.2.pyc
Executable file
Binary file not shown.
BIN
tests/db/__pycache__/test_models.cpython-313-pytest-9.0.2.pyc
Executable file
BIN
tests/db/__pycache__/test_models.cpython-313-pytest-9.0.2.pyc
Executable file
Binary file not shown.
BIN
tests/db/__pycache__/test_repository.cpython-313-pytest-9.0.2.pyc
Executable file
BIN
tests/db/__pycache__/test_repository.cpython-313-pytest-9.0.2.pyc
Executable file
Binary file not shown.
87
tests/db/test_connection.py
Executable file
87
tests/db/test_connection.py
Executable 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
89
tests/db/test_models.py
Executable 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
82
tests/db/test_repository.py
Executable 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
|
||||
Reference in New Issue
Block a user