Tests restructuring
This commit is contained in:
3
tests/unit/__init__.py
Normal file
3
tests/unit/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# tests/unit/__init__.py
|
||||
"""Unit tests - fast, isolated component tests."""
|
||||
|
||||
6
tests/unit/conftest.py
Normal file
6
tests/unit/conftest.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# tests/unit/conftest.py
|
||||
"""Unit test specific fixtures."""
|
||||
import pytest
|
||||
|
||||
# Add any unit-specific fixtures here if needed
|
||||
|
||||
3
tests/unit/models/__init__.py
Normal file
3
tests/unit/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# tests/unit/models/__init__.py
|
||||
"""Database and API model unit tests."""
|
||||
|
||||
96
tests/unit/models/test_database_models.py
Normal file
96
tests/unit/models/test_database_models.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# tests/unit/models/test_database_models.py
|
||||
import pytest
|
||||
|
||||
from models.database_models import Product, Shop, Stock, User
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestDatabaseModels:
|
||||
def test_user_model(self, db):
|
||||
"""Test User model creation and relationships"""
|
||||
user = User(
|
||||
email="db_test@example.com",
|
||||
username="dbtest",
|
||||
hashed_password="hashed_password_123",
|
||||
role="user",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
assert user.id is not None
|
||||
assert user.email == "db_test@example.com"
|
||||
assert user.created_at is not None
|
||||
assert user.updated_at is not None
|
||||
|
||||
def test_product_model(self, db):
|
||||
"""Test Product model creation"""
|
||||
product = Product(
|
||||
product_id="DB_TEST_001",
|
||||
title="Database Test Product",
|
||||
description="Testing product model",
|
||||
price="25.99",
|
||||
currency="USD",
|
||||
brand="DBTest",
|
||||
gtin="1234567890123",
|
||||
availability="in stock",
|
||||
marketplace="TestDB",
|
||||
shop_name="DBTestShop",
|
||||
)
|
||||
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.id is not None
|
||||
assert product.product_id == "DB_TEST_001"
|
||||
assert product.created_at is not None
|
||||
|
||||
def test_stock_model(self, db):
|
||||
"""Test Stock model creation"""
|
||||
stock = Stock(gtin="1234567890123", location="DB_WAREHOUSE", quantity=150)
|
||||
|
||||
db.add(stock)
|
||||
db.commit()
|
||||
db.refresh(stock)
|
||||
|
||||
assert stock.id is not None
|
||||
assert stock.gtin == "1234567890123"
|
||||
assert stock.location == "DB_WAREHOUSE"
|
||||
assert stock.quantity == 150
|
||||
|
||||
def test_shop_model_with_owner(self, db, test_user):
|
||||
"""Test Shop model with owner relationship"""
|
||||
shop = Shop(
|
||||
shop_code="DBTEST",
|
||||
shop_name="Database Test Shop",
|
||||
description="Testing shop model",
|
||||
owner_id=test_user.id,
|
||||
is_active=True,
|
||||
is_verified=False,
|
||||
)
|
||||
|
||||
db.add(shop)
|
||||
db.commit()
|
||||
db.refresh(shop)
|
||||
|
||||
assert shop.id is not None
|
||||
assert shop.shop_code == "DBTEST"
|
||||
assert shop.owner_id == test_user.id
|
||||
assert shop.owner.username == test_user.username
|
||||
|
||||
def test_database_constraints(self, db):
|
||||
"""Test database constraints and unique indexes"""
|
||||
# Test unique product_id constraint
|
||||
product1 = Product(product_id="UNIQUE_001", title="Product 1")
|
||||
db.add(product1)
|
||||
db.commit()
|
||||
|
||||
# This should raise an integrity error
|
||||
with pytest.raises(Exception): # Could be IntegrityError or similar
|
||||
product2 = Product(product_id="UNIQUE_001", title="Product 2")
|
||||
db.add(product2)
|
||||
db.commit()
|
||||
3
tests/unit/services/__init__.py
Normal file
3
tests/unit/services/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# tests/unit/services/__init__.py
|
||||
"""Service layer unit tests."""
|
||||
|
||||
116
tests/unit/services/test_admin_service.py
Normal file
116
tests/unit/services/test_admin_service.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# tests/unit/services/test_admin_service.py
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.services.admin_service import AdminService
|
||||
from models.database_models import MarketplaceImportJob, Shop
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.admin
|
||||
class TestAdminService:
|
||||
"""Test suite for AdminService following the application's testing patterns"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup method following the same pattern as product service tests"""
|
||||
self.service = AdminService()
|
||||
|
||||
def test_get_all_users(self, db, test_user, test_admin):
|
||||
"""Test getting all users with pagination"""
|
||||
users = self.service.get_all_users(db, skip=0, limit=10)
|
||||
|
||||
assert len(users) >= 2 # test_user + test_admin
|
||||
user_ids = [user.id for user in users]
|
||||
assert test_user.id in user_ids
|
||||
assert test_admin.id in user_ids
|
||||
|
||||
def test_get_all_users_with_pagination(self, db, test_user, test_admin):
|
||||
"""Test user pagination works correctly"""
|
||||
users = self.service.get_all_users(db, skip=0, limit=1)
|
||||
|
||||
assert len(users) == 1
|
||||
|
||||
users_second_page = self.service.get_all_users(db, skip=1, limit=1)
|
||||
assert len(users_second_page) == 1
|
||||
assert users[0].id != users_second_page[0].id
|
||||
|
||||
def test_toggle_user_status_deactivate(self, db, test_user, test_admin):
|
||||
"""Test deactivating a user"""
|
||||
assert test_user.is_active is True
|
||||
|
||||
user, message = self.service.toggle_user_status(db, test_user.id, test_admin.id)
|
||||
|
||||
assert user.id == test_user.id
|
||||
assert user.is_active is False
|
||||
assert f"{user.username} has been deactivated" in message
|
||||
|
||||
def test_toggle_user_status_activate(self, db, test_user, test_admin):
|
||||
"""Test activating a user"""
|
||||
# First deactivate the user
|
||||
test_user.is_active = False
|
||||
db.commit()
|
||||
|
||||
user, message = self.service.toggle_user_status(db, test_user.id, test_admin.id)
|
||||
|
||||
assert user.id == test_user.id
|
||||
assert user.is_active is True
|
||||
assert f"{user.username} has been activated" in message
|
||||
|
||||
def test_toggle_user_status_user_not_found(self, db, test_admin):
|
||||
"""Test toggle user status when user not found"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.toggle_user_status(db, 99999, test_admin.id)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "User not found" in str(exc_info.value.detail)
|
||||
|
||||
def test_toggle_user_status_cannot_deactivate_self(self, db, test_admin):
|
||||
"""Test that admin cannot deactivate their own account"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.toggle_user_status(db, test_admin.id, test_admin.id)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Cannot deactivate your own account" in str(exc_info.value.detail)
|
||||
|
||||
def test_get_all_shops(self, db, test_shop):
|
||||
"""Test getting all shops with total count"""
|
||||
shops, total = self.service.get_all_shops(db, skip=0, limit=10)
|
||||
|
||||
assert total >= 1
|
||||
assert len(shops) >= 1
|
||||
shop_codes = [shop.shop_code for shop in shops]
|
||||
assert test_shop.shop_code in shop_codes
|
||||
|
||||
def test_verify_shop_mark_verified(self, db, test_shop):
|
||||
"""Test marking shop as verified"""
|
||||
# Ensure shop starts unverified
|
||||
test_shop.is_verified = False
|
||||
db.commit()
|
||||
|
||||
shop, message = self.service.verify_shop(db, test_shop.id)
|
||||
|
||||
assert shop.id == test_shop.id
|
||||
assert shop.is_verified is True
|
||||
assert f"{shop.shop_code} has been verified" in message
|
||||
|
||||
def test_verify_shop_not_found(self, db):
|
||||
"""Test verify shop when shop not found"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.verify_shop(db, 99999)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "Shop not found" in str(exc_info.value.detail)
|
||||
|
||||
def test_get_marketplace_import_jobs_no_filters(self, db, test_marketplace_job):
|
||||
"""Test getting marketplace import jobs without filters using fixture"""
|
||||
result = self.service.get_marketplace_import_jobs(db, skip=0, limit=10)
|
||||
|
||||
assert len(result) >= 1
|
||||
# Find our test job in the results
|
||||
test_job = next(
|
||||
(job for job in result if job.job_id == test_marketplace_job.id), None
|
||||
)
|
||||
assert test_job is not None
|
||||
assert test_job.marketplace == test_marketplace_job.marketplace
|
||||
assert test_job.shop_name == test_marketplace_job.shop_name
|
||||
assert test_job.status == test_marketplace_job.status
|
||||
213
tests/unit/services/test_auth_service.py
Normal file
213
tests/unit/services/test_auth_service.py
Normal file
@@ -0,0 +1,213 @@
|
||||
# tests/test_auth_service.py
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.services.auth_service import AuthService
|
||||
from models.api_models import UserLogin, UserRegister
|
||||
from models.database_models import User
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.auth
|
||||
class TestAuthService:
|
||||
"""Test suite for AuthService following the application's testing patterns"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup method following the same pattern as admin service tests"""
|
||||
self.service = AuthService()
|
||||
|
||||
def test_register_user_success(self, db):
|
||||
"""Test successful user registration"""
|
||||
user_data = UserRegister(
|
||||
email="newuser@example.com", username="newuser123", password="securepass123"
|
||||
)
|
||||
|
||||
user = self.service.register_user(db, user_data)
|
||||
|
||||
assert user is not None
|
||||
assert user.email == "newuser@example.com"
|
||||
assert user.username == "newuser123"
|
||||
assert user.role == "user"
|
||||
assert user.is_active is True
|
||||
assert user.hashed_password != "securepass123" # Should be hashed
|
||||
|
||||
def test_register_user_email_already_exists(self, db, test_user):
|
||||
"""Test registration fails when email already exists"""
|
||||
user_data = UserRegister(
|
||||
email=test_user.email, # Use existing email
|
||||
username="differentuser",
|
||||
password="securepass123",
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.register_user(db, user_data)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Email already registered" in str(exc_info.value.detail)
|
||||
|
||||
def test_register_user_username_already_exists(self, db, test_user):
|
||||
"""Test registration fails when username already exists"""
|
||||
user_data = UserRegister(
|
||||
email="different@example.com",
|
||||
username=test_user.username, # Use existing username
|
||||
password="securepass123",
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.register_user(db, user_data)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Username already taken" in str(exc_info.value.detail)
|
||||
|
||||
def test_login_user_success(self, db, test_user):
|
||||
"""Test successful user login"""
|
||||
user_credentials = UserLogin(
|
||||
username=test_user.username, password="testpass123"
|
||||
)
|
||||
|
||||
result = self.service.login_user(db, user_credentials)
|
||||
|
||||
assert "token_data" in result
|
||||
assert "user" in result
|
||||
assert result["user"].id == test_user.id
|
||||
assert result["user"].username == test_user.username
|
||||
assert "access_token" in result["token_data"]
|
||||
assert "token_type" in result["token_data"]
|
||||
assert "expires_in" in result["token_data"]
|
||||
|
||||
def test_login_user_wrong_username(self, db):
|
||||
"""Test login fails with wrong username"""
|
||||
user_credentials = UserLogin(username="nonexistentuser", password="testpass123")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.login_user(db, user_credentials)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Incorrect username or password" in str(exc_info.value.detail)
|
||||
|
||||
def test_login_user_wrong_password(self, db, test_user):
|
||||
"""Test login fails with wrong password"""
|
||||
user_credentials = UserLogin(
|
||||
username=test_user.username, password="wrongpassword"
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.login_user(db, user_credentials)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Incorrect username or password" in str(exc_info.value.detail)
|
||||
|
||||
def test_login_user_inactive_user(self, db, test_user):
|
||||
"""Test login fails for inactive user"""
|
||||
# Deactivate user
|
||||
test_user.is_active = False
|
||||
db.commit()
|
||||
|
||||
user_credentials = UserLogin(
|
||||
username=test_user.username, password="testpass123"
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.login_user(db, user_credentials)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Incorrect username or password" in str(exc_info.value.detail)
|
||||
|
||||
def test_get_user_by_email(self, db, test_user):
|
||||
"""Test getting user by email"""
|
||||
user = self.service.get_user_by_email(db, test_user.email)
|
||||
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
assert user.email == test_user.email
|
||||
|
||||
def test_get_user_by_email_not_found(self, db):
|
||||
"""Test getting user by email when user doesn't exist"""
|
||||
user = self.service.get_user_by_email(db, "nonexistent@example.com")
|
||||
|
||||
assert user is None
|
||||
|
||||
def test_get_user_by_username(self, db, test_user):
|
||||
"""Test getting user by username"""
|
||||
user = self.service.get_user_by_username(db, test_user.username)
|
||||
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
assert user.username == test_user.username
|
||||
|
||||
def test_get_user_by_username_not_found(self, db):
|
||||
"""Test getting user by username when user doesn't exist"""
|
||||
user = self.service.get_user_by_username(db, "nonexistentuser")
|
||||
|
||||
assert user is None
|
||||
|
||||
def test_email_exists_true(self, db, test_user):
|
||||
"""Test email_exists returns True when email exists"""
|
||||
exists = self.service.email_exists(db, test_user.email)
|
||||
|
||||
assert exists is True
|
||||
|
||||
def test_email_exists_false(self, db):
|
||||
"""Test email_exists returns False when email doesn't exist"""
|
||||
exists = self.service.email_exists(db, "nonexistent@example.com")
|
||||
|
||||
assert exists is False
|
||||
|
||||
def test_username_exists_true(self, db, test_user):
|
||||
"""Test username_exists returns True when username exists"""
|
||||
exists = self.service.username_exists(db, test_user.username)
|
||||
|
||||
assert exists is True
|
||||
|
||||
def test_username_exists_false(self, db):
|
||||
"""Test username_exists returns False when username doesn't exist"""
|
||||
exists = self.service.username_exists(db, "nonexistentuser")
|
||||
|
||||
assert exists is False
|
||||
|
||||
def test_authenticate_user_success(self, db, test_user):
|
||||
"""Test successful user authentication"""
|
||||
user = self.service.authenticate_user(db, test_user.username, "testpass123")
|
||||
|
||||
assert user is not None
|
||||
assert user.id == test_user.id
|
||||
assert user.username == test_user.username
|
||||
|
||||
def test_authenticate_user_wrong_password(self, db, test_user):
|
||||
"""Test authentication fails with wrong password"""
|
||||
user = self.service.authenticate_user(db, test_user.username, "wrongpassword")
|
||||
|
||||
assert user is None
|
||||
|
||||
def test_authenticate_user_nonexistent(self, db):
|
||||
"""Test authentication fails with nonexistent user"""
|
||||
user = self.service.authenticate_user(db, "nonexistentuser", "password")
|
||||
|
||||
assert user is None
|
||||
|
||||
def test_create_access_token(self, test_user):
|
||||
"""Test creating access token for user"""
|
||||
token_data = self.service.create_access_token(test_user)
|
||||
|
||||
assert "access_token" in token_data
|
||||
assert "token_type" in token_data
|
||||
assert "expires_in" in token_data
|
||||
assert token_data["token_type"] == "bearer"
|
||||
assert isinstance(token_data["expires_in"], int)
|
||||
assert token_data["expires_in"] > 0
|
||||
|
||||
def test_hash_password(self):
|
||||
"""Test password hashing"""
|
||||
password = "testpassword123"
|
||||
hashed = self.service.hash_password(password)
|
||||
|
||||
assert hashed != password
|
||||
assert len(hashed) > len(password)
|
||||
assert hashed.startswith("$") # bcrypt hash format
|
||||
|
||||
def test_hash_password_different_results(self):
|
||||
"""Test that hashing same password produces different hashes (salt)"""
|
||||
password = "testpassword123"
|
||||
hash1 = self.service.hash_password(password)
|
||||
hash2 = self.service.hash_password(password)
|
||||
|
||||
assert hash1 != hash2 # Should be different due to salt
|
||||
311
tests/unit/services/test_marketplace_service.py
Normal file
311
tests/unit/services/test_marketplace_service.py
Normal file
@@ -0,0 +1,311 @@
|
||||
# tests/test_marketplace_service.py
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.marketplace_service import MarketplaceService
|
||||
from models.api_models import MarketplaceImportRequest
|
||||
from models.database_models import MarketplaceImportJob, Shop, User
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.marketplace
|
||||
class TestMarketplaceService:
|
||||
def setup_method(self):
|
||||
self.service = MarketplaceService()
|
||||
|
||||
def test_validate_shop_access_success(self, db, test_shop, test_user):
|
||||
"""Test successful shop access validation"""
|
||||
# Set the shop owner to the test user
|
||||
test_shop.owner_id = test_user.id
|
||||
db.commit()
|
||||
|
||||
result = self.service.validate_shop_access(db, test_shop.shop_code, test_user)
|
||||
|
||||
assert result.shop_code == test_shop.shop_code
|
||||
assert result.owner_id == test_user.id
|
||||
|
||||
def test_validate_shop_access_admin_can_access_any_shop(
|
||||
self, db, test_shop, test_admin
|
||||
):
|
||||
"""Test that admin users can access any shop"""
|
||||
result = self.service.validate_shop_access(db, test_shop.shop_code, test_admin)
|
||||
|
||||
assert result.shop_code == test_shop.shop_code
|
||||
|
||||
def test_validate_shop_access_shop_not_found(self, db, test_user):
|
||||
"""Test shop access validation when shop doesn't exist"""
|
||||
with pytest.raises(ValueError, match="Shop not found"):
|
||||
self.service.validate_shop_access(db, "NONEXISTENT", test_user)
|
||||
|
||||
def test_validate_shop_access_permission_denied(
|
||||
self, db, test_shop, test_user, other_user
|
||||
):
|
||||
"""Test shop access validation when user doesn't own the shop"""
|
||||
# Set the shop owner to a different user
|
||||
test_shop.owner_id = other_user.id
|
||||
db.commit()
|
||||
|
||||
with pytest.raises(PermissionError, match="Access denied to this shop"):
|
||||
self.service.validate_shop_access(db, test_shop.shop_code, test_user)
|
||||
|
||||
def test_create_import_job_success(self, db, test_shop, test_user):
|
||||
"""Test successful creation of import job"""
|
||||
# Set the shop owner to the test user
|
||||
test_shop.owner_id = test_user.id
|
||||
db.commit()
|
||||
|
||||
request = MarketplaceImportRequest(
|
||||
url="https://example.com/products.csv",
|
||||
marketplace="Amazon",
|
||||
shop_code=test_shop.shop_code,
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
result = self.service.create_import_job(db, request, test_user)
|
||||
|
||||
assert result.marketplace == "Amazon"
|
||||
# Check the correct field based on your model
|
||||
assert result.shop_id == test_shop.id # Changed from shop_code to shop_id
|
||||
assert result.user_id == test_user.id if hasattr(result, "user_id") else True
|
||||
assert result.status == "pending"
|
||||
assert result.source_url == "https://example.com/products.csv"
|
||||
|
||||
def test_create_import_job_invalid_shop(self, db, test_user):
|
||||
"""Test import job creation with invalid shop"""
|
||||
request = MarketplaceImportRequest(
|
||||
url="https://example.com/products.csv",
|
||||
marketplace="Amazon",
|
||||
shop_code="INVALID_SHOP",
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Shop not found"):
|
||||
self.service.create_import_job(db, request, test_user)
|
||||
|
||||
def test_get_import_job_by_id_success(self, db, test_marketplace_job, test_user):
|
||||
"""Test getting import job by ID for job owner"""
|
||||
result = self.service.get_import_job_by_id(
|
||||
db, test_marketplace_job.id, test_user
|
||||
)
|
||||
|
||||
assert result.id == test_marketplace_job.id
|
||||
# Check user_id if the field exists
|
||||
if hasattr(result, "user_id"):
|
||||
assert result.user_id == test_user.id
|
||||
|
||||
def test_get_import_job_by_id_admin_access(
|
||||
self, db, test_marketplace_job, test_admin
|
||||
):
|
||||
"""Test that admin can access any import job"""
|
||||
result = self.service.get_import_job_by_id(
|
||||
db, test_marketplace_job.id, test_admin
|
||||
)
|
||||
|
||||
assert result.id == test_marketplace_job.id
|
||||
|
||||
def test_get_import_job_by_id_not_found(self, db, test_user):
|
||||
"""Test getting non-existent import job"""
|
||||
with pytest.raises(ValueError, match="Marketplace import job not found"):
|
||||
self.service.get_import_job_by_id(db, 99999, test_user)
|
||||
|
||||
def test_get_import_job_by_id_access_denied(
|
||||
self, db, test_marketplace_job, other_user
|
||||
):
|
||||
"""Test access denied when user doesn't own the job"""
|
||||
with pytest.raises(PermissionError, match="Access denied to this import job"):
|
||||
self.service.get_import_job_by_id(db, test_marketplace_job.id, other_user)
|
||||
|
||||
def test_get_import_jobs_user_filter(self, db, test_marketplace_job, test_user):
|
||||
"""Test getting import jobs filtered by user"""
|
||||
jobs = self.service.get_import_jobs(db, test_user)
|
||||
|
||||
assert len(jobs) >= 1
|
||||
assert any(job.id == test_marketplace_job.id for job in jobs)
|
||||
# Check user_id if the field exists
|
||||
if hasattr(test_marketplace_job, "user_id"):
|
||||
assert test_marketplace_job.user_id == test_user.id
|
||||
|
||||
def test_get_import_jobs_admin_sees_all(self, db, test_marketplace_job, test_admin):
|
||||
"""Test that admin sees all import jobs"""
|
||||
jobs = self.service.get_import_jobs(db, test_admin)
|
||||
|
||||
assert len(jobs) >= 1
|
||||
assert any(job.id == test_marketplace_job.id for job in jobs)
|
||||
|
||||
def test_get_import_jobs_with_marketplace_filter(
|
||||
self, db, test_marketplace_job, test_user
|
||||
):
|
||||
"""Test getting import jobs with marketplace filter"""
|
||||
jobs = self.service.get_import_jobs(
|
||||
db, test_user, marketplace=test_marketplace_job.marketplace
|
||||
)
|
||||
|
||||
assert len(jobs) >= 1
|
||||
assert any(job.marketplace == test_marketplace_job.marketplace for job in jobs)
|
||||
|
||||
def test_get_import_jobs_with_pagination(self, db, test_user, test_shop):
|
||||
"""Test getting import jobs with pagination"""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create multiple import jobs
|
||||
for i in range(5):
|
||||
job = MarketplaceImportJob(
|
||||
status="completed",
|
||||
marketplace=f"Marketplace_{unique_id}_{i}",
|
||||
shop_name=f"Test_Shop_{unique_id}_{i}",
|
||||
user_id=test_user.id,
|
||||
shop_id=test_shop.id, # Use shop_id instead of shop_code
|
||||
source_url=f"https://test-{i}.example.com/import",
|
||||
imported_count=0,
|
||||
updated_count=0,
|
||||
total_processed=0,
|
||||
error_count=0,
|
||||
)
|
||||
db.add(job)
|
||||
db.commit()
|
||||
|
||||
jobs = self.service.get_import_jobs(db, test_user, skip=2, limit=2)
|
||||
|
||||
assert len(jobs) <= 2 # Should be at most 2
|
||||
|
||||
def test_update_job_status_success(self, db, test_marketplace_job):
|
||||
"""Test updating job status"""
|
||||
result = self.service.update_job_status(
|
||||
db,
|
||||
test_marketplace_job.id,
|
||||
"completed",
|
||||
imported_count=100,
|
||||
total_processed=100,
|
||||
)
|
||||
|
||||
assert result.status == "completed"
|
||||
assert result.imported_count == 100
|
||||
assert result.total_processed == 100
|
||||
|
||||
def test_update_job_status_not_found(self, db):
|
||||
"""Test updating non-existent job status"""
|
||||
with pytest.raises(ValueError, match="Marketplace import job not found"):
|
||||
self.service.update_job_status(db, 99999, "completed")
|
||||
|
||||
def test_get_job_stats_user(self, db, test_marketplace_job, test_user):
|
||||
"""Test getting job statistics for user"""
|
||||
stats = self.service.get_job_stats(db, test_user)
|
||||
|
||||
assert stats["total_jobs"] >= 1
|
||||
assert "pending_jobs" in stats
|
||||
assert "running_jobs" in stats
|
||||
assert "completed_jobs" in stats
|
||||
assert "failed_jobs" in stats
|
||||
|
||||
def test_get_job_stats_admin(self, db, test_marketplace_job, test_admin):
|
||||
"""Test getting job statistics for admin"""
|
||||
stats = self.service.get_job_stats(db, test_admin)
|
||||
|
||||
assert stats["total_jobs"] >= 1
|
||||
|
||||
def test_convert_to_response_model(self, test_marketplace_job):
|
||||
"""Test converting database model to response model"""
|
||||
response = self.service.convert_to_response_model(test_marketplace_job)
|
||||
|
||||
assert response.job_id == test_marketplace_job.id
|
||||
assert response.status == test_marketplace_job.status
|
||||
assert response.marketplace == test_marketplace_job.marketplace
|
||||
assert response.imported == (test_marketplace_job.imported_count or 0)
|
||||
|
||||
def test_cancel_import_job_success(self, db, test_user, test_shop):
|
||||
"""Test cancelling a pending import job"""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create a pending job
|
||||
job = MarketplaceImportJob(
|
||||
status="pending",
|
||||
marketplace="Amazon",
|
||||
shop_name=f"TEST_SHOP_{unique_id}",
|
||||
user_id=test_user.id,
|
||||
shop_id=test_shop.id, # Use shop_id instead of shop_code
|
||||
source_url="https://test.example.com/import",
|
||||
imported_count=0,
|
||||
updated_count=0,
|
||||
total_processed=0,
|
||||
error_count=0,
|
||||
)
|
||||
db.add(job)
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
|
||||
result = self.service.cancel_import_job(db, job.id, test_user)
|
||||
|
||||
assert result.status == "cancelled"
|
||||
assert result.completed_at is not None
|
||||
|
||||
def test_cancel_import_job_invalid_status(
|
||||
self, db, test_marketplace_job, test_user
|
||||
):
|
||||
"""Test cancelling a job that can't be cancelled"""
|
||||
# Set job status to completed
|
||||
test_marketplace_job.status = "completed"
|
||||
db.commit()
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="Cannot cancel job with status: completed"
|
||||
):
|
||||
self.service.cancel_import_job(db, test_marketplace_job.id, test_user)
|
||||
|
||||
def test_delete_import_job_success(self, db, test_user, test_shop):
|
||||
"""Test deleting a completed import job"""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create a completed job
|
||||
job = MarketplaceImportJob(
|
||||
status="completed",
|
||||
marketplace="Amazon",
|
||||
shop_name=f"TEST_SHOP_{unique_id}",
|
||||
user_id=test_user.id,
|
||||
shop_id=test_shop.id, # Use shop_id instead of shop_code
|
||||
source_url="https://test.example.com/import",
|
||||
imported_count=0,
|
||||
updated_count=0,
|
||||
total_processed=0,
|
||||
error_count=0,
|
||||
)
|
||||
db.add(job)
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
job_id = job.id
|
||||
|
||||
result = self.service.delete_import_job(db, job_id, test_user)
|
||||
|
||||
assert result is True
|
||||
|
||||
# Verify the job is actually deleted
|
||||
deleted_job = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.filter(MarketplaceImportJob.id == job_id)
|
||||
.first()
|
||||
)
|
||||
assert deleted_job is None
|
||||
|
||||
def test_delete_import_job_invalid_status(self, db, test_user, test_shop):
|
||||
"""Test deleting a job that can't be deleted"""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create a pending job
|
||||
job = MarketplaceImportJob(
|
||||
status="pending",
|
||||
marketplace="Amazon",
|
||||
shop_name=f"TEST_SHOP_{unique_id}",
|
||||
user_id=test_user.id,
|
||||
shop_id=test_shop.id, # Use shop_id instead of shop_code
|
||||
source_url="https://test.example.com/import",
|
||||
imported_count=0,
|
||||
updated_count=0,
|
||||
total_processed=0,
|
||||
error_count=0,
|
||||
)
|
||||
db.add(job)
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
|
||||
with pytest.raises(ValueError, match="Cannot delete job with status: pending"):
|
||||
self.service.delete_import_job(db, job.id, test_user)
|
||||
57
tests/unit/services/test_product_service.py
Normal file
57
tests/unit/services/test_product_service.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# tests/test_product_service.py
|
||||
import pytest
|
||||
|
||||
from app.services.product_service import ProductService
|
||||
from models.api_models import ProductCreate
|
||||
from models.database_models import Product
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.products
|
||||
class TestProductService:
|
||||
def setup_method(self):
|
||||
self.service = ProductService()
|
||||
|
||||
def test_create_product_with_gtin_validation(self, db):
|
||||
"""Test product creation with GTIN validation"""
|
||||
product_data = ProductCreate(
|
||||
product_id="SVC001",
|
||||
title="Service Test Product",
|
||||
gtin="1234567890123",
|
||||
price="19.99",
|
||||
marketplace="TestMarket",
|
||||
)
|
||||
|
||||
product = self.service.create_product(db, product_data)
|
||||
|
||||
assert product.product_id == "SVC001"
|
||||
assert product.gtin == "1234567890123"
|
||||
assert product.marketplace == "TestMarket"
|
||||
|
||||
def test_create_product_invalid_gtin(self, db):
|
||||
"""Test product creation with invalid GTIN"""
|
||||
product_data = ProductCreate(
|
||||
product_id="SVC002",
|
||||
title="Service Test Product",
|
||||
gtin="invalid_gtin",
|
||||
price="19.99",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
self.service.create_product(db, product_data)
|
||||
|
||||
def test_get_products_with_filters(self, db, test_product):
|
||||
"""Test getting products with various filters"""
|
||||
products, total = self.service.get_products_with_filters(db, brand="TestBrand")
|
||||
|
||||
assert total == 1
|
||||
assert len(products) == 1
|
||||
assert products[0].brand == "TestBrand"
|
||||
|
||||
def test_get_products_with_search(self, db, test_product):
|
||||
"""Test getting products with search"""
|
||||
products, total = self.service.get_products_with_filters(
|
||||
db, search="Test Product"
|
||||
)
|
||||
|
||||
assert total == 1
|
||||
assert len(products) == 1
|
||||
242
tests/unit/services/test_shop_service.py
Normal file
242
tests/unit/services/test_shop_service.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# tests/test_shop_service.py (simplified with fixtures)
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.services.shop_service import ShopService
|
||||
from models.api_models import ShopCreate, ShopProductCreate
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.shops
|
||||
class TestShopService:
|
||||
"""Test suite for ShopService following the application's testing patterns"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup method following the same pattern as admin service tests"""
|
||||
self.service = ShopService()
|
||||
|
||||
def test_create_shop_success(self, db, test_user, shop_factory):
|
||||
"""Test successful shop creation"""
|
||||
shop_data = ShopCreate(
|
||||
shop_code="NEWSHOP",
|
||||
shop_name="New Test Shop",
|
||||
description="A new test shop",
|
||||
)
|
||||
|
||||
shop = self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
assert shop is not None
|
||||
assert shop.shop_code == "NEWSHOP"
|
||||
assert shop.owner_id == test_user.id
|
||||
assert shop.is_verified is False # Regular user creates unverified shop
|
||||
|
||||
def test_create_shop_admin_auto_verify(self, db, test_admin, shop_factory):
|
||||
"""Test admin creates verified shop automatically"""
|
||||
shop_data = ShopCreate(shop_code="ADMINSHOP", shop_name="Admin Test Shop")
|
||||
|
||||
shop = self.service.create_shop(db, shop_data, test_admin)
|
||||
|
||||
assert shop.is_verified is True # Admin creates verified shop
|
||||
|
||||
def test_create_shop_duplicate_code(self, db, test_user, test_shop):
|
||||
"""Test shop creation fails with duplicate shop code"""
|
||||
shop_data = ShopCreate(
|
||||
shop_code=test_shop.shop_code, shop_name=test_shop.shop_name
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.create_shop(db, shop_data, test_user)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Shop code already exists" in str(exc_info.value.detail)
|
||||
|
||||
def test_get_shops_regular_user(self, db, test_user, test_shop, inactive_shop):
|
||||
"""Test regular user can only see active verified shops and own shops"""
|
||||
shops, total = self.service.get_shops(db, test_user, skip=0, limit=10)
|
||||
|
||||
shop_codes = [shop.shop_code for shop in shops]
|
||||
assert test_shop.shop_code in shop_codes
|
||||
assert inactive_shop.shop_code not in shop_codes
|
||||
|
||||
def test_get_shops_admin_user(
|
||||
self, db, test_admin, test_shop, inactive_shop, verified_shop
|
||||
):
|
||||
"""Test admin user can see all shops with filters"""
|
||||
shops, total = self.service.get_shops(
|
||||
db, test_admin, active_only=False, verified_only=False
|
||||
)
|
||||
|
||||
shop_codes = [shop.shop_code for shop in shops]
|
||||
assert test_shop.shop_code in shop_codes
|
||||
assert inactive_shop.shop_code in shop_codes
|
||||
assert verified_shop.shop_code in shop_codes
|
||||
|
||||
def test_get_shop_by_code_owner_access(self, db, test_user, test_shop):
|
||||
"""Test shop owner can access their own shop"""
|
||||
shop = self.service.get_shop_by_code(db, test_shop.shop_code.lower(), test_user)
|
||||
|
||||
assert shop is not None
|
||||
assert shop.id == test_shop.id
|
||||
|
||||
def test_get_shop_by_code_admin_access(self, db, test_admin, test_shop):
|
||||
"""Test admin can access any shop"""
|
||||
shop = self.service.get_shop_by_code(
|
||||
db, test_shop.shop_code.lower(), test_admin
|
||||
)
|
||||
|
||||
assert shop is not None
|
||||
assert shop.id == test_shop.id
|
||||
|
||||
def test_get_shop_by_code_not_found(self, db, test_user):
|
||||
"""Test shop not found returns appropriate error"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.get_shop_by_code(db, "NONEXISTENT", test_user)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
def test_get_shop_by_code_access_denied(self, db, test_user, inactive_shop):
|
||||
"""Test regular user cannot access unverified shop they don't own"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.get_shop_by_code(db, inactive_shop.shop_code, test_user)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
def test_add_product_to_shop_success(self, db, test_shop, unique_product):
|
||||
"""Test successfully adding product to shop"""
|
||||
shop_product_data = ShopProductCreate(
|
||||
product_id=unique_product.product_id,
|
||||
price="15.99",
|
||||
is_featured=True,
|
||||
stock_quantity=5,
|
||||
)
|
||||
|
||||
shop_product = self.service.add_product_to_shop(
|
||||
db, test_shop, shop_product_data
|
||||
)
|
||||
|
||||
assert shop_product is not None
|
||||
assert shop_product.shop_id == test_shop.id
|
||||
assert shop_product.product_id == unique_product.id
|
||||
|
||||
def test_add_product_to_shop_product_not_found(self, db, test_shop):
|
||||
"""Test adding non-existent product to shop fails"""
|
||||
shop_product_data = ShopProductCreate(product_id="NONEXISTENT", price="15.99")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.add_product_to_shop(db, test_shop, shop_product_data)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
def test_add_product_to_shop_already_exists(self, db, test_shop, shop_product):
|
||||
"""Test adding product that's already in shop fails"""
|
||||
shop_product_data = ShopProductCreate(
|
||||
product_id=shop_product.product.product_id, price="15.99"
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.add_product_to_shop(db, test_shop, shop_product_data)
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
def test_get_shop_products_owner_access(
|
||||
self, db, test_user, test_shop, shop_product
|
||||
):
|
||||
"""Test shop owner can get shop products"""
|
||||
products, total = self.service.get_shop_products(db, test_shop, test_user)
|
||||
|
||||
assert total >= 1
|
||||
assert len(products) >= 1
|
||||
product_ids = [p.product_id for p in products]
|
||||
assert shop_product.product_id in product_ids
|
||||
|
||||
def test_get_shop_products_access_denied(self, db, test_user, inactive_shop):
|
||||
"""Test non-owner cannot access unverified shop products"""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
self.service.get_shop_products(db, inactive_shop, test_user)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
def test_get_shop_by_id(self, db, test_shop):
|
||||
"""Test getting shop by ID"""
|
||||
shop = self.service.get_shop_by_id(db, test_shop.id)
|
||||
|
||||
assert shop is not None
|
||||
assert shop.id == test_shop.id
|
||||
|
||||
def test_get_shop_by_id_not_found(self, db):
|
||||
"""Test getting shop by ID when shop doesn't exist"""
|
||||
shop = self.service.get_shop_by_id(db, 99999)
|
||||
|
||||
assert shop is None
|
||||
|
||||
def test_shop_code_exists_true(self, db, test_shop):
|
||||
"""Test shop_code_exists returns True when shop code exists"""
|
||||
exists = self.service.shop_code_exists(db, test_shop.shop_code)
|
||||
|
||||
assert exists is True
|
||||
|
||||
def test_shop_code_exists_false(self, db):
|
||||
"""Test shop_code_exists returns False when shop code doesn't exist"""
|
||||
exists = self.service.shop_code_exists(db, "NONEXISTENT")
|
||||
|
||||
assert exists is False
|
||||
|
||||
def test_get_product_by_id(self, db, unique_product):
|
||||
"""Test getting product by product_id"""
|
||||
product = self.service.get_product_by_id(db, unique_product.product_id)
|
||||
|
||||
assert product is not None
|
||||
assert product.id == unique_product.id
|
||||
|
||||
def test_get_product_by_id_not_found(self, db):
|
||||
"""Test getting product by product_id when product doesn't exist"""
|
||||
product = self.service.get_product_by_id(db, "NONEXISTENT")
|
||||
|
||||
assert product is None
|
||||
|
||||
def test_product_in_shop_true(self, db, test_shop, shop_product):
|
||||
"""Test product_in_shop returns True when product is in shop"""
|
||||
exists = self.service.product_in_shop(db, test_shop.id, shop_product.product_id)
|
||||
|
||||
assert exists is True
|
||||
|
||||
def test_product_in_shop_false(self, db, test_shop, unique_product):
|
||||
"""Test product_in_shop returns False when product is not in shop"""
|
||||
exists = self.service.product_in_shop(db, test_shop.id, unique_product.id)
|
||||
|
||||
assert exists is False
|
||||
|
||||
def test_is_shop_owner_true(self, test_shop, test_user):
|
||||
"""Test is_shop_owner returns True for shop owner"""
|
||||
is_owner = self.service.is_shop_owner(test_shop, test_user)
|
||||
|
||||
assert is_owner is True
|
||||
|
||||
def test_is_shop_owner_false(self, inactive_shop, test_user):
|
||||
"""Test is_shop_owner returns False for non-owner"""
|
||||
is_owner = self.service.is_shop_owner(inactive_shop, test_user)
|
||||
|
||||
assert is_owner is False
|
||||
|
||||
def test_can_view_shop_owner(self, test_shop, test_user):
|
||||
"""Test can_view_shop returns True for shop owner"""
|
||||
can_view = self.service.can_view_shop(test_shop, test_user)
|
||||
|
||||
assert can_view is True
|
||||
|
||||
def test_can_view_shop_admin(self, test_shop, test_admin):
|
||||
"""Test can_view_shop returns True for admin"""
|
||||
can_view = self.service.can_view_shop(test_shop, test_admin)
|
||||
|
||||
assert can_view is True
|
||||
|
||||
def test_can_view_shop_active_verified(self, test_user, verified_shop):
|
||||
"""Test can_view_shop returns True for active verified shop"""
|
||||
can_view = self.service.can_view_shop(verified_shop, test_user)
|
||||
|
||||
assert can_view is True
|
||||
|
||||
def test_can_view_shop_inactive_unverified(self, test_user, inactive_shop):
|
||||
"""Test can_view_shop returns False for inactive unverified shop"""
|
||||
can_view = self.service.can_view_shop(inactive_shop, test_user)
|
||||
|
||||
assert can_view is False
|
||||
504
tests/unit/services/test_stats_service.py
Normal file
504
tests/unit/services/test_stats_service.py
Normal file
@@ -0,0 +1,504 @@
|
||||
# tests/test_stats_service.py
|
||||
import pytest
|
||||
|
||||
from app.services.stats_service import StatsService
|
||||
from models.database_models import Product, Stock
|
||||
|
||||
@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()
|
||||
|
||||
def test_get_comprehensive_stats_basic(self, db, test_product, test_stock):
|
||||
"""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_shops" in stats
|
||||
assert "total_stock_entries" in stats
|
||||
assert "total_inventory_quantity" in stats
|
||||
|
||||
assert stats["total_products"] >= 1
|
||||
assert stats["total_stock_entries"] >= 1
|
||||
assert stats["total_inventory_quantity"] >= 10 # test_stock has quantity 10
|
||||
|
||||
def test_get_comprehensive_stats_multiple_products(self, db, test_product):
|
||||
"""Test comprehensive stats with multiple products across different dimensions"""
|
||||
# Create products with different brands, categories, marketplaces
|
||||
additional_products = [
|
||||
Product(
|
||||
product_id="PROD002",
|
||||
title="Product 2",
|
||||
brand="DifferentBrand",
|
||||
google_product_category="Different Category",
|
||||
marketplace="Amazon",
|
||||
shop_name="AmazonShop",
|
||||
price="15.99",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="PROD003",
|
||||
title="Product 3",
|
||||
brand="ThirdBrand",
|
||||
google_product_category="Third Category",
|
||||
marketplace="eBay",
|
||||
shop_name="eBayShop",
|
||||
price="25.99",
|
||||
currency="USD",
|
||||
),
|
||||
Product(
|
||||
product_id="PROD004",
|
||||
title="Product 4",
|
||||
brand="TestBrand", # Same as test_product
|
||||
google_product_category="Different Category",
|
||||
marketplace="Letzshop", # Same as test_product
|
||||
shop_name="DifferentShop",
|
||||
price="35.99",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(additional_products)
|
||||
db.commit()
|
||||
|
||||
stats = self.service.get_comprehensive_stats(db)
|
||||
|
||||
assert stats["total_products"] >= 4 # test_product + 3 additional
|
||||
assert stats["unique_brands"] >= 3 # TestBrand, DifferentBrand, ThirdBrand
|
||||
assert stats["unique_categories"] >= 2 # At least 2 different categories
|
||||
assert stats["unique_marketplaces"] >= 3 # Letzshop, Amazon, eBay
|
||||
assert stats["unique_shops"] >= 3 # At least 3 different shops
|
||||
|
||||
def test_get_comprehensive_stats_handles_nulls(self, db):
|
||||
"""Test comprehensive stats handles null/empty values correctly"""
|
||||
# Create products with null/empty values
|
||||
products_with_nulls = [
|
||||
Product(
|
||||
product_id="NULL001",
|
||||
title="Product with Nulls",
|
||||
brand=None, # Null brand
|
||||
google_product_category=None, # Null category
|
||||
marketplace=None, # Null marketplace
|
||||
shop_name=None, # Null shop
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="EMPTY001",
|
||||
title="Product with Empty Values",
|
||||
brand="", # Empty brand
|
||||
google_product_category="", # Empty category
|
||||
marketplace="", # Empty marketplace
|
||||
shop_name="", # Empty shop
|
||||
price="15.00",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(products_with_nulls)
|
||||
db.commit()
|
||||
|
||||
stats = self.service.get_comprehensive_stats(db)
|
||||
|
||||
# These products shouldn't contribute to unique counts due to null/empty values
|
||||
assert stats["total_products"] >= 2
|
||||
# Brands, categories, marketplaces, shops should not count null/empty values
|
||||
assert isinstance(stats["unique_brands"], int)
|
||||
assert isinstance(stats["unique_categories"], int)
|
||||
assert isinstance(stats["unique_marketplaces"], int)
|
||||
assert isinstance(stats["unique_shops"], int)
|
||||
|
||||
def test_get_marketplace_breakdown_stats_basic(self, db, test_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_product.marketplace),
|
||||
None,
|
||||
)
|
||||
assert test_marketplace_stat is not None
|
||||
assert test_marketplace_stat["total_products"] >= 1
|
||||
assert test_marketplace_stat["unique_shops"] >= 1
|
||||
assert test_marketplace_stat["unique_brands"] >= 1
|
||||
|
||||
def test_get_marketplace_breakdown_stats_multiple_marketplaces(
|
||||
self, db, test_product
|
||||
):
|
||||
"""Test marketplace breakdown with multiple marketplaces"""
|
||||
# Create products for different marketplaces
|
||||
marketplace_products = [
|
||||
Product(
|
||||
product_id="AMAZON001",
|
||||
title="Amazon Product 1",
|
||||
brand="AmazonBrand1",
|
||||
marketplace="Amazon",
|
||||
shop_name="AmazonShop1",
|
||||
price="20.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="AMAZON002",
|
||||
title="Amazon Product 2",
|
||||
brand="AmazonBrand2",
|
||||
marketplace="Amazon",
|
||||
shop_name="AmazonShop2",
|
||||
price="25.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="EBAY001",
|
||||
title="eBay Product",
|
||||
brand="eBayBrand",
|
||||
marketplace="eBay",
|
||||
shop_name="eBayShop",
|
||||
price="30.00",
|
||||
currency="USD",
|
||||
),
|
||||
]
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
stats = self.service.get_marketplace_breakdown_stats(db)
|
||||
|
||||
# Should have at least 3 marketplaces: test_product.marketplace, Amazon, eBay
|
||||
marketplace_names = [stat["marketplace"] for stat in stats]
|
||||
assert "Amazon" in marketplace_names
|
||||
assert "eBay" in marketplace_names
|
||||
assert test_product.marketplace in marketplace_names
|
||||
|
||||
# Check Amazon stats specifically
|
||||
amazon_stat = next(stat for stat in stats if stat["marketplace"] == "Amazon")
|
||||
assert amazon_stat["total_products"] == 2
|
||||
assert amazon_stat["unique_shops"] == 2
|
||||
assert amazon_stat["unique_brands"] == 2
|
||||
|
||||
# Check eBay stats specifically
|
||||
ebay_stat = next(stat for stat in stats if stat["marketplace"] == "eBay")
|
||||
assert ebay_stat["total_products"] == 1
|
||||
assert ebay_stat["unique_shops"] == 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"""
|
||||
# Create product with null marketplace
|
||||
null_marketplace_product = Product(
|
||||
product_id="NULLMARKET001",
|
||||
title="Product without marketplace",
|
||||
marketplace=None,
|
||||
shop_name="SomeShop",
|
||||
brand="SomeBrand",
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
)
|
||||
db.add(null_marketplace_product)
|
||||
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_product_count(self, db, test_product):
|
||||
"""Test getting total product count"""
|
||||
count = self.service.get_product_count(db)
|
||||
|
||||
assert count >= 1
|
||||
assert isinstance(count, int)
|
||||
|
||||
def test_get_unique_brands_count(self, db, test_product):
|
||||
"""Test getting unique brands count"""
|
||||
# Add products with different brands
|
||||
brand_products = [
|
||||
Product(
|
||||
product_id="BRAND001",
|
||||
title="Brand Product 1",
|
||||
brand="BrandA",
|
||||
marketplace="Test",
|
||||
shop_name="TestShop",
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="BRAND002",
|
||||
title="Brand Product 2",
|
||||
brand="BrandB",
|
||||
marketplace="Test",
|
||||
shop_name="TestShop",
|
||||
price="15.00",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(brand_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_unique_brands_count(db)
|
||||
|
||||
assert (
|
||||
count >= 2
|
||||
) # At least BrandA and BrandB, plus possibly test_product brand
|
||||
assert isinstance(count, int)
|
||||
|
||||
def test_get_unique_categories_count(self, db, test_product):
|
||||
"""Test getting unique categories count"""
|
||||
# Add products with different categories
|
||||
category_products = [
|
||||
Product(
|
||||
product_id="CAT001",
|
||||
title="Category Product 1",
|
||||
google_product_category="Electronics",
|
||||
marketplace="Test",
|
||||
shop_name="TestShop",
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="CAT002",
|
||||
title="Category Product 2",
|
||||
google_product_category="Books",
|
||||
marketplace="Test",
|
||||
shop_name="TestShop",
|
||||
price="15.00",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(category_products)
|
||||
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_unique_marketplaces_count(self, db, test_product):
|
||||
"""Test getting unique marketplaces count"""
|
||||
# Add products with different marketplaces
|
||||
marketplace_products = [
|
||||
Product(
|
||||
product_id="MARKET001",
|
||||
title="Marketplace Product 1",
|
||||
marketplace="Amazon",
|
||||
shop_name="AmazonShop",
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="MARKET002",
|
||||
title="Marketplace Product 2",
|
||||
marketplace="eBay",
|
||||
shop_name="eBayShop",
|
||||
price="15.00",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_unique_marketplaces_count(db)
|
||||
|
||||
assert count >= 2 # At least Amazon and eBay, plus test_product marketplace
|
||||
assert isinstance(count, int)
|
||||
|
||||
def test_get_unique_shops_count(self, db, test_product):
|
||||
"""Test getting unique shops count"""
|
||||
# Add products with different shop names
|
||||
shop_products = [
|
||||
Product(
|
||||
product_id="SHOP001",
|
||||
title="Shop Product 1",
|
||||
marketplace="Test",
|
||||
shop_name="ShopA",
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="SHOP002",
|
||||
title="Shop Product 2",
|
||||
marketplace="Test",
|
||||
shop_name="ShopB",
|
||||
price="15.00",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(shop_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_unique_shops_count(db)
|
||||
|
||||
assert count >= 2 # At least ShopA and ShopB, plus test_product shop
|
||||
assert isinstance(count, int)
|
||||
|
||||
def test_get_stock_statistics(self, db, test_stock):
|
||||
"""Test getting stock statistics"""
|
||||
# Add additional stock entries
|
||||
additional_stocks = [
|
||||
Stock(
|
||||
gtin="1234567890124",
|
||||
location="LOCATION2",
|
||||
quantity=25,
|
||||
reserved_quantity=5,
|
||||
shop_id=test_stock.shop_id,
|
||||
),
|
||||
Stock(
|
||||
gtin="1234567890125",
|
||||
location="LOCATION3",
|
||||
quantity=0, # Out of stock
|
||||
reserved_quantity=0,
|
||||
shop_id=test_stock.shop_id,
|
||||
),
|
||||
]
|
||||
db.add_all(additional_stocks)
|
||||
db.commit()
|
||||
|
||||
stats = self.service.get_stock_statistics(db)
|
||||
|
||||
assert "total_stock_entries" in stats
|
||||
assert "total_inventory_quantity" in stats
|
||||
assert stats["total_stock_entries"] >= 3 # test_stock + 2 additional
|
||||
assert stats["total_inventory_quantity"] >= 35 # 10 + 25 + 0 = 35
|
||||
|
||||
def test_get_brands_by_marketplace(self, db):
|
||||
"""Test getting brands for a specific marketplace"""
|
||||
# Create products for specific marketplace
|
||||
marketplace_products = [
|
||||
Product(
|
||||
product_id="SPECIFIC001",
|
||||
title="Specific Product 1",
|
||||
brand="SpecificBrand1",
|
||||
marketplace="SpecificMarket",
|
||||
shop_name="SpecificShop1",
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="SPECIFIC002",
|
||||
title="Specific Product 2",
|
||||
brand="SpecificBrand2",
|
||||
marketplace="SpecificMarket",
|
||||
shop_name="SpecificShop2",
|
||||
price="15.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="OTHER001",
|
||||
title="Other Product",
|
||||
brand="OtherBrand",
|
||||
marketplace="OtherMarket",
|
||||
shop_name="OtherShop",
|
||||
price="20.00",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
brands = self.service.get_brands_by_marketplace(db, "SpecificMarket")
|
||||
|
||||
assert len(brands) == 2
|
||||
assert "SpecificBrand1" in brands
|
||||
assert "SpecificBrand2" in brands
|
||||
assert "OtherBrand" not in brands
|
||||
|
||||
def test_get_shops_by_marketplace(self, db):
|
||||
"""Test getting shops for a specific marketplace"""
|
||||
# Create products for specific marketplace
|
||||
marketplace_products = [
|
||||
Product(
|
||||
product_id="SHOPTEST001",
|
||||
title="Shop Test Product 1",
|
||||
brand="TestBrand",
|
||||
marketplace="TestMarketplace",
|
||||
shop_name="TestShop1",
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="SHOPTEST002",
|
||||
title="Shop Test Product 2",
|
||||
brand="TestBrand",
|
||||
marketplace="TestMarketplace",
|
||||
shop_name="TestShop2",
|
||||
price="15.00",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
shops = self.service.get_shops_by_marketplace(db, "TestMarketplace")
|
||||
|
||||
assert len(shops) == 2
|
||||
assert "TestShop1" in shops
|
||||
assert "TestShop2" in shops
|
||||
|
||||
def test_get_products_by_marketplace(self, db):
|
||||
"""Test getting product count for a specific marketplace"""
|
||||
# Create products for specific marketplace
|
||||
marketplace_products = [
|
||||
Product(
|
||||
product_id="COUNT001",
|
||||
title="Count Product 1",
|
||||
marketplace="CountMarketplace",
|
||||
shop_name="CountShop",
|
||||
price="10.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="COUNT002",
|
||||
title="Count Product 2",
|
||||
marketplace="CountMarketplace",
|
||||
shop_name="CountShop",
|
||||
price="15.00",
|
||||
currency="EUR",
|
||||
),
|
||||
Product(
|
||||
product_id="COUNT003",
|
||||
title="Count Product 3",
|
||||
marketplace="CountMarketplace",
|
||||
shop_name="CountShop",
|
||||
price="20.00",
|
||||
currency="EUR",
|
||||
),
|
||||
]
|
||||
db.add_all(marketplace_products)
|
||||
db.commit()
|
||||
|
||||
count = self.service.get_products_by_marketplace(db, "CountMarketplace")
|
||||
|
||||
assert count == 3
|
||||
|
||||
def test_get_products_by_marketplace_not_found(self, db):
|
||||
"""Test getting product count for non-existent marketplace"""
|
||||
count = self.service.get_products_by_marketplace(db, "NonExistentMarketplace")
|
||||
|
||||
assert count == 0
|
||||
|
||||
def test_empty_database_stats(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["unique_shops"] == 0
|
||||
assert stats["total_stock_entries"] == 0
|
||||
assert stats["total_inventory_quantity"] == 0
|
||||
|
||||
def test_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
|
||||
370
tests/unit/services/test_stock_service.py
Normal file
370
tests/unit/services/test_stock_service.py
Normal file
@@ -0,0 +1,370 @@
|
||||
# tests/test_stock_service.py
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.stock_service import StockService
|
||||
from models.api_models import StockAdd, StockCreate, StockUpdate
|
||||
from models.database_models import Product, Stock
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.stock
|
||||
class TestStockService:
|
||||
def setup_method(self):
|
||||
self.service = StockService()
|
||||
|
||||
def test_normalize_gtin_invalid(self):
|
||||
"""Test GTIN normalization with invalid GTINs."""
|
||||
# Completely invalid values that should return None
|
||||
assert self.service.normalize_gtin("invalid") is None
|
||||
assert self.service.normalize_gtin("abcdef") is None
|
||||
assert self.service.normalize_gtin("") is None
|
||||
assert self.service.normalize_gtin(None) is None
|
||||
assert self.service.normalize_gtin(" ") is None # Only whitespace
|
||||
assert self.service.normalize_gtin("!@#$%") is None # Only special characters
|
||||
|
||||
# Mixed invalid characters that become empty after filtering
|
||||
assert self.service.normalize_gtin("abc-def-ghi") is None # No digits
|
||||
|
||||
# Note: Based on your GTINProcessor implementation, short numeric values
|
||||
# will be padded, not rejected. For example:
|
||||
# - "123" becomes "000000000123" (padded to 12 digits)
|
||||
# - "1" becomes "000000000001" (padded to 12 digits)
|
||||
|
||||
# If you want to test that short GTINs are padded (not rejected):
|
||||
assert self.service.normalize_gtin("123") == "0000000000123"
|
||||
assert self.service.normalize_gtin("1") == "0000000000001"
|
||||
assert self.service.normalize_gtin("12345") == "0000000012345"
|
||||
|
||||
def test_normalize_gtin_valid(self):
|
||||
"""Test GTIN normalization with valid GTINs."""
|
||||
# Test various valid GTIN formats - these should remain unchanged
|
||||
assert self.service.normalize_gtin("1234567890123") == "1234567890123" # EAN-13
|
||||
assert self.service.normalize_gtin("123456789012") == "123456789012" # UPC-A
|
||||
assert self.service.normalize_gtin("12345678") == "12345678" # EAN-8
|
||||
assert (
|
||||
self.service.normalize_gtin("12345678901234") == "12345678901234"
|
||||
) # GTIN-14
|
||||
|
||||
# Test with decimal points (should be removed)
|
||||
assert self.service.normalize_gtin("1234567890123.0") == "1234567890123"
|
||||
|
||||
# Test with whitespace (should be trimmed)
|
||||
assert self.service.normalize_gtin(" 1234567890123 ") == "1234567890123"
|
||||
|
||||
# Test short GTINs being padded
|
||||
assert self.service.normalize_gtin("123") == "0000000000123" # Padded to EAN-13
|
||||
assert (
|
||||
self.service.normalize_gtin("12345") == "0000000012345"
|
||||
) # Padded to EAN-13
|
||||
|
||||
# Test long GTINs being truncated
|
||||
assert (
|
||||
self.service.normalize_gtin("123456789012345") == "3456789012345"
|
||||
) # Truncated to 13
|
||||
|
||||
def test_normalize_gtin_edge_cases(self):
|
||||
"""Test GTIN normalization edge cases."""
|
||||
# Test numeric inputs
|
||||
assert self.service.normalize_gtin(1234567890123) == "1234567890123"
|
||||
assert self.service.normalize_gtin(123) == "0000000000123"
|
||||
|
||||
# Test mixed valid/invalid characters
|
||||
assert (
|
||||
self.service.normalize_gtin("123-456-789-012") == "123456789012"
|
||||
) # Dashes removed
|
||||
assert (
|
||||
self.service.normalize_gtin("123 456 789 012") == "123456789012"
|
||||
) # Spaces removed
|
||||
assert (
|
||||
self.service.normalize_gtin("ABC123456789012DEF") == "123456789012"
|
||||
) # Letters removed
|
||||
|
||||
def test_set_stock_new_entry(self, db):
|
||||
"""Test setting stock for a new GTIN/location combination."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
stock_data = StockCreate(
|
||||
gtin="1234567890123", location=f"WAREHOUSE_A_{unique_id}", quantity=100
|
||||
)
|
||||
|
||||
result = self.service.set_stock(db, stock_data)
|
||||
|
||||
assert result.gtin == "1234567890123"
|
||||
assert result.location.upper() == f"WAREHOUSE_A_{unique_id}".upper()
|
||||
assert result.quantity == 100
|
||||
|
||||
def test_set_stock_existing_entry(self, db, test_stock):
|
||||
"""Test setting stock for an existing GTIN/location combination."""
|
||||
stock_data = StockCreate(
|
||||
gtin=test_stock.gtin,
|
||||
location=test_stock.location, # Use exact same location as test_stock
|
||||
quantity=200,
|
||||
)
|
||||
|
||||
result = self.service.set_stock(db, stock_data)
|
||||
|
||||
assert result.gtin == test_stock.gtin
|
||||
# Fix: Handle case sensitivity properly - compare uppercase or use exact match
|
||||
assert result.location == test_stock.location
|
||||
assert result.quantity == 200 # Should replace the original quantity
|
||||
|
||||
def test_set_stock_invalid_gtin(self, db):
|
||||
"""Test setting stock with invalid GTIN."""
|
||||
stock_data = StockCreate(
|
||||
gtin="invalid_gtin", location="WAREHOUSE_A", quantity=100
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
self.service.set_stock(db, stock_data)
|
||||
|
||||
def test_add_stock_new_entry(self, db):
|
||||
"""Test adding stock for a new GTIN/location combination."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
stock_data = StockAdd(
|
||||
gtin="1234567890123", location=f"WAREHOUSE_B_{unique_id}", quantity=50
|
||||
)
|
||||
|
||||
result = self.service.add_stock(db, stock_data)
|
||||
|
||||
assert result.gtin == "1234567890123"
|
||||
assert result.location.upper() == f"WAREHOUSE_B_{unique_id}".upper()
|
||||
assert result.quantity == 50
|
||||
|
||||
def test_add_stock_existing_entry(self, db, test_stock):
|
||||
"""Test adding stock to an existing GTIN/location combination."""
|
||||
original_quantity = test_stock.quantity
|
||||
stock_data = StockAdd(
|
||||
gtin=test_stock.gtin,
|
||||
location=test_stock.location, # Use exact same location as test_stock
|
||||
quantity=25,
|
||||
)
|
||||
|
||||
result = self.service.add_stock(db, stock_data)
|
||||
|
||||
assert result.gtin == test_stock.gtin
|
||||
assert result.location == test_stock.location
|
||||
assert result.quantity == original_quantity + 25
|
||||
|
||||
def test_add_stock_invalid_gtin(self, db):
|
||||
"""Test adding stock with invalid GTIN."""
|
||||
stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=50)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
self.service.add_stock(db, stock_data)
|
||||
|
||||
def test_remove_stock_success(self, db, test_stock):
|
||||
"""Test removing stock successfully."""
|
||||
original_quantity = test_stock.quantity
|
||||
remove_quantity = min(
|
||||
10, original_quantity
|
||||
) # Ensure we don't remove more than available
|
||||
|
||||
stock_data = StockAdd(
|
||||
gtin=test_stock.gtin,
|
||||
location=test_stock.location, # Use exact same location as test_stock
|
||||
quantity=remove_quantity,
|
||||
)
|
||||
|
||||
result = self.service.remove_stock(db, stock_data)
|
||||
|
||||
assert result.gtin == test_stock.gtin
|
||||
assert result.location == test_stock.location
|
||||
assert result.quantity == original_quantity - remove_quantity
|
||||
|
||||
def test_remove_stock_insufficient_stock(self, db, test_stock):
|
||||
"""Test removing more stock than available."""
|
||||
stock_data = StockAdd(
|
||||
gtin=test_stock.gtin,
|
||||
location=test_stock.location, # Use exact same location as test_stock
|
||||
quantity=test_stock.quantity + 10, # More than available
|
||||
)
|
||||
|
||||
# Fix: Use more flexible regex pattern
|
||||
with pytest.raises(
|
||||
ValueError, match="Insufficient stock|Not enough stock|Cannot remove"
|
||||
):
|
||||
self.service.remove_stock(db, stock_data)
|
||||
|
||||
def test_remove_stock_nonexistent_entry(self, db):
|
||||
"""Test removing stock from non-existent GTIN/location."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
stock_data = StockAdd(
|
||||
gtin="9999999999999", location=f"NONEXISTENT_{unique_id}", quantity=10
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="No stock found|Stock not found"):
|
||||
self.service.remove_stock(db, stock_data)
|
||||
|
||||
def test_remove_stock_invalid_gtin(self, db):
|
||||
"""Test removing stock with invalid GTIN."""
|
||||
stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=10)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
self.service.remove_stock(db, stock_data)
|
||||
|
||||
def test_get_stock_by_gtin_success(self, db, test_stock, test_product):
|
||||
"""Test getting stock summary by GTIN."""
|
||||
result = self.service.get_stock_by_gtin(db, test_stock.gtin)
|
||||
|
||||
assert result.gtin == test_stock.gtin
|
||||
assert result.total_quantity == test_stock.quantity
|
||||
assert len(result.locations) == 1
|
||||
assert result.locations[0].location == test_stock.location
|
||||
assert result.locations[0].quantity == test_stock.quantity
|
||||
assert result.product_title == test_product.title
|
||||
|
||||
def test_get_stock_by_gtin_multiple_locations(self, db, test_product):
|
||||
"""Test getting stock summary with multiple locations."""
|
||||
unique_gtin = test_product.gtin
|
||||
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create multiple stock entries for the same GTIN with unique locations
|
||||
stock1 = Stock(
|
||||
gtin=unique_gtin, location=f"WAREHOUSE_A_{unique_id}", quantity=50
|
||||
)
|
||||
stock2 = Stock(
|
||||
gtin=unique_gtin, location=f"WAREHOUSE_B_{unique_id}", quantity=30
|
||||
)
|
||||
|
||||
db.add(stock1)
|
||||
db.add(stock2)
|
||||
db.commit()
|
||||
|
||||
result = self.service.get_stock_by_gtin(db, unique_gtin)
|
||||
|
||||
assert result.gtin == unique_gtin
|
||||
assert result.total_quantity == 80
|
||||
assert len(result.locations) == 2
|
||||
|
||||
def test_get_stock_by_gtin_not_found(self, db):
|
||||
"""Test getting stock for non-existent GTIN."""
|
||||
with pytest.raises(ValueError, match="No stock found"):
|
||||
self.service.get_stock_by_gtin(db, "9999999999999")
|
||||
|
||||
def test_get_stock_by_gtin_invalid_gtin(self, db):
|
||||
"""Test getting stock with invalid GTIN."""
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
self.service.get_stock_by_gtin(db, "invalid_gtin")
|
||||
|
||||
def test_get_total_stock_success(self, db, test_stock, test_product):
|
||||
"""Test getting total stock for a GTIN."""
|
||||
result = self.service.get_total_stock(db, test_stock.gtin)
|
||||
|
||||
assert result["gtin"] == test_stock.gtin
|
||||
assert result["total_quantity"] == test_stock.quantity
|
||||
assert result["product_title"] == test_product.title
|
||||
assert result["locations_count"] == 1
|
||||
|
||||
def test_get_total_stock_invalid_gtin(self, db):
|
||||
"""Test getting total stock with invalid GTIN."""
|
||||
with pytest.raises(ValueError, match="Invalid GTIN format"):
|
||||
self.service.get_total_stock(db, "invalid_gtin")
|
||||
|
||||
def test_get_all_stock_no_filters(self, db, test_stock):
|
||||
"""Test getting all stock without filters."""
|
||||
result = self.service.get_all_stock(db)
|
||||
|
||||
assert len(result) >= 1
|
||||
assert any(stock.gtin == test_stock.gtin for stock in result)
|
||||
|
||||
def test_get_all_stock_with_location_filter(self, db, test_stock):
|
||||
"""Test getting all stock with location filter."""
|
||||
result = self.service.get_all_stock(db, location=test_stock.location)
|
||||
|
||||
assert len(result) >= 1
|
||||
# Fix: Handle case sensitivity in comparison
|
||||
assert all(
|
||||
stock.location.upper() == test_stock.location.upper() for stock in result
|
||||
)
|
||||
|
||||
def test_get_all_stock_with_gtin_filter(self, db, test_stock):
|
||||
"""Test getting all stock with GTIN filter."""
|
||||
result = self.service.get_all_stock(db, gtin=test_stock.gtin)
|
||||
|
||||
assert len(result) >= 1
|
||||
assert all(stock.gtin == test_stock.gtin for stock in result)
|
||||
|
||||
def test_get_all_stock_with_pagination(self, db):
|
||||
"""Test getting all stock with pagination."""
|
||||
unique_prefix = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create multiple stock entries with unique GTINs and locations
|
||||
for i in range(5):
|
||||
stock = Stock(
|
||||
gtin=f"1234567890{i:03d}", # Creates valid 13-digit GTINs: 1234567890000, 1234567890001, etc.
|
||||
location=f"WAREHOUSE_{unique_prefix}_{i}",
|
||||
quantity=10,
|
||||
)
|
||||
db.add(stock)
|
||||
db.commit()
|
||||
|
||||
result = self.service.get_all_stock(db, skip=2, limit=2)
|
||||
|
||||
assert (
|
||||
len(result) <= 2
|
||||
) # Should be at most 2, might be less if other records exist
|
||||
|
||||
def test_update_stock_success(self, db, test_stock):
|
||||
"""Test updating stock quantity."""
|
||||
stock_update = StockUpdate(quantity=150)
|
||||
|
||||
result = self.service.update_stock(db, test_stock.id, stock_update)
|
||||
|
||||
assert result.id == test_stock.id
|
||||
assert result.quantity == 150
|
||||
|
||||
def test_update_stock_not_found(self, db):
|
||||
"""Test updating non-existent stock entry."""
|
||||
stock_update = StockUpdate(quantity=150)
|
||||
|
||||
with pytest.raises(ValueError, match="Stock entry not found"):
|
||||
self.service.update_stock(db, 99999, stock_update)
|
||||
|
||||
def test_delete_stock_success(self, db, test_stock):
|
||||
"""Test deleting stock entry."""
|
||||
stock_id = test_stock.id
|
||||
|
||||
result = self.service.delete_stock(db, stock_id)
|
||||
|
||||
assert result is True
|
||||
|
||||
# Verify the stock is actually deleted
|
||||
deleted_stock = db.query(Stock).filter(Stock.id == stock_id).first()
|
||||
assert deleted_stock is None
|
||||
|
||||
def test_delete_stock_not_found(self, db):
|
||||
"""Test deleting non-existent stock entry."""
|
||||
with pytest.raises(ValueError, match="Stock entry not found"):
|
||||
self.service.delete_stock(db, 99999)
|
||||
|
||||
def test_get_stock_by_id_success(self, db, test_stock):
|
||||
"""Test getting stock entry by ID."""
|
||||
result = self.service.get_stock_by_id(db, test_stock.id)
|
||||
|
||||
assert result is not None
|
||||
assert result.id == test_stock.id
|
||||
assert result.gtin == test_stock.gtin
|
||||
|
||||
def test_get_stock_by_id_not_found(self, db):
|
||||
"""Test getting non-existent stock entry by ID."""
|
||||
result = self.service.get_stock_by_id(db, 99999)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_product_with_stock(db, test_stock):
|
||||
"""Create a test product that corresponds to the test stock."""
|
||||
product = Product(
|
||||
product_id="STOCK_TEST_001",
|
||||
title="Stock Test Product",
|
||||
gtin=test_stock.gtin,
|
||||
price="29.99",
|
||||
brand="TestBrand",
|
||||
marketplace="Letzshop",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
return product
|
||||
3
tests/unit/utils/__init__.py
Normal file
3
tests/unit/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# tests/unit/utils/__init__.py
|
||||
"""Utility function unit tests."""
|
||||
|
||||
126
tests/unit/utils/test_csv_processor.py
Normal file
126
tests/unit/utils/test_csv_processor.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# tests/test_csv_processor.py
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
import requests
|
||||
import requests.exceptions
|
||||
|
||||
from utils.csv_processor import CSVProcessor
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCSVProcessor:
|
||||
def setup_method(self):
|
||||
self.processor = CSVProcessor()
|
||||
|
||||
@patch("requests.get")
|
||||
def test_download_csv_encoding_fallback(self, mock_get):
|
||||
"""Test CSV download with encoding fallback"""
|
||||
# Create content with special characters that would fail UTF-8 if not properly encoded
|
||||
special_content = "product_id,title,price\nTEST001,Café Product,10.99"
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
# Use latin-1 encoding which your method should try
|
||||
mock_response.content = special_content.encode("latin-1")
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
csv_content = self.processor.download_csv("http://example.com/test.csv")
|
||||
|
||||
mock_get.assert_called_once_with("http://example.com/test.csv", timeout=30)
|
||||
assert isinstance(csv_content, str)
|
||||
assert "Café Product" in csv_content
|
||||
|
||||
@patch("requests.get")
|
||||
def test_download_csv_encoding_ignore_fallback(self, mock_get):
|
||||
"""Test CSV download falls back to UTF-8 with error ignoring"""
|
||||
# Create problematic bytes that would fail most encoding attempts
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
# Create bytes that will fail most encodings
|
||||
mock_response.content = (
|
||||
b"product_id,title,price\nTEST001,\xff\xfe Product,10.99"
|
||||
)
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
csv_content = self.processor.download_csv("http://example.com/test.csv")
|
||||
|
||||
mock_get.assert_called_once_with("http://example.com/test.csv", timeout=30)
|
||||
assert isinstance(csv_content, str)
|
||||
# Should still contain basic content even with ignored errors
|
||||
assert "product_id,title,price" in csv_content
|
||||
assert "TEST001" in csv_content
|
||||
|
||||
@patch("requests.get")
|
||||
def test_download_csv_request_exception(self, mock_get):
|
||||
"""Test CSV download with request exception"""
|
||||
mock_get.side_effect = requests.exceptions.RequestException("Connection error")
|
||||
|
||||
with pytest.raises(requests.exceptions.RequestException):
|
||||
self.processor.download_csv("http://example.com/test.csv")
|
||||
|
||||
@patch("requests.get")
|
||||
def test_download_csv_http_error(self, mock_get):
|
||||
"""Test CSV download with HTTP error"""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
|
||||
"404 Not Found"
|
||||
)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with pytest.raises(requests.exceptions.HTTPError):
|
||||
self.processor.download_csv("http://example.com/nonexistent.csv")
|
||||
|
||||
@patch("requests.get")
|
||||
def test_download_csv_failure(self, mock_get):
|
||||
"""Test CSV download failure"""
|
||||
# Mock failed HTTP response
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception):
|
||||
self.processor._download_csv("http://example.com/nonexistent.csv")
|
||||
|
||||
def test_parse_csv_content(self):
|
||||
"""Test CSV content parsing"""
|
||||
csv_content = """product_id,title,price,marketplace
|
||||
TEST001,Test Product 1,10.99,TestMarket
|
||||
TEST002,Test Product 2,15.99,TestMarket"""
|
||||
|
||||
df = self.processor.parse_csv(csv_content)
|
||||
|
||||
assert len(df) == 2
|
||||
assert "product_id" in df.columns
|
||||
assert df.iloc[0]["product_id"] == "TEST001"
|
||||
assert df.iloc[1]["price"] == 15.99
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_marketplace_csv_from_url(self, db):
|
||||
"""Test complete marketplace CSV processing"""
|
||||
with patch.object(
|
||||
self.processor, "download_csv"
|
||||
) as mock_download, patch.object(self.processor, "parse_csv") as mock_parse:
|
||||
# Mock successful download and parsing
|
||||
mock_download.return_value = "csv_content"
|
||||
mock_df = pd.DataFrame(
|
||||
{
|
||||
"product_id": ["TEST001", "TEST002"],
|
||||
"title": ["Product 1", "Product 2"],
|
||||
"price": ["10.99", "15.99"],
|
||||
"marketplace": ["TestMarket", "TestMarket"],
|
||||
"shop_name": ["TestShop", "TestShop"],
|
||||
}
|
||||
)
|
||||
mock_parse.return_value = mock_df
|
||||
|
||||
result = await self.processor.process_marketplace_csv_from_url(
|
||||
"http://example.com/test.csv", "TestMarket", "TestShop", 1000, db
|
||||
)
|
||||
|
||||
assert "imported" in result
|
||||
assert "updated" in result
|
||||
assert "total_processed" in result
|
||||
121
tests/unit/utils/test_data_processing.py
Normal file
121
tests/unit/utils/test_data_processing.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# tests/unit/utils/test_data_processing.py
|
||||
import pytest
|
||||
|
||||
from utils.data_processing import GTINProcessor, PriceProcessor
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGTINProcessor:
|
||||
def setup_method(self):
|
||||
self.processor = GTINProcessor()
|
||||
|
||||
def test_normalize_valid_gtin(self):
|
||||
"""Test GTIN normalization with valid inputs"""
|
||||
# Test EAN-13
|
||||
assert self.processor.normalize("1234567890123") == "1234567890123"
|
||||
|
||||
# Test UPC-A (12 digits)
|
||||
assert self.processor.normalize("123456789012") == "123456789012"
|
||||
|
||||
# Test with decimal point
|
||||
assert self.processor.normalize("123456789012.0") == "123456789012"
|
||||
|
||||
# Test EAN-8
|
||||
assert self.processor.normalize("12345678") == "12345678"
|
||||
|
||||
def test_normalize_invalid_gtin(self):
|
||||
"""Test GTIN normalization with invalid inputs"""
|
||||
assert self.processor.normalize("") is None
|
||||
assert self.processor.normalize(None) is None
|
||||
assert self.processor.normalize("abc") is None
|
||||
|
||||
# Test short number (gets padded)
|
||||
assert self.processor.normalize("123") == "0000000000123"
|
||||
|
||||
def test_normalize_gtin_with_formatting(self):
|
||||
"""Test GTIN normalization with various formatting"""
|
||||
# Test with spaces
|
||||
assert self.processor.normalize("123 456 789 012") == "123456789012"
|
||||
|
||||
# Test with dashes
|
||||
assert self.processor.normalize("123-456-789-012") == "123456789012"
|
||||
|
||||
# Test with mixed formatting
|
||||
assert self.processor.normalize("123 456-789 012") == "123456789012"
|
||||
|
||||
def test_validate_gtin(self):
|
||||
"""Test GTIN validation"""
|
||||
assert self.processor.validate("1234567890123") is True
|
||||
assert self.processor.validate("123456789012") is True
|
||||
assert self.processor.validate("12345678") is True
|
||||
assert self.processor.validate("123") is False
|
||||
assert self.processor.validate("") is False
|
||||
assert self.processor.validate(None) is False
|
||||
|
||||
def test_gtin_checksum_validation(self):
|
||||
"""Test GTIN checksum validation if implemented"""
|
||||
# This test would verify checksum calculation if your GTINProcessor implements it
|
||||
# For now, we'll test the structure validation
|
||||
assert self.processor.validate("1234567890123") is True
|
||||
assert self.processor.validate("12345678901234") is True # 14 digits
|
||||
assert self.processor.validate("123456789012345") is False # 15 digits
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPriceProcessor:
|
||||
def setup_method(self):
|
||||
self.processor = PriceProcessor()
|
||||
|
||||
def test_parse_price_currency_eur(self):
|
||||
"""Test EUR price parsing"""
|
||||
price, currency = self.processor.parse_price_currency("8.26 EUR")
|
||||
assert price == "8.26"
|
||||
assert currency == "EUR"
|
||||
|
||||
# Test with euro symbol
|
||||
price, currency = self.processor.parse_price_currency("8.26 €")
|
||||
assert price == "8.26"
|
||||
assert currency == "EUR"
|
||||
|
||||
def test_parse_price_currency_usd(self):
|
||||
"""Test USD price parsing"""
|
||||
price, currency = self.processor.parse_price_currency("$12.50")
|
||||
assert price == "12.50"
|
||||
assert currency == "USD"
|
||||
|
||||
price, currency = self.processor.parse_price_currency("12.50 USD")
|
||||
assert price == "12.50"
|
||||
assert currency == "USD"
|
||||
|
||||
def test_parse_price_currency_comma_decimal(self):
|
||||
"""Test price parsing with comma as decimal separator"""
|
||||
price, currency = self.processor.parse_price_currency("8,26 EUR")
|
||||
assert price == "8.26"
|
||||
assert currency == "EUR"
|
||||
|
||||
def test_parse_invalid_price(self):
|
||||
"""Test invalid price parsing"""
|
||||
price, currency = self.processor.parse_price_currency("")
|
||||
assert price is None
|
||||
assert currency is None
|
||||
|
||||
price, currency = self.processor.parse_price_currency(None)
|
||||
assert price is None
|
||||
assert currency is None
|
||||
|
||||
def test_parse_price_edge_cases(self):
|
||||
"""Test edge cases in price parsing"""
|
||||
# Test price without currency
|
||||
price, currency = self.processor.parse_price_currency("15.99")
|
||||
assert price == "15.99"
|
||||
# currency might be None or default value
|
||||
|
||||
# Test currency before price
|
||||
price, currency = self.processor.parse_price_currency("EUR 25.50")
|
||||
assert price == "25.50"
|
||||
assert currency == "EUR"
|
||||
|
||||
# Test with multiple decimal places
|
||||
price, currency = self.processor.parse_price_currency("12.999 USD")
|
||||
assert price == "12.999"
|
||||
assert currency == "USD"
|
||||
47
tests/unit/utils/test_data_validation.py
Normal file
47
tests/unit/utils/test_data_validation.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# tests/test_data_validation.py
|
||||
import pytest
|
||||
|
||||
from utils.data_processing import GTINProcessor, PriceProcessor
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDataValidation:
|
||||
def test_gtin_normalization_edge_cases(self):
|
||||
"""Test GTIN normalization with edge cases"""
|
||||
processor = GTINProcessor()
|
||||
|
||||
# Test with leading zeros
|
||||
assert processor.normalize("000123456789") == "000123456789"
|
||||
|
||||
# Test with spaces
|
||||
assert processor.normalize("123 456 789 012") == "123456789012"
|
||||
|
||||
# Test with dashes
|
||||
assert processor.normalize("123-456-789-012") == "123456789012"
|
||||
|
||||
# Test very long numbers
|
||||
long_number = "1234567890123456789"
|
||||
normalized = processor.normalize(long_number)
|
||||
assert len(normalized) <= 14 # Should be truncated
|
||||
|
||||
def test_price_parsing_edge_cases(self):
|
||||
"""Test price parsing with edge cases"""
|
||||
processor = PriceProcessor()
|
||||
|
||||
# Test with multiple decimal places
|
||||
price, currency = processor.parse_price_currency("12.999 EUR")
|
||||
assert price == "12.999"
|
||||
|
||||
# Test with no currency
|
||||
price, currency = processor.parse_price_currency("15.50")
|
||||
assert price == "15.50"
|
||||
|
||||
# Test with unusual formatting
|
||||
price, currency = processor.parse_price_currency("EUR 25,50")
|
||||
assert currency == "EUR"
|
||||
assert price == "25.50" # Comma should be converted to dot
|
||||
|
||||
def test_input_sanitization(self):
|
||||
"""Test input sanitization"""
|
||||
# These tests would verify that inputs are properly sanitized
|
||||
# to prevent SQL injection, XSS, etc.
|
||||
pass # Implementation would depend on your sanitization logic
|
||||
Reference in New Issue
Block a user