Files
scrap/tests/api/test_products_funcs.py
Gilles Soulier 152c2724fc feat: improve SPA scraping and increase test coverage
- Add SPA support for Playwright with wait_for_network_idle and extra_wait_ms
- Add BaseStore.get_spa_config() and requires_playwright() methods
- Implement AliExpress SPA config with JSON price extraction patterns
- Fix Amazon price parsing to prioritize whole+fraction combination
- Fix AliExpress regex patterns (remove double backslashes)
- Add CLI tests: detect, doctor, fetch, parse, run commands
- Add API tests: auth, logs, products, scraping_logs, webhooks

Tests: 417 passed, 85% coverage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 14:46:55 +01:00

268 lines
8.9 KiB
Python

"""Tests fonctions API produits avec mocks."""
from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from pricewatch.app.api.main import (
create_product,
get_product,
update_product,
delete_product,
list_prices,
create_price,
update_price,
delete_price,
)
from pricewatch.app.api.schemas import ProductCreate, ProductUpdate, PriceHistoryCreate, PriceHistoryUpdate
class MockProduct:
"""Mock Product model."""
def __init__(self, **kwargs):
self.id = kwargs.get("id", 1)
self.source = kwargs.get("source", "amazon")
self.reference = kwargs.get("reference", "REF123")
self.url = kwargs.get("url", "https://example.com")
self.title = kwargs.get("title", "Test Product")
self.category = kwargs.get("category")
self.description = kwargs.get("description")
self.currency = kwargs.get("currency", "EUR")
self.msrp = kwargs.get("msrp")
self.first_seen_at = kwargs.get("first_seen_at", datetime.now())
self.last_updated_at = kwargs.get("last_updated_at", datetime.now())
class MockPrice:
"""Mock PriceHistory model."""
def __init__(self, **kwargs):
self.id = kwargs.get("id", 1)
self.product_id = kwargs.get("product_id", 1)
self.price = kwargs.get("price", 99.99)
self.shipping_cost = kwargs.get("shipping_cost")
self.stock_status = kwargs.get("stock_status", "in_stock")
self.fetch_method = kwargs.get("fetch_method", "http")
self.fetch_status = kwargs.get("fetch_status", "success")
self.fetched_at = kwargs.get("fetched_at", datetime.now())
class TestCreateProduct:
"""Tests create_product."""
def test_create_success(self):
"""Cree un produit avec succes."""
session = MagicMock()
session.add = MagicMock()
session.commit = MagicMock()
session.refresh = MagicMock()
payload = ProductCreate(
source="amazon",
reference="NEW123",
url="https://amazon.fr/dp/NEW123",
title="New Product",
currency="EUR",
)
with patch("pricewatch.app.api.main.Product") as MockProductClass:
mock_product = MockProduct(reference="NEW123")
MockProductClass.return_value = mock_product
with patch("pricewatch.app.api.main._product_to_out") as mock_to_out:
mock_to_out.return_value = MagicMock()
result = create_product(payload, session)
session.add.assert_called_once()
session.commit.assert_called_once()
def test_create_duplicate(self):
"""Cree un produit duplique leve 409."""
session = MagicMock()
session.add = MagicMock()
session.commit = MagicMock(side_effect=IntegrityError("duplicate", {}, None))
session.rollback = MagicMock()
payload = ProductCreate(
source="amazon",
reference="DUPE",
url="https://amazon.fr/dp/DUPE",
title="Duplicate",
currency="EUR",
)
with patch("pricewatch.app.api.main.Product"):
with pytest.raises(HTTPException) as exc_info:
create_product(payload, session)
assert exc_info.value.status_code == 409
def test_create_db_error(self):
"""Erreur DB leve 500."""
session = MagicMock()
session.add = MagicMock()
session.commit = MagicMock(side_effect=SQLAlchemyError("db error"))
session.rollback = MagicMock()
payload = ProductCreate(
source="amazon",
reference="ERR",
url="https://amazon.fr/dp/ERR",
title="Error",
currency="EUR",
)
with patch("pricewatch.app.api.main.Product"):
with pytest.raises(HTTPException) as exc_info:
create_product(payload, session)
assert exc_info.value.status_code == 500
class TestGetProduct:
"""Tests get_product."""
def test_get_not_found(self):
"""Produit non trouve leve 404."""
session = MagicMock()
mock_query = MagicMock()
mock_query.filter.return_value.one_or_none.return_value = None
session.query.return_value = mock_query
with pytest.raises(HTTPException) as exc_info:
get_product(99999, session)
assert exc_info.value.status_code == 404
class TestUpdateProduct:
"""Tests update_product."""
def test_update_not_found(self):
"""Update produit non trouve leve 404."""
session = MagicMock()
mock_query = MagicMock()
mock_query.filter.return_value.one_or_none.return_value = None
session.query.return_value = mock_query
payload = ProductUpdate(title="Updated")
with pytest.raises(HTTPException) as exc_info:
update_product(99999, payload, session)
assert exc_info.value.status_code == 404
def test_update_db_error(self):
"""Erreur DB lors d'update leve 500."""
session = MagicMock()
mock_product = MockProduct()
mock_query = MagicMock()
mock_query.filter.return_value.one_or_none.return_value = mock_product
session.query.return_value = mock_query
session.commit = MagicMock(side_effect=SQLAlchemyError("error"))
session.rollback = MagicMock()
payload = ProductUpdate(title="Updated")
with pytest.raises(HTTPException) as exc_info:
update_product(1, payload, session)
assert exc_info.value.status_code == 500
class TestDeleteProduct:
"""Tests delete_product."""
def test_delete_not_found(self):
"""Delete produit non trouve leve 404."""
session = MagicMock()
mock_query = MagicMock()
mock_query.filter.return_value.one_or_none.return_value = None
session.query.return_value = mock_query
with pytest.raises(HTTPException) as exc_info:
delete_product(99999, session)
assert exc_info.value.status_code == 404
def test_delete_success(self):
"""Delete produit avec succes."""
session = MagicMock()
mock_product = MockProduct()
mock_query = MagicMock()
mock_query.filter.return_value.one_or_none.return_value = mock_product
session.query.return_value = mock_query
session.delete = MagicMock()
session.commit = MagicMock()
result = delete_product(1, session)
assert result == {"status": "deleted"}
session.delete.assert_called_once()
def test_delete_db_error(self):
"""Erreur DB lors de delete leve 500."""
session = MagicMock()
mock_product = MockProduct()
mock_query = MagicMock()
mock_query.filter.return_value.one_or_none.return_value = mock_product
session.query.return_value = mock_query
session.delete = MagicMock()
session.commit = MagicMock(side_effect=SQLAlchemyError("error"))
session.rollback = MagicMock()
with pytest.raises(HTTPException) as exc_info:
delete_product(1, session)
assert exc_info.value.status_code == 500
class TestCreatePrice:
"""Tests create_price."""
def test_create_price_db_error(self):
"""Erreur DB lors de creation prix."""
session = MagicMock()
session.add = MagicMock()
session.commit = MagicMock(side_effect=SQLAlchemyError("error"))
session.rollback = MagicMock()
payload = PriceHistoryCreate(
product_id=1,
price=99.99,
fetch_method="http",
fetch_status="success",
fetched_at=datetime.now(),
)
with patch("pricewatch.app.api.main.PriceHistory"):
with pytest.raises(HTTPException) as exc_info:
create_price(payload, session)
assert exc_info.value.status_code == 500
class TestUpdatePrice:
"""Tests update_price."""
def test_update_price_not_found(self):
"""Update prix non trouve leve 404."""
session = MagicMock()
mock_query = MagicMock()
mock_query.filter.return_value.one_or_none.return_value = None
session.query.return_value = mock_query
payload = PriceHistoryUpdate(price=149.99)
with pytest.raises(HTTPException) as exc_info:
update_price(99999, payload, session)
assert exc_info.value.status_code == 404
class TestDeletePrice:
"""Tests delete_price."""
def test_delete_price_not_found(self):
"""Delete prix non trouve leve 404."""
session = MagicMock()
mock_query = MagicMock()
mock_query.filter.return_value.one_or_none.return_value = None
session.query.return_value = mock_query
with pytest.raises(HTTPException) as exc_info:
delete_price(99999, session)
assert exc_info.value.status_code == 404