shop product refactoring

This commit is contained in:
2025-10-04 23:38:53 +02:00
parent 4d2866af5e
commit 0114b6c46e
68 changed files with 2234 additions and 2236 deletions

View File

@@ -2,7 +2,7 @@
import pytest
from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop
from models.database.vendor import Vendor
from models.database.stock import Stock
from models.database.user import User
@@ -41,7 +41,7 @@ class TestDatabaseModels:
gtin="1234567890123",
availability="in stock",
marketplace="TestDB",
shop_name="DBTestShop",
vendor_name="DBTestVendor",
)
db.add(marketplace_product)
@@ -65,25 +65,25 @@ class TestDatabaseModels:
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",
def test_vendor_model_with_owner(self, db, test_user):
"""Test Vendor model with owner relationship"""
vendor = Vendor(
vendor_code="DBTEST",
vendor_name="Database Test Vendor",
description="Testing vendor model",
owner_id=test_user.id,
is_active=True,
is_verified=False,
)
db.add(shop)
db.add(vendor)
db.commit()
db.refresh(shop)
db.refresh(vendor)
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
assert vendor.id is not None
assert vendor.vendor_code == "DBTEST"
assert vendor.owner_id == test_user.id
assert vendor.owner.username == test_user.username
def test_database_constraints(self, db):
"""Test database constraints and unique indexes"""

View File

@@ -5,13 +5,13 @@ from app.exceptions import (
UserNotFoundException,
UserStatusChangeException,
CannotModifySelfException,
ShopNotFoundException,
ShopVerificationException,
VendorNotFoundException,
VendorVerificationException,
AdminOperationException,
)
from app.services.admin_service import AdminService
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.shop import Shop
from models.database.vendor import Vendor
@pytest.mark.unit
@@ -93,80 +93,80 @@ class TestAdminService:
assert exception.error_code == "USER_STATUS_CHANGE_FAILED"
assert "Cannot modify another admin user" in exception.message
# Shop Management Tests
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)
# Vendor Management Tests
def test_get_all_vendors(self, db, test_vendor):
"""Test getting all vendors with total count"""
vendors, total = self.service.get_all_vendors(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
assert len(vendors) >= 1
vendor_codes = [vendor.vendor_code for vendor in vendors]
assert test_vendor.vendor_code in vendor_codes
def test_get_all_shops_with_pagination(self, db, test_shop, verified_shop):
"""Test shop pagination works correctly"""
shops, total = self.service.get_all_shops(db, skip=0, limit=1)
def test_get_all_vendors_with_pagination(self, db, test_vendor, verified_vendor):
"""Test vendor pagination works correctly"""
vendors, total = self.service.get_all_vendors(db, skip=0, limit=1)
assert total >= 2
assert len(shops) == 1
assert len(vendors) == 1
shops_second_page, _ = self.service.get_all_shops(db, skip=1, limit=1)
assert len(shops_second_page) >= 0
if len(shops_second_page) > 0:
assert shops[0].id != shops_second_page[0].id
vendors_second_page, _ = self.service.get_all_vendors(db, skip=1, limit=1)
assert len(vendors_second_page) >= 0
if len(vendors_second_page) > 0:
assert vendors[0].id != vendors_second_page[0].id
def test_verify_shop_mark_verified(self, db, test_shop):
"""Test marking shop as verified"""
# Ensure shop starts unverified
test_shop.is_verified = False
def test_verify_vendor_mark_verified(self, db, test_vendor):
"""Test marking vendor as verified"""
# Ensure vendor starts unverified
test_vendor.is_verified = False
db.commit()
shop, message = self.service.verify_shop(db, test_shop.id)
vendor, message = self.service.verify_vendor(db, test_vendor.id)
assert shop.id == test_shop.id
assert shop.is_verified is True
assert test_shop.shop_code in message
assert vendor.id == test_vendor.id
assert vendor.is_verified is True
assert test_vendor.vendor_code in message
assert "verified" in message
def test_verify_shop_mark_unverified(self, db, verified_shop):
"""Test marking verified shop as unverified"""
shop, message = self.service.verify_shop(db, verified_shop.id)
def test_verify_vendor_mark_unverified(self, db, verified_vendor):
"""Test marking verified vendor as unverified"""
vendor, message = self.service.verify_vendor(db, verified_vendor.id)
assert shop.id == verified_shop.id
assert shop.is_verified is False
assert verified_shop.shop_code in message
assert vendor.id == verified_vendor.id
assert vendor.is_verified is False
assert verified_vendor.vendor_code in message
assert "unverified" in message
def test_verify_shop_not_found(self, db):
"""Test verify shop when shop not found"""
with pytest.raises(ShopNotFoundException) as exc_info:
self.service.verify_shop(db, 99999)
def test_verify_vendor_not_found(self, db):
"""Test verify vendor when vendor not found"""
with pytest.raises(VendorNotFoundException) as exc_info:
self.service.verify_vendor(db, 99999)
exception = exc_info.value
assert exception.error_code == "SHOP_NOT_FOUND"
assert exception.error_code == "VENDOR_NOT_FOUND"
assert "99999" in exception.message
def test_toggle_shop_status_deactivate(self, db, test_shop):
"""Test deactivating a shop"""
original_status = test_shop.is_active
def test_toggle_vendor_status_deactivate(self, db, test_vendor):
"""Test deactivating a vendor """
original_status = test_vendor.is_active
shop, message = self.service.toggle_shop_status(db, test_shop.id)
vendor, message = self.service.toggle_vendor_status(db, test_vendor.id)
assert shop.id == test_shop.id
assert shop.is_active != original_status
assert test_shop.shop_code in message
assert vendor.id == test_vendor.id
assert vendor.is_active != original_status
assert test_vendor.vendor_code in message
if original_status:
assert "deactivated" in message
else:
assert "activated" in message
def test_toggle_shop_status_not_found(self, db):
"""Test toggle shop status when shop not found"""
with pytest.raises(ShopNotFoundException) as exc_info:
self.service.toggle_shop_status(db, 99999)
def test_toggle_vendor_status_not_found(self, db):
"""Test toggle vendor status when vendor not found"""
with pytest.raises(VendorNotFoundException) as exc_info:
self.service.toggle_vendor_status(db, 99999)
exception = exc_info.value
assert exception.error_code == "SHOP_NOT_FOUND"
assert exception.error_code == "VENDOR_NOT_FOUND"
# Marketplace Import Jobs Tests
def test_get_marketplace_import_jobs_no_filters(self, db, test_marketplace_import_job):
@@ -180,7 +180,7 @@ class TestAdminService:
)
assert test_job is not None
assert test_job.marketplace == test_marketplace_import_job.marketplace
assert test_job.shop_name == test_marketplace_import_job.shop_name
assert test_job.vendor_name == test_marketplace_import_job.vendor_name
assert test_job.status == test_marketplace_import_job.status
def test_get_marketplace_import_jobs_with_marketplace_filter(self, db, test_marketplace_import_job):
@@ -193,15 +193,15 @@ class TestAdminService:
for job in result:
assert test_marketplace_import_job.marketplace.lower() in job.marketplace.lower()
def test_get_marketplace_import_jobs_with_shop_filter(self, db, test_marketplace_import_job):
"""Test filtering marketplace import jobs by shop name"""
def test_get_marketplace_import_jobs_with_vendor_filter(self, db, test_marketplace_import_job):
"""Test filtering marketplace import jobs by vendor name"""
result = self.service.get_marketplace_import_jobs(
db, shop_name=test_marketplace_import_job.shop_name, skip=0, limit=10
db, vendor_name=test_marketplace_import_job.vendor_name, skip=0, limit=10
)
assert len(result) >= 1
for job in result:
assert test_marketplace_import_job.shop_name.lower() in job.shop_name.lower()
assert test_marketplace_import_job.vendor_name.lower() in job.vendor_name.lower()
def test_get_marketplace_import_jobs_with_status_filter(self, db, test_marketplace_import_job):
"""Test filtering marketplace import jobs by status"""
@@ -242,21 +242,21 @@ class TestAdminService:
assert stats["total_users"] >= 2 # test_user + test_admin
assert stats["active_users"] + stats["inactive_users"] == stats["total_users"]
def test_get_shop_statistics(self, db, test_shop):
"""Test getting shop statistics"""
stats = self.service.get_shop_statistics(db)
def test_get_vendor_statistics(self, db, test_vendor):
"""Test getting vendor statistics"""
stats = self.service.get_vendor_statistics(db)
assert "total_shops" in stats
assert "active_shops" in stats
assert "verified_shops" in stats
assert "total_vendors" in stats
assert "active_vendors" in stats
assert "verified_vendors" in stats
assert "verification_rate" in stats
assert isinstance(stats["total_shops"], int)
assert isinstance(stats["active_shops"], int)
assert isinstance(stats["verified_shops"], int)
assert isinstance(stats["total_vendors"], int)
assert isinstance(stats["active_vendors"], int)
assert isinstance(stats["verified_vendors"], int)
assert isinstance(stats["verification_rate"], (int, float))
assert stats["total_shops"] >= 1
assert stats["total_vendors"] >= 1
# Error Handling Tests
def test_get_all_users_database_error(self, db_with_error, test_admin):
@@ -268,14 +268,14 @@ class TestAdminService:
assert exception.error_code == "ADMIN_OPERATION_FAILED"
assert "get_all_users" in exception.message
def test_get_all_shops_database_error(self, db_with_error):
"""Test handling database errors in get_all_shops"""
def test_get_all_vendors_database_error(self, db_with_error):
"""Test handling database errors in get_all_vendors"""
with pytest.raises(AdminOperationException) as exc_info:
self.service.get_all_shops(db_with_error, skip=0, limit=10)
self.service.get_all_vendors(db_with_error, skip=0, limit=10)
exception = exc_info.value
assert exception.error_code == "ADMIN_OPERATION_FAILED"
assert "get_all_shops" in exception.message
assert "get_all_vendors" in exception.message
# Edge Cases
def test_get_all_users_empty_database(self, empty_db):
@@ -283,10 +283,10 @@ class TestAdminService:
users = self.service.get_all_users(empty_db, skip=0, limit=10)
assert len(users) == 0
def test_get_all_shops_empty_database(self, empty_db):
"""Test getting shops when database is empty"""
shops, total = self.service.get_all_shops(empty_db, skip=0, limit=10)
assert len(shops) == 0
def test_get_all_vendors_empty_database(self, empty_db):
"""Test getting vendors when database is empty"""
vendors, total = self.service.get_all_vendors(empty_db, skip=0, limit=10)
assert len(vendors) == 0
assert total == 0
def test_user_statistics_empty_database(self, empty_db):
@@ -298,11 +298,11 @@ class TestAdminService:
assert stats["inactive_users"] == 0
assert stats["activation_rate"] == 0
def test_shop_statistics_empty_database(self, empty_db):
"""Test shop statistics when no shops exist"""
stats = self.service.get_shop_statistics(empty_db)
def test_vendor_statistics_empty_database(self, empty_db):
"""Test vendor statistics when no vendors exist"""
stats = self.service.get_vendor_statistics(empty_db)
assert stats["total_shops"] == 0
assert stats["active_shops"] == 0
assert stats["verified_shops"] == 0
assert stats["total_vendors"] == 0
assert stats["active_vendors"] == 0
assert stats["verified_vendors"] == 0
assert stats["verification_rate"] == 0

View File

@@ -10,12 +10,12 @@ from app.exceptions.marketplace_import_job import (
ImportJobCannotBeCancelledException,
ImportJobCannotBeDeletedException,
)
from app.exceptions.shop import ShopNotFoundException, UnauthorizedShopAccessException
from app.exceptions.vendor import VendorNotFoundException, UnauthorizedVendorAccessException
from app.exceptions.base import ValidationException
from app.services.marketplace_import_job_service import MarketplaceImportJobService
from models.schemas.marketplace_import_job import MarketplaceImportJobRequest
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.shop import Shop
from models.database.vendor import Vendor
from models.database.user import User
@@ -25,107 +25,107 @@ class TestMarketplaceService:
def setup_method(self):
self.service = MarketplaceImportJobService()
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
def test_validate_vendor_access_success(self, db, test_vendor, test_user):
"""Test successful vendor access validation"""
# Set the vendor owner to the test user
test_vendor.owner_id = test_user.id
db.commit()
result = self.service.validate_shop_access(db, test_shop.shop_code, test_user)
result = self.service.validate_vendor_access(db, test_vendor.vendor_code, test_user)
assert result.shop_code == test_shop.shop_code
assert result.vendor_code == test_vendor.vendor_code
assert result.owner_id == test_user.id
def test_validate_shop_access_admin_can_access_any_shop(
self, db, test_shop, test_admin
def test_validate_vendor_access_admin_can_access_any_vendor(
self, db, test_vendor, test_admin
):
"""Test that admin users can access any shop"""
result = self.service.validate_shop_access(db, test_shop.shop_code, test_admin)
"""Test that admin users can access any vendor """
result = self.service.validate_vendor_access(db, test_vendor.vendor_code, test_admin)
assert result.shop_code == test_shop.shop_code
assert result.vendor_code == test_vendor.vendor_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(ShopNotFoundException) as exc_info:
self.service.validate_shop_access(db, "NONEXISTENT", test_user)
def test_validate_vendor_access_vendor_not_found(self, db, test_user):
"""Test vendor access validation when vendor doesn't exist"""
with pytest.raises(VendorNotFoundException) as exc_info:
self.service.validate_vendor_access(db, "NONEXISTENT", test_user)
exception = exc_info.value
assert exception.error_code == "SHOP_NOT_FOUND"
assert exception.error_code == "VENDOR_NOT_FOUND"
assert exception.status_code == 404
assert "NONEXISTENT" in exception.message
def test_validate_shop_access_permission_denied(
self, db, test_shop, test_user, other_user
def test_validate_vendor_access_permission_denied(
self, db, test_vendor, 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
"""Test vendor access validation when user doesn't own the vendor """
# Set the vendor owner to a different user
test_vendor.owner_id = other_user.id
db.commit()
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
self.service.validate_shop_access(db, test_shop.shop_code, test_user)
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
self.service.validate_vendor_access(db, test_vendor.vendor_code, test_user)
exception = exc_info.value
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"
assert exception.status_code == 403
assert test_shop.shop_code in exception.message
assert test_vendor.vendor_code in exception.message
def test_create_import_job_success(self, db, test_shop, test_user):
def test_create_import_job_success(self, db, test_vendor, test_user):
"""Test successful creation of import job"""
# Set the shop owner to the test user
test_shop.owner_id = test_user.id
# Set the vendor owner to the test user
test_vendor.owner_id = test_user.id
db.commit()
request = MarketplaceImportJobRequest(
url="https://example.com/products.csv",
marketplace="Amazon",
shop_code=test_shop.shop_code,
vendor_code=test_vendor.vendor_code,
batch_size=1000,
)
result = self.service.create_import_job(db, request, test_user)
assert result.marketplace == "Amazon"
assert result.shop_id == test_shop.id
assert result.vendor_id == test_vendor.id
assert result.user_id == test_user.id
assert result.status == "pending"
assert result.source_url == "https://example.com/products.csv"
assert result.shop_name == test_shop.shop_name
assert result.vendor_name == test_vendor.vendor_name
def test_create_import_job_invalid_shop(self, db, test_user):
"""Test import job creation with invalid shop"""
def test_create_import_job_invalid_vendor(self, db, test_user):
"""Test import job creation with invalid vendor """
request = MarketplaceImportJobRequest(
url="https://example.com/products.csv",
marketplace="Amazon",
shop_code="INVALID_SHOP",
vendor_code="INVALID_SHOP",
batch_size=1000,
)
with pytest.raises(ShopNotFoundException) as exc_info:
with pytest.raises(VendorNotFoundException) as exc_info:
self.service.create_import_job(db, request, test_user)
exception = exc_info.value
assert exception.error_code == "SHOP_NOT_FOUND"
assert exception.error_code == "VENDOR_NOT_FOUND"
assert "INVALID_SHOP" in exception.message
def test_create_import_job_unauthorized_access(self, db, test_shop, test_user, other_user):
"""Test import job creation with unauthorized shop access"""
# Set the shop owner to a different user
test_shop.owner_id = other_user.id
def test_create_import_job_unauthorized_access(self, db, test_vendor, test_user, other_user):
"""Test import job creation with unauthorized vendor access"""
# Set the vendor owner to a different user
test_vendor.owner_id = other_user.id
db.commit()
request = MarketplaceImportJobRequest(
url="https://example.com/products.csv",
marketplace="Amazon",
shop_code=test_shop.shop_code,
vendor_code=test_vendor.vendor_code,
batch_size=1000,
)
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
self.service.create_import_job(db, request, test_user)
exception = exc_info.value
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"
def test_get_import_job_by_id_success(self, db, test_marketplace_import_job, test_user):
"""Test getting import job by ID for job owner"""
@@ -194,7 +194,7 @@ class TestMarketplaceService:
assert len(jobs) >= 1
assert any(job.marketplace == test_marketplace_import_job.marketplace for job in jobs)
def test_get_import_jobs_with_pagination(self, db, test_user, test_shop):
def test_get_import_jobs_with_pagination(self, db, test_user, test_vendor):
"""Test getting import jobs with pagination"""
unique_id = str(uuid.uuid4())[:8]
@@ -203,9 +203,9 @@ class TestMarketplaceService:
job = MarketplaceImportJob(
status="completed",
marketplace=f"Marketplace_{unique_id}_{i}",
shop_name=f"Test_Shop_{unique_id}_{i}",
vendor_name=f"Test_vendor_{unique_id}_{i}",
user_id=test_user.id,
shop_id=test_shop.id,
vendor_id=test_vendor.id,
source_url=f"https://test-{i}.example.com/import",
imported_count=0,
updated_count=0,
@@ -296,7 +296,7 @@ class TestMarketplaceService:
assert response.marketplace == test_marketplace_import_job.marketplace
assert response.imported == (test_marketplace_import_job.imported_count or 0)
def test_cancel_import_job_success(self, db, test_user, test_shop):
def test_cancel_import_job_success(self, db, test_user, test_vendor):
"""Test cancelling a pending import job"""
unique_id = str(uuid.uuid4())[:8]
@@ -304,9 +304,9 @@ class TestMarketplaceService:
job = MarketplaceImportJob(
status="pending",
marketplace="Amazon",
shop_name=f"TEST_SHOP_{unique_id}",
vendor_name=f"TEST_VENDOR_{unique_id}",
user_id=test_user.id,
shop_id=test_shop.id,
vendor_id=test_vendor.id,
source_url="https://test.example.com/import",
imported_count=0,
updated_count=0,
@@ -354,7 +354,7 @@ class TestMarketplaceService:
assert exception.status_code == 400
assert "completed" in exception.message
def test_delete_import_job_success(self, db, test_user, test_shop):
def test_delete_import_job_success(self, db, test_user, test_vendor):
"""Test deleting a completed import job"""
unique_id = str(uuid.uuid4())[:8]
@@ -362,9 +362,9 @@ class TestMarketplaceService:
job = MarketplaceImportJob(
status="completed",
marketplace="Amazon",
shop_name=f"TEST_SHOP_{unique_id}",
vendor_name=f"TEST_VENDOR_{unique_id}",
user_id=test_user.id,
shop_id=test_shop.id,
vendor_id=test_vendor.id,
source_url="https://test.example.com/import",
imported_count=0,
updated_count=0,
@@ -404,7 +404,7 @@ class TestMarketplaceService:
exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_OWNED"
def test_delete_import_job_invalid_status(self, db, test_user, test_shop):
def test_delete_import_job_invalid_status(self, db, test_user, test_vendor):
"""Test deleting a job that can't be deleted"""
unique_id = str(uuid.uuid4())[:8]
@@ -412,9 +412,9 @@ class TestMarketplaceService:
job = MarketplaceImportJob(
status="pending",
marketplace="Amazon",
shop_name=f"TEST_SHOP_{unique_id}",
vendor_name=f"TEST_VENDOR_{unique_id}",
user_id=test_user.id,
shop_id=test_shop.id,
vendor_id=test_vendor.id,
source_url="https://test.example.com/import",
imported_count=0,
updated_count=0,
@@ -434,25 +434,25 @@ class TestMarketplaceService:
assert "pending" in exception.message
# Test edge cases and error scenarios
def test_validate_shop_access_case_insensitive(self, db, test_shop, test_user):
"""Test shop access validation is case insensitive"""
test_shop.owner_id = test_user.id
def test_validate_vendor_access_case_insensitive(self, db, test_vendor, test_user):
"""Test vendor access validation is case insensitive"""
test_vendor.owner_id = test_user.id
db.commit()
# Test with lowercase shop code
result = self.service.validate_shop_access(db, test_shop.shop_code.lower(), test_user)
assert result.shop_code == test_shop.shop_code
# Test with lowercase vendor code
result = self.service.validate_vendor_access(db, test_vendor.vendor_code.lower(), test_user)
assert result.vendor_code == test_vendor.vendor_code
# Test with uppercase shop code
result = self.service.validate_shop_access(db, test_shop.shop_code.upper(), test_user)
assert result.shop_code == test_shop.shop_code
# Test with uppercase vendor code
result = self.service.validate_vendor_access(db, test_vendor.vendor_code.upper(), test_user)
assert result.vendor_code == test_vendor.vendor_code
def test_create_import_job_database_error(self, db_with_error, test_user):
"""Test import job creation handles database errors"""
request = MarketplaceImportJobRequest(
url="https://example.com/products.csv",
marketplace="Amazon",
shop_code="TEST_SHOP",
vendor_code="TEST_SHOP",
batch_size=1000,
)

View File

@@ -1,365 +0,0 @@
# tests/test_shop_service.py (updated to use custom exceptions)
import pytest
from app.services.shop_service import ShopService
from app.exceptions import (
ShopNotFoundException,
ShopAlreadyExistsException,
UnauthorizedShopAccessException,
InvalidShopDataException,
MarketplaceProductNotFoundException,
ProductAlreadyExistsException,
MaxShopsReachedException,
ValidationException,
)
from models.schemas.shop import ShopCreate
from models.schemas.product import ProductCreate
@pytest.mark.unit
@pytest.mark.shops
class TestShopService:
"""Test suite for ShopService following the application's exception 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(ShopAlreadyExistsException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
assert exception.status_code == 409
assert exception.error_code == "SHOP_ALREADY_EXISTS"
assert test_shop.shop_code.upper() in exception.message
assert "shop_code" in exception.details
def test_create_shop_invalid_data_empty_code(self, db, test_user):
"""Test shop creation fails with empty shop code"""
shop_data = ShopCreate(shop_code="", shop_name="Test Shop")
with pytest.raises(InvalidShopDataException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
assert exception.status_code == 422
assert exception.error_code == "INVALID_SHOP_DATA"
assert exception.details["field"] == "shop_code"
def test_create_shop_invalid_data_empty_name(self, db, test_user):
"""Test shop creation fails with empty shop name"""
shop_data = ShopCreate(shop_code="VALIDCODE", shop_name="")
with pytest.raises(InvalidShopDataException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
assert exception.error_code == "INVALID_SHOP_DATA"
assert exception.details["field"] == "shop_name"
def test_create_shop_invalid_code_format(self, db, test_user):
"""Test shop creation fails with invalid shop code format"""
shop_data = ShopCreate(shop_code="INVALID@CODE!", shop_name="Test Shop")
with pytest.raises(InvalidShopDataException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
assert exception.error_code == "INVALID_SHOP_DATA"
assert exception.details["field"] == "shop_code"
assert "letters, numbers, underscores, and hyphens" in exception.message
def test_create_shop_max_shops_reached(self, db, test_user, monkeypatch):
"""Test shop creation fails when user reaches maximum shops"""
# Mock the shop count check to simulate user at limit
def mock_check_shop_limit(self, db, user):
raise MaxShopsReachedException(max_shops=5, user_id=user.id)
monkeypatch.setattr(ShopService, "_check_shop_limit", mock_check_shop_limit)
shop_data = ShopCreate(shop_code="NEWSHOP", shop_name="New Shop")
with pytest.raises(MaxShopsReachedException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
assert exception.status_code == 400
assert exception.error_code == "MAX_SHOPS_REACHED"
assert exception.details["max_shops"] == 5
assert exception.details["user_id"] == test_user.id
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 raises proper exception"""
with pytest.raises(ShopNotFoundException) as exc_info:
self.service.get_shop_by_code(db, "NONEXISTENT", test_user)
exception = exc_info.value
assert exception.status_code == 404
assert exception.error_code == "SHOP_NOT_FOUND"
assert exception.details["resource_type"] == "Shop"
assert exception.details["identifier"] == "NONEXISTENT"
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(UnauthorizedShopAccessException) as exc_info:
self.service.get_shop_by_code(db, inactive_shop.shop_code, test_user)
exception = exc_info.value
assert exception.status_code == 403
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
assert exception.details["shop_code"] == inactive_shop.shop_code
assert exception.details["user_id"] == test_user.id
def test_add_product_to_shop_success(self, db, test_shop, unique_product):
"""Test successfully adding product to shop"""
product_data = ProductCreate(
marketplace_product_id=unique_product.marketplace_product_id,
price="15.99",
is_featured=True,
)
product = self.service.add_product_to_shop(
db, test_shop, product_data
)
assert product is not None
assert product.shop_id == test_shop.id
assert product.marketplace_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"""
product_data = ProductCreate(marketplace_product_id="NONEXISTENT", price="15.99")
with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
self.service.add_product_to_shop(db, test_shop, product_data)
exception = exc_info.value
assert exception.status_code == 404
assert exception.error_code == "PRODUCT_NOT_FOUND"
assert exception.details["resource_type"] == "MarketplaceProduct"
assert exception.details["identifier"] == "NONEXISTENT"
def test_add_product_to_shop_already_exists(self, db, test_shop, test_product):
"""Test adding product that's already in shop fails"""
product_data = ProductCreate(
marketplace_product_id=test_product.marketplace_product.marketplace_product_id, price="15.99"
)
with pytest.raises(ProductAlreadyExistsException) as exc_info:
self.service.add_product_to_shop(db, test_shop, product_data)
exception = exc_info.value
assert exception.status_code == 409
assert exception.error_code == "PRODUCT_ALREADY_EXISTS"
assert exception.details["shop_code"] == test_shop.shop_code
assert exception.details["marketplace_product_id"] == test_product.marketplace_product.marketplace_product_id
def test_get_products_owner_access(
self, db, test_user, test_shop, test_product
):
"""Test shop owner can get shop products"""
products, total = self.service.get_products(db, test_shop, test_user)
assert total >= 1
assert len(products) >= 1
product_ids = [p.marketplace_product_id for p in products]
assert test_product.marketplace_product_id in product_ids
def test_get_products_access_denied(self, db, test_user, inactive_shop):
"""Test non-owner cannot access unverified shop products"""
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
self.service.get_products(db, inactive_shop, test_user)
exception = exc_info.value
assert exception.status_code == 403
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
assert exception.details["shop_code"] == inactive_shop.shop_code
assert exception.details["user_id"] == test_user.id
def test_get_products_with_filters(self, db, test_user, test_shop, test_product):
"""Test getting shop products with various filters"""
# Test active only filter
products, total = self.service.get_products(
db, test_shop, test_user, active_only=True
)
assert all(p.is_active for p in products)
# Test featured only filter
products, total = self.service.get_products(
db, test_shop, test_user, featured_only=True
)
assert all(p.is_featured for p in products)
# Test exception handling for generic errors
def test_create_shop_database_error(self, db, test_user, monkeypatch):
"""Test shop creation handles database errors gracefully"""
def mock_commit():
raise Exception("Database connection failed")
monkeypatch.setattr(db, "commit", mock_commit)
shop_data = ShopCreate(shop_code="NEWSHOP", shop_name="Test Shop")
with pytest.raises(ValidationException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
assert exception.status_code == 422
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to create shop" in exception.message
def test_get_shops_database_error(self, db, test_user, monkeypatch):
"""Test get shops handles database errors gracefully"""
def mock_query(*args):
raise Exception("Database query failed")
monkeypatch.setattr(db, "query", mock_query)
with pytest.raises(ValidationException) as exc_info:
self.service.get_shops(db, test_user)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to retrieve shops" in exception.message
def test_add_product_database_error(self, db, test_shop, unique_product, monkeypatch):
"""Test add product handles database errors gracefully"""
def mock_commit():
raise Exception("Database commit failed")
monkeypatch.setattr(db, "commit", mock_commit)
product_data = ProductCreate(
marketplace_product_id=unique_product.marketplace_product_id, price="15.99"
)
with pytest.raises(ValidationException) as exc_info:
self.service.add_product_to_shop(db, test_shop, product_data)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to add product to shop" in exception.message
@pytest.mark.unit
@pytest.mark.shops
class TestShopServiceExceptionDetails:
"""Additional tests focusing specifically on exception structure and details"""
def setup_method(self):
self.service = ShopService()
def test_exception_to_dict_structure(self, db, test_user, test_shop):
"""Test that exceptions can be properly serialized to dict for API responses"""
shop_data = ShopCreate(
shop_code=test_shop.shop_code, shop_name="Duplicate"
)
with pytest.raises(ShopAlreadyExistsException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
exception_dict = exception.to_dict()
# Verify structure matches expected API response format
assert "error_code" in exception_dict
assert "message" in exception_dict
assert "status_code" in exception_dict
assert "details" in exception_dict
# Verify values
assert exception_dict["error_code"] == "SHOP_ALREADY_EXISTS"
assert exception_dict["status_code"] == 409
assert isinstance(exception_dict["details"], dict)
def test_validation_exception_field_details(self, db, test_user):
"""Test validation exceptions include field-specific details"""
shop_data = ShopCreate(shop_code="", shop_name="Test")
with pytest.raises(InvalidShopDataException) as exc_info:
self.service.create_shop(db, shop_data, test_user)
exception = exc_info.value
assert exception.details["field"] == "shop_code"
assert exception.status_code == 422
assert "required" in exception.message.lower()
def test_authorization_exception_user_details(self, db, test_user, inactive_shop):
"""Test authorization exceptions include user context"""
with pytest.raises(UnauthorizedShopAccessException) as exc_info:
self.service.get_shop_by_code(db, inactive_shop.shop_code, test_user)
exception = exc_info.value
assert exception.details["shop_code"] == inactive_shop.shop_code
assert exception.details["user_id"] == test_user.id
assert "Unauthorized access" in exception.message

View File

@@ -23,7 +23,7 @@ class TestStatsService:
assert "unique_brands" in stats
assert "unique_categories" in stats
assert "unique_marketplaces" in stats
assert "unique_shops" in stats
assert "unique_vendors" in stats
assert "total_stock_entries" in stats
assert "total_inventory_quantity" in stats
@@ -41,7 +41,7 @@ class TestStatsService:
brand="DifferentBrand",
google_product_category="Different Category",
marketplace="Amazon",
shop_name="AmazonShop",
vendor_name="AmazonShop",
price="15.99",
currency="EUR",
),
@@ -51,7 +51,7 @@ class TestStatsService:
brand="ThirdBrand",
google_product_category="Third Category",
marketplace="eBay",
shop_name="eBayShop",
vendor_name="eBayShop",
price="25.99",
currency="USD",
),
@@ -61,7 +61,7 @@ class TestStatsService:
brand="TestBrand", # Same as test_marketplace_product
google_product_category="Different Category",
marketplace="Letzshop", # Same as test_marketplace_product
shop_name="DifferentShop",
vendor_name="DifferentShop",
price="35.99",
currency="EUR",
),
@@ -75,7 +75,7 @@ class TestStatsService:
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
assert stats["unique_vendors"] >= 3 # At least 3 different vendors
def test_get_comprehensive_stats_handles_nulls(self, db):
"""Test comprehensive stats handles null/empty values correctly"""
@@ -87,7 +87,7 @@ class TestStatsService:
brand=None, # Null brand
google_product_category=None, # Null category
marketplace=None, # Null marketplace
shop_name=None, # Null shop
vendor_name=None, # Null vendor
price="10.00",
currency="EUR",
),
@@ -97,7 +97,7 @@ class TestStatsService:
brand="", # Empty brand
google_product_category="", # Empty category
marketplace="", # Empty marketplace
shop_name="", # Empty shop
vendor_name="", # Empty vendor
price="15.00",
currency="EUR",
),
@@ -109,11 +109,11 @@ class TestStatsService:
# 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
# Brands, categories, marketplaces, vendors 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)
assert isinstance(stats["unique_vendors"], int)
def test_get_marketplace_breakdown_stats_basic(self, db, test_marketplace_product):
"""Test getting marketplace breakdown stats with basic data"""
@@ -129,7 +129,7 @@ class TestStatsService:
)
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_vendors"] >= 1
assert test_marketplace_stat["unique_brands"] >= 1
def test_get_marketplace_breakdown_stats_multiple_marketplaces(
@@ -143,7 +143,7 @@ class TestStatsService:
title="Amazon MarketplaceProduct 1",
brand="AmazonBrand1",
marketplace="Amazon",
shop_name="AmazonShop1",
vendor_name="AmazonShop1",
price="20.00",
currency="EUR",
),
@@ -152,7 +152,7 @@ class TestStatsService:
title="Amazon MarketplaceProduct 2",
brand="AmazonBrand2",
marketplace="Amazon",
shop_name="AmazonShop2",
vendor_name="AmazonShop2",
price="25.00",
currency="EUR",
),
@@ -161,7 +161,7 @@ class TestStatsService:
title="eBay MarketplaceProduct",
brand="eBayBrand",
marketplace="eBay",
shop_name="eBayShop",
vendor_name="eBayShop",
price="30.00",
currency="USD",
),
@@ -180,13 +180,13 @@ class TestStatsService:
# 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_vendors"] == 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_vendors"] == 1
assert ebay_stat["unique_brands"] == 1
def test_get_marketplace_breakdown_stats_excludes_nulls(self, db):
@@ -196,7 +196,7 @@ class TestStatsService:
marketplace_product_id="NULLMARKET001",
title="MarketplaceProduct without marketplace",
marketplace=None,
shop_name="SomeShop",
vendor_name="SomeShop",
brand="SomeBrand",
price="10.00",
currency="EUR",
@@ -228,7 +228,7 @@ class TestStatsService:
title="Brand MarketplaceProduct 1",
brand="BrandA",
marketplace="Test",
shop_name="TestShop",
vendor_name="TestVendor",
price="10.00",
currency="EUR",
),
@@ -237,7 +237,7 @@ class TestStatsService:
title="Brand MarketplaceProduct 2",
brand="BrandB",
marketplace="Test",
shop_name="TestShop",
vendor_name="TestVendor",
price="15.00",
currency="EUR",
),
@@ -261,7 +261,7 @@ class TestStatsService:
title="Category MarketplaceProduct 1",
google_product_category="Electronics",
marketplace="Test",
shop_name="TestShop",
vendor_name="TestVendor",
price="10.00",
currency="EUR",
),
@@ -270,7 +270,7 @@ class TestStatsService:
title="Category MarketplaceProduct 2",
google_product_category="Books",
marketplace="Test",
shop_name="TestShop",
vendor_name="TestVendor",
price="15.00",
currency="EUR",
),
@@ -291,7 +291,7 @@ class TestStatsService:
marketplace_product_id="MARKET001",
title="Marketplace MarketplaceProduct 1",
marketplace="Amazon",
shop_name="AmazonShop",
vendor_name="AmazonShop",
price="10.00",
currency="EUR",
),
@@ -299,7 +299,7 @@ class TestStatsService:
marketplace_product_id="MARKET002",
title="Marketplace MarketplaceProduct 2",
marketplace="eBay",
shop_name="eBayShop",
vendor_name="eBayShop",
price="15.00",
currency="EUR",
),
@@ -312,23 +312,23 @@ class TestStatsService:
assert count >= 2 # At least Amazon and eBay, plus test_marketplace_product marketplace
assert isinstance(count, int)
def test_get_unique_shops_count(self, db, test_marketplace_product):
"""Test getting unique shops count"""
# Add products with different shop names
def test_get_unique_vendors_count(self, db, test_marketplace_product):
"""Test getting unique vendors count"""
# Add products with different vendor names
products = [
MarketplaceProduct(
marketplace_product_id="SHOP001",
title="Shop MarketplaceProduct 1",
title="Vendor MarketplaceProduct 1",
marketplace="Test",
shop_name="ShopA",
vendor_name="ShopA",
price="10.00",
currency="EUR",
),
MarketplaceProduct(
marketplace_product_id="SHOP002",
title="Shop MarketplaceProduct 2",
title="Vendor MarketplaceProduct 2",
marketplace="Test",
shop_name="ShopB",
vendor_name="ShopB",
price="15.00",
currency="EUR",
),
@@ -336,9 +336,9 @@ class TestStatsService:
db.add_all(products)
db.commit()
count = self.service._get_unique_shops_count(db)
count = self.service._get_unique_vendors_count(db)
assert count >= 2 # At least ShopA and ShopB, plus test_marketplace_product shop
assert count >= 2 # At least ShopA and ShopB, plus test_marketplace_product vendor
assert isinstance(count, int)
def test_get_stock_statistics(self, db, test_stock):
@@ -350,14 +350,14 @@ class TestStatsService:
location="LOCATION2",
quantity=25,
reserved_quantity=5,
shop_id=test_stock.shop_id,
vendor_id=test_stock.vendor_id,
),
Stock(
gtin="1234567890125",
location="LOCATION3",
quantity=0, # Out of stock
reserved_quantity=0,
shop_id=test_stock.shop_id,
vendor_id=test_stock.vendor_id,
),
]
db.add_all(additional_stocks)
@@ -379,7 +379,7 @@ class TestStatsService:
title="Specific MarketplaceProduct 1",
brand="SpecificBrand1",
marketplace="SpecificMarket",
shop_name="SpecificShop1",
vendor_name="SpecificShop1",
price="10.00",
currency="EUR",
),
@@ -388,7 +388,7 @@ class TestStatsService:
title="Specific MarketplaceProduct 2",
brand="SpecificBrand2",
marketplace="SpecificMarket",
shop_name="SpecificShop2",
vendor_name="SpecificShop2",
price="15.00",
currency="EUR",
),
@@ -397,7 +397,7 @@ class TestStatsService:
title="Other MarketplaceProduct",
brand="OtherBrand",
marketplace="OtherMarket",
shop_name="OtherShop",
vendor_name="OtherShop",
price="20.00",
currency="EUR",
),
@@ -412,25 +412,25 @@ class TestStatsService:
assert "SpecificBrand2" in brands
assert "OtherBrand" not in brands
def test_get_shops_by_marketplace(self, db):
"""Test getting shops for a specific marketplace"""
def test_get_vendors_by_marketplace(self, db):
"""Test getting vendors for a specific marketplace"""
# Create products for specific marketplace
marketplace_products = [
MarketplaceProduct(
marketplace_product_id="SHOPTEST001",
title="Shop Test MarketplaceProduct 1",
title="Vendor Test MarketplaceProduct 1",
brand="TestBrand",
marketplace="TestMarketplace",
shop_name="TestShop1",
vendor_name="TestVendor1",
price="10.00",
currency="EUR",
),
MarketplaceProduct(
marketplace_product_id="SHOPTEST002",
title="Shop Test MarketplaceProduct 2",
title="Vendor Test MarketplaceProduct 2",
brand="TestBrand",
marketplace="TestMarketplace",
shop_name="TestShop2",
vendor_name="TestVendor2",
price="15.00",
currency="EUR",
),
@@ -438,11 +438,11 @@ class TestStatsService:
db.add_all(marketplace_products)
db.commit()
shops = self.service._get_shops_by_marketplace(db, "TestMarketplace")
vendors =self.service._get_vendors_by_marketplace(db, "TestMarketplace")
assert len(shops) == 2
assert "TestShop1" in shops
assert "TestShop2" in shops
assert len(vendors) == 2
assert "TestVendor1" in vendors
assert "TestVendor2" in vendors
def test_get_products_by_marketplace(self, db):
"""Test getting product count for a specific marketplace"""
@@ -452,7 +452,7 @@ class TestStatsService:
marketplace_product_id="COUNT001",
title="Count MarketplaceProduct 1",
marketplace="CountMarketplace",
shop_name="CountShop",
vendor_name="CountShop",
price="10.00",
currency="EUR",
),
@@ -460,7 +460,7 @@ class TestStatsService:
marketplace_product_id="COUNT002",
title="Count MarketplaceProduct 2",
marketplace="CountMarketplace",
shop_name="CountShop",
vendor_name="CountShop",
price="15.00",
currency="EUR",
),
@@ -468,7 +468,7 @@ class TestStatsService:
marketplace_product_id="COUNT003",
title="Count MarketplaceProduct 3",
marketplace="CountMarketplace",
shop_name="CountShop",
vendor_name="CountShop",
price="20.00",
currency="EUR",
),
@@ -494,7 +494,7 @@ class TestStatsService:
assert stats["unique_brands"] == 0
assert stats["unique_categories"] == 0
assert stats["unique_marketplaces"] == 0
assert stats["unique_shops"] == 0
assert stats["unique_vendors"] == 0
assert stats["total_stock_entries"] == 0
assert stats["total_inventory_quantity"] == 0

View File

@@ -0,0 +1,365 @@
# tests/test_vendor_service.py (updated to use custom exceptions)
import pytest
from app.services.vendor_service import VendorService
from app.exceptions import (
VendorNotFoundException,
VendorAlreadyExistsException,
UnauthorizedVendorAccessException,
InvalidVendorDataException,
MarketplaceProductNotFoundException,
ProductAlreadyExistsException,
MaxVendorsReachedException,
ValidationException,
)
from models.schemas.vendor import VendorCreate
from models.schemas.product import ProductCreate
@pytest.mark.unit
@pytest.mark.vendors
class TestVendorService:
"""Test suite for ShopService following the application's exception patterns"""
def setup_method(self):
"""Setup method following the same pattern as admin service tests"""
self.service = VendorService()
def test_create_vendor_success(self, db, test_user, vendor_factory):
"""Test successful vendor creation"""
vendor_data = VendorCreate(
vendor_code="NEWVENDOR",
vendor_name="New Test Shop",
description="A new test vendor ",
)
vendor = self.service.create_vendor(db, vendor_data, test_user)
assert vendor is not None
assert vendor.vendor_code == "NEWVENDOR"
assert vendor.owner_id == test_user.id
assert vendor.is_verified is False # Regular user creates unverified vendor
def test_create_vendor_admin_auto_verify(self, db, test_admin, vendor_factory):
"""Test admin creates verified vendor automatically"""
vendor_data = VendorCreate(vendor_code="ADMINSHOP", vendor_name="Admin Test Shop")
vendor = self.service.create_vendor(db, vendor_data, test_admin)
assert vendor.is_verified is True # Admin creates verified vendor
def test_create_vendor_duplicate_code(self, db, test_user, test_vendor):
"""Test vendor creation fails with duplicate vendor code"""
vendor_data = VendorCreate(
vendor_code=test_vendor.vendor_code, vendor_name=test_vendor.vendor_name
)
with pytest.raises(VendorAlreadyExistsException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
assert exception.status_code == 409
assert exception.error_code == "VENDOR_ALREADY_EXISTS"
assert test_vendor.vendor_code.upper() in exception.message
assert "vendor_code" in exception.details
def test_create_vendor_invalid_data_empty_code(self, db, test_user):
"""Test vendor creation fails with empty vendor code"""
vendor_data = VendorCreate(vendor_code="", vendor_name="Test Shop")
with pytest.raises(InvalidVendorDataException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
assert exception.status_code == 422
assert exception.error_code == "INVALID_VENDOR_DATA"
assert exception.details["field"] == "vendor_code"
def test_create_vendor_invalid_data_empty_name(self, db, test_user):
"""Test vendor creation fails with empty vendor name"""
vendor_data = VendorCreate(vendor_code="VALIDCODE", vendor_name="")
with pytest.raises(InvalidVendorDataException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
assert exception.error_code == "INVALID_VENDOR_DATA"
assert exception.details["field"] == "vendor_name"
def test_create_vendor_invalid_code_format(self, db, test_user):
"""Test vendor creation fails with invalid vendor code format"""
vendor_data = VendorCreate(vendor_code="INVALID@CODE!", vendor_name="Test Shop")
with pytest.raises(InvalidVendorDataException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
assert exception.error_code == "INVALID_VENDOR_DATA"
assert exception.details["field"] == "vendor_code"
assert "letters, numbers, underscores, and hyphens" in exception.message
def test_create_vendor_max_vendors_reached(self, db, test_user, monkeypatch):
"""Test vendor creation fails when user reaches maximum vendors"""
# Mock the vendor count check to simulate user at limit
def mock_check_vendor_limit(self, db, user):
raise MaxVendorsReachedException(max_vendors=5, user_id=user.id)
monkeypatch.setattr(VendorService, "_check_vendor_limit", mock_check_vendor_limit)
vendor_data = VendorCreate(vendor_code="NEWVENDOR", vendor_name="New Vendor")
with pytest.raises(MaxVendorsReachedException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
assert exception.status_code == 400
assert exception.error_code == "MAX_VENDORS_REACHED"
assert exception.details["max_vendors"] == 5
assert exception.details["user_id"] == test_user.id
def test_get_vendors_regular_user(self, db, test_user, test_vendor, inactive_vendor):
"""Test regular user can only see active verified vendors and own vendors"""
vendors, total = self.service.get_vendors(db, test_user, skip=0, limit=10)
vendor_codes = [vendor.vendor_code for vendor in vendors]
assert test_vendor.vendor_code in vendor_codes
assert inactive_vendor.vendor_code not in vendor_codes
def test_get_vendors_admin_user(
self, db, test_admin, test_vendor, inactive_vendor, verified_vendor
):
"""Test admin user can see all vendors with filters"""
vendors, total = self.service.get_vendors(
db, test_admin, active_only=False, verified_only=False
)
vendor_codes = [vendor.vendor_code for vendor in vendors]
assert test_vendor.vendor_code in vendor_codes
assert inactive_vendor.vendor_code in vendor_codes
assert verified_vendor.vendor_code in vendor_codes
def test_get_vendor_by_code_owner_access(self, db, test_user, test_vendor):
"""Test vendor owner can access their own vendor """
vendor = self.service.get_vendor_by_code(db, test_vendor.vendor_code.lower(), test_user)
assert vendor is not None
assert vendor.id == test_vendor.id
def test_get_vendor_by_code_admin_access(self, db, test_admin, test_vendor):
"""Test admin can access any vendor """
vendor = self.service.get_vendor_by_code(
db, test_vendor.vendor_code.lower(), test_admin
)
assert vendor is not None
assert vendor.id == test_vendor.id
def test_get_vendor_by_code_not_found(self, db, test_user):
"""Test vendor not found raises proper exception"""
with pytest.raises(VendorNotFoundException) as exc_info:
self.service.get_vendor_by_code(db, "NONEXISTENT", test_user)
exception = exc_info.value
assert exception.status_code == 404
assert exception.error_code == "VENDOR_NOT_FOUND"
assert exception.details["resource_type"] == "Shop"
assert exception.details["identifier"] == "NONEXISTENT"
def test_get_vendor_by_code_access_denied(self, db, test_user, inactive_vendor):
"""Test regular user cannot access unverified vendor they don't own"""
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
self.service.get_vendor_by_code(db, inactive_vendor.vendor_code, test_user)
exception = exc_info.value
assert exception.status_code == 403
assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"
assert exception.details["vendor_code"] == inactive_vendor.vendor_code
assert exception.details["user_id"] == test_user.id
def test_add_product_to_vendor_success(self, db, test_vendor, unique_product):
"""Test successfully adding product to vendor """
product_data = ProductCreate(
marketplace_product_id=unique_product.marketplace_product_id,
price="15.99",
is_featured=True,
)
product = self.service.add_product_to_catalog(
db, test_vendor, product_data
)
assert product is not None
assert product.vendor_id == test_vendor.id
assert product.marketplace_product_id == unique_product.id
def test_add_product_to_vendor_product_not_found(self, db, test_vendor):
"""Test adding non-existent product to vendor fails"""
product_data = ProductCreate(marketplace_product_id="NONEXISTENT", price="15.99")
with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
self.service.add_product_to_catalog(db, test_vendor, product_data)
exception = exc_info.value
assert exception.status_code == 404
assert exception.error_code == "PRODUCT_NOT_FOUND"
assert exception.details["resource_type"] == "MarketplaceProduct"
assert exception.details["identifier"] == "NONEXISTENT"
def test_add_product_to_vendor_already_exists(self, db, test_vendor, test_product):
"""Test adding product that's already in vendor fails"""
product_data = ProductCreate(
marketplace_product_id=test_product.marketplace_product.marketplace_product_id, price="15.99"
)
with pytest.raises(ProductAlreadyExistsException) as exc_info:
self.service.add_product_to_catalog(db, test_vendor, product_data)
exception = exc_info.value
assert exception.status_code == 409
assert exception.error_code == "PRODUCT_ALREADY_EXISTS"
assert exception.details["vendor_code"] == test_vendor.vendor_code
assert exception.details["marketplace_product_id"] == test_product.marketplace_product.marketplace_product_id
def test_get_products_owner_access(
self, db, test_user, test_vendor, test_product
):
"""Test vendor owner can get vendor products"""
products, total = self.service.get_products(db, test_vendor, test_user)
assert total >= 1
assert len(products) >= 1
product_ids = [p.marketplace_product_id for p in products]
assert test_product.marketplace_product_id in product_ids
def test_get_products_access_denied(self, db, test_user, inactive_vendor):
"""Test non-owner cannot access unverified vendor products"""
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
self.service.get_products(db, inactive_vendor, test_user)
exception = exc_info.value
assert exception.status_code == 403
assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"
assert exception.details["vendor_code"] == inactive_vendor.vendor_code
assert exception.details["user_id"] == test_user.id
def test_get_products_with_filters(self, db, test_user, test_vendor, test_product):
"""Test getting vendor products with various filters"""
# Test active only filter
products, total = self.service.get_products(
db, test_vendor, test_user, active_only=True
)
assert all(p.is_active for p in products)
# Test featured only filter
products, total = self.service.get_products(
db, test_vendor, test_user, featured_only=True
)
assert all(p.is_featured for p in products)
# Test exception handling for generic errors
def test_create_vendor_database_error(self, db, test_user, monkeypatch):
"""Test vendor creation handles database errors gracefully"""
def mock_commit():
raise Exception("Database connection failed")
monkeypatch.setattr(db, "commit", mock_commit)
vendor_data = VendorCreate(vendor_code="NEWVENDOR", vendor_name="Test Shop")
with pytest.raises(ValidationException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
assert exception.status_code == 422
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to create vendor " in exception.message
def test_get_vendors_database_error(self, db, test_user, monkeypatch):
"""Test get vendors handles database errors gracefully"""
def mock_query(*args):
raise Exception("Database query failed")
monkeypatch.setattr(db, "query", mock_query)
with pytest.raises(ValidationException) as exc_info:
self.service.get_vendors(db, test_user)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to retrieve vendors" in exception.message
def test_add_product_database_error(self, db, test_vendor, unique_product, monkeypatch):
"""Test add product handles database errors gracefully"""
def mock_commit():
raise Exception("Database commit failed")
monkeypatch.setattr(db, "commit", mock_commit)
product_data = ProductCreate(
marketplace_product_id=unique_product.marketplace_product_id, price="15.99"
)
with pytest.raises(ValidationException) as exc_info:
self.service.add_product_to_catalog(db, test_vendor, product_data)
exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to add product to vendor " in exception.message
@pytest.mark.unit
@pytest.mark.vendors
class TestVendorServiceExceptionDetails:
"""Additional tests focusing specifically on exception structure and details"""
def setup_method(self):
self.service = VendorService()
def test_exception_to_dict_structure(self, db, test_user, test_vendor):
"""Test that exceptions can be properly serialized to dict for API responses"""
vendor_data = VendorCreate(
vendor_code=test_vendor.vendor_code, vendor_name="Duplicate"
)
with pytest.raises(VendorAlreadyExistsException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
exception_dict = exception.to_dict()
# Verify structure matches expected API response format
assert "error_code" in exception_dict
assert "message" in exception_dict
assert "status_code" in exception_dict
assert "details" in exception_dict
# Verify values
assert exception_dict["error_code"] == "VENDOR_ALREADY_EXISTS"
assert exception_dict["status_code"] == 409
assert isinstance(exception_dict["details"], dict)
def test_validation_exception_field_details(self, db, test_user):
"""Test validation exceptions include field-specific details"""
vendor_data = VendorCreate(vendor_code="", vendor_name="Test")
with pytest.raises(InvalidVendorDataException) as exc_info:
self.service.create_vendor(db, vendor_data, test_user)
exception = exc_info.value
assert exception.details["field"] == "vendor_code"
assert exception.status_code == 422
assert "required" in exception.message.lower()
def test_authorization_exception_user_details(self, db, test_user, inactive_vendor):
"""Test authorization exceptions include user context"""
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
self.service.get_vendor_by_code(db, inactive_vendor.vendor_code, test_user)
exception = exc_info.value
assert exception.details["vendor_code"] == inactive_vendor.vendor_code
assert exception.details["user_id"] == test_user.id
assert "Unauthorized access" in exception.message

View File

@@ -116,13 +116,13 @@ TEST002,Test MarketplaceProduct 2,15.99,TestMarket"""
"title": ["MarketplaceProduct 1", "MarketplaceProduct 2"],
"price": ["10.99", "15.99"],
"marketplace": ["TestMarket", "TestMarket"],
"shop_name": ["TestShop", "TestShop"],
"vendor_name": ["TestVendor", "TestVendor"],
}
)
mock_parse.return_value = mock_df
result = await self.processor.process_marketplace_csv_from_url(
"http://example.com/test.csv", "TestMarket", "TestShop", 1000, db
"http://example.com/test.csv", "TestMarket", "TestVendor", 1000, db
)
assert "imported" in result