Files
orion/tests/unit/services/test_stats_service.py
Samir Boulahtit aad18c27ab
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
refactor: remove all backward compatibility code across 70 files
Clean up 28 backward compatibility instances identified in the codebase.
The app is not live, so all shims are replaced with the target architecture:

- Remove legacy Inventory.location column (use bin_location exclusively)
- Remove dashboard _extract_metric_value helper (use flat metrics dict)
- Remove legacy stat field duplicates (total_stores, total_imports, etc.)
- Remove 13 re-export shims and class aliases across modules
- Remove module-enabling JSON fallback (use PlatformModule junction table)
- Remove menu_to_legacy_format() conversion (return dataclasses directly)
- Remove title/description from MarketplaceProductBase schema
- Clean billing convenience method docstrings
- Clean test fixtures and backward-compat comments
- Add PlatformModule seeding to init_production.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:20:29 +01:00

566 lines
21 KiB
Python

# tests/unit/services/test_stats_service.py
"""Unit tests for StatsService following the application's testing patterns."""
import uuid
from unittest.mock import patch
import pytest
from sqlalchemy.exc import SQLAlchemyError
from app.modules.analytics.services.stats_service import StatsService
from app.modules.inventory.models import Inventory
from app.modules.marketplace.models import (
MarketplaceProduct,
MarketplaceProductTranslation,
)
from app.modules.tenancy.exceptions import (
AdminOperationException,
StoreNotFoundException,
)
def create_marketplace_product_with_translation(
db, marketplace_product_id, title, language="en", **kwargs
):
"""Helper to create a MarketplaceProduct with its translation."""
product = MarketplaceProduct(
marketplace_product_id=marketplace_product_id, **kwargs
)
db.add(product)
db.flush() # Get the product ID
translation = MarketplaceProductTranslation(
marketplace_product_id=product.id,
language=language,
title=title,
)
db.add(translation)
return product
@pytest.mark.unit
@pytest.mark.stats
class TestStatsService:
"""Test suite for StatsService following the application's testing patterns."""
def setup_method(self):
"""Setup method following the same pattern as other service tests."""
self.service = StatsService()
# ==================== get_comprehensive_stats Tests ====================
def test_get_comprehensive_stats_basic(self, db, test_marketplace_product):
"""Test getting comprehensive stats with basic data."""
stats = self.service.get_comprehensive_stats(db)
assert "total_products" in stats
assert "unique_brands" in stats
assert "unique_categories" in stats
assert "unique_marketplaces" in stats
assert "unique_stores" in stats
assert "total_inventory_entries" in stats
assert "total_inventory_quantity" in stats
# Verify types
assert isinstance(stats["total_products"], int)
assert isinstance(stats["unique_brands"], int)
assert isinstance(stats["unique_categories"], int)
assert isinstance(stats["unique_marketplaces"], int)
assert isinstance(stats["unique_stores"], int)
assert isinstance(stats["total_inventory_entries"], int)
assert isinstance(stats["total_inventory_quantity"], int)
def test_get_comprehensive_stats_with_products(self, db, test_product):
"""Test comprehensive stats counts Product records correctly."""
stats = self.service.get_comprehensive_stats(db)
assert stats["total_products"] >= 1
def test_get_comprehensive_stats_multiple_marketplaces(
self, db, test_marketplace_product
):
"""Test comprehensive stats with multiple marketplaces."""
unique_id = str(uuid.uuid4())[:8]
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"PROD002_{unique_id}",
title="MarketplaceProduct 2",
brand="DifferentBrand",
google_product_category="Different Category",
marketplace="Amazon",
store_name="AmazonStore",
price="15.99",
currency="EUR",
)
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"PROD003_{unique_id}",
title="MarketplaceProduct 3",
brand="ThirdBrand",
google_product_category="Third Category",
marketplace="eBay",
store_name="eBayStore",
price="25.99",
currency="USD",
)
db.commit()
stats = self.service.get_comprehensive_stats(db)
# Should count unique marketplaces from MarketplaceProduct table
assert stats["unique_marketplaces"] >= 3 # Letzshop, Amazon, eBay
def test_get_comprehensive_stats_handles_nulls(self, db):
"""Test comprehensive stats handles null/empty values correctly."""
unique_id = str(uuid.uuid4())[:8]
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"NULL001_{unique_id}",
title="MarketplaceProduct with Nulls",
brand=None,
google_product_category=None,
marketplace=None,
store_name=None,
price="10.00",
currency="EUR",
)
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"EMPTY001_{unique_id}",
title="MarketplaceProduct with Empty Values",
brand="",
google_product_category="",
marketplace="",
store_name="",
price="15.00",
currency="EUR",
)
db.commit()
stats = self.service.get_comprehensive_stats(db)
# Should not throw error - null/empty values handled gracefully
assert isinstance(stats["unique_brands"], int)
assert isinstance(stats["unique_categories"], int)
assert isinstance(stats["unique_marketplaces"], int)
def test_get_comprehensive_stats_empty_database(self, db):
"""Test stats with empty database."""
stats = self.service.get_comprehensive_stats(db)
assert stats["total_products"] == 0
assert stats["unique_brands"] == 0
assert stats["unique_categories"] == 0
assert stats["unique_marketplaces"] == 0
assert stats["total_inventory_entries"] == 0
assert stats["total_inventory_quantity"] == 0
def test_get_comprehensive_stats_database_error(self, db):
"""Test comprehensive stats handles database errors."""
with patch.object(db, "query", side_effect=SQLAlchemyError("DB Error")):
with pytest.raises(AdminOperationException) as exc_info:
self.service.get_comprehensive_stats(db)
assert exc_info.value.details.get("operation") == "get_comprehensive_stats"
# ==================== get_marketplace_breakdown_stats Tests ====================
def test_get_marketplace_breakdown_stats_basic(self, db, test_marketplace_product):
"""Test getting marketplace breakdown stats with basic data."""
stats = self.service.get_marketplace_breakdown_stats(db)
assert isinstance(stats, list)
assert len(stats) >= 1
# Find our test marketplace in the results
test_marketplace_stat = next(
(
stat
for stat in stats
if stat["marketplace"] == test_marketplace_product.marketplace
),
None,
)
assert test_marketplace_stat is not None
assert test_marketplace_stat["total_products"] >= 1
assert "unique_stores" in test_marketplace_stat
assert "unique_brands" in test_marketplace_stat
def test_get_marketplace_breakdown_stats_multiple_marketplaces(
self, db, test_marketplace_product
):
"""Test marketplace breakdown with multiple marketplaces."""
unique_id = str(uuid.uuid4())[:8]
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"AMAZON001_{unique_id}",
title="Amazon MarketplaceProduct 1",
brand="AmazonBrand1",
marketplace="Amazon",
store_name="AmazonStore1",
price="20.00",
currency="EUR",
)
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"AMAZON002_{unique_id}",
title="Amazon MarketplaceProduct 2",
brand="AmazonBrand2",
marketplace="Amazon",
store_name="AmazonStore2",
price="25.00",
currency="EUR",
)
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"EBAY001_{unique_id}",
title="eBay MarketplaceProduct",
brand="eBayBrand",
marketplace="eBay",
store_name="eBayStore",
price="30.00",
currency="USD",
)
db.commit()
stats = self.service.get_marketplace_breakdown_stats(db)
marketplace_names = [stat["marketplace"] for stat in stats]
assert "Amazon" in marketplace_names
assert "eBay" in marketplace_names
# Check Amazon stats
amazon_stat = next(stat for stat in stats if stat["marketplace"] == "Amazon")
assert amazon_stat["total_products"] == 2
assert amazon_stat["unique_stores"] == 2
assert amazon_stat["unique_brands"] == 2
# Check eBay stats
ebay_stat = next(stat for stat in stats if stat["marketplace"] == "eBay")
assert ebay_stat["total_products"] == 1
assert ebay_stat["unique_stores"] == 1
assert ebay_stat["unique_brands"] == 1
def test_get_marketplace_breakdown_stats_excludes_nulls(self, db):
"""Test marketplace breakdown excludes products with null marketplaces."""
unique_id = str(uuid.uuid4())[:8]
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"NULLMARKET001_{unique_id}",
title="MarketplaceProduct without marketplace",
marketplace=None,
store_name="SomeStore",
brand="SomeBrand",
price="10.00",
currency="EUR",
)
db.commit()
stats = self.service.get_marketplace_breakdown_stats(db)
# Should not include any stats for null marketplace
marketplace_names = [
stat["marketplace"] for stat in stats if stat["marketplace"] is not None
]
assert None not in marketplace_names
def test_get_marketplace_breakdown_empty_database(self, db):
"""Test marketplace breakdown with empty database."""
stats = self.service.get_marketplace_breakdown_stats(db)
assert isinstance(stats, list)
assert len(stats) == 0
def test_get_marketplace_breakdown_stats_database_error(self, db):
"""Test marketplace breakdown handles database errors."""
with patch.object(db, "query", side_effect=SQLAlchemyError("DB Error")):
with pytest.raises(AdminOperationException) as exc_info:
self.service.get_marketplace_breakdown_stats(db)
assert (
exc_info.value.details.get("operation")
== "get_marketplace_breakdown_stats"
)
# ==================== get_store_stats Tests ====================
def test_get_store_stats_success(self, db, test_store, test_product):
"""Test getting store statistics successfully."""
stats = self.service.get_store_stats(db, test_store.id)
# New flat structure compatible with StoreDashboardStatsResponse
assert "total_products" in stats
assert "active_products" in stats
assert "total_orders" in stats
assert "total_customers" in stats
assert "total_revenue" in stats
assert "total_inventory_quantity" in stats
assert stats["total_products"] >= 0
assert stats["total_inventory_quantity"] >= 0
def test_get_store_stats_store_not_found(self, db):
"""Test store stats with non-existent store."""
with pytest.raises(StoreNotFoundException):
self.service.get_store_stats(db, 99999)
def test_get_store_stats_with_inventory(
self, db, test_store, test_product, test_inventory
):
"""Test store stats includes inventory data."""
stats = self.service.get_store_stats(db, test_store.id)
assert stats["total_inventory_quantity"] >= test_inventory.quantity
assert stats["reserved_inventory_quantity"] >= 0
def test_get_store_stats_database_error(self, db, test_store):
"""Test store stats handles database errors after store check."""
# Mock query to fail after first successful call (store check)
original_query = db.query
call_count = [0]
def mock_query(*args, **kwargs):
call_count[0] += 1
if call_count[0] > 1:
raise SQLAlchemyError("DB Error")
return original_query(*args, **kwargs)
with patch.object(db, "query", side_effect=mock_query):
with pytest.raises(AdminOperationException) as exc_info:
self.service.get_store_stats(db, test_store.id)
assert exc_info.value.details.get("operation") == "get_store_stats"
# ==================== get_store_analytics Tests ====================
def test_get_store_analytics_success(self, db, test_store):
"""Test getting store analytics successfully."""
analytics = self.service.get_store_analytics(db, test_store.id)
assert "period" in analytics
assert "start_date" in analytics
assert "imports" in analytics
assert "catalog" in analytics
assert "inventory" in analytics
def test_get_store_analytics_different_periods(self, db, test_store):
"""Test store analytics with different time periods."""
for period in ["7d", "30d", "90d", "1y"]:
analytics = self.service.get_store_analytics(
db, test_store.id, period=period
)
assert analytics["period"] == period
def test_get_store_analytics_store_not_found(self, db):
"""Test store analytics with non-existent store."""
with pytest.raises(StoreNotFoundException):
self.service.get_store_analytics(db, 99999)
# ==================== get_store_statistics Tests ====================
def test_get_store_statistics_success(self, db, test_store):
"""Test getting store statistics for admin dashboard."""
stats = self.service.get_store_statistics(db)
assert "total" in stats
assert "verified" in stats
assert "pending" in stats
assert "inactive" in stats
assert "verification_rate" in stats
assert stats["total"] >= 1
def test_get_store_statistics_calculates_rates(self, db, test_store):
"""Test store statistics calculates rates correctly."""
stats = self.service.get_store_statistics(db)
if stats["total"] > 0:
expected_rate = stats["verified"] / stats["total"] * 100
assert abs(stats["verification_rate"] - expected_rate) < 0.01
def test_get_store_statistics_database_error(self, db):
"""Test store statistics handles database errors."""
with patch.object(db, "query", side_effect=SQLAlchemyError("DB Error")):
with pytest.raises(AdminOperationException) as exc_info:
self.service.get_store_statistics(db)
assert exc_info.value.details.get("operation") == "get_store_statistics"
# ==================== get_user_statistics Tests ====================
def test_get_user_statistics_success(self, db, test_user):
"""Test getting user statistics."""
stats = self.service.get_user_statistics(db)
assert "total_users" in stats
assert "active_users" in stats
assert "inactive_users" in stats
assert "admin_users" in stats
assert "activation_rate" in stats
assert stats["total_users"] >= 1
def test_get_user_statistics_calculates_correctly(self, db, test_user, test_admin):
"""Test user statistics calculates values correctly."""
stats = self.service.get_user_statistics(db)
assert stats["total_users"] == stats["active_users"] + stats["inactive_users"]
assert stats["admin_users"] >= 1 # test_admin
def test_get_user_statistics_database_error(self, db):
"""Test user statistics handles database errors."""
with patch.object(db, "query", side_effect=SQLAlchemyError("DB Error")):
with pytest.raises(AdminOperationException) as exc_info:
self.service.get_user_statistics(db)
assert exc_info.value.details.get("operation") == "get_user_statistics"
# ==================== get_import_statistics Tests ====================
def test_get_import_statistics_success(self, db):
"""Test getting import statistics."""
stats = self.service.get_import_statistics(db)
assert "total" in stats
assert "completed" in stats
assert "failed" in stats
assert "success_rate" in stats
def test_get_import_statistics_with_jobs(
self, db, test_store, test_marketplace_import_job
):
"""Test import statistics with existing jobs."""
stats = self.service.get_import_statistics(db)
assert stats["total"] >= 1
assert stats["completed"] >= 1 # test job has completed status
def test_get_import_statistics_calculates_rate(self, db):
"""Test import statistics calculates success rate."""
stats = self.service.get_import_statistics(db)
if stats["total"] > 0:
expected_rate = stats["completed"] / stats["total"] * 100
assert abs(stats["success_rate"] - expected_rate) < 0.01
else:
assert stats["success_rate"] == 0
def test_get_import_statistics_handles_errors(self, db):
"""Test import statistics returns zeros on error."""
with patch.object(db, "query", side_effect=SQLAlchemyError("DB Error")):
stats = self.service.get_import_statistics(db)
# Should return default values, not raise exception
assert stats["total"] == 0
assert stats["completed"] == 0
assert stats["failed"] == 0
assert stats["success_rate"] == 0
# ==================== Private Helper Method Tests ====================
def test_parse_period_known_values(self):
"""Test period parsing for known values."""
assert self.service._parse_period("7d") == 7
assert self.service._parse_period("30d") == 30
assert self.service._parse_period("90d") == 90
assert self.service._parse_period("1y") == 365
def test_parse_period_unknown_defaults(self):
"""Test period parsing defaults to 30 for unknown values."""
assert self.service._parse_period("unknown") == 30
assert self.service._parse_period("") == 30
def test_get_unique_brands_count(self, db, test_marketplace_product):
"""Test getting unique brands count."""
unique_id = str(uuid.uuid4())[:8]
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"BRAND001_{unique_id}",
title="Brand MarketplaceProduct 1",
brand="BrandA",
marketplace="Test",
store_name="TestStore",
price="10.00",
currency="EUR",
)
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"BRAND002_{unique_id}",
title="Brand MarketplaceProduct 2",
brand="BrandB",
marketplace="Test",
store_name="TestStore",
price="15.00",
currency="EUR",
)
db.commit()
count = self.service._get_unique_brands_count(db)
assert count >= 2 # At least BrandA and BrandB
assert isinstance(count, int)
def test_get_unique_categories_count(self, db, test_marketplace_product):
"""Test getting unique categories count."""
unique_id = str(uuid.uuid4())[:8]
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"CAT001_{unique_id}",
title="Category MarketplaceProduct 1",
google_product_category="Electronics",
marketplace="Test",
store_name="TestStore",
price="10.00",
currency="EUR",
)
create_marketplace_product_with_translation(
db,
marketplace_product_id=f"CAT002_{unique_id}",
title="Category MarketplaceProduct 2",
google_product_category="Books",
marketplace="Test",
store_name="TestStore",
price="15.00",
currency="EUR",
)
db.commit()
count = self.service._get_unique_categories_count(db)
assert count >= 2 # At least Electronics and Books
assert isinstance(count, int)
def test_get_inventory_statistics(self, db, test_inventory):
"""Test getting inventory statistics."""
unique_id = str(uuid.uuid4())[:8]
additional_inventory = Inventory(
gtin=f"123456789{unique_id[:4]}",
warehouse="strassen",
bin_location=f"ST-{unique_id[:2]}-01",
quantity=25,
reserved_quantity=5,
store_id=test_inventory.store_id,
product_id=test_inventory.product_id,
)
db.add(additional_inventory)
db.commit()
stats = self.service._get_inventory_statistics(db)
assert "total_entries" in stats
assert "total_quantity" in stats
assert "total_reserved" in stats
assert "total_available" in stats
assert stats["total_entries"] >= 2
assert stats["total_quantity"] >= test_inventory.quantity + 25
def test_get_inventory_statistics_empty(self, db):
"""Test inventory statistics with empty database."""
stats = self.service._get_inventory_statistics(db)
assert stats["total_entries"] == 0
assert stats["total_quantity"] == 0
assert stats["total_reserved"] == 0
assert stats["total_available"] == 0