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

@@ -10,7 +10,7 @@ from main import app
# Import all models to ensure they're registered with Base metadata
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop
from models.database.vendor import Vendor
from models.database.product import Product
from models.database.stock import Stock
from models.database.user import User
@@ -89,7 +89,7 @@ def cleanup():
pytest_plugins = [
"tests.fixtures.auth_fixtures",
"tests.fixtures.marketplace_product_fixtures",
"tests.fixtures.shop_fixtures",
"tests.fixtures.vendor_fixtures",
"tests.fixtures.marketplace_import_job_fixtures",
"tests.fixtures.testing_fixtures",
]

View File

@@ -5,14 +5,14 @@ from models.database.marketplace_import_job import MarketplaceImportJob
@pytest.fixture
def test_marketplace_import_job(db, test_shop, test_user):
def test_marketplace_import_job(db, test_vendor, test_user):
"""Create a test marketplace import job"""
job = MarketplaceImportJob(
marketplace="amazon",
shop_name="Test Import Shop",
vendor_name="Test Import Shop",
status="completed",
source_url="https://test-marketplace.example.com/import",
shop_id=test_shop.id,
vendor_id=test_vendor.id,
user_id=test_user.id,
imported_count=5,
updated_count=3,
@@ -26,14 +26,14 @@ def test_marketplace_import_job(db, test_shop, test_user):
return job
def create_test_marketplace_import_job(db, shop_id, user_id, **kwargs):
def create_test_marketplace_import_job(db, vendor_id, user_id, **kwargs):
"""Helper function to create MarketplaceImportJob with defaults"""
defaults = {
"marketplace": "test",
"shop_name": "Test Shop",
"vendor_name": "Test Shop",
"status": "pending",
"source_url": "https://test.example.com/import",
"shop_id": shop_id,
"vendor_id": vendor_id,
"user_id": user_id,
"imported_count": 0,
"updated_count": 0,

View File

@@ -19,7 +19,7 @@ def test_marketplace_product(db):
gtin="1234567890123",
availability="in stock",
marketplace="Letzshop",
shop_name="TestShop",
vendor_name="TestVendor",
)
db.add(marketplace_product)
db.commit()
@@ -41,7 +41,7 @@ def unique_product(db):
gtin=f"123456789{unique_id[:4]}",
availability="in stock",
marketplace="Letzshop",
shop_name=f"UniqueShop_{unique_id}",
vendor_name=f"UniqueShop_{unique_id}",
google_product_category=f"UniqueCategory_{unique_id}",
)
db.add(marketplace_product)
@@ -65,7 +65,7 @@ def multiple_products(db):
currency="EUR",
brand=f"MultiBrand_{i % 3}", # Create 3 different brands
marketplace=f"MultiMarket_{i % 2}", # Create 2 different marketplaces
shop_name=f"MultiShop_{i}",
vendor_name=f"MultiShop_{i}",
google_product_category=f"MultiCategory_{i % 2}", # Create 2 different categories
gtin=f"1234567890{i}{unique_id[:2]}",
)
@@ -89,7 +89,7 @@ def create_unique_marketplace_product_factory():
"price": "15.99",
"currency": "EUR",
"marketplace": "TestMarket",
"shop_name": "TestShop",
"vendor_name": "TestVendor",
}
defaults.update(kwargs)

View File

@@ -21,11 +21,11 @@ def empty_db(db):
# Clear only the tables that are relevant for admin service testing
# In order to respect foreign key constraints
tables_to_clear = [
"marketplace_import_jobs", # Has foreign keys to shops and users
"products", # Has foreign keys to shops and products
"marketplace_import_jobs", # Has foreign keys to vendors and users
"products", # Has foreign keys to vendors and products
"stock", # Fixed: singular not plural
"products", # Referenced by products
"shops", # Has foreign key to users
"vendors", # Has foreign key to users
"users" # Base table
]

View File

@@ -1,90 +1,90 @@
# tests/fixtures/shop_fixtures.py
# tests/fixtures/vendor_fixtures.py
import uuid
import pytest
from models.database.shop import Shop
from models.database.vendor import Vendor
from models.database.product import Product
from models.database.stock import Stock
@pytest.fixture
def test_shop(db, test_user):
"""Create a test shop with unique shop code"""
def test_vendor(db, test_user):
"""Create a test vendor with unique vendor code"""
unique_id = str(uuid.uuid4())[:8].upper() # Make unique ID uppercase
shop = Shop(
shop_code=f"TESTSHOP_{unique_id}", # Will be all uppercase
shop_name=f"Test Shop {unique_id.lower()}", # Keep display name readable
vendor = Vendor(
vendor_code=f"TESTVENDOR_{unique_id}", # Will be all uppercase
vendor_name=f"Test Vendor {unique_id.lower()}", # Keep display name readable
owner_id=test_user.id,
is_active=True,
is_verified=True,
)
db.add(shop)
db.add(vendor)
db.commit()
db.refresh(shop)
return shop
db.refresh(vendor)
return vendor
@pytest.fixture
def unique_shop(db, test_user):
"""Create a unique shop for tests that need isolated shop data"""
def unique_vendor(db, test_user):
"""Create a unique vendor for tests that need isolated vendor data"""
unique_id = str(uuid.uuid4())[:8]
shop = Shop(
shop_code=f"UNIQUESHOP_{unique_id}",
shop_name=f"Unique Test Shop {unique_id}",
description=f"A unique test shop {unique_id}",
vendor = Vendor(
vendor_code=f"UNIQUEVENDOR_{unique_id}",
vendor_name=f"Unique Test Vendor {unique_id}",
description=f"A unique test vendor {unique_id}",
owner_id=test_user.id,
is_active=True,
is_verified=True,
)
db.add(shop)
db.add(vendor)
db.commit()
db.refresh(shop)
return shop
db.refresh(vendor)
return vendor
@pytest.fixture
def inactive_shop(db, other_user):
"""Create an inactive shop owned by other_user"""
def inactive_vendor(db, other_user):
"""Create an inactive vendor owned by other_user"""
unique_id = str(uuid.uuid4())[:8]
shop = Shop(
shop_code=f"INACTIVE_{unique_id}",
shop_name=f"Inactive Shop {unique_id}",
vendor = Vendor(
vendor_code=f"INACTIVE_{unique_id}",
vendor_name=f"Inactive Vendor {unique_id}",
owner_id=other_user.id,
is_active=False,
is_verified=False,
)
db.add(shop)
db.add(vendor)
db.commit()
db.refresh(shop)
return shop
db.refresh(vendor)
return vendor
@pytest.fixture
def verified_shop(db, other_user):
"""Create a verified shop owned by other_user"""
def verified_vendor(db, other_user):
"""Create a verified vendor owned by other_user"""
unique_id = str(uuid.uuid4())[:8]
shop = Shop(
shop_code=f"VERIFIED_{unique_id}",
shop_name=f"Verified Shop {unique_id}",
vendor = Vendor(
vendor_code=f"VERIFIED_{unique_id}",
vendor_name=f"Verified Vendor {unique_id}",
owner_id=other_user.id,
is_active=True,
is_verified=True,
)
db.add(shop)
db.add(vendor)
db.commit()
db.refresh(shop)
return shop
db.refresh(vendor)
return vendor
@pytest.fixture
def test_product(db, test_shop, unique_product):
"""Create a shop product relationship"""
def test_product(db, test_vendor, unique_product):
"""Create a vendor product relationship"""
product = Product(
shop_id=test_shop.id, marketplace_product_id=unique_product.id, is_active=True
vendor_id=test_vendor.id, marketplace_product_id=unique_product.id, is_active=True
)
# Add optional fields if they exist in your model
if hasattr(Product, "shop_price"):
if hasattr(Product, "price"):
product.price = 24.99
if hasattr(Product, "is_featured"):
product.is_featured = False
@@ -98,7 +98,7 @@ def test_product(db, test_shop, unique_product):
@pytest.fixture
def test_stock(db, test_marketplace_product, test_shop):
def test_stock(db, test_marketplace_product, test_vendor):
"""Create test stock entry"""
unique_id = str(uuid.uuid4())[:8].upper() # Short unique identifier
stock = Stock(
@@ -106,7 +106,7 @@ def test_stock(db, test_marketplace_product, test_shop):
location=f"WAREHOUSE_A_{unique_id}",
quantity=10,
reserved_quantity=0,
shop_id=test_shop.id, # Add shop_id reference
vendor_id=test_vendor.id, # Add vendor_id reference
)
db.add(stock)
db.commit()
@@ -115,7 +115,7 @@ def test_stock(db, test_marketplace_product, test_shop):
@pytest.fixture
def multiple_stocks(db, multiple_products, test_shop):
def multiple_stocks(db, multiple_products, test_vendor):
"""Create multiple stock entries for testing"""
stocks = []
@@ -125,7 +125,7 @@ def multiple_stocks(db, multiple_products, test_shop):
location=f"LOC_{i}",
quantity=10 + (i * 5), # Different quantities
reserved_quantity=i,
shop_id=test_shop.id,
vendor_id=test_vendor.id,
)
stocks.append(stock)
@@ -136,30 +136,30 @@ def multiple_stocks(db, multiple_products, test_shop):
return stocks
def create_unique_shop_factory():
"""Factory function to create unique shops in tests"""
def create_unique_vendor_factory():
"""Factory function to create unique vendors in tests"""
def _create_shop(db, owner_id, **kwargs):
def _create_vendor(db, owner_id, **kwargs):
unique_id = str(uuid.uuid4())[:8]
defaults = {
"shop_code": f"FACTORY_{unique_id}",
"shop_name": f"Factory Shop {unique_id}",
"vendor_code": f"FACTORY_{unique_id}",
"vendor_name": f"Factory Vendor {unique_id}",
"owner_id": owner_id,
"is_active": True,
"is_verified": False,
}
defaults.update(kwargs)
shop = Shop(**defaults)
db.add(shop)
vendor = Vendor(**defaults)
db.add(vendor)
db.commit()
db.refresh(shop)
return shop
db.refresh(vendor)
return vendor
return _create_shop
return _create_vendor
@pytest.fixture
def shop_factory():
"""Fixture that provides a shop factory function"""
return create_unique_shop_factory()
def vendor_factory():
"""Fixture that provides a vendor factory function"""
return create_unique_vendor_factory()

View File

@@ -74,67 +74,67 @@ class TestAdminAPI:
assert data["error_code"] == "USER_STATUS_CHANGE_FAILED"
assert "Cannot modify another admin user" in data["message"]
def test_get_all_shops_admin(self, client, admin_headers, test_shop):
"""Test admin getting all shops"""
response = client.get("/api/v1/admin/shops", headers=admin_headers)
def test_get_all_vendors_admin(self, client, admin_headers, test_vendor):
"""Test admin getting all vendors"""
response = client.get("/api/v1/admin/vendors", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
assert len(data["shops"]) >= 1
assert len(data["vendors"]) >= 1
# Check that test_shop is in the response
shop_codes = [
shop["shop_code"] for shop in data["shops"] if "shop_code" in shop
# Check that test_vendor is in the response
vendor_codes = [
vendor ["vendor_code"] for vendor in data["vendors"] if "vendor_code" in vendor
]
assert test_shop.shop_code in shop_codes
assert test_vendor.vendor_code in vendor_codes
def test_get_all_shops_non_admin(self, client, auth_headers):
"""Test non-admin trying to access admin shop endpoint"""
response = client.get("/api/v1/admin/shops", headers=auth_headers)
def test_get_all_vendors_non_admin(self, client, auth_headers):
"""Test non-admin trying to access admin vendor endpoint"""
response = client.get("/api/v1/admin/vendors", headers=auth_headers)
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "ADMIN_REQUIRED"
def test_verify_shop_admin(self, client, admin_headers, test_shop):
"""Test admin verifying/unverifying shop"""
def test_verify_vendor_admin(self, client, admin_headers, test_vendor):
"""Test admin verifying/unverifying vendor """
response = client.put(
f"/api/v1/admin/shops/{test_shop.id}/verify", headers=admin_headers
f"/api/v1/admin/vendors/{test_vendor.id}/verify", headers=admin_headers
)
assert response.status_code == 200
message = response.json()["message"]
assert "verified" in message or "unverified" in message
assert test_shop.shop_code in message
assert test_vendor.vendor_code in message
def test_verify_shop_not_found(self, client, admin_headers):
"""Test admin verifying non-existent shop"""
response = client.put("/api/v1/admin/shops/99999/verify", headers=admin_headers)
def test_verify_vendor_not_found(self, client, admin_headers):
"""Test admin verifying non-existent vendor """
response = client.put("/api/v1/admin/vendors/99999/verify", headers=admin_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "SHOP_NOT_FOUND"
assert "Shop with ID '99999' not found" in data["message"]
assert data["error_code"] == "VENDOR_NOT_FOUND"
assert "Vendor with ID '99999' not found" in data["message"]
def test_toggle_shop_status_admin(self, client, admin_headers, test_shop):
"""Test admin toggling shop status"""
def test_toggle_vendor_status_admin(self, client, admin_headers, test_vendor):
"""Test admin toggling vendor status"""
response = client.put(
f"/api/v1/admin/shops/{test_shop.id}/status", headers=admin_headers
f"/api/v1/admin/vendors/{test_vendor.id}/status", headers=admin_headers
)
assert response.status_code == 200
message = response.json()["message"]
assert "activated" in message or "deactivated" in message
assert test_shop.shop_code in message
assert test_vendor.vendor_code in message
def test_toggle_shop_status_not_found(self, client, admin_headers):
"""Test admin toggling status for non-existent shop"""
response = client.put("/api/v1/admin/shops/99999/status", headers=admin_headers)
def test_toggle_vendor_status_not_found(self, client, admin_headers):
"""Test admin toggling status for non-existent vendor """
response = client.put("/api/v1/admin/vendors/99999/status", headers=admin_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "SHOP_NOT_FOUND"
assert data["error_code"] == "VENDOR_NOT_FOUND"
def test_get_marketplace_import_jobs_admin(
self, client, admin_headers, test_marketplace_import_job
@@ -191,17 +191,17 @@ class TestAdminAPI:
assert "activation_rate" in data
assert isinstance(data["total_users"], int)
def test_get_shop_statistics(self, client, admin_headers):
"""Test admin getting shop statistics"""
response = client.get("/api/v1/admin/stats/shops", headers=admin_headers)
def test_get_vendor_statistics(self, client, admin_headers):
"""Test admin getting vendor statistics"""
response = client.get("/api/v1/admin/stats/vendors", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert "total_shops" in data
assert "active_shops" in data
assert "verified_shops" in data
assert "total_vendors" in data
assert "active_vendors" in data
assert "verified_vendors" in data
assert "verification_rate" in data
assert isinstance(data["total_shops"], int)
assert isinstance(data["total_vendors"], int)
def test_admin_pagination_users(self, client, admin_headers, test_user, test_admin):
"""Test user pagination works correctly"""
@@ -221,14 +221,14 @@ class TestAdminAPI:
data = response.json()
assert len(data) >= 0 # Could be 1 or 0 depending on total users
def test_admin_pagination_shops(self, client, admin_headers, test_shop):
"""Test shop pagination works correctly"""
def test_admin_pagination_vendors(self, client, admin_headers, test_vendor):
"""Test vendor pagination works correctly"""
response = client.get(
"/api/v1/admin/shops?skip=0&limit=1", headers=admin_headers
"/api/v1/admin/vendors?skip=0&limit=1", headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
assert len(data["shops"]) >= 0
assert len(data["vendors"]) >= 0
assert "skip" in data
assert "limit" in data

View File

@@ -8,15 +8,15 @@ import pytest
@pytest.mark.api
@pytest.mark.marketplace
class TestMarketplaceImportJobAPI:
def test_import_from_marketplace(self, client, auth_headers, test_shop, test_user):
def test_import_from_marketplace(self, client, auth_headers, test_vendor, test_user):
"""Test marketplace import endpoint - just test job creation"""
# Ensure user owns the shop
test_shop.owner_id = test_user.id
# Ensure user owns the vendor
test_vendor.owner_id = test_user.id
import_data = {
"url": "https://example.com/products.csv",
"marketplace": "TestMarket",
"shop_code": test_shop.shop_code,
"vendor_code": test_vendor.vendor_code,
}
response = client.post(
@@ -28,15 +28,15 @@ class TestMarketplaceImportJobAPI:
assert data["status"] == "pending"
assert data["marketplace"] == "TestMarket"
assert "job_id" in data
assert data["shop_code"] == test_shop.shop_code
assert data["shop_id"] == test_shop.id
assert data["vendor_code"] == test_vendor.vendor_code
assert data["vendor_id"] == test_vendor.id
def test_import_from_marketplace_invalid_shop(self, client, auth_headers):
"""Test marketplace import with invalid shop"""
def test_import_from_marketplace_invalid_vendor(self, client, auth_headers):
"""Test marketplace import with invalid vendor """
import_data = {
"url": "https://example.com/products.csv",
"marketplace": "TestMarket",
"shop_code": "NONEXISTENT",
"vendor_code": "NONEXISTENT",
}
response = client.post(
@@ -45,18 +45,18 @@ class TestMarketplaceImportJobAPI:
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "SHOP_NOT_FOUND"
assert data["error_code"] == "VENDOR_NOT_FOUND"
assert "NONEXISTENT" in data["message"]
def test_import_from_marketplace_unauthorized_shop(self, client, auth_headers, test_shop, other_user):
"""Test marketplace import with unauthorized shop access"""
# Set shop owner to different user
test_shop.owner_id = other_user.id
def test_import_from_marketplace_unauthorized_vendor(self, client, auth_headers, test_vendor, other_user):
"""Test marketplace import with unauthorized vendor access"""
# Set vendor owner to different user
test_vendor.owner_id = other_user.id
import_data = {
"url": "https://example.com/products.csv",
"marketplace": "TestMarket",
"shop_code": test_shop.shop_code,
"vendor_code": test_vendor.vendor_code,
}
response = client.post(
@@ -65,15 +65,15 @@ class TestMarketplaceImportJobAPI:
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "UNAUTHORIZED_SHOP_ACCESS"
assert test_shop.shop_code in data["message"]
assert data["error_code"] == "UNAUTHORIZED_VENDOR_ACCESS"
assert test_vendor.vendor_code in data["message"]
def test_import_from_marketplace_validation_error(self, client, auth_headers):
"""Test marketplace import with invalid request data"""
import_data = {
"url": "", # Empty URL
"marketplace": "", # Empty marketplace
# Missing shop_code
# Missing vendor_code
}
response = client.post(
@@ -85,12 +85,12 @@ class TestMarketplaceImportJobAPI:
assert data["error_code"] == "VALIDATION_ERROR"
assert "Request validation failed" in data["message"]
def test_import_from_marketplace_admin_access(self, client, admin_headers, test_shop):
"""Test that admin can import for any shop"""
def test_import_from_marketplace_admin_access(self, client, admin_headers, test_vendor):
"""Test that admin can import for any vendor """
import_data = {
"url": "https://example.com/products.csv",
"marketplace": "AdminMarket",
"shop_code": test_shop.shop_code,
"vendor_code": test_vendor.vendor_code,
}
response = client.post(
@@ -100,7 +100,7 @@ class TestMarketplaceImportJobAPI:
assert response.status_code == 200
data = response.json()
assert data["marketplace"] == "AdminMarket"
assert data["shop_code"] == test_shop.shop_code
assert data["vendor_code"] == test_vendor.vendor_code
def test_get_marketplace_import_status(self, client, auth_headers, test_marketplace_import_job):
"""Test getting marketplace import status"""
@@ -195,7 +195,7 @@ class TestMarketplaceImportJobAPI:
assert isinstance(data["total_jobs"], int)
assert data["total_jobs"] >= 1
def test_cancel_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db):
def test_cancel_marketplace_import_job(self, client, auth_headers, test_user, test_vendor, db):
"""Test cancelling a marketplace import job"""
# Create a pending job that can be cancelled
from models.database.marketplace_import_job import MarketplaceImportJob
@@ -205,9 +205,9 @@ class TestMarketplaceImportJobAPI:
job = MarketplaceImportJob(
status="pending",
marketplace="TestMarket",
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,
@@ -256,7 +256,7 @@ class TestMarketplaceImportJobAPI:
assert data["error_code"] == "IMPORT_JOB_CANNOT_BE_CANCELLED"
assert "completed" in data["message"]
def test_delete_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db):
def test_delete_marketplace_import_job(self, client, auth_headers, test_user, test_vendor, db):
"""Test deleting a marketplace import job"""
# Create a completed job that can be deleted
from models.database.marketplace_import_job import MarketplaceImportJob
@@ -266,9 +266,9 @@ class TestMarketplaceImportJobAPI:
job = MarketplaceImportJob(
status="completed",
marketplace="TestMarket",
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,
@@ -299,7 +299,7 @@ class TestMarketplaceImportJobAPI:
data = response.json()
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
def test_delete_marketplace_import_job_cannot_delete(self, client, auth_headers, test_user, test_shop, db):
def test_delete_marketplace_import_job_cannot_delete(self, client, auth_headers, test_user, test_vendor, db):
"""Test deleting a job that cannot be deleted"""
# Create a pending job that cannot be deleted
from models.database.marketplace_import_job import MarketplaceImportJob
@@ -309,9 +309,9 @@ class TestMarketplaceImportJobAPI:
job = MarketplaceImportJob(
status="pending",
marketplace="TestMarket",
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,
@@ -344,7 +344,7 @@ class TestMarketplaceImportJobAPI:
import_data = {
"url": "https://example.com/products.csv",
"marketplace": "TestMarket",
"shop_code": "TEST_SHOP",
"vendor_code": "TEST_SHOP",
}
response = client.post("/api/v1/marketplace/import-product", json=import_data)
@@ -374,7 +374,7 @@ class TestMarketplaceImportJobAPI:
data = response.json()
assert data["job_id"] == test_marketplace_import_job.id
def test_admin_can_cancel_any_job(self, client, admin_headers, test_user, test_shop, db):
def test_admin_can_cancel_any_job(self, client, admin_headers, test_user, test_vendor, db):
"""Test that admin can cancel any job"""
# Create a pending job owned by different user
from models.database.marketplace_import_job import MarketplaceImportJob
@@ -384,9 +384,9 @@ class TestMarketplaceImportJobAPI:
job = MarketplaceImportJob(
status="pending",
marketplace="TestMarket",
shop_name=f"Test_Shop_{unique_id}",
vendor_name=f"Test_vendor_{unique_id}",
user_id=test_user.id, # Different user
shop_id=test_shop.id,
vendor_id=test_vendor.id,
source_url="https://test.example.com/import",
imported_count=0,
updated_count=0,
@@ -406,16 +406,16 @@ class TestMarketplaceImportJobAPI:
data = response.json()
assert data["status"] == "cancelled"
def test_rate_limiting_applied(self, client, auth_headers, test_shop, test_user):
def test_rate_limiting_applied(self, client, auth_headers, test_vendor, test_user):
"""Test that rate limiting is applied to import endpoint"""
# This test verifies that the rate_limit decorator is present
# Actual rate limiting testing would require multiple requests
test_shop.owner_id = test_user.id
test_vendor.owner_id = test_user.id
import_data = {
"url": "https://example.com/products.csv",
"marketplace": "TestMarket",
"shop_code": test_shop.shop_code,
"vendor_code": test_vendor.vendor_code,
}
response = client.post(

View File

@@ -65,19 +65,19 @@ class TestExportFunctionality:
assert f"EXP1_{unique_suffix}" in csv_content
assert f"EXP2_{unique_suffix}" not in csv_content # Should be filtered out
def test_csv_export_with_shop_filter_success(self, client, auth_headers, db):
"""Test CSV export with shop name filtering successfully"""
def test_csv_export_with_vendor_filter_success(self, client, auth_headers, db):
"""Test CSV export with vendor name filtering successfully"""
unique_suffix = str(uuid.uuid4())[:8]
products = [
MarketplaceProduct(
marketplace_product_id=f"SHOP1_{unique_suffix}",
title=f"Shop1 MarketplaceProduct {unique_suffix}",
shop_name="TestShop1"
vendor_name="TestVendor1"
),
MarketplaceProduct(
marketplace_product_id=f"SHOP2_{unique_suffix}",
title=f"Shop2 MarketplaceProduct {unique_suffix}",
shop_name="TestShop2"
vendor_name="TestVendor2"
),
]
@@ -85,7 +85,7 @@ class TestExportFunctionality:
db.commit()
response = client.get(
"/api/v1/marketplace/product?shop_name=TestShop1", headers=auth_headers
"/api/v1/marketplace/product?vendor_name=TestVendor1", headers=auth_headers
)
assert response.status_code == 200
@@ -94,26 +94,26 @@ class TestExportFunctionality:
assert f"SHOP2_{unique_suffix}" not in csv_content # Should be filtered out
def test_csv_export_with_combined_filters_success(self, client, auth_headers, db):
"""Test CSV export with combined marketplace and shop filters successfully"""
"""Test CSV export with combined marketplace and vendor filters successfully"""
unique_suffix = str(uuid.uuid4())[:8]
products = [
MarketplaceProduct(
marketplace_product_id=f"COMBO1_{unique_suffix}",
title=f"Combo MarketplaceProduct 1 {unique_suffix}",
marketplace="Amazon",
shop_name="TestShop"
vendor_name="TestVendor"
),
MarketplaceProduct(
marketplace_product_id=f"COMBO2_{unique_suffix}",
title=f"Combo MarketplaceProduct 2 {unique_suffix}",
marketplace="eBay",
shop_name="TestShop"
vendor_name="TestVendor"
),
MarketplaceProduct(
marketplace_product_id=f"COMBO3_{unique_suffix}",
title=f"Combo MarketplaceProduct 3 {unique_suffix}",
marketplace="Amazon",
shop_name="OtherShop"
vendor_name="OtherShop"
),
]
@@ -121,7 +121,7 @@ class TestExportFunctionality:
db.commit()
response = client.get(
"/api/v1/marketplace/product?marketplace=Amazon&shop_name=TestShop",
"/api/v1/marketplace/product?marketplace=Amazon&vendor_name=TestVendor",
headers=auth_headers
)
assert response.status_code == 200
@@ -129,7 +129,7 @@ class TestExportFunctionality:
csv_content = response.content.decode("utf-8")
assert f"COMBO1_{unique_suffix}" in csv_content # Matches both filters
assert f"COMBO2_{unique_suffix}" not in csv_content # Wrong marketplace
assert f"COMBO3_{unique_suffix}" not in csv_content # Wrong shop
assert f"COMBO3_{unique_suffix}" not in csv_content # Wrong vendor
def test_csv_export_no_results(self, client, auth_headers):
"""Test CSV export with filters that return no results"""

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
@pytest.mark.integration
@pytest.mark.api
@@ -181,34 +181,34 @@ class TestPagination:
overlap = set(first_page_ids) & set(second_page_ids)
assert len(overlap) == 0, "Pages should not have overlapping products"
def test_shop_pagination_success(self, client, admin_headers, db, test_user):
"""Test pagination for shop listing successfully"""
def test_vendor_pagination_success(self, client, admin_headers, db, test_user):
"""Test pagination for vendor listing successfully"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Create multiple shops for pagination testing
from models.database.shop import Shop
shops = []
# Create multiple vendors for pagination testing
from models.database.vendor import Vendor
vendors =[]
for i in range(15):
shop = Shop(
shop_code=f"PAGESHOP{i:03d}_{unique_suffix}",
shop_name=f"Pagination Shop {i}",
vendor = Vendor(
vendor_code=f"PAGESHOP{i:03d}_{unique_suffix}",
vendor_name=f"Pagination Vendor {i}",
owner_id=test_user.id,
is_active=True,
)
shops.append(shop)
vendors.append(vendor)
db.add_all(shops)
db.add_all(vendors)
db.commit()
# Test first page (assuming admin endpoint exists)
response = client.get(
"/api/v1/shop?limit=5&skip=0", headers=admin_headers
"/api/v1/vendor ?limit=5&skip=0", headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert len(data["shops"]) == 5
assert data["total"] >= 15 # At least our test shops
assert len(data["vendors"]) == 5
assert data["total"] >= 15 # At least our test vendors
assert data["skip"] == 0
assert data["limit"] == 5

View File

@@ -1,389 +0,0 @@
# tests/integration/api/v1/test_shop_endpoints.py
import pytest
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.shops
class TestShopsAPI:
def test_create_shop_success(self, client, auth_headers):
"""Test creating a new shop successfully"""
shop_data = {
"shop_code": "NEWSHOP001",
"shop_name": "New Shop",
"description": "A new test shop",
}
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
assert response.status_code == 200
data = response.json()
assert data["shop_code"] == "NEWSHOP001"
assert data["shop_name"] == "New Shop"
assert data["is_active"] is True
def test_create_shop_duplicate_code_returns_conflict(self, client, auth_headers, test_shop):
"""Test creating shop with duplicate code returns ShopAlreadyExistsException"""
shop_data = {
"shop_code": test_shop.shop_code,
"shop_name": "Different Name",
"description": "Different description",
}
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
assert response.status_code == 409
data = response.json()
assert data["error_code"] == "SHOP_ALREADY_EXISTS"
assert data["status_code"] == 409
assert test_shop.shop_code in data["message"]
assert data["details"]["shop_code"] == test_shop.shop_code
def test_create_shop_missing_shop_code_validation_error(self, client, auth_headers):
"""Test creating shop without shop_code returns ValidationException"""
shop_data = {
"shop_name": "Shop without Code",
"description": "Missing shop code",
}
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
assert data["status_code"] == 422
assert "Request validation failed" in data["message"]
assert "validation_errors" in data["details"]
def test_create_shop_empty_shop_name_validation_error(self, client, auth_headers):
"""Test creating shop with empty shop_name returns ShopValidationException"""
shop_data = {
"shop_code": "EMPTYNAME",
"shop_name": "", # Empty shop name
"description": "Shop with empty name",
}
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "INVALID_SHOP_DATA"
assert data["status_code"] == 422
assert "Shop name is required" in data["message"]
assert data["details"]["field"] == "shop_name"
def test_create_shop_max_shops_reached_business_logic_error(self, client, auth_headers, db, test_user):
"""Test creating shop when max shops reached returns MaxShopsReachedException"""
# This test would require creating the maximum allowed shops first
# The exact implementation depends on your business rules
# For now, we'll test the structure of what the error should look like
# In a real scenario, you'd create max_shops number of shops first
# Assuming max shops is enforced at service level
# This test validates the expected response structure
pass # Implementation depends on your max_shops business logic
def test_get_shops_success(self, client, auth_headers, test_shop):
"""Test getting shops list successfully"""
response = client.get("/api/v1/shop", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
assert len(data["shops"]) >= 1
# Find our test shop
test_shop_found = any(s["shop_code"] == test_shop.shop_code for s in data["shops"])
assert test_shop_found
def test_get_shops_with_filters(self, client, auth_headers, test_shop):
"""Test getting shops with filtering options"""
# Test active_only filter
response = client.get("/api/v1/shop?active_only=true", headers=auth_headers)
assert response.status_code == 200
data = response.json()
for shop in data["shops"]:
assert shop["is_active"] is True
# Test verified_only filter
response = client.get("/api/v1/shop?verified_only=true", headers=auth_headers)
assert response.status_code == 200
# Response should only contain verified shops
def test_get_shop_by_code_success(self, client, auth_headers, test_shop):
"""Test getting specific shop successfully"""
response = client.get(
f"/api/v1/shop/{test_shop.shop_code}", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["shop_code"] == test_shop.shop_code
assert data["shop_name"] == test_shop.shop_name
def test_get_shop_by_code_not_found(self, client, auth_headers):
"""Test getting nonexistent shop returns ShopNotFoundException"""
response = client.get("/api/v1/shop/NONEXISTENT", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "SHOP_NOT_FOUND"
assert data["status_code"] == 404
assert "NONEXISTENT" in data["message"]
assert data["details"]["resource_type"] == "Shop"
assert data["details"]["identifier"] == "NONEXISTENT"
def test_get_shop_unauthorized_access(self, client, auth_headers, test_shop, other_user, db):
"""Test accessing shop owned by another user returns UnauthorizedShopAccessException"""
# Change shop owner to other user AND make it unverified/inactive
# so that non-owner users cannot access it
test_shop.owner_id = other_user.id
test_shop.is_verified = False # Make it not publicly accessible
db.commit()
response = client.get(
f"/api/v1/shop/{test_shop.shop_code}", headers=auth_headers
)
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "UNAUTHORIZED_SHOP_ACCESS"
assert data["status_code"] == 403
assert test_shop.shop_code in data["message"]
assert data["details"]["shop_code"] == test_shop.shop_code
def test_get_shop_unauthorized_access_with_inactive_shop(self, client, auth_headers, inactive_shop):
"""Test accessing inactive shop owned by another user returns UnauthorizedShopAccessException"""
# inactive_shop fixture already creates an unverified, inactive shop owned by other_user
response = client.get(
f"/api/v1/shop/{inactive_shop.shop_code}", headers=auth_headers
)
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "UNAUTHORIZED_SHOP_ACCESS"
assert data["status_code"] == 403
assert inactive_shop.shop_code in data["message"]
assert data["details"]["shop_code"] == inactive_shop.shop_code
def test_get_shop_public_access_allowed(self, client, auth_headers, verified_shop):
"""Test accessing verified shop owned by another user is allowed (public access)"""
# verified_shop fixture creates a verified, active shop owned by other_user
# This should allow public access per your business logic
response = client.get(
f"/api/v1/shop/{verified_shop.shop_code}", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["shop_code"] == verified_shop.shop_code
assert data["shop_name"] == verified_shop.shop_name
def test_add_product_to_shop_success(self, client, auth_headers, test_shop, unique_product):
"""Test adding product to shop successfully"""
product_data = {
"marketplace_product_id": unique_product.marketplace_product_id, # Use string marketplace_product_id, not database id
"price": 29.99,
"is_active": True,
"is_featured": False,
}
response = client.post(
f"/api/v1/shop/{test_shop.shop_code}/products",
headers=auth_headers,
json=product_data
)
assert response.status_code == 200
data = response.json()
# The response structure contains nested product data
assert data["shop_id"] == test_shop.id
assert data["price"] == 29.99
assert data["is_active"] is True
assert data["is_featured"] is False
# MarketplaceProduct details are nested in the 'marketplace_product' field
assert "marketplace_product" in data
assert data["marketplace_product"]["marketplace_product_id"] == unique_product.marketplace_product_id
assert data["marketplace_product"]["id"] == unique_product.id
def test_add_product_to_shop_already_exists_conflict(self, client, auth_headers, test_shop, test_product):
"""Test adding product that already exists in shop returns ProductAlreadyExistsException"""
# test_product fixture already creates a relationship, get the marketplace_product_id string
existing_product = test_product.marketplace_product
product_data = {
"marketplace_product_id": existing_product.marketplace_product_id, # Use string marketplace_product_id
"shop_price": 29.99,
}
response = client.post(
f"/api/v1/shop/{test_shop.shop_code}/products",
headers=auth_headers,
json=product_data
)
assert response.status_code == 409
data = response.json()
assert data["error_code"] == "PRODUCT_ALREADY_EXISTS"
assert data["status_code"] == 409
assert test_shop.shop_code in data["message"]
assert existing_product.marketplace_product_id in data["message"]
def test_add_nonexistent_product_to_shop_not_found(self, client, auth_headers, test_shop):
"""Test adding nonexistent product to shop returns MarketplaceProductNotFoundException"""
product_data = {
"marketplace_product_id": "NONEXISTENT_PRODUCT", # Use string marketplace_product_id that doesn't exist
"shop_price": 29.99,
}
response = client.post(
f"/api/v1/shop/{test_shop.shop_code}/products",
headers=auth_headers,
json=product_data
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "PRODUCT_NOT_FOUND"
assert data["status_code"] == 404
assert "NONEXISTENT_PRODUCT" in data["message"]
def test_get_products_success(self, client, auth_headers, test_shop, test_product):
"""Test getting shop products successfully"""
response = client.get(
f"/api/v1/shop/{test_shop.shop_code}/products",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
assert len(data["products"]) >= 1
assert "shop" in data
assert data["shop"]["shop_code"] == test_shop.shop_code
def test_get_products_with_filters(self, client, auth_headers, test_shop):
"""Test getting shop products with filtering"""
# Test active_only filter
response = client.get(
f"/api/v1/shop/{test_shop.shop_code}/products?active_only=true",
headers=auth_headers
)
assert response.status_code == 200
# Test featured_only filter
response = client.get(
f"/api/v1/shop/{test_shop.shop_code}/products?featured_only=true",
headers=auth_headers
)
assert response.status_code == 200
def test_get_products_from_nonexistent_shop_not_found(self, client, auth_headers):
"""Test getting products from nonexistent shop returns ShopNotFoundException"""
response = client.get(
"/api/v1/shop/NONEXISTENT/products",
headers=auth_headers
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "SHOP_NOT_FOUND"
assert data["status_code"] == 404
assert "NONEXISTENT" in data["message"]
def test_shop_not_active_business_logic_error(self, client, auth_headers, test_shop, db):
"""Test accessing inactive shop returns ShopNotActiveException (if enforced)"""
# Set shop to inactive
test_shop.is_active = False
db.commit()
# Depending on your business logic, this might return an error
response = client.get(
f"/api/v1/shop/{test_shop.shop_code}", headers=auth_headers
)
# If your service enforces active shop requirement
if response.status_code == 400:
data = response.json()
assert data["error_code"] == "SHOP_NOT_ACTIVE"
assert data["status_code"] == 400
assert test_shop.shop_code in data["message"]
def test_shop_not_verified_business_logic_error(self, client, auth_headers, test_shop, db):
"""Test operations requiring verification returns ShopNotVerifiedException (if enforced)"""
# Set shop to unverified
test_shop.is_verified = False
db.commit()
# Test adding products (might require verification)
product_data = {
"marketplace_product_id": 1,
"shop_price": 29.99,
}
response = client.post(
f"/api/v1/shop/{test_shop.shop_code}/products",
headers=auth_headers,
json=product_data
)
# If your service requires verification for adding products
if response.status_code == 400:
data = response.json()
assert data["error_code"] == "SHOP_NOT_VERIFIED"
assert data["status_code"] == 400
assert test_shop.shop_code in data["message"]
def test_get_shop_without_auth_returns_invalid_token(self, client):
"""Test that shop endpoints require authentication returns InvalidTokenException"""
response = client.get("/api/v1/shop")
assert response.status_code == 401
data = response.json()
assert data["error_code"] == "INVALID_TOKEN"
assert data["status_code"] == 401
def test_pagination_validation_errors(self, client, auth_headers):
"""Test pagination parameter validation"""
# Test negative skip
response = client.get("/api/v1/shop?skip=-1", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
# Test zero limit
response = client.get("/api/v1/shop?limit=0", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
# Test excessive limit
response = client.get("/api/v1/shop?limit=10000", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
def test_exception_structure_consistency(self, client, auth_headers):
"""Test that all shop exceptions follow the consistent LetzShopException structure"""
# Test with a known error case
response = client.get("/api/v1/shop/NONEXISTENT", headers=auth_headers)
assert response.status_code == 404
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"
assert isinstance(data["error_code"], str)
assert isinstance(data["message"], str)
assert isinstance(data["status_code"], int)
# Details field should be present for domain-specific exceptions
if "details" in data:
assert isinstance(data["details"], dict)

View File

@@ -15,7 +15,7 @@ class TestStatsAPI:
assert "unique_brands" in data
assert "unique_categories" in data
assert "unique_marketplaces" in data
assert "unique_shops" in data
assert "unique_vendors" in data
assert data["total_products"] >= 1
def test_get_marketplace_stats(self, client, auth_headers, test_marketplace_product):

View File

@@ -0,0 +1,389 @@
# tests/integration/api/v1/test_vendor_endpoints.py
import pytest
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.vendors
class TestVendorsAPI:
def test_create_vendor_success(self, client, auth_headers):
"""Test creating a new vendor successfully"""
vendor_data = {
"vendor_code": "NEWVENDOR001",
"vendor_name": "New Vendor",
"description": "A new test vendor ",
}
response = client.post("/api/v1/vendor ", headers=auth_headers, json=vendor_data)
assert response.status_code == 200
data = response.json()
assert data["vendor_code"] == "NEWVENDOR001"
assert data["vendor_name"] == "New Vendor"
assert data["is_active"] is True
def test_create_vendor_duplicate_code_returns_conflict(self, client, auth_headers, test_vendor):
"""Test creating vendor with duplicate code returns VendorAlreadyExistsException"""
vendor_data = {
"vendor_code": test_vendor.vendor_code,
"vendor_name": "Different Name",
"description": "Different description",
}
response = client.post("/api/v1/vendor ", headers=auth_headers, json=vendor_data)
assert response.status_code == 409
data = response.json()
assert data["error_code"] == "VENDOR_ALREADY_EXISTS"
assert data["status_code"] == 409
assert test_vendor.vendor_code in data["message"]
assert data["details"]["vendor_code"] == test_vendor.vendor_code
def test_create_vendor_missing_vendor_code_validation_error(self, client, auth_headers):
"""Test creating vendor without vendor_code returns ValidationException"""
vendor_data = {
"vendor_name": "Vendor without Code",
"description": "Missing vendor code",
}
response = client.post("/api/v1/vendor ", headers=auth_headers, json=vendor_data)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
assert data["status_code"] == 422
assert "Request validation failed" in data["message"]
assert "validation_errors" in data["details"]
def test_create_vendor_empty_vendor_name_validation_error(self, client, auth_headers):
"""Test creating vendor with empty vendor_name returns VendorValidationException"""
vendor_data = {
"vendor_code": "EMPTYNAME",
"vendor_name": "", # Empty vendor name
"description": "Vendor with empty name",
}
response = client.post("/api/v1/vendor ", headers=auth_headers, json=vendor_data)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "INVALID_VENDOR_DATA"
assert data["status_code"] == 422
assert "Vendor name is required" in data["message"]
assert data["details"]["field"] == "vendor_name"
def test_create_vendor_max_vendors_reached_business_logic_error(self, client, auth_headers, db, test_user):
"""Test creating vendor when max vendors reached returns MaxVendorsReachedException"""
# This test would require creating the maximum allowed vendors first
# The exact implementation depends on your business rules
# For now, we'll test the structure of what the error should look like
# In a real scenario, you'd create max_vendors number of vendors first
# Assuming max vendors is enforced at service level
# This test validates the expected response structure
pass # Implementation depends on your max_vendors business logic
def test_get_vendors_success(self, client, auth_headers, test_vendor):
"""Test getting vendors list successfully"""
response = client.get("/api/v1/vendor ", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
assert len(data["vendors"]) >= 1
# Find our test vendor
test_vendor_found = any(s["vendor_code"] == test_vendor.vendor_code for s in data["vendors"])
assert test_vendor_found
def test_get_vendors_with_filters(self, client, auth_headers, test_vendor):
"""Test getting vendors with filtering options"""
# Test active_only filter
response = client.get("/api/v1/vendor ?active_only=true", headers=auth_headers)
assert response.status_code == 200
data = response.json()
for vendor in data["vendors"]:
assert vendor ["is_active"] is True
# Test verified_only filter
response = client.get("/api/v1/vendor ?verified_only=true", headers=auth_headers)
assert response.status_code == 200
# Response should only contain verified vendors
def test_get_vendor_by_code_success(self, client, auth_headers, test_vendor):
"""Test getting specific vendor successfully"""
response = client.get(
f"/api/v1/vendor /{test_vendor.vendor_code}", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["vendor_code"] == test_vendor.vendor_code
assert data["vendor_name"] == test_vendor.vendor_name
def test_get_vendor_by_code_not_found(self, client, auth_headers):
"""Test getting nonexistent vendor returns VendorNotFoundException"""
response = client.get("/api/v1/vendor /NONEXISTENT", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "VENDOR_NOT_FOUND"
assert data["status_code"] == 404
assert "NONEXISTENT" in data["message"]
assert data["details"]["resource_type"] == "Vendor"
assert data["details"]["identifier"] == "NONEXISTENT"
def test_get_vendor_unauthorized_access(self, client, auth_headers, test_vendor, other_user, db):
"""Test accessing vendor owned by another user returns UnauthorizedVendorAccessException"""
# Change vendor owner to other user AND make it unverified/inactive
# so that non-owner users cannot access it
test_vendor.owner_id = other_user.id
test_vendor.is_verified = False # Make it not publicly accessible
db.commit()
response = client.get(
f"/api/v1/vendor /{test_vendor.vendor_code}", headers=auth_headers
)
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "UNAUTHORIZED_VENDOR_ACCESS"
assert data["status_code"] == 403
assert test_vendor.vendor_code in data["message"]
assert data["details"]["vendor_code"] == test_vendor.vendor_code
def test_get_vendor_unauthorized_access_with_inactive_vendor(self, client, auth_headers, inactive_vendor):
"""Test accessing inactive vendor owned by another user returns UnauthorizedVendorAccessException"""
# inactive_vendor fixture already creates an unverified, inactive vendor owned by other_user
response = client.get(
f"/api/v1/vendor /{inactive_vendor.vendor_code}", headers=auth_headers
)
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "UNAUTHORIZED_VENDOR_ACCESS"
assert data["status_code"] == 403
assert inactive_vendor.vendor_code in data["message"]
assert data["details"]["vendor_code"] == inactive_vendor.vendor_code
def test_get_vendor_public_access_allowed(self, client, auth_headers, verified_vendor):
"""Test accessing verified vendor owned by another user is allowed (public access)"""
# verified_vendor fixture creates a verified, active vendor owned by other_user
# This should allow public access per your business logic
response = client.get(
f"/api/v1/vendor /{verified_vendor.vendor_code}", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["vendor_code"] == verified_vendor.vendor_code
assert data["vendor_name"] == verified_vendor.vendor_name
def test_add_product_to_vendor_success(self, client, auth_headers, test_vendor, unique_product):
"""Test adding product to vendor successfully"""
product_data = {
"marketplace_product_id": unique_product.marketplace_product_id, # Use string marketplace_product_id, not database id
"price": 29.99,
"is_active": True,
"is_featured": False,
}
response = client.post(
f"/api/v1/vendor /{test_vendor.vendor_code}/products",
headers=auth_headers,
json=product_data
)
assert response.status_code == 200
data = response.json()
# The response structure contains nested product data
assert data["vendor_id"] == test_vendor.id
assert data["price"] == 29.99
assert data["is_active"] is True
assert data["is_featured"] is False
# MarketplaceProduct details are nested in the 'marketplace_product' field
assert "marketplace_product" in data
assert data["marketplace_product"]["marketplace_product_id"] == unique_product.marketplace_product_id
assert data["marketplace_product"]["id"] == unique_product.id
def test_add_product_to_vendor_already_exists_conflict(self, client, auth_headers, test_vendor, test_product):
"""Test adding product that already exists in vendor returns ProductAlreadyExistsException"""
# test_product fixture already creates a relationship, get the marketplace_product_id string
existing_product = test_product.marketplace_product
product_data = {
"marketplace_product_id": existing_product.marketplace_product_id, # Use string marketplace_product_id
"price": 29.99,
}
response = client.post(
f"/api/v1/vendor /{test_vendor.vendor_code}/products",
headers=auth_headers,
json=product_data
)
assert response.status_code == 409
data = response.json()
assert data["error_code"] == "PRODUCT_ALREADY_EXISTS"
assert data["status_code"] == 409
assert test_vendor.vendor_code in data["message"]
assert existing_product.marketplace_product_id in data["message"]
def test_add_nonexistent_product_to_vendor_not_found(self, client, auth_headers, test_vendor):
"""Test adding nonexistent product to vendor returns MarketplaceProductNotFoundException"""
product_data = {
"marketplace_product_id": "NONEXISTENT_PRODUCT", # Use string marketplace_product_id that doesn't exist
"price": 29.99,
}
response = client.post(
f"/api/v1/vendor /{test_vendor.vendor_code}/products",
headers=auth_headers,
json=product_data
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "PRODUCT_NOT_FOUND"
assert data["status_code"] == 404
assert "NONEXISTENT_PRODUCT" in data["message"]
def test_get_products_success(self, client, auth_headers, test_vendor, test_product):
"""Test getting vendor products successfully"""
response = client.get(
f"/api/v1/vendor /{test_vendor.vendor_code}/products",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
assert len(data["products"]) >= 1
assert "vendor " in data
assert data["vendor "]["vendor_code"] == test_vendor.vendor_code
def test_get_products_with_filters(self, client, auth_headers, test_vendor):
"""Test getting vendor products with filtering"""
# Test active_only filter
response = client.get(
f"/api/v1/vendor /{test_vendor.vendor_code}/products?active_only=true",
headers=auth_headers
)
assert response.status_code == 200
# Test featured_only filter
response = client.get(
f"/api/v1/vendor /{test_vendor.vendor_code}/products?featured_only=true",
headers=auth_headers
)
assert response.status_code == 200
def test_get_products_from_nonexistent_vendor_not_found(self, client, auth_headers):
"""Test getting products from nonexistent vendor returns VendorNotFoundException"""
response = client.get(
"/api/v1/vendor /NONEXISTENT/products",
headers=auth_headers
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "VENDOR_NOT_FOUND"
assert data["status_code"] == 404
assert "NONEXISTENT" in data["message"]
def test_vendor_not_active_business_logic_error(self, client, auth_headers, test_vendor, db):
"""Test accessing inactive vendor returns VendorNotActiveException (if enforced)"""
# Set vendor to inactive
test_vendor.is_active = False
db.commit()
# Depending on your business logic, this might return an error
response = client.get(
f"/api/v1/vendor /{test_vendor.vendor_code}", headers=auth_headers
)
# If your service enforces active vendor requirement
if response.status_code == 400:
data = response.json()
assert data["error_code"] == "VENDOR_NOT_ACTIVE"
assert data["status_code"] == 400
assert test_vendor.vendor_code in data["message"]
def test_vendor_not_verified_business_logic_error(self, client, auth_headers, test_vendor, db):
"""Test operations requiring verification returns VendorNotVerifiedException (if enforced)"""
# Set vendor to unverified
test_vendor.is_verified = False
db.commit()
# Test adding products (might require verification)
product_data = {
"marketplace_product_id": 1,
"price": 29.99,
}
response = client.post(
f"/api/v1/vendor /{test_vendor.vendor_code}/products",
headers=auth_headers,
json=product_data
)
# If your service requires verification for adding products
if response.status_code == 400:
data = response.json()
assert data["error_code"] == "VENDOR_NOT_VERIFIED"
assert data["status_code"] == 400
assert test_vendor.vendor_code in data["message"]
def test_get_vendor_without_auth_returns_invalid_token(self, client):
"""Test that vendor endpoints require authentication returns InvalidTokenException"""
response = client.get("/api/v1/vendor ")
assert response.status_code == 401
data = response.json()
assert data["error_code"] == "INVALID_TOKEN"
assert data["status_code"] == 401
def test_pagination_validation_errors(self, client, auth_headers):
"""Test pagination parameter validation"""
# Test negative skip
response = client.get("/api/v1/vendor ?skip=-1", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
# Test zero limit
response = client.get("/api/v1/vendor ?limit=0", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
# Test excessive limit
response = client.get("/api/v1/vendor ?limit=10000", headers=auth_headers)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
def test_exception_structure_consistency(self, client, auth_headers):
"""Test that all vendor exceptions follow the consistent LetzShopException structure"""
# Test with a known error case
response = client.get("/api/v1/vendor /NONEXISTENT", headers=auth_headers)
assert response.status_code == 404
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"
assert isinstance(data["error_code"], str)
assert isinstance(data["message"], str)
assert isinstance(data["status_code"], int)
# Details field should be present for domain-specific exceptions
if "details" in data:
assert isinstance(data["details"], dict)

View File

@@ -10,10 +10,10 @@ class TestAuthentication:
"""Test that protected endpoints reject unauthenticated requests"""
protected_endpoints = [
"/api/v1/admin/users",
"/api/v1/admin/shops",
"/api/v1/admin/vendors",
"/api/v1/marketplace/import-jobs",
"/api/v1/marketplace/product",
"/api/v1/shop",
"/api/v1/vendor ",
"/api/v1/stats",
"/api/v1/stock",
]

View File

@@ -16,7 +16,7 @@ class TestAuthorization:
"""Test that admin users can access admin endpoints"""
admin_endpoints = [
"/api/v1/admin/users",
"/api/v1/admin/shops",
"/api/v1/admin/vendors",
"/api/v1/admin/marketplace-import-jobs",
]
@@ -36,15 +36,15 @@ class TestAuthorization:
response = client.get(endpoint, headers=auth_headers)
assert response.status_code == 200 # Regular user should have access
def test_shop_owner_access_control(
self, client, auth_headers, test_shop, other_user
def test_vendor_owner_access_control(
self, client, auth_headers, test_vendor, other_user
):
"""Test that users can only access their own shops"""
# Test accessing own shop (should work)
"""Test that users can only access their own vendors"""
# Test accessing own vendor (should work)
response = client.get(
f"/api/v1/shop/{test_shop.shop_code}", headers=auth_headers
f"/api/v1/vendor /{test_vendor.vendor_code}", headers=auth_headers
)
# Response depends on your implementation - could be 200 or 404 if shop doesn't belong to user
# Response depends on your implementation - could be 200 or 404 if vendor doesn't belong to user
# The exact assertion depends on your shop access control implementation
# The exact assertion depends on your vendor access control implementation
assert response.status_code in [200, 403, 404]

View File

@@ -13,15 +13,15 @@ from models.database.marketplace_import_job import MarketplaceImportJob
@pytest.mark.marketplace
class TestBackgroundTasks:
@pytest.mark.asyncio
async def test_marketplace_import_success(self, db, test_user, test_shop):
async def test_marketplace_import_success(self, db, test_user, test_vendor):
"""Test successful marketplace import background task"""
# Create import job
job = MarketplaceImportJob(
status="pending",
source_url="http://example.com/test.csv",
shop_name="TESTSHOP",
vendor_name="TESTSHOP",
marketplace="TestMarket",
shop_id=test_shop.id,
vendor_id=test_vendor.id,
user_id=test_user.id,
)
db.add(job)
@@ -67,15 +67,15 @@ class TestBackgroundTasks:
assert updated_job.completed_at is not None
@pytest.mark.asyncio
async def test_marketplace_import_failure(self, db, test_user, test_shop):
async def test_marketplace_import_failure(self, db, test_user, test_vendor):
"""Test marketplace import failure handling"""
# Create import job
job = MarketplaceImportJob(
status="pending",
source_url="http://example.com/test.csv",
shop_name="TESTSHOP",
vendor_name="TESTSHOP",
marketplace="TestMarket",
shop_id=test_shop.id,
vendor_id=test_vendor.id,
user_id=test_user.id,
)
db.add(job)
@@ -151,15 +151,15 @@ class TestBackgroundTasks:
mock_instance.process_marketplace_csv_from_url.assert_not_called()
@pytest.mark.asyncio
async def test_marketplace_import_with_errors(self, db, test_user, test_shop):
async def test_marketplace_import_with_errors(self, db, test_user, test_vendor):
"""Test marketplace import with some errors"""
# Create import job
job = MarketplaceImportJob(
status="pending",
source_url="http://example.com/test.csv",
shop_name="TESTSHOP",
vendor_name="TESTSHOP",
marketplace="TestMarket",
shop_id=test_shop.id,
vendor_id=test_vendor.id,
user_id=test_user.id,
)
db.add(job)

View File

@@ -61,22 +61,22 @@ class TestIntegrationFlows:
assert response.json()["total"] == 1
def test_product_workflow(self, client, auth_headers):
"""Test shop creation and product management workflow"""
# 1. Create a shop
shop_data = {
"shop_code": "FLOWSHOP",
"shop_name": "Integration Flow Shop",
"description": "Test shop for integration",
"""Test vendor creation and product management workflow"""
# 1. Create a vendor
vendor_data = {
"vendor_code": "FLOWSHOP",
"vendor_name": "Integration Flow Shop",
"description": "Test vendor for integration",
}
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
response = client.post("/api/v1/vendor ", headers=auth_headers, json=vendor_data)
assert response.status_code == 200
shop = response.json()
vendor = response.json()
# 2. Create a product
product_data = {
"marketplace_product_id": "SHOPFLOW001",
"title": "Shop Flow MarketplaceProduct",
"title": "Vendor Flow MarketplaceProduct",
"price": "15.99",
"marketplace": "ShopFlow",
}
@@ -87,11 +87,11 @@ class TestIntegrationFlows:
assert response.status_code == 200
product = response.json()
# 3. Add product to shop (if endpoint exists)
# This would test the shop-product association
# 3. Add product to vendor (if endpoint exists)
# This would test the vendor -product association
# 4. Get shop details
response = client.get(f"/api/v1/shop/{shop['shop_code']}", headers=auth_headers)
# 4. Get vendor details
response = client.get(f"/api/v1/vendor /{vendor ['vendor_code']}", headers=auth_headers)
assert response.status_code == 200
def test_stock_operations_workflow(self, client, auth_headers):

View File

@@ -1,6 +1,6 @@
# tests/system/test_error_handling.py
"""
System tests for error handling across the LetzShop API.
System tests for error handling across the LetzVendor API.
Tests the complete error handling flow from FastAPI through custom exception handlers
to ensure proper HTTP status codes, error structures, and client-friendly responses.
@@ -16,7 +16,7 @@ class TestErrorHandling:
def test_invalid_json_request(self, client, auth_headers):
"""Test handling of malformed JSON requests"""
response = client.post(
"/api/v1/shop",
"/api/v1/vendor ",
headers=auth_headers,
content="{ invalid json syntax"
)
@@ -27,13 +27,13 @@ class TestErrorHandling:
assert data["message"] == "Request validation failed"
assert "validation_errors" in data["details"]
def test_missing_required_fields_shop_creation(self, client, auth_headers):
def test_missing_required_fields_vendor_creation(self, client, auth_headers):
"""Test validation errors for missing required fields"""
# Missing shop_name
# Missing vendor_name
response = client.post(
"/api/v1/shop",
"/api/v1/vendor ",
headers=auth_headers,
json={"shop_code": "TESTSHOP"}
json={"vendor_code": "TESTSHOP"}
)
assert response.status_code == 422
@@ -42,28 +42,28 @@ class TestErrorHandling:
assert data["status_code"] == 422
assert "validation_errors" in data["details"]
def test_invalid_field_format_shop_creation(self, client, auth_headers):
def test_invalid_field_format_vendor_creation(self, client, auth_headers):
"""Test validation errors for invalid field formats"""
# Invalid shop_code format (contains special characters)
# Invalid vendor_code format (contains special characters)
response = client.post(
"/api/v1/shop",
"/api/v1/vendor ",
headers=auth_headers,
json={
"shop_code": "INVALID@SHOP!",
"shop_name": "Test Shop"
"vendor_code": "INVALID@SHOP!",
"vendor_name": "Test Shop"
}
)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "INVALID_SHOP_DATA"
assert data["error_code"] == "INVALID_VENDOR_DATA"
assert data["status_code"] == 422
assert data["details"]["field"] == "shop_code"
assert data["details"]["field"] == "vendor_code"
assert "letters, numbers, underscores, and hyphens" in data["message"]
def test_missing_authentication_token(self, client):
"""Test authentication required endpoints without token"""
response = client.get("/api/v1/shop")
response = client.get("/api/v1/vendor ")
assert response.status_code == 401
data = response.json()
@@ -73,7 +73,7 @@ class TestErrorHandling:
def test_invalid_authentication_token(self, client):
"""Test endpoints with invalid JWT token"""
headers = {"Authorization": "Bearer invalid_token_here"}
response = client.get("/api/v1/shop", headers=headers)
response = client.get("/api/v1/vendor ", headers=headers)
assert response.status_code == 401
data = response.json()
@@ -85,19 +85,19 @@ class TestErrorHandling:
# This would require creating an expired token for testing
expired_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.expired.token"
headers = {"Authorization": f"Bearer {expired_token}"}
response = client.get("/api/v1/shop", headers=headers)
response = client.get("/api/v1/vendor ", headers=headers)
assert response.status_code == 401
data = response.json()
assert data["status_code"] == 401
def test_shop_not_found(self, client, auth_headers):
"""Test accessing non-existent shop"""
response = client.get("/api/v1/shop/NONEXISTENT", headers=auth_headers)
def test_vendor_not_found(self, client, auth_headers):
"""Test accessing non-existent vendor """
response = client.get("/api/v1/vendor /NONEXISTENT", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "SHOP_NOT_FOUND"
assert data["error_code"] == "VENDOR_NOT_FOUND"
assert data["status_code"] == 404
assert data["details"]["resource_type"] == "Shop"
assert data["details"]["identifier"] == "NONEXISTENT"
@@ -113,20 +113,20 @@ class TestErrorHandling:
assert data["details"]["resource_type"] == "MarketplaceProduct"
assert data["details"]["identifier"] == "NONEXISTENT"
def test_duplicate_shop_creation(self, client, auth_headers, test_shop):
"""Test creating shop with duplicate shop code"""
shop_data = {
"shop_code": test_shop.shop_code,
"shop_name": "Duplicate Shop"
def test_duplicate_vendor_creation(self, client, auth_headers, test_vendor):
"""Test creating vendor with duplicate vendor code"""
vendor_data = {
"vendor_code": test_vendor.vendor_code,
"vendor_name": "Duplicate Shop"
}
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
response = client.post("/api/v1/vendor ", headers=auth_headers, json=vendor_data)
assert response.status_code == 409
data = response.json()
assert data["error_code"] == "SHOP_ALREADY_EXISTS"
assert data["error_code"] == "VENDOR_ALREADY_EXISTS"
assert data["status_code"] == 409
assert data["details"]["shop_code"] == test_shop.shop_code.upper()
assert data["details"]["vendor_code"] == test_vendor.vendor_code.upper()
def test_duplicate_product_creation(self, client, auth_headers, test_marketplace_product):
"""Test creating product with duplicate product ID"""
@@ -144,15 +144,15 @@ class TestErrorHandling:
assert data["status_code"] == 409
assert data["details"]["marketplace_product_id"] == test_marketplace_product.marketplace_product_id
def test_unauthorized_shop_access(self, client, auth_headers, inactive_shop):
"""Test accessing shop without proper permissions"""
response = client.get(f"/api/v1/shop/{inactive_shop.shop_code}", headers=auth_headers)
def test_unauthorized_vendor_access(self, client, auth_headers, inactive_vendor):
"""Test accessing vendor without proper permissions"""
response = client.get(f"/api/v1/vendor /{inactive_vendor.vendor_code}", headers=auth_headers)
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "UNAUTHORIZED_SHOP_ACCESS"
assert data["error_code"] == "UNAUTHORIZED_VENDOR_ACCESS"
assert data["status_code"] == 403
assert data["details"]["shop_code"] == inactive_shop.shop_code
assert data["details"]["vendor_code"] == inactive_vendor.vendor_code
def test_insufficient_permissions(self, client, auth_headers, admin_only_endpoint="/api/v1/admin/users"):
"""Test accessing admin endpoints with regular user"""
@@ -164,29 +164,29 @@ class TestErrorHandling:
assert data["error_code"] in ["ADMIN_REQUIRED", "INSUFFICIENT_PERMISSIONS"]
assert data["status_code"] == 403
def test_business_logic_violation_max_shops(self, client, auth_headers, monkeypatch):
"""Test business logic violation - creating too many shops"""
# This test would require mocking the shop limit check
# For now, test the error structure when creating multiple shops
shops_created = []
def test_business_logic_violation_max_vendors(self, client, auth_headers, monkeypatch):
"""Test business logic violation - creating too many vendors"""
# This test would require mocking the vendor limit check
# For now, test the error structure when creating multiple vendors
vendors_created = []
for i in range(6): # Assume limit is 5
shop_data = {
"shop_code": f"SHOP{i:03d}",
"shop_name": f"Test Shop {i}"
vendor_data = {
"vendor_code": f"SHOP{i:03d}",
"vendor_name": f"Test Vendor {i}"
}
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
shops_created.append(response)
response = client.post("/api/v1/vendor ", headers=auth_headers, json=vendor_data)
vendors_created.append(response)
# At least one should succeed, and if limit is enforced, later ones should fail
success_count = sum(1 for r in shops_created if r.status_code in [200, 201])
success_count = sum(1 for r in vendors_created if r.status_code in [200, 201])
assert success_count >= 1
# If any failed due to limit, check error structure
failed_responses = [r for r in shops_created if r.status_code == 400]
failed_responses = [r for r in vendors_created if r.status_code == 400]
if failed_responses:
data = failed_responses[0].json()
assert data["error_code"] == "MAX_SHOPS_REACHED"
assert "max_shops" in data["details"]
assert data["error_code"] == "MAX_VENDORS_REACHED"
assert "max_vendors" in data["details"]
def test_validation_error_invalid_gtin(self, client, auth_headers):
"""Test validation error for invalid GTIN format"""
@@ -204,7 +204,7 @@ class TestErrorHandling:
assert data["status_code"] == 422
assert data["details"]["field"] == "gtin"
def test_stock_insufficient_quantity(self, client, auth_headers, test_shop, test_marketplace_product):
def test_stock_insufficient_quantity(self, client, auth_headers, test_vendor, test_marketplace_product):
"""Test business logic error for insufficient stock"""
# First create some stock
stock_data = {
@@ -246,7 +246,7 @@ class TestErrorHandling:
def test_method_not_allowed(self, client, auth_headers):
"""Test 405 for wrong HTTP method on existing endpoints"""
# Try DELETE on an endpoint that only supports GET
response = client.delete("/api/v1/shop", headers=auth_headers)
response = client.delete("/api/v1/vendor ", headers=auth_headers)
assert response.status_code == 405
# FastAPI automatically handles 405 errors
@@ -255,9 +255,9 @@ class TestErrorHandling:
"""Test handling of unsupported content types"""
headers = {**auth_headers, "Content-Type": "application/xml"}
response = client.post(
"/api/v1/shop",
"/api/v1/vendor ",
headers=headers,
content="<shop><code>TEST</code></shop>"
content="<vendor ><code>TEST</code></vendor >"
)
assert response.status_code in [400, 415, 422]
@@ -265,13 +265,13 @@ class TestErrorHandling:
def test_large_payload_handling(self, client, auth_headers):
"""Test handling of unusually large payloads"""
large_description = "x" * 100000 # Very long description
shop_data = {
"shop_code": "LARGESHOP",
"shop_name": "Large Shop",
vendor_data = {
"vendor_code": "LARGESHOP",
"vendor_name": "Large Shop",
"description": large_description
}
response = client.post("/api/v1/shop", headers=auth_headers, json=shop_data)
response = client.post("/api/v1/vendor ", headers=auth_headers, json=vendor_data)
# Should either accept it or reject with appropriate error
assert response.status_code in [200, 201, 413, 422]
@@ -285,7 +285,7 @@ class TestErrorHandling:
# Make rapid requests to potentially trigger rate limiting
responses = []
for _ in range(50): # Aggressive request count
response = client.get("/api/v1/shop", headers=auth_headers)
response = client.get("/api/v1/vendor ", headers=auth_headers)
responses.append(response)
# Check if any rate limiting occurred and verify error structure
@@ -305,12 +305,12 @@ class TestErrorHandling:
response = client.get("/health")
assert response.status_code == 200
def test_marketplace_import_errors(self, client, auth_headers, test_shop):
def test_marketplace_import_errors(self, client, auth_headers, test_vendor):
"""Test marketplace import specific errors"""
# Test invalid marketplace
import_data = {
"marketplace": "INVALID_MARKETPLACE",
"shop_code": test_shop.shop_code
"vendor_code": test_vendor.vendor_code
}
response = client.post("/api/v1/imports", headers=auth_headers, json=import_data)
@@ -344,7 +344,7 @@ class TestErrorHandling:
def test_error_response_consistency(self, client, auth_headers):
"""Test that all error responses follow consistent structure"""
test_cases = [
("/api/v1/shop/NONEXISTENT", 404),
("/api/v1/vendor /NONEXISTENT", 404),
("/api/v1/marketplace/product/NONEXISTENT", 404),
]
@@ -365,7 +365,7 @@ class TestErrorHandling:
def test_cors_error_handling(self, client):
"""Test CORS errors are handled properly"""
# Test preflight request
response = client.options("/api/v1/shop")
response = client.options("/api/v1/vendor ")
# Should either succeed or be handled gracefully
assert response.status_code in [200, 204, 405]
@@ -373,7 +373,7 @@ class TestErrorHandling:
def test_authentication_error_details(self, client):
"""Test authentication error provides helpful details"""
# Test missing Authorization header
response = client.get("/api/v1/shop")
response = client.get("/api/v1/vendor ")
assert response.status_code == 401
data = response.json()
@@ -406,7 +406,7 @@ class TestErrorRecovery:
assert health_response.status_code == 200
# API endpoints may or may not work depending on system state
api_response = client.get("/api/v1/shop", headers=auth_headers)
api_response = client.get("/api/v1/vendor ", headers=auth_headers)
# Should get either data or a proper error, not a crash
assert api_response.status_code in [200, 401, 403, 500, 503]
@@ -416,7 +416,7 @@ class TestErrorRecovery:
with caplog.at_level(logging.ERROR):
# Trigger an error
client.get("/api/v1/shop/NONEXISTENT", headers=auth_headers)
client.get("/api/v1/vendor /NONEXISTENT", headers=auth_headers)
# Check that error was logged (if your app logs 404s as errors)
# Adjust based on your logging configuration

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