Tests restructuring

This commit is contained in:
2025-09-19 21:23:57 +02:00
parent d0924f90c4
commit 366093bbc6
70 changed files with 212 additions and 1957 deletions

3
tests/unit/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# tests/unit/__init__.py
"""Unit tests - fast, isolated component tests."""

6
tests/unit/conftest.py Normal file
View File

@@ -0,0 +1,6 @@
# tests/unit/conftest.py
"""Unit test specific fixtures."""
import pytest
# Add any unit-specific fixtures here if needed

View File

@@ -0,0 +1,3 @@
# tests/unit/models/__init__.py
"""Database and API model unit tests."""

View 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()

View File

@@ -0,0 +1,3 @@
# tests/unit/services/__init__.py
"""Service layer unit tests."""

View 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

View 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

View 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)

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,3 @@
# tests/unit/utils/__init__.py
"""Utility function unit tests."""

View 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

View 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"

View 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