shop product refactoring
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
6
tests/fixtures/testing_fixtures.py
vendored
6
tests/fixtures/testing_fixtures.py
vendored
@@ -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
|
||||
]
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
389
tests/integration/api/v1/test_vendor_endpoints.py
Normal file
389
tests/integration/api/v1/test_vendor_endpoints.py
Normal 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)
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
365
tests/unit/services/test_vendor_service.py
Normal file
365
tests/unit/services/test_vendor_service.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user