major refactoring adding vendor and customer features

This commit is contained in:
2025-10-11 09:09:25 +02:00
parent f569995883
commit dd16198276
126 changed files with 15109 additions and 3747 deletions

View File

@@ -12,7 +12,7 @@ from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.marketplace_product import MarketplaceProduct
from models.database.vendor import Vendor
from models.database.product import Product
from models.database.stock import Stock
from models.database.inventory import Inventory
from models.database.user import User
# Use in-memory SQLite database for tests
@@ -90,6 +90,7 @@ pytest_plugins = [
"tests.fixtures.auth_fixtures",
"tests.fixtures.marketplace_product_fixtures",
"tests.fixtures.vendor_fixtures",
"tests.fixtures.customer_fixtures",
"tests.fixtures.marketplace_import_job_fixtures",
"tests.fixtures.testing_fixtures",
]

42
tests/fixtures/customer_fixtures.py vendored Normal file
View File

@@ -0,0 +1,42 @@
# tests/fixtures/customer_fixtures.py
import pytest
from models.database.customer import Customer, CustomerAddress
@pytest.fixture
def test_customer(db, test_vendor):
"""Create a test customer"""
customer = Customer(
vendor_id=test_vendor.id,
email="testcustomer@example.com",
hashed_password="hashed_password",
first_name="John",
last_name="Doe",
customer_number="TEST001",
is_active=True,
)
db.add(customer)
db.commit()
db.refresh(customer)
return customer
@pytest.fixture
def test_customer_address(db, test_vendor, test_customer):
"""Create a test customer address"""
address = CustomerAddress(
vendor_id=test_vendor.id,
customer_id=test_customer.id,
address_type="shipping",
first_name="John",
last_name="Doe",
address_line_1="123 Main St",
city="Luxembourg",
postal_code="L-1234",
country="Luxembourg",
is_default=True,
)
db.add(address)
db.commit()
db.refresh(address)
return address

View File

@@ -1,6 +1,5 @@
# tests/fixtures/marketplace_import_job_fixtures.py
import pytest
from models.database.marketplace_import_job import MarketplaceImportJob
@@ -9,7 +8,7 @@ def test_marketplace_import_job(db, test_vendor, test_user):
"""Create a test marketplace import job"""
job = MarketplaceImportJob(
marketplace="amazon",
vendor_name="Test Import Vendor",
# REMOVED: vendor_name field doesn't exist
status="completed",
source_url="https://test-marketplace.example.com/import",
vendor_id=test_vendor.id,
@@ -30,7 +29,7 @@ def create_test_marketplace_import_job(db, vendor_id, user_id, **kwargs):
"""Helper function to create MarketplaceImportJob with defaults"""
defaults = {
"marketplace": "test",
"vendor_name": "Test Vendor",
# REMOVED: name field
"status": "pending",
"source_url": "https://test.example.com/import",
"vendor_id": vendor_id,

View File

@@ -17,7 +17,7 @@ def test_marketplace_product(db):
currency="EUR",
brand="TestBrand",
gtin="1234567890123",
availability="in stock",
availability="in inventory",
marketplace="Letzshop",
vendor_name="TestVendor",
)
@@ -39,7 +39,7 @@ def unique_product(db):
currency="EUR",
brand=f"UniqueBrand_{unique_id}",
gtin=f"123456789{unique_id[:4]}",
availability="in stock",
availability="in inventory",
marketplace="Letzshop",
vendor_name=f"UniqueVendor_{unique_id}",
google_product_category=f"UniqueCategory_{unique_id}",
@@ -89,7 +89,7 @@ def create_unique_marketplace_product_factory():
"price": "15.99",
"currency": "EUR",
"marketplace": "TestMarket",
"vendor_name": "TestVendor",
"name": "TestVendor",
}
defaults.update(kwargs)
@@ -109,15 +109,15 @@ def marketplace_product_factory():
@pytest.fixture
def test_marketplace_product_with_stock(db, test_marketplace_product, test_stock):
"""MarketplaceProduct with associated stock record."""
def test_marketplace_product_with_inventory(db, test_marketplace_product, test_inventory):
"""MarketplaceProduct with associated inventory record."""
# Ensure they're linked by GTIN
if test_marketplace_product.gtin != test_stock.gtin:
test_stock.gtin = test_marketplace_product.gtin
if test_marketplace_product.gtin != test_inventory.gtin:
test_inventory.gtin = test_marketplace_product.gtin
db.commit()
db.refresh(test_stock)
db.refresh(test_inventory)
return {
'marketplace_product': test_marketplace_product,
'stock': test_stock
'inventory': test_inventory
}

View File

@@ -23,7 +23,7 @@ def empty_db(db):
tables_to_clear = [
"marketplace_import_jobs", # Has foreign keys to vendors and users
"products", # Has foreign keys to vendors and products
"stock", # Fixed: singular not plural
"inventory", # Fixed: singular not plural
"products", # Referenced by products
"vendors", # Has foreign key to users
"users" # Base table

View File

@@ -1,28 +1,28 @@
# tests/fixtures/vendor_fixtures.py
import uuid
import pytest
from models.database.vendor import Vendor
from models.database.product import Product
from models.database.stock import Stock
from models.database.inventory import Inventory
@pytest.fixture
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
unique_id = str(uuid.uuid4())[:8].upper()
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,
vendor_code=f"TESTVENDOR_{unique_id}",
subdomain=f"testvendor{unique_id.lower()}", # ADDED
name=f"Test Vendor {unique_id.lower()}", # FIXED
owner_user_id=test_user.id,
is_active=True,
is_verified=True,
)
db.add(vendor)
db.commit()
db.refresh(vendor)
return vendor
return vendor
@pytest.fixture
@@ -30,17 +30,18 @@ def unique_vendor(db, test_user):
"""Create a unique vendor for tests that need isolated vendor data"""
unique_id = str(uuid.uuid4())[:8]
vendor = Vendor(
vendor_code=f"UNIQUEVENDOR_{unique_id}",
vendor_name=f"Unique Test Vendor {unique_id}",
vendor_code=f"UNIQUEVENDOR_{unique_id.upper()}",
subdomain=f"uniquevendor{unique_id.lower()}", # ADDED
name=f"Unique Test Vendor {unique_id}", # FIXED
description=f"A unique test vendor {unique_id}",
owner_id=test_user.id,
owner_user_id=test_user.id,
is_active=True,
is_verified=True,
)
db.add(vendor)
db.commit()
db.refresh(vendor)
return vendor
return vendor
@pytest.fixture
@@ -48,16 +49,17 @@ def inactive_vendor(db, other_user):
"""Create an inactive vendor owned by other_user"""
unique_id = str(uuid.uuid4())[:8]
vendor = Vendor(
vendor_code=f"INACTIVE_{unique_id}",
vendor_name=f"Inactive Vendor {unique_id}",
owner_id=other_user.id,
vendor_code=f"INACTIVE_{unique_id.upper()}",
subdomain=f"inactive{unique_id.lower()}", # ADDED
name=f"Inactive Vendor {unique_id}", # FIXED
owner_user_id=other_user.id,
is_active=False,
is_verified=False,
)
db.add(vendor)
db.commit()
db.refresh(vendor)
return vendor
return vendor
@pytest.fixture
@@ -65,32 +67,30 @@ def verified_vendor(db, other_user):
"""Create a verified vendor owned by other_user"""
unique_id = str(uuid.uuid4())[:8]
vendor = Vendor(
vendor_code=f"VERIFIED_{unique_id}",
vendor_name=f"Verified Vendor {unique_id}",
owner_id=other_user.id,
vendor_code=f"VERIFIED_{unique_id.upper()}",
subdomain=f"verified{unique_id.lower()}", # ADDED
name=f"Verified Vendor {unique_id}", # FIXED
owner_user_id=other_user.id,
is_active=True,
is_verified=True,
)
db.add(vendor)
db.commit()
db.refresh(vendor)
return vendor
return vendor
@pytest.fixture
def test_product(db, test_vendor, unique_product):
"""Create a vendor product relationship"""
product = Product(
vendor_id=test_vendor.id, marketplace_product_id=unique_product.id, is_active=True
vendor_id=test_vendor.id,
marketplace_product_id=unique_product.id,
is_active=True,
price=24.99,
is_featured=False,
min_quantity=1,
)
# Add optional fields if they exist in your model
if hasattr(Product, "price"):
product.price = 24.99
if hasattr(Product, "is_featured"):
product.is_featured = False
if hasattr(Product, "min_quantity"):
product.min_quantity = 1
db.add(product)
db.commit()
db.refresh(product)
@@ -98,53 +98,54 @@ def test_product(db, test_vendor, unique_product):
@pytest.fixture
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(
gtin=test_marketplace_product.gtin, # Use gtin instead of marketplace_product_id
@pytest.fixture
def test_inventory(db, test_product):
"""Create test inventory entry linked to product."""
unique_id = str(uuid.uuid4())[:8].upper()
inventory = Inventory(
product_id=test_product.id,
vendor_id=test_product.vendor_id,
location=f"WAREHOUSE_A_{unique_id}",
quantity=10,
reserved_quantity=0,
vendor_id=test_vendor.id, # Add vendor_id reference
quantity=100,
reserved_quantity=10,
gtin=test_product.marketplace_product.gtin, # Optional reference
)
db.add(stock)
db.add(inventory)
db.commit()
db.refresh(stock)
return stock
db.refresh(inventory)
return inventory
@pytest.fixture
def multiple_stocks(db, multiple_products, test_vendor):
"""Create multiple stock entries for testing"""
stocks = []
def multiple_inventory_entries(db, multiple_products, test_vendor):
"""Create multiple inventory entries for testing"""
inventory_entries = []
for i, product in enumerate(multiple_products):
stock = Stock(
inventory = Inventory(
gtin=product.gtin,
location=f"LOC_{i}",
quantity=10 + (i * 5), # Different quantities
quantity=10 + (i * 5),
reserved_quantity=i,
vendor_id=test_vendor.id,
)
stocks.append(stock)
inventory_entries.append(inventory)
db.add_all(stocks)
db.add_all(inventory_entries)
db.commit()
for stock in stocks:
db.refresh(stock)
return stocks
for inventory in inventory_entries:
db.refresh(inventory)
return inventory_entries
def create_unique_vendor_factory():
"""Factory function to create unique vendors in tests"""
def _create_vendor(db, owner_id, **kwargs):
def _create_vendor(db, owner_user_id, **kwargs):
unique_id = str(uuid.uuid4())[:8]
defaults = {
"vendor_code": f"FACTORY_{unique_id}",
"vendor_name": f"Factory Vendor {unique_id}",
"owner_id": owner_id,
"vendor_code": f"FACTORY_{unique_id.upper()}",
"subdomain": f"factory{unique_id.lower()}", # ADDED
"name": f"Factory Vendor {unique_id}", # FIXED
"owner_user_id": owner_user_id,
"is_active": True,
"is_verified": False,
}
@@ -154,7 +155,7 @@ def create_unique_vendor_factory():
db.add(vendor)
db.commit()
db.refresh(vendor)
return vendor
return vendor
return _create_vendor

View File

@@ -0,0 +1,455 @@
# tests/integration/api/v1/test_inventory_endpoints.py
import pytest
from models.database.inventory import Inventory
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.inventory
class TestInventoryAPI:
def test_set_inventory_new_success(self, client, auth_headers):
"""Test setting inventory for new GTIN successfully"""
inventory_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 100,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["location"] == "WAREHOUSE_A"
assert data["quantity"] == 100
def test_set_inventory_existing_success(self, client, auth_headers, db):
"""Test updating existing inventory successfully"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(inventory)
db.commit()
inventory_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 75,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75 # Should be replaced, not added
def test_set_inventory_invalid_gtin_validation_error(self, client, auth_headers):
"""Test setting inventory with invalid GTIN returns ValidationException"""
inventory_data = {
"gtin": "", # Empty GTIN
"location": "WAREHOUSE_A",
"quantity": 100,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "INVENTORY_VALIDATION_FAILED"
assert data["status_code"] == 422
assert "GTIN is required" in data["message"]
def test_set_inventory_invalid_quantity_validation_error(self, client, auth_headers):
"""Test setting inventory with invalid quantity returns InvalidQuantityException"""
inventory_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": -10, # Negative quantity
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
assert response.status_code in [400, 422]
data = response.json()
assert data["error_code"] in ["INVALID_QUANTITY", "VALIDATION_ERROR"]
if data["error_code"] == "INVALID_QUANTITY":
assert data["status_code"] == 422
assert data["details"]["field"] == "quantity"
def test_add_inventory_success(self, client, auth_headers, db):
"""Test adding to existing inventory successfully"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(inventory)
db.commit()
inventory_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 25,
}
response = client.post(
"/api/v1/inventory/add", headers=auth_headers, json=inventory_data
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75 # 50 + 25
def test_add_inventory_creates_new_if_not_exists(self, client, auth_headers):
"""Test adding to nonexistent inventory creates new inventory entry"""
inventory_data = {
"gtin": "9999999999999",
"location": "WAREHOUSE_A",
"quantity": 25,
}
response = client.post(
"/api/v1/inventory/add", headers=auth_headers, json=inventory_data
)
# Your service creates new inventory if it doesn't exist (upsert behavior)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "9999999999999"
assert data["location"] == "WAREHOUSE_A"
assert data["quantity"] == 25
def test_remove_inventory_success(self, client, auth_headers, db):
"""Test removing from existing inventory successfully"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(inventory)
db.commit()
inventory_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 15,
}
response = client.post(
"/api/v1/inventory/remove", headers=auth_headers, json=inventory_data
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 35 # 50 - 15
def test_remove_inventory_insufficient_returns_business_logic_error(self, client, auth_headers, db):
"""Test removing more inventory than available returns InsufficientInventoryException"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=10)
db.add(inventory)
db.commit()
inventory_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 20,
}
response = client.post(
"/api/v1/inventory/remove", headers=auth_headers, json=inventory_data
)
assert response.status_code == 400
data = response.json()
assert data["error_code"] == "INSUFFICIENT_INVENTORY"
assert data["status_code"] == 400
assert "Insufficient inventory" in data["message"]
assert data["details"]["gtin"] == "1234567890123"
assert data["details"]["location"] == "WAREHOUSE_A"
assert data["details"]["requested_quantity"] == 20
assert data["details"]["available_quantity"] == 10
def test_remove_inventory_not_found(self, client, auth_headers):
"""Test removing from nonexistent inventory returns InventoryNotFoundException"""
inventory_data = {
"gtin": "9999999999999",
"location": "WAREHOUSE_A",
"quantity": 15,
}
response = client.post(
"/api/v1/inventory/remove", headers=auth_headers, json=inventory_data
)
# This should actually return 404 since you can't remove from non-existent inventory
# If it returns 200, your service might create inventory with negative quantity
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "INVENTORY_NOT_FOUND"
assert data["status_code"] == 404
def test_negative_inventory_not_allowed_business_logic_error(self, client, auth_headers, db):
"""Test operations resulting in negative inventory returns NegativeInventoryException"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=5)
db.add(inventory)
db.commit()
inventory_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 10,
}
response = client.post(
"/api/v1/inventory/remove", headers=auth_headers, json=inventory_data
)
assert response.status_code == 400
data = response.json()
# This might be caught as INSUFFICIENT_INVENTORY or NEGATIVE_INVENTORY_NOT_ALLOWED
assert data["error_code"] in ["INSUFFICIENT_INVENTORY", "NEGATIVE_INVENTORY_NOT_ALLOWED"]
assert data["status_code"] == 400
def test_get_inventory_by_gtin_success(self, client, auth_headers, db):
"""Test getting inventory summary for GTIN successfully"""
# Create inventory in multiple locations
inventory1 = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
inventory2 = Inventory(gtin="1234567890123", location="WAREHOUSE_B", quantity=25)
db.add_all([inventory1, inventory2])
db.commit()
response = client.get("/api/v1/inventory/1234567890123", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["total_quantity"] == 75
assert len(data["locations"]) == 2
def test_get_inventory_by_gtin_not_found(self, client, auth_headers):
"""Test getting inventory for nonexistent GTIN returns InventoryNotFoundException"""
response = client.get("/api/v1/inventory/9999999999999", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "INVENTORY_NOT_FOUND"
assert data["status_code"] == 404
assert "9999999999999" in data["message"]
assert data["details"]["resource_type"] == "Inventory"
assert data["details"]["identifier"] == "9999999999999"
def test_get_total_inventory_success(self, client, auth_headers, db):
"""Test getting total inventory for GTIN successfully"""
# Create inventory in multiple locations
inventory1 = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
inventory2 = Inventory(gtin="1234567890123", location="WAREHOUSE_B", quantity=25)
db.add_all([inventory1, inventory2])
db.commit()
response = client.get("/api/v1/inventory/1234567890123/total", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["total_quantity"] == 75
assert data["locations_count"] == 2
def test_get_total_inventory_not_found(self, client, auth_headers):
"""Test getting total inventory for nonexistent GTIN returns InventoryNotFoundException"""
response = client.get("/api/v1/inventory/9999999999999/total", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "INVENTORY_NOT_FOUND"
assert data["status_code"] == 404
def test_get_all_inventory_success(self, client, auth_headers, db):
"""Test getting all inventory entries successfully"""
# Create some inventory entries
inventory1 = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
inventory2 = Inventory(gtin="9876543210987", location="WAREHOUSE_B", quantity=25)
db.add_all([inventory1, inventory2])
db.commit()
response = client.get("/api/v1/inventory", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert len(data) >= 2
def test_get_all_inventory_with_filters(self, client, auth_headers, db):
"""Test getting inventory entries with filtering"""
# Create inventory entries
inventory1 = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
inventory2 = Inventory(gtin="9876543210987", location="WAREHOUSE_B", quantity=25)
db.add_all([inventory1, inventory2])
db.commit()
# Filter by location
response = client.get("/api/v1/inventory?location=WAREHOUSE_A", headers=auth_headers)
assert response.status_code == 200
data = response.json()
for inventory in data:
assert inventory["location"] == "WAREHOUSE_A"
# Filter by GTIN
response = client.get("/api/v1/inventory?gtin=1234567890123", headers=auth_headers)
assert response.status_code == 200
data = response.json()
for inventory in data:
assert inventory["gtin"] == "1234567890123"
def test_update_inventory_success(self, client, auth_headers, db):
"""Test updating inventory quantity successfully"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(inventory)
db.commit()
db.refresh(inventory)
update_data = {"quantity": 75}
response = client.put(
f"/api/v1/inventory/{inventory.id}",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75
def test_update_inventory_not_found(self, client, auth_headers):
"""Test updating nonexistent inventory returns InventoryNotFoundException"""
update_data = {"quantity": 75}
response = client.put(
"/api/v1/inventory/99999",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "INVENTORY_NOT_FOUND"
assert data["status_code"] == 404
def test_update_inventory_invalid_quantity(self, client, auth_headers, db):
"""Test updating inventory with invalid quantity returns ValidationException"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(inventory)
db.commit()
db.refresh(inventory)
update_data = {"quantity": -10} # Negative quantity
response = client.put(
f"/api/v1/inventory/{inventory.id}",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "INVALID_QUANTITY"
assert data["status_code"] == 422
assert "Quantity cannot be negative" in data["message"]
assert data["details"]["field"] == "quantity"
def test_delete_inventory_success(self, client, auth_headers, db):
"""Test deleting inventory entry successfully"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(inventory)
db.commit()
db.refresh(inventory)
response = client.delete(
f"/api/v1/inventory/{inventory.id}",
headers=auth_headers,
)
assert response.status_code == 200
assert "deleted successfully" in response.json()["message"]
def test_delete_inventory_not_found(self, client, auth_headers):
"""Test deleting nonexistent inventory returns InventoryNotFoundException"""
response = client.delete(
"/api/v1/inventory/99999",
headers=auth_headers,
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "INVENTORY_NOT_FOUND"
assert data["status_code"] == 404
def test_location_not_found_error(self, client, auth_headers):
"""Test operations on nonexistent location returns LocationNotFoundException (if implemented)"""
inventory_data = {
"gtin": "1234567890123",
"location": "NONEXISTENT_LOCATION",
"quantity": 100,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
# This depends on whether your service validates locations
if response.status_code == 404:
data = response.json()
assert data["error_code"] == "LOCATION_NOT_FOUND"
assert data["status_code"] == 404
def test_invalid_inventory_operation_error(self, client, auth_headers):
"""Test invalid inventory operations return InvalidInventoryOperationException"""
# This would test business logic validation
# The exact scenario depends on your business rules
pass # Implementation depends on specific business rules
def test_get_inventory_without_auth_returns_invalid_token(self, client):
"""Test that inventory endpoints require authentication returns InvalidTokenException"""
response = client.get("/api/v1/inventory")
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/inventory?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/inventory?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/inventory?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 inventory exceptions follow the consistent LetzShopException structure"""
# Test with a known error case
response = client.get("/api/v1/inventory/9999999999999", headers=auth_headers)
assert response.status_code == 404
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"
assert isinstance(data["error_code"], str)
assert isinstance(data["message"], str)
assert isinstance(data["status_code"], int)
# Details field should be present for domain-specific exceptions
if "details" in data:
assert isinstance(data["details"], dict)

View File

@@ -11,7 +11,7 @@ class TestMarketplaceImportJobAPI:
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 vendor
test_vendor.owner_id = test_user.id
test_vendor.owner_user_id = test_user.id
import_data = {
"url": "https://example.com/products.csv",
@@ -51,7 +51,7 @@ class TestMarketplaceImportJobAPI:
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
test_vendor.owner_user_id = other_user.id
import_data = {
"url": "https://example.com/products.csv",
@@ -410,7 +410,7 @@ class TestMarketplaceImportJobAPI:
"""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_vendor.owner_id = test_user.id
test_vendor.owner_user_id = test_user.id
import_data = {
"url": "https://example.com/products.csv",

View File

@@ -85,7 +85,7 @@ class TestExportFunctionality:
db.commit()
response = client.get(
"/api/v1/marketplace/product?vendor_name=TestVendor1", headers=auth_headers
"/api/v1/marketplace/product?name=TestVendor1", headers=auth_headers
)
assert response.status_code == 200
@@ -121,7 +121,7 @@ class TestExportFunctionality:
db.commit()
response = client.get(
"/api/v1/marketplace/product?marketplace=Amazon&vendor_name=TestVendor",
"/api/v1/marketplace/product?marketplace=Amazon&name=TestVendor",
headers=auth_headers
)
assert response.status_code == 200

View File

@@ -57,7 +57,7 @@ class TestMarketplaceProductsAPI:
"price": "15.99",
"brand": "NewBrand",
"gtin": "9876543210987",
"availability": "in stock",
"availability": "in inventory",
"marketplace": "Amazon",
}
@@ -80,7 +80,7 @@ class TestMarketplaceProductsAPI:
"price": "15.99",
"brand": "NewBrand",
"gtin": "9876543210987",
"availability": "in stock",
"availability": "in inventory",
"marketplace": "Amazon",
}

View File

@@ -193,7 +193,7 @@ class TestPagination:
vendor = Vendor(
vendor_code=f"PAGEVENDOR{i:03d}_{unique_suffix}",
vendor_name=f"Pagination Vendor {i}",
owner_id=test_user.id,
owner_user_id=test_user.id,
is_active=True,
)
vendors.append(vendor)
@@ -212,33 +212,33 @@ class TestPagination:
assert data["skip"] == 0
assert data["limit"] == 5
def test_stock_pagination_success(self, client, auth_headers, db):
"""Test pagination for stock listing successfully"""
def test_inventory_pagination_success(self, client, auth_headers, db):
"""Test pagination for inventory listing successfully"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Create multiple stock entries
from models.database.stock import Stock
stocks = []
# Create multiple inventory entries
from models.database.inventory import Inventory
inventory_entries = []
for i in range(20):
stock = Stock(
inventory = Inventory(
gtin=f"123456789{i:04d}",
location=f"LOC_{unique_suffix}_{i}",
quantity=10 + i,
)
stocks.append(stock)
inventory_entries.append(inventory)
db.add_all(stocks)
db.add_all(inventory_entries)
db.commit()
# Test first page
response = client.get("/api/v1/stock?limit=8&skip=0", headers=auth_headers)
response = client.get("/api/v1/inventory?limit=8&skip=0", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert len(data) == 8
# Test second page
response = client.get("/api/v1/stock?limit=8&skip=8", headers=auth_headers)
response = client.get("/api/v1/inventory?limit=8&skip=8", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert len(data) == 8

View File

@@ -1,455 +0,0 @@
# tests/integration/api/v1/test_stock_endpoints.py
import pytest
from models.database.stock import Stock
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.stock
class TestStockAPI:
def test_set_stock_new_success(self, client, auth_headers):
"""Test setting stock for new GTIN successfully"""
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 100,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["location"] == "WAREHOUSE_A"
assert data["quantity"] == 100
def test_set_stock_existing_success(self, client, auth_headers, db):
"""Test updating existing stock successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 75,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75 # Should be replaced, not added
def test_set_stock_invalid_gtin_validation_error(self, client, auth_headers):
"""Test setting stock with invalid GTIN returns ValidationException"""
stock_data = {
"gtin": "", # Empty GTIN
"location": "WAREHOUSE_A",
"quantity": 100,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "STOCK_VALIDATION_FAILED"
assert data["status_code"] == 422
assert "GTIN is required" in data["message"]
def test_set_stock_invalid_quantity_validation_error(self, client, auth_headers):
"""Test setting stock with invalid quantity returns InvalidQuantityException"""
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": -10, # Negative quantity
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
assert response.status_code in [400, 422]
data = response.json()
assert data["error_code"] in ["INVALID_QUANTITY", "VALIDATION_ERROR"]
if data["error_code"] == "INVALID_QUANTITY":
assert data["status_code"] == 422
assert data["details"]["field"] == "quantity"
def test_add_stock_success(self, client, auth_headers, db):
"""Test adding to existing stock successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 25,
}
response = client.post(
"/api/v1/stock/add", headers=auth_headers, json=stock_data
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75 # 50 + 25
def test_add_stock_creates_new_if_not_exists(self, client, auth_headers):
"""Test adding to nonexistent stock creates new stock entry"""
stock_data = {
"gtin": "9999999999999",
"location": "WAREHOUSE_A",
"quantity": 25,
}
response = client.post(
"/api/v1/stock/add", headers=auth_headers, json=stock_data
)
# Your service creates new stock if it doesn't exist (upsert behavior)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "9999999999999"
assert data["location"] == "WAREHOUSE_A"
assert data["quantity"] == 25
def test_remove_stock_success(self, client, auth_headers, db):
"""Test removing from existing stock successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 15,
}
response = client.post(
"/api/v1/stock/remove", headers=auth_headers, json=stock_data
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 35 # 50 - 15
def test_remove_stock_insufficient_returns_business_logic_error(self, client, auth_headers, db):
"""Test removing more stock than available returns InsufficientStockException"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=10)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 20,
}
response = client.post(
"/api/v1/stock/remove", headers=auth_headers, json=stock_data
)
assert response.status_code == 400
data = response.json()
assert data["error_code"] == "INSUFFICIENT_STOCK"
assert data["status_code"] == 400
assert "Insufficient stock" in data["message"]
assert data["details"]["gtin"] == "1234567890123"
assert data["details"]["location"] == "WAREHOUSE_A"
assert data["details"]["requested_quantity"] == 20
assert data["details"]["available_quantity"] == 10
def test_remove_stock_not_found(self, client, auth_headers):
"""Test removing from nonexistent stock returns StockNotFoundException"""
stock_data = {
"gtin": "9999999999999",
"location": "WAREHOUSE_A",
"quantity": 15,
}
response = client.post(
"/api/v1/stock/remove", headers=auth_headers, json=stock_data
)
# This should actually return 404 since you can't remove from non-existent stock
# If it returns 200, your service might create stock with negative quantity
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
def test_negative_stock_not_allowed_business_logic_error(self, client, auth_headers, db):
"""Test operations resulting in negative stock returns NegativeStockException"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=5)
db.add(stock)
db.commit()
stock_data = {
"gtin": "1234567890123",
"location": "WAREHOUSE_A",
"quantity": 10,
}
response = client.post(
"/api/v1/stock/remove", headers=auth_headers, json=stock_data
)
assert response.status_code == 400
data = response.json()
# This might be caught as INSUFFICIENT_STOCK or NEGATIVE_STOCK_NOT_ALLOWED
assert data["error_code"] in ["INSUFFICIENT_STOCK", "NEGATIVE_STOCK_NOT_ALLOWED"]
assert data["status_code"] == 400
def test_get_stock_by_gtin_success(self, client, auth_headers, db):
"""Test getting stock summary for GTIN successfully"""
# Create stock in multiple locations
stock1 = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
stock2 = Stock(gtin="1234567890123", location="WAREHOUSE_B", quantity=25)
db.add_all([stock1, stock2])
db.commit()
response = client.get("/api/v1/stock/1234567890123", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["total_quantity"] == 75
assert len(data["locations"]) == 2
def test_get_stock_by_gtin_not_found(self, client, auth_headers):
"""Test getting stock for nonexistent GTIN returns StockNotFoundException"""
response = client.get("/api/v1/stock/9999999999999", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
assert "9999999999999" in data["message"]
assert data["details"]["resource_type"] == "Stock"
assert data["details"]["identifier"] == "9999999999999"
def test_get_total_stock_success(self, client, auth_headers, db):
"""Test getting total stock for GTIN successfully"""
# Create stock in multiple locations
stock1 = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
stock2 = Stock(gtin="1234567890123", location="WAREHOUSE_B", quantity=25)
db.add_all([stock1, stock2])
db.commit()
response = client.get("/api/v1/stock/1234567890123/total", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["gtin"] == "1234567890123"
assert data["total_quantity"] == 75
assert data["locations_count"] == 2
def test_get_total_stock_not_found(self, client, auth_headers):
"""Test getting total stock for nonexistent GTIN returns StockNotFoundException"""
response = client.get("/api/v1/stock/9999999999999/total", headers=auth_headers)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
def test_get_all_stock_success(self, client, auth_headers, db):
"""Test getting all stock entries successfully"""
# Create some stock entries
stock1 = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
stock2 = Stock(gtin="9876543210987", location="WAREHOUSE_B", quantity=25)
db.add_all([stock1, stock2])
db.commit()
response = client.get("/api/v1/stock", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert len(data) >= 2
def test_get_all_stock_with_filters(self, client, auth_headers, db):
"""Test getting stock entries with filtering"""
# Create stock entries
stock1 = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
stock2 = Stock(gtin="9876543210987", location="WAREHOUSE_B", quantity=25)
db.add_all([stock1, stock2])
db.commit()
# Filter by location
response = client.get("/api/v1/stock?location=WAREHOUSE_A", headers=auth_headers)
assert response.status_code == 200
data = response.json()
for stock in data:
assert stock["location"] == "WAREHOUSE_A"
# Filter by GTIN
response = client.get("/api/v1/stock?gtin=1234567890123", headers=auth_headers)
assert response.status_code == 200
data = response.json()
for stock in data:
assert stock["gtin"] == "1234567890123"
def test_update_stock_success(self, client, auth_headers, db):
"""Test updating stock quantity successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
db.refresh(stock)
update_data = {"quantity": 75}
response = client.put(
f"/api/v1/stock/{stock.id}",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 200
data = response.json()
assert data["quantity"] == 75
def test_update_stock_not_found(self, client, auth_headers):
"""Test updating nonexistent stock returns StockNotFoundException"""
update_data = {"quantity": 75}
response = client.put(
"/api/v1/stock/99999",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
def test_update_stock_invalid_quantity(self, client, auth_headers, db):
"""Test updating stock with invalid quantity returns ValidationException"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
db.refresh(stock)
update_data = {"quantity": -10} # Negative quantity
response = client.put(
f"/api/v1/stock/{stock.id}",
headers=auth_headers,
json=update_data,
)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "INVALID_QUANTITY"
assert data["status_code"] == 422
assert "Quantity cannot be negative" in data["message"]
assert data["details"]["field"] == "quantity"
def test_delete_stock_success(self, client, auth_headers, db):
"""Test deleting stock entry successfully"""
# Create initial stock
stock = Stock(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
db.add(stock)
db.commit()
db.refresh(stock)
response = client.delete(
f"/api/v1/stock/{stock.id}",
headers=auth_headers,
)
assert response.status_code == 200
assert "deleted successfully" in response.json()["message"]
def test_delete_stock_not_found(self, client, auth_headers):
"""Test deleting nonexistent stock returns StockNotFoundException"""
response = client.delete(
"/api/v1/stock/99999",
headers=auth_headers,
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "STOCK_NOT_FOUND"
assert data["status_code"] == 404
def test_location_not_found_error(self, client, auth_headers):
"""Test operations on nonexistent location returns LocationNotFoundException (if implemented)"""
stock_data = {
"gtin": "1234567890123",
"location": "NONEXISTENT_LOCATION",
"quantity": 100,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
# This depends on whether your service validates locations
if response.status_code == 404:
data = response.json()
assert data["error_code"] == "LOCATION_NOT_FOUND"
assert data["status_code"] == 404
def test_invalid_stock_operation_error(self, client, auth_headers):
"""Test invalid stock operations return InvalidStockOperationException"""
# This would test business logic validation
# The exact scenario depends on your business rules
pass # Implementation depends on specific business rules
def test_get_stock_without_auth_returns_invalid_token(self, client):
"""Test that stock endpoints require authentication returns InvalidTokenException"""
response = client.get("/api/v1/stock")
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/stock?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/stock?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/stock?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 stock exceptions follow the consistent LetzShopException structure"""
# Test with a known error case
response = client.get("/api/v1/stock/9999999999999", headers=auth_headers)
assert response.status_code == 404
data = response.json()
# Verify exception structure matches LetzShopException.to_dict()
required_fields = ["error_code", "message", "status_code"]
for field in required_fields:
assert field in data, f"Missing required field: {field}"
assert isinstance(data["error_code"], str)
assert isinstance(data["message"], str)
assert isinstance(data["status_code"], int)
# Details field should be present for domain-specific exceptions
if "details" in data:
assert isinstance(data["details"], dict)

View File

@@ -11,7 +11,7 @@ class TestVendorsAPI:
"""Test creating a new vendor successfully"""
vendor_data = {
"vendor_code": "NEWVENDOR001",
"vendor_name": "New Vendor",
"name": "New Vendor",
"description": "A new test vendor ",
}
@@ -20,14 +20,14 @@ class TestVendorsAPI:
assert response.status_code == 200
data = response.json()
assert data["vendor_code"] == "NEWVENDOR001"
assert data["vendor_name"] == "New Vendor"
assert data["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",
"name": "Different Name",
"description": "Different description",
}
@@ -43,7 +43,7 @@ class TestVendorsAPI:
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",
"name": "Vendor without Code",
"description": "Missing vendor code",
}
@@ -57,10 +57,10 @@ class TestVendorsAPI:
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"""
"""Test creating vendor with empty name returns VendorValidationException"""
vendor_data = {
"vendor_code": "EMPTYNAME",
"vendor_name": "", # Empty vendor name
"name": "", # Empty vendor name
"description": "Vendor with empty name",
}
@@ -71,7 +71,7 @@ class TestVendorsAPI:
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"
assert data["details"]["field"] == "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"""
@@ -121,7 +121,7 @@ class TestVendorsAPI:
assert response.status_code == 200
data = response.json()
assert data["vendor_code"] == test_vendor.vendor_code
assert data["vendor_name"] == test_vendor.vendor_name
assert data["name"] == test_vendor.name
def test_get_vendor_by_code_not_found(self, client, auth_headers):
"""Test getting nonexistent vendor returns VendorNotFoundException"""
@@ -139,7 +139,7 @@ class TestVendorsAPI:
"""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.owner_user_id = other_user.id
test_vendor.is_verified = False # Make it not publicly accessible
db.commit()
@@ -179,7 +179,7 @@ class TestVendorsAPI:
assert response.status_code == 200
data = response.json()
assert data["vendor_code"] == verified_vendor.vendor_code
assert data["vendor_name"] == verified_vendor.vendor_name
assert data["name"] == verified_vendor.name
def test_add_product_to_vendor_success(self, client, auth_headers, test_vendor, unique_product):
"""Test adding product to vendor successfully"""

View File

@@ -15,7 +15,7 @@ class TestAuthentication:
"/api/v1/marketplace/product",
"/api/v1/vendor",
"/api/v1/stats",
"/api/v1/stock",
"/api/v1/inventory",
]
for endpoint in protected_endpoints:

View File

@@ -29,7 +29,7 @@ class TestAuthorization:
user_endpoints = [
"/api/v1/marketplace/product",
"/api/v1/stats",
"/api/v1/stock",
"/api/v1/inventory",
]
for endpoint in user_endpoints:

View File

@@ -16,7 +16,7 @@ class TestIntegrationFlows:
"price": "29.99",
"brand": "FlowBrand",
"gtin": "1111222233334",
"availability": "in stock",
"availability": "in inventory",
"marketplace": "TestFlow",
}
@@ -26,23 +26,23 @@ class TestIntegrationFlows:
assert response.status_code == 200
product = response.json()
# 2. Add stock for the product
stock_data = {
# 2. Add inventory for the product
inventory_data = {
"gtin": product["gtin"],
"location": "MAIN_WAREHOUSE",
"quantity": 50,
}
response = client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
assert response.status_code == 200
# 3. Get product with stock info
# 3. Get product with inventory info
response = client.get(
f"/api/v1/marketplace/product/{product['marketplace_product_id']}", headers=auth_headers
)
assert response.status_code == 200
product_detail = response.json()
assert product_detail["stock_info"]["total_quantity"] == 50
assert product_detail["inventory_info"]["total_quantity"] == 50
# 4. Update product
update_data = {"title": "Updated Integration Test MarketplaceProduct"}
@@ -65,7 +65,7 @@ class TestIntegrationFlows:
# 1. Create a vendor
vendor_data = {
"vendor_code": "FLOWVENDOR",
"vendor_name": "Integration Flow Vendor",
"name": "Integration Flow Vendor",
"description": "Test vendor for integration",
}
@@ -94,38 +94,38 @@ class TestIntegrationFlows:
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):
"""Test complete stock management workflow"""
def test_inventory_operations_workflow(self, client, auth_headers):
"""Test complete inventory management workflow"""
gtin = "9999888877776"
location = "TEST_WAREHOUSE"
# 1. Set initial stock
# 1. Set initial inventory
response = client.post(
"/api/v1/stock",
"/api/v1/inventory",
headers=auth_headers,
json={"gtin": gtin, "location": location, "quantity": 100},
)
assert response.status_code == 200
# 2. Add more stock
# 2. Add more inventory
response = client.post(
"/api/v1/stock/add",
"/api/v1/inventory/add",
headers=auth_headers,
json={"gtin": gtin, "location": location, "quantity": 25},
)
assert response.status_code == 200
assert response.json()["quantity"] == 125
# 3. Remove some stock
# 3. Remove some inventory
response = client.post(
"/api/v1/stock/remove",
"/api/v1/inventory/remove",
headers=auth_headers,
json={"gtin": gtin, "location": location, "quantity": 30},
)
assert response.status_code == 200
assert response.json()["quantity"] == 95
# 4. Check total stock
response = client.get(f"/api/v1/stock/{gtin}/total", headers=auth_headers)
# 4. Check total inventory
response = client.get(f"/api/v1/inventory/{gtin}/total", headers=auth_headers)
assert response.status_code == 200
assert response.json()["total_quantity"] == 95

View File

@@ -29,7 +29,7 @@ class TestErrorHandling:
def test_missing_required_fields_vendor_creation(self, client, auth_headers):
"""Test validation errors for missing required fields"""
# Missing vendor_name
# Missing name
response = client.post(
"/api/v1/vendor",
headers=auth_headers,
@@ -50,7 +50,7 @@ class TestErrorHandling:
headers=auth_headers,
json={
"vendor_code": "INVALID@VENDOR!",
"vendor_name": "Test Vendor"
"name": "Test Vendor"
}
)
@@ -117,7 +117,7 @@ class TestErrorHandling:
"""Test creating vendor with duplicate vendor code"""
vendor_data = {
"vendor_code": test_vendor.vendor_code,
"vendor_name": "Duplicate Vendor"
"name": "Duplicate Vendor"
}
response = client.post("/api/v1/vendor", headers=auth_headers, json=vendor_data)
@@ -172,7 +172,7 @@ class TestErrorHandling:
for i in range(6): # Assume limit is 5
vendor_data = {
"vendor_code": f"VENDOR{i:03d}",
"vendor_name": f"Test Vendor {i}"
"name": f"Test Vendor {i}"
}
response = client.post("/api/v1/vendor", headers=auth_headers, json=vendor_data)
vendors_created.append(response)
@@ -204,15 +204,15 @@ class TestErrorHandling:
assert data["status_code"] == 422
assert data["details"]["field"] == "gtin"
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 = {
def test_inventory_insufficient_quantity(self, client, auth_headers, test_vendor, test_marketplace_product):
"""Test business logic error for insufficient inventory"""
# First create some inventory
inventory_data = {
"gtin": test_marketplace_product.gtin,
"location": "WAREHOUSE_A",
"quantity": 5
}
client.post("/api/v1/stock", headers=auth_headers, json=stock_data)
client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
# Try to remove more than available using your remove endpoint
remove_data = {
@@ -220,12 +220,12 @@ class TestErrorHandling:
"location": "WAREHOUSE_A",
"quantity": 10 # More than the 5 we added
}
response = client.post("/api/v1/stock/remove", headers=auth_headers, json=remove_data)
response = client.post("/api/v1/inventory/remove", headers=auth_headers, json=remove_data)
# This should ALWAYS fail with insufficient stock error
# This should ALWAYS fail with insufficient inventory error
assert response.status_code == 400
data = response.json()
assert data["error_code"] == "INSUFFICIENT_STOCK"
assert data["error_code"] == "INSUFFICIENT_INVENTORY"
assert data["status_code"] == 400
assert "requested_quantity" in data["details"]
assert "available_quantity" in data["details"]
@@ -267,7 +267,7 @@ class TestErrorHandling:
large_description = "x" * 100000 # Very long description
vendor_data = {
"vendor_code": "LARGEVENDOR",
"vendor_name": "Large Vendor",
"name": "Large Vendor",
"description": large_description
}

View File

@@ -1,16 +1,24 @@
# tests/unit/models/test_database_models.py
import pytest
from datetime import datetime, timezone
from sqlalchemy.exc import IntegrityError
from models.database.marketplace_product import MarketplaceProduct
from models.database.vendor import Vendor
from models.database.stock import Stock
from models.database.vendor import Vendor, VendorUser, Role
from models.database.inventory import Inventory
from models.database.user import User
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.product import Product
from models.database.customer import Customer, CustomerAddress
from models.database.order import Order, OrderItem
@pytest.mark.unit
@pytest.mark.database
class TestDatabaseModels:
def test_user_model(self, db):
class TestUserModel:
"""Test User model"""
def test_user_creation(self, db):
"""Test User model creation and relationships"""
user = User(
email="db_test@example.com",
@@ -26,52 +34,49 @@ class TestDatabaseModels:
assert user.id is not None
assert user.email == "db_test@example.com"
assert user.username == "dbtest"
assert user.role == "user"
assert user.is_active is True
assert user.created_at is not None
assert user.updated_at is not None
def test_product_model(self, db):
"""Test MarketplaceProduct model creation"""
marketplace_product = MarketplaceProduct(
marketplace_product_id="DB_TEST_001",
title="Database Test MarketplaceProduct",
description="Testing product model",
price="25.99",
currency="USD",
brand="DBTest",
gtin="1234567890123",
availability="in stock",
marketplace="TestDB",
vendor_name="DBTestVendor",
def test_user_email_uniqueness(self, db):
"""Test email unique constraint"""
user1 = User(
email="unique@example.com",
username="user1",
hashed_password="hash1",
)
db.add(marketplace_product)
db.add(user1)
db.commit()
db.refresh(marketplace_product)
assert marketplace_product.id is not None
assert marketplace_product.marketplace_product_id == "DB_TEST_001"
assert marketplace_product.created_at is not None
# Duplicate email should raise error
with pytest.raises(IntegrityError):
user2 = User(
email="unique@example.com",
username="user2",
hashed_password="hash2",
)
db.add(user2)
db.commit()
def test_stock_model(self, db):
"""Test Stock model creation"""
stock = Stock(gtin="1234567890123", location="DB_WAREHOUSE", quantity=150)
db.add(stock)
db.commit()
db.refresh(stock)
@pytest.mark.unit
@pytest.mark.database
class TestVendorModel:
"""Test Vendor model"""
assert stock.id is not None
assert stock.gtin == "1234567890123"
assert stock.location == "DB_WAREHOUSE"
assert stock.quantity == 150
def test_vendor_model_with_owner(self, db, test_user):
def test_vendor_creation_with_owner(self, db, test_user):
"""Test Vendor model with owner relationship"""
vendor = Vendor(
vendor_code="DBTEST",
vendor_name="Database Test Vendor",
subdomain="dbtest",
name="Database Test Vendor",
description="Testing vendor model",
owner_id=test_user.id,
owner_user_id=test_user.id,
contact_email="contact@dbtest.com",
contact_phone="+1234567890",
business_address="123 Test Street",
is_active=True,
is_verified=False,
)
@@ -82,18 +87,508 @@ class TestDatabaseModels:
assert vendor.id is not None
assert vendor.vendor_code == "DBTEST"
assert vendor.owner_id == test_user.id
assert vendor.subdomain == "dbtest"
assert vendor.name == "Database Test Vendor"
assert vendor.owner_user_id == test_user.id
assert vendor.owner.username == test_user.username
assert vendor.contact_email == "contact@dbtest.com"
assert vendor.is_active is True
assert vendor.is_verified is False
def test_database_constraints(self, db):
"""Test database constraints and unique indexes"""
# Test unique marketplace_product_id constraint
product1 = MarketplaceProduct(marketplace_product_id="UNIQUE_001", title="MarketplaceProduct 1")
def test_vendor_with_letzshop_urls(self, db, test_user):
"""Test Vendor model with multi-language Letzshop URLs"""
vendor = Vendor(
vendor_code="MULTILANG",
subdomain="multilang",
name="Multi-Language Vendor",
owner_user_id=test_user.id,
letzshop_csv_url_fr="https://example.com/feed_fr.csv",
letzshop_csv_url_en="https://example.com/feed_en.csv",
letzshop_csv_url_de="https://example.com/feed_de.csv",
is_active=True,
)
db.add(vendor)
db.commit()
db.refresh(vendor)
assert vendor.letzshop_csv_url_fr == "https://example.com/feed_fr.csv"
assert vendor.letzshop_csv_url_en == "https://example.com/feed_en.csv"
assert vendor.letzshop_csv_url_de == "https://example.com/feed_de.csv"
def test_vendor_code_uniqueness(self, db, test_user):
"""Test vendor_code unique constraint"""
vendor1 = Vendor(
vendor_code="UNIQUE",
subdomain="unique1",
name="Unique Vendor 1",
owner_user_id=test_user.id,
)
db.add(vendor1)
db.commit()
# Duplicate vendor_code should raise error
with pytest.raises(IntegrityError):
vendor2 = Vendor(
vendor_code="UNIQUE",
subdomain="unique2",
name="Unique Vendor 2",
owner_user_id=test_user.id,
)
db.add(vendor2)
db.commit()
def test_subdomain_uniqueness(self, db, test_user):
"""Test subdomain unique constraint"""
vendor1 = Vendor(
vendor_code="VENDOR1",
subdomain="testsubdomain",
name="Vendor 1",
owner_user_id=test_user.id,
)
db.add(vendor1)
db.commit()
# Duplicate subdomain should raise error
with pytest.raises(IntegrityError):
vendor2 = Vendor(
vendor_code="VENDOR2",
subdomain="testsubdomain",
name="Vendor 2",
owner_user_id=test_user.id,
)
db.add(vendor2)
db.commit()
@pytest.mark.unit
@pytest.mark.database
class TestTeamModels:
"""Test VendorUser and Role models"""
def test_role_creation(self, db, test_vendor):
"""Test Role model creation"""
role = Role(
vendor_id=test_vendor.id,
name="Manager",
permissions=["products.create", "orders.view"],
)
db.add(role)
db.commit()
db.refresh(role)
assert role.id is not None
assert role.vendor_id == test_vendor.id
assert role.name == "Manager"
assert "products.create" in role.permissions
assert "orders.view" in role.permissions
def test_vendor_user_creation(self, db, test_vendor, test_user):
"""Test VendorUser model for team management"""
# Create a role
role = Role(
vendor_id=test_vendor.id,
name="Manager",
permissions=["products.create", "orders.view"],
)
db.add(role)
db.commit()
# Create vendor user
vendor_user = VendorUser(
vendor_id=test_vendor.id,
user_id=test_user.id,
role_id=role.id,
is_active=True,
)
db.add(vendor_user)
db.commit()
db.refresh(vendor_user)
assert vendor_user.id is not None
assert vendor_user.vendor_id == test_vendor.id
assert vendor_user.user_id == test_user.id
assert vendor_user.role.name == "Manager"
assert "products.create" in vendor_user.role.permissions
def test_vendor_user_uniqueness(self, db, test_vendor, test_user):
"""Test vendor_user unique constraint (one user per vendor)"""
role = Role(
vendor_id=test_vendor.id,
name="Editor",
permissions=["products.view"],
)
db.add(role)
db.commit()
vendor_user1 = VendorUser(
vendor_id=test_vendor.id,
user_id=test_user.id,
role_id=role.id,
)
db.add(vendor_user1)
db.commit()
# Same user can't be added to same vendor twice
with pytest.raises(IntegrityError):
vendor_user2 = VendorUser(
vendor_id=test_vendor.id,
user_id=test_user.id,
role_id=role.id,
)
db.add(vendor_user2)
db.commit()
@pytest.mark.unit
@pytest.mark.database
class TestMarketplaceProductModel:
"""Test MarketplaceProduct model"""
def test_marketplace_product_creation(self, db, test_vendor):
"""Test MarketplaceProduct model creation with vendor_id"""
marketplace_product = MarketplaceProduct(
vendor_id=test_vendor.id,
marketplace_product_id="DB_TEST_001",
title="Database Test Product",
description="Testing product model",
price="25.99",
currency="USD",
brand="DBTest",
gtin="1234567890123",
availability="in stock",
marketplace="Letzshop",
)
db.add(marketplace_product)
db.commit()
db.refresh(marketplace_product)
assert marketplace_product.id is not None
assert marketplace_product.vendor_id == test_vendor.id
assert marketplace_product.marketplace_product_id == "DB_TEST_001"
assert marketplace_product.title == "Database Test Product"
assert marketplace_product.marketplace == "Letzshop"
assert marketplace_product.created_at is not None
def test_marketplace_product_id_uniqueness(self, db, test_vendor):
"""Test unique marketplace_product_id constraint"""
product1 = MarketplaceProduct(
vendor_id=test_vendor.id,
marketplace_product_id="UNIQUE_001",
title="Product 1",
marketplace="Letzshop"
)
db.add(product1)
db.commit()
# This should raise an integrity error
with pytest.raises(Exception): # Could be IntegrityError or similar
product2 = MarketplaceProduct(marketplace_product_id="UNIQUE_001", title="MarketplaceProduct 2")
# Duplicate marketplace_product_id should raise error
with pytest.raises(IntegrityError):
product2 = MarketplaceProduct(
vendor_id=test_vendor.id,
marketplace_product_id="UNIQUE_001",
title="Product 2",
marketplace="Letzshop"
)
db.add(product2)
db.commit()
@pytest.mark.unit
@pytest.mark.database
class TestProductModel:
"""Test Product (vendor catalog) model"""
def test_product_creation(self, db, test_vendor, test_marketplace_product):
"""Test Product model linking vendor catalog to marketplace product"""
product = Product(
vendor_id=test_vendor.id,
marketplace_product_id=test_marketplace_product.id,
product_id="VENDOR_PROD_001",
price=89.99, # Vendor override price
currency="EUR",
availability="in stock",
is_featured=True,
is_active=True,
)
db.add(product)
db.commit()
db.refresh(product)
assert product.id is not None
assert product.vendor_id == test_vendor.id
assert product.marketplace_product_id == test_marketplace_product.id
assert product.price == 89.99
assert product.is_featured is True
assert product.vendor.vendor_code == test_vendor.vendor_code
assert product.marketplace_product.title == test_marketplace_product.title
def test_product_unique_per_vendor(self, db, test_vendor, test_marketplace_product):
"""Test that same marketplace product can't be added twice to vendor catalog"""
product1 = Product(
vendor_id=test_vendor.id,
marketplace_product_id=test_marketplace_product.id,
is_active=True,
)
db.add(product1)
db.commit()
# Same marketplace product to same vendor should fail
with pytest.raises(IntegrityError):
product2 = Product(
vendor_id=test_vendor.id,
marketplace_product_id=test_marketplace_product.id,
is_active=True,
)
db.add(product2)
db.commit()
@pytest.mark.unit
@pytest.mark.database
class TestInventoryModel:
"""Test Inventory model"""
def test_inventory_creation_with_product(self, db, test_vendor, test_product):
"""Test Inventory model linked to product"""
inventory = Inventory(
product_id=test_product.id,
vendor_id=test_vendor.id,
location="WAREHOUSE_A",
quantity=150,
reserved_quantity=10,
gtin=test_product.marketplace_product.gtin,
)
db.add(inventory)
db.commit()
db.refresh(inventory)
assert inventory.id is not None
assert inventory.product_id == test_product.id
assert inventory.vendor_id == test_vendor.id
assert inventory.location == "WAREHOUSE_A"
assert inventory.quantity == 150
assert inventory.reserved_quantity == 10
assert inventory.available_quantity == 140 # 150 - 10
def test_inventory_unique_product_location(self, db, test_vendor, test_product):
"""Test unique constraint on product_id + location"""
inventory1 = Inventory(
product_id=test_product.id,
vendor_id=test_vendor.id,
location="WAREHOUSE_A",
quantity=100,
)
db.add(inventory1)
db.commit()
# Same product + location should fail
with pytest.raises(IntegrityError):
inventory2 = Inventory(
product_id=test_product.id,
vendor_id=test_vendor.id,
location="WAREHOUSE_A",
quantity=50,
)
db.add(inventory2)
db.commit()
@pytest.mark.unit
@pytest.mark.database
class TestMarketplaceImportJobModel:
"""Test MarketplaceImportJob model"""
def test_import_job_creation(self, db, test_user, test_vendor):
"""Test MarketplaceImportJob model with relationships"""
import_job = MarketplaceImportJob(
vendor_id=test_vendor.id,
user_id=test_user.id,
marketplace="Letzshop",
source_url="https://example.com/feed.csv",
status="pending",
imported_count=0,
updated_count=0,
error_count=0,
total_processed=0,
)
db.add(import_job)
db.commit()
db.refresh(import_job)
assert import_job.id is not None
assert import_job.vendor_id == test_vendor.id
assert import_job.user_id == test_user.id
assert import_job.marketplace == "Letzshop"
assert import_job.source_url == "https://example.com/feed.csv"
assert import_job.status == "pending"
assert import_job.vendor.vendor_code == test_vendor.vendor_code
assert import_job.user.username == test_user.username
@pytest.mark.unit
@pytest.mark.database
class TestCustomerModel:
"""Test Customer model"""
def test_customer_creation(self, db, test_vendor):
"""Test Customer model with vendor isolation"""
customer = Customer(
vendor_id=test_vendor.id,
email="customer@example.com",
hashed_password="hashed_password",
first_name="John",
last_name="Doe",
customer_number="CUST001",
is_active=True,
)
db.add(customer)
db.commit()
db.refresh(customer)
assert customer.id is not None
assert customer.vendor_id == test_vendor.id
assert customer.email == "customer@example.com"
assert customer.customer_number == "CUST001"
assert customer.first_name == "John"
assert customer.last_name == "Doe"
assert customer.vendor.vendor_code == test_vendor.vendor_code
def test_customer_email_unique_per_vendor(self, db, test_vendor):
"""Test email is unique within vendor scope only"""
customer1 = Customer(
vendor_id=test_vendor.id,
email="same@example.com",
hashed_password="hash1",
first_name="Customer",
last_name="One",
customer_number="CUST001",
)
db.add(customer1)
db.commit()
# Same email in same vendor should fail
with pytest.raises(IntegrityError):
customer2 = Customer(
vendor_id=test_vendor.id,
email="same@example.com",
hashed_password="hash2",
first_name="Customer",
last_name="Two",
customer_number="CUST002",
)
db.add(customer2)
db.commit()
def test_customer_address_creation(self, db, test_vendor, test_customer):
"""Test CustomerAddress model"""
address = CustomerAddress(
vendor_id=test_vendor.id,
customer_id=test_customer.id,
address_type="shipping",
first_name="John",
last_name="Doe",
address_line_1="123 Main St",
city="Luxembourg",
postal_code="L-1234",
country="Luxembourg",
is_default=True,
)
db.add(address)
db.commit()
db.refresh(address)
assert address.id is not None
assert address.vendor_id == test_vendor.id
assert address.customer_id == test_customer.id
assert address.address_type == "shipping"
assert address.is_default is True
@pytest.mark.unit
@pytest.mark.database
class TestOrderModel:
"""Test Order model"""
def test_order_creation(self, db, test_vendor, test_customer, test_customer_address):
"""Test Order model with customer relationship"""
order = Order(
vendor_id=test_vendor.id,
customer_id=test_customer.id,
order_number="ORD-001",
status="pending",
subtotal=99.99,
total_amount=99.99,
currency="EUR",
shipping_address_id=test_customer_address.id,
billing_address_id=test_customer_address.id,
)
db.add(order)
db.commit()
db.refresh(order)
assert order.id is not None
assert order.vendor_id == test_vendor.id
assert order.customer_id == test_customer.id
assert order.order_number == "ORD-001"
assert order.status == "pending"
assert float(order.total_amount) == 99.99
def test_order_item_creation(self, db, test_order, test_product):
"""Test OrderItem model"""
order_item = OrderItem(
order_id=test_order.id,
product_id=test_product.id,
product_name=test_product.marketplace_product.title,
product_sku=test_product.product_id,
quantity=2,
unit_price=49.99,
total_price=99.98,
)
db.add(order_item)
db.commit()
db.refresh(order_item)
assert order_item.id is not None
assert order_item.order_id == test_order.id
assert order_item.product_id == test_product.id
assert order_item.quantity == 2
assert float(order_item.unit_price) == 49.99
assert float(order_item.total_price) == 99.98
def test_order_number_uniqueness(self, db, test_vendor, test_customer, test_customer_address):
"""Test order_number unique constraint"""
order1 = Order(
vendor_id=test_vendor.id,
customer_id=test_customer.id,
order_number="UNIQUE-ORD-001",
status="pending",
subtotal=50.00,
total_amount=50.00,
shipping_address_id=test_customer_address.id,
billing_address_id=test_customer_address.id,
)
db.add(order1)
db.commit()
# Duplicate order number should fail
with pytest.raises(IntegrityError):
order2 = Order(
vendor_id=test_vendor.id,
customer_id=test_customer.id,
order_number="UNIQUE-ORD-001",
status="pending",
subtotal=75.00,
total_amount=75.00,
shipping_address_id=test_customer_address.id,
billing_address_id=test_customer_address.id,
)
db.add(order2)
db.commit()

View File

@@ -180,7 +180,7 @@ class TestAdminService:
)
assert test_job is not None
assert test_job.marketplace == test_marketplace_import_job.marketplace
assert test_job.vendor_name == test_marketplace_import_job.vendor_name
assert test_job.vendor_name == test_marketplace_import_job.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):
@@ -196,12 +196,12 @@ class TestAdminService:
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, vendor_name=test_marketplace_import_job.vendor_name, skip=0, limit=10
db, vendor_name=test_marketplace_import_job.name, skip=0, limit=10
)
assert len(result) >= 1
for job in result:
assert test_marketplace_import_job.vendor_name.lower() in job.vendor_name.lower()
assert test_marketplace_import_job.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"""

View File

@@ -0,0 +1,505 @@
# tests/test_inventory_service.py
import uuid
import pytest
from app.services.inventory_service import InventoryService
from app.exceptions import (
InventoryNotFoundException,
InsufficientInventoryException,
InvalidInventoryOperationException,
InventoryValidationException,
NegativeInventoryException,
InvalidQuantityException,
ValidationException,
)
from models.schemas.inventory import InventoryAdd, InventoryCreate, InventoryUpdate
from models.database.marketplace_product import MarketplaceProduct
from models.database.inventory import Inventory
@pytest.mark.unit
@pytest.mark.inventory
class TestInventoryService:
def setup_method(self):
self.service = InventoryService()
def test_normalize_gtin_invalid(self):
"""Test GTIN normalization with invalid GTINs."""
# Completely invalid values that should return None
assert self.service._normalize_gtin("invalid") is None
assert self.service._normalize_gtin("abcdef") is None
assert self.service._normalize_gtin("") is None
assert self.service._normalize_gtin(None) is None
assert self.service._normalize_gtin(" ") is None # Only whitespace
assert self.service._normalize_gtin("!@#$%") is None # Only special characters
# Mixed invalid characters that become empty after filtering
assert self.service._normalize_gtin("abc-def-ghi") is None # No digits
def test_normalize_gtin_valid(self):
"""Test GTIN normalization with valid GTINs."""
# Test various valid GTIN formats - these should remain unchanged
assert self.service._normalize_gtin("1234567890123") == "1234567890123" # EAN-13
assert self.service._normalize_gtin("123456789012") == "123456789012" # UPC-A
assert self.service._normalize_gtin("12345678") == "12345678" # EAN-8
assert self.service._normalize_gtin("12345678901234") == "12345678901234" # GTIN-14
# Test with decimal points (should be removed)
assert self.service._normalize_gtin("1234567890123.0") == "1234567890123"
# Test with whitespace (should be trimmed)
assert self.service._normalize_gtin(" 1234567890123 ") == "1234567890123"
# Test short GTINs being padded
assert self.service._normalize_gtin("123") == "0000000000123" # Padded to EAN-13
assert self.service._normalize_gtin("12345") == "0000000012345" # Padded to EAN-13
# Test long GTINs being truncated
assert self.service._normalize_gtin("123456789012345") == "3456789012345" # Truncated to 13
def test_normalize_gtin_edge_cases(self):
"""Test GTIN normalization edge cases."""
# Test numeric inputs
assert self.service._normalize_gtin(1234567890123) == "1234567890123"
assert self.service._normalize_gtin(123) == "0000000000123"
# Test mixed valid/invalid characters
assert self.service._normalize_gtin("123-456-789-012") == "123456789012" # Dashes removed
assert self.service._normalize_gtin("123 456 789 012") == "123456789012" # Spaces removed
assert self.service._normalize_gtin("ABC123456789012DEF") == "123456789012" # Letters removed
def test_set_inventory_new_entry_success(self, db):
"""Test setting inventory for a new GTIN/location combination successfully."""
unique_id = str(uuid.uuid4())[:8]
inventory_data = InventoryCreate(
gtin="1234567890123", location=f"WAREHOUSE_A_{unique_id}", quantity=100
)
result = self.service.set_inventory(db, inventory_data)
assert result.gtin == "1234567890123"
assert result.location.upper() == f"WAREHOUSE_A_{unique_id}".upper()
assert result.quantity == 100
def test_set_inventory_existing_entry_success(self, db, test_inventory):
"""Test setting inventory for an existing GTIN/location combination successfully."""
inventory_data = InventoryCreate(
gtin=test_inventory.gtin,
location=test_inventory.location, # Use exact same location as test_inventory
quantity=200,
)
result = self.service.set_inventory(db, inventory_data)
assert result.gtin == test_inventory.gtin
assert result.location == test_inventory.location
assert result.quantity == 200 # Should replace the original quantity
def test_set_inventory_invalid_gtin_validation_error(self, db):
"""Test setting inventory with invalid GTIN returns InventoryValidationException."""
inventory_data = InventoryCreate(
gtin="invalid_gtin", location="WAREHOUSE_A", quantity=100
)
with pytest.raises(InventoryValidationException) as exc_info:
self.service.set_inventory(db, inventory_data)
assert exc_info.value.error_code == "INVENTORY_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
assert exc_info.value.details.get("field") == "gtin"
def test_set_inventory_invalid_quantity_error(self, db):
"""Test setting inventory with invalid quantity through service validation."""
# Test the service validation directly instead of going through Pydantic schema
# This bypasses the Pydantic validation to test service layer validation
# Create a mock inventory data object that bypasses Pydantic validation
class MockInventoryData:
def __init__(self, gtin, location, quantity):
self.gtin = gtin
self.location = location
self.quantity = quantity
mock_inventory_data = MockInventoryData("1234567890123", "WAREHOUSE_A", -10)
# Test the internal validation method directly
with pytest.raises(InvalidQuantityException) as exc_info:
self.service._validate_quantity(-10, allow_zero=True)
assert exc_info.value.error_code == "INVALID_QUANTITY"
assert "Quantity cannot be negative" in str(exc_info.value)
assert exc_info.value.details.get("quantity") == -10
def test_add_inventory_new_entry_success(self, db):
"""Test adding inventory for a new GTIN/location combination successfully."""
unique_id = str(uuid.uuid4())[:8]
inventory_data = InventoryAdd(
gtin="1234567890123", location=f"WAREHOUSE_B_{unique_id}", quantity=50
)
result = self.service.add_inventory(db, inventory_data)
assert result.gtin == "1234567890123"
assert result.location.upper() == f"WAREHOUSE_B_{unique_id}".upper()
assert result.quantity == 50
def test_add_inventory_existing_entry_success(self, db, test_inventory):
"""Test adding inventory to an existing GTIN/location combination successfully."""
original_quantity = test_inventory.quantity
inventory_data = InventoryAdd(
gtin=test_inventory.gtin,
location=test_inventory.location, # Use exact same location as test_inventory
quantity=25,
)
result = self.service.add_inventory(db, inventory_data)
assert result.gtin == test_inventory.gtin
assert result.location == test_inventory.location
assert result.quantity == original_quantity + 25
def test_add_inventory_invalid_gtin_validation_error(self, db):
"""Test adding inventory with invalid GTIN returns InventoryValidationException."""
inventory_data = InventoryAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=50)
with pytest.raises(InventoryValidationException) as exc_info:
self.service.add_inventory(db, inventory_data)
assert exc_info.value.error_code == "INVENTORY_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
def test_add_inventory_invalid_quantity_error(self, db):
"""Test adding inventory with invalid quantity through service validation."""
# Test zero quantity which should fail for add_inventory (doesn't allow zero)
# This tests the service validation: allow_zero=False for add operations
with pytest.raises(InvalidQuantityException) as exc_info:
self.service._validate_quantity(0, allow_zero=False)
assert exc_info.value.error_code == "INVALID_QUANTITY"
assert "Quantity must be positive" in str(exc_info.value)
def test_remove_inventory_success(self, db, test_inventory):
"""Test removing inventory successfully."""
original_quantity = test_inventory.quantity
remove_quantity = min(10, original_quantity) # Ensure we don't remove more than available
inventory_data = InventoryAdd(
gtin=test_inventory.gtin,
location=test_inventory.location, # Use exact same location as test_inventory
quantity=remove_quantity,
)
result = self.service.remove_inventory(db, inventory_data)
assert result.gtin == test_inventory.gtin
assert result.location == test_inventory.location
assert result.quantity == original_quantity - remove_quantity
def test_remove_inventory_insufficient_inventory_error(self, db, test_inventory):
"""Test removing more inventory than available returns InsufficientInventoryException."""
inventory_data = InventoryAdd(
gtin=test_inventory.gtin,
location=test_inventory.location, # Use exact same location as test_inventory
quantity=test_inventory.quantity + 10, # More than available
)
with pytest.raises(InsufficientInventoryException) as exc_info:
self.service.remove_inventory(db, inventory_data)
assert exc_info.value.error_code == "INSUFFICIENT_INVENTORY"
assert exc_info.value.details["gtin"] == test_inventory.gtin
assert exc_info.value.details["location"] == test_inventory.location
assert exc_info.value.details["requested_quantity"] == test_inventory.quantity + 10
assert exc_info.value.details["available_quantity"] == test_inventory.quantity
def test_remove_inventory_nonexistent_entry_not_found(self, db):
"""Test removing inventory from non-existent GTIN/location returns InventoryNotFoundException."""
unique_id = str(uuid.uuid4())[:8]
inventory_data = InventoryAdd(
gtin="9999999999999", location=f"NONEXISTENT_{unique_id}", quantity=10
)
with pytest.raises(InventoryNotFoundException) as exc_info:
self.service.remove_inventory(db, inventory_data)
assert exc_info.value.error_code == "INVENTORY_NOT_FOUND"
assert "9999999999999" in str(exc_info.value)
assert exc_info.value.details["resource_type"] == "Inventory"
def test_remove_inventory_invalid_gtin_validation_error(self, db):
"""Test removing inventory with invalid GTIN returns InventoryValidationException."""
inventory_data = InventoryAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=10)
with pytest.raises(InventoryValidationException) as exc_info:
self.service.remove_inventory(db, inventory_data)
assert exc_info.value.error_code == "INVENTORY_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
def test_remove_inventory_negative_result_error(self, db, test_inventory):
"""Test removing inventory that would result in negative quantity returns NegativeInventoryException."""
# This is handled by InsufficientInventoryException, but test the logic
inventory_data = InventoryAdd(
gtin=test_inventory.gtin,
location=test_inventory.location,
quantity=test_inventory.quantity + 1, # One more than available
)
with pytest.raises(InsufficientInventoryException) as exc_info:
self.service.remove_inventory(db, inventory_data)
# The service prevents negative inventory through InsufficientInventoryException
assert exc_info.value.error_code == "INSUFFICIENT_INVENTORY"
def test_get_inventory_by_gtin_success(self, db, test_inventory, test_marketplace_product):
"""Test getting inventory summary by GTIN successfully."""
result = self.service.get_inventory_by_gtin(db, test_inventory.gtin)
assert result.gtin == test_inventory.gtin
assert result.total_quantity == test_inventory.quantity
assert len(result.locations) == 1
assert result.locations[0].location == test_inventory.location
assert result.locations[0].quantity == test_inventory.quantity
assert result.product_title == test_marketplace_product.title
def test_get_inventory_by_gtin_multiple_locations_success(self, db, test_marketplace_product):
"""Test getting inventory summary with multiple locations successfully."""
unique_gtin = test_marketplace_product.gtin
unique_id = str(uuid.uuid4())[:8]
# Create multiple inventory entries for the same GTIN with unique locations
inventory1 = Inventory(gtin=unique_gtin, location=f"WAREHOUSE_A_{unique_id}", quantity=50)
inventory2 = Inventory(gtin=unique_gtin, location=f"WAREHOUSE_B_{unique_id}", quantity=30)
db.add(inventory1)
db.add(inventory2)
db.commit()
result = self.service.get_inventory_by_gtin(db, unique_gtin)
assert result.gtin == unique_gtin
assert result.total_quantity == 80
assert len(result.locations) == 2
def test_get_inventory_by_gtin_not_found_error(self, db):
"""Test getting inventory for non-existent GTIN returns InventoryNotFoundException."""
with pytest.raises(InventoryNotFoundException) as exc_info:
self.service.get_inventory_by_gtin(db, "9999999999999")
assert exc_info.value.error_code == "INVENTORY_NOT_FOUND"
assert "9999999999999" in str(exc_info.value)
assert exc_info.value.details["resource_type"] == "Inventory"
def test_get_inventory_by_gtin_invalid_gtin_validation_error(self, db):
"""Test getting inventory with invalid GTIN returns InventoryValidationException."""
with pytest.raises(InventoryValidationException) as exc_info:
self.service.get_inventory_by_gtin(db, "invalid_gtin")
assert exc_info.value.error_code == "INVENTORY_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
def test_get_total_inventory_success(self, db, test_inventory, test_marketplace_product):
"""Test getting total inventory for a GTIN successfully."""
result = self.service.get_total_inventory(db, test_inventory.gtin)
assert result["gtin"] == test_inventory.gtin
assert result["total_quantity"] == test_inventory.quantity
assert result["product_title"] == test_marketplace_product.title
assert result["locations_count"] == 1
def test_get_total_inventory_invalid_gtin_validation_error(self, db):
"""Test getting total inventory with invalid GTIN returns InventoryValidationException."""
with pytest.raises(InventoryValidationException) as exc_info:
self.service.get_total_inventory(db, "invalid_gtin")
assert exc_info.value.error_code == "INVENTORY_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
def test_get_total_inventory_not_found_error(self, db):
"""Test getting total inventory for non-existent GTIN returns InventoryNotFoundException."""
with pytest.raises(InventoryNotFoundException) as exc_info:
self.service.get_total_inventory(db, "9999999999999")
assert exc_info.value.error_code == "INVENTORY_NOT_FOUND"
def test_get_all_inventory_no_filters_success(self, db, test_inventory):
"""Test getting all inventory without filters successfully."""
result = self.service.get_all_inventory(db)
assert len(result) >= 1
assert any(inventory.gtin == test_inventory.gtin for inventory in result)
def test_get_all_inventory_with_location_filter_success(self, db, test_inventory):
"""Test getting all inventory with location filter successfully."""
result = self.service.get_all_inventory(db, location=test_inventory.location)
assert len(result) >= 1
# Check that all returned inventory match the filter (case insensitive)
for inventory in result:
assert test_inventory.location.upper() in inventory.location.upper()
def test_get_all_inventory_with_gtin_filter_success(self, db, test_inventory):
"""Test getting all inventory with GTIN filter successfully."""
result = self.service.get_all_inventory(db, gtin=test_inventory.gtin)
assert len(result) >= 1
assert all(inventory.gtin == test_inventory.gtin for inventory in result)
def test_get_all_inventory_with_pagination_success(self, db):
"""Test getting all inventory with pagination successfully."""
unique_prefix = str(uuid.uuid4())[:8]
# Create multiple inventory entries with unique GTINs and locations
for i in range(5):
inventory = Inventory(
gtin=f"1234567890{i:03d}", # Creates valid 13-digit GTINs
location=f"WAREHOUSE_{unique_prefix}_{i}",
quantity=10,
)
db.add(inventory)
db.commit()
result = self.service.get_all_inventory(db, skip=2, limit=2)
assert len(result) <= 2 # Should be at most 2, might be less if other records exist
def test_update_inventory_success(self, db, test_inventory):
"""Test updating inventory quantity successfully."""
inventory_update = InventoryUpdate(quantity=150)
result = self.service.update_inventory(db, test_inventory.id, inventory_update)
assert result.id == test_inventory.id
assert result.quantity == 150
def test_update_inventory_not_found_error(self, db):
"""Test updating non-existent inventory entry returns InventoryNotFoundException."""
inventory_update = InventoryUpdate(quantity=150)
with pytest.raises(InventoryNotFoundException) as exc_info:
self.service.update_inventory(db, 99999, inventory_update)
assert exc_info.value.error_code == "INVENTORY_NOT_FOUND"
assert "99999" in str(exc_info.value)
def test_update_inventory_invalid_quantity_error(self, db, test_inventory):
"""Test updating inventory with invalid quantity returns InvalidQuantityException."""
inventory_update = InventoryUpdate(quantity=-10)
with pytest.raises(InvalidQuantityException) as exc_info:
self.service.update_inventory(db, test_inventory.id, inventory_update)
assert exc_info.value.error_code == "INVALID_QUANTITY"
assert "Quantity cannot be negative" in str(exc_info.value)
def test_delete_inventory_success(self, db, test_inventory):
"""Test deleting inventory entry successfully."""
inventory_id = test_inventory.id
result = self.service.delete_inventory(db, inventory_id)
assert result is True
# Verify the inventory is actually deleted
deleted_inventory = db.query(Inventory).filter(Inventory.id == inventory_id).first()
assert deleted_inventory is None
def test_delete_inventory_not_found_error(self, db):
"""Test deleting non-existent inventory entry returns InventoryNotFoundException."""
with pytest.raises(InventoryNotFoundException) as exc_info:
self.service.delete_inventory(db, 99999)
assert exc_info.value.error_code == "INVENTORY_NOT_FOUND"
assert "99999" in str(exc_info.value)
def test_get_low_inventory_items_success(self, db, test_inventory, test_marketplace_product):
"""Test getting low inventory items successfully."""
# Set inventory to a low value
test_inventory.quantity = 5
db.commit()
result = self.service.get_low_inventory_items(db, threshold=10)
assert len(result) >= 1
low_inventory_item = next((item for item in result if item["gtin"] == test_inventory.gtin), None)
assert low_inventory_item is not None
assert low_inventory_item["current_quantity"] == 5
assert low_inventory_item["location"] == test_inventory.location
assert low_inventory_item["product_title"] == test_marketplace_product.title
def test_get_low_inventory_items_invalid_threshold_error(self, db):
"""Test getting low inventory items with invalid threshold returns InvalidQuantityException."""
with pytest.raises(InvalidQuantityException) as exc_info:
self.service.get_low_inventory_items(db, threshold=-5)
assert exc_info.value.error_code == "INVALID_QUANTITY"
assert "Threshold must be non-negative" in str(exc_info.value)
def test_get_inventory_summary_by_location_success(self, db, test_inventory):
"""Test getting inventory summary by location successfully."""
result = self.service.get_inventory_summary_by_location(db, test_inventory.location)
assert result["location"] == test_inventory.location.upper() # Service normalizes to uppercase
assert result["total_items"] >= 1
assert result["total_quantity"] >= test_inventory.quantity
assert result["unique_gtins"] >= 1
def test_get_inventory_summary_by_location_empty_result(self, db):
"""Test getting inventory summary for location with no inventory."""
unique_id = str(uuid.uuid4())[:8]
result = self.service.get_inventory_summary_by_location(db, f"EMPTY_LOCATION_{unique_id}")
assert result["total_items"] == 0
assert result["total_quantity"] == 0
assert result["unique_gtins"] == 0
def test_validate_quantity_edge_cases(self, db):
"""Test quantity validation with edge cases."""
# Test zero quantity with allow_zero=True (should succeed)
inventory_data = InventoryCreate(gtin="1234567890123", location="WAREHOUSE_A", quantity=0)
result = self.service.set_inventory(db, inventory_data)
assert result.quantity == 0
# Test zero quantity with add_inventory (should fail - doesn't allow zero)
inventory_data_add = InventoryAdd(gtin="1234567890123", location="WAREHOUSE_B", quantity=0)
with pytest.raises(InvalidQuantityException):
self.service.add_inventory(db, inventory_data_add)
def test_exception_structure_consistency(self, db):
"""Test that all exceptions follow the consistent LetzShopException structure."""
# Test with a known error case
with pytest.raises(InventoryNotFoundException) as exc_info:
self.service.get_inventory_by_gtin(db, "9999999999999")
exception = exc_info.value
# Verify exception structure matches LetzShopException.to_dict()
assert hasattr(exception, 'error_code')
assert hasattr(exception, 'message')
assert hasattr(exception, 'status_code')
assert hasattr(exception, 'details')
assert isinstance(exception.error_code, str)
assert isinstance(exception.message, str)
assert isinstance(exception.status_code, int)
assert isinstance(exception.details, dict)
@pytest.fixture
def test_product_with_inventory(db, test_inventory):
"""Create a test product that corresponds to the test inventory."""
product = MarketplaceProduct(
marketplace_product_id="MP_TEST_001",
title="Inventory Test MarketplaceProduct",
gtin=test_inventory.gtin,
price="29.99",
brand="TestBrand",
marketplace="Letzshop",
)
db.add(product)
db.commit()
db.refresh(product)
return product

View File

@@ -28,13 +28,13 @@ class TestMarketplaceService:
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
test_vendor.owner_user_id = test_user.id
db.commit()
result = self.service.validate_vendor_access(db, test_vendor.vendor_code, test_user)
assert result.vendor_code == test_vendor.vendor_code
assert result.owner_id == test_user.id
assert result.owner_user_id == test_user.id
def test_validate_vendor_access_admin_can_access_any_vendor(
self, db, test_vendor, test_admin
@@ -59,7 +59,7 @@ class TestMarketplaceService:
):
"""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
test_vendor.owner_user_id = other_user.id
db.commit()
with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
@@ -73,7 +73,7 @@ class TestMarketplaceService:
def test_create_import_job_success(self, db, test_vendor, test_user):
"""Test successful creation of import job"""
# Set the vendor owner to the test user
test_vendor.owner_id = test_user.id
test_vendor.owner_user_id = test_user.id
db.commit()
request = MarketplaceImportJobRequest(
@@ -90,7 +90,7 @@ class TestMarketplaceService:
assert result.user_id == test_user.id
assert result.status == "pending"
assert result.source_url == "https://example.com/products.csv"
assert result.vendor_name == test_vendor.vendor_name
assert result.vendor_name == test_vendor.name
def test_create_import_job_invalid_vendor(self, db, test_user):
"""Test import job creation with invalid vendor """
@@ -111,7 +111,7 @@ class TestMarketplaceService:
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
test_vendor.owner_user_id = other_user.id
db.commit()
request = MarketplaceImportJobRequest(
@@ -436,7 +436,7 @@ class TestMarketplaceService:
# Test edge cases and error scenarios
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
test_vendor.owner_user_id = test_user.id
db.commit()
# Test with lowercase vendor code

View File

@@ -229,23 +229,23 @@ class TestProductService:
assert exc_info.value.error_code == "PRODUCT_NOT_FOUND"
assert "NONEXISTENT" in str(exc_info.value)
def test_get_stock_info_success(self, db, test_marketplace_product_with_stock):
"""Test getting stock info for product with stock"""
def test_get_inventory_info_success(self, db, test_marketplace_product_with_inventory):
"""Test getting inventory info for product with inventory"""
# Extract the product from the dictionary
marketplace_product = test_marketplace_product_with_stock['marketplace_product']
marketplace_product = test_marketplace_product_with_inventory['marketplace_product']
stock_info = self.service.get_stock_info(db, marketplace_product.gtin)
inventory_info = self.service.get_inventory_info(db, marketplace_product.gtin)
assert stock_info is not None
assert stock_info.gtin == marketplace_product.gtin
assert stock_info.total_quantity > 0
assert len(stock_info.locations) > 0
assert inventory_info is not None
assert inventory_info.gtin == marketplace_product.gtin
assert inventory_info.total_quantity > 0
assert len(inventory_info.locations) > 0
def test_get_stock_info_no_stock(self, db, test_marketplace_product):
"""Test getting stock info for product without stock"""
stock_info = self.service.get_stock_info(db, test_marketplace_product.gtin or "1234567890123")
def test_get_inventory_info_no_inventory(self, db, test_marketplace_product):
"""Test getting inventory info for product without inventory"""
inventory_info = self.service.get_inventory_info(db, test_marketplace_product.gtin or "1234567890123")
assert stock_info is None
assert inventory_info is None
def test_product_exists_true(self, db, test_marketplace_product):
"""Test product_exists returns True for existing product"""

View File

@@ -3,7 +3,7 @@ import pytest
from app.services.stats_service import StatsService
from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock
from models.database.inventory import Inventory
@pytest.mark.unit
@@ -15,7 +15,7 @@ class TestStatsService:
"""Setup method following the same pattern as other service tests"""
self.service = StatsService()
def test_get_comprehensive_stats_basic(self, db, test_marketplace_product, test_stock):
def test_get_comprehensive_stats_basic(self, db, test_marketplace_product, test_inventory):
"""Test getting comprehensive stats with basic data"""
stats = self.service.get_comprehensive_stats(db)
@@ -24,12 +24,12 @@ class TestStatsService:
assert "unique_categories" in stats
assert "unique_marketplaces" in stats
assert "unique_vendors" in stats
assert "total_stock_entries" in stats
assert "total_inventory_entries" in stats
assert "total_inventory_quantity" in stats
assert stats["total_products"] >= 1
assert stats["total_stock_entries"] >= 1
assert stats["total_inventory_quantity"] >= 10 # test_stock has quantity 10
assert stats["total_inventory_entries"] >= 1
assert stats["total_inventory_quantity"] >= 10 # test_inventory has quantity 10
def test_get_comprehensive_stats_multiple_products(self, db, test_marketplace_product):
"""Test comprehensive stats with multiple products across different dimensions"""
@@ -341,33 +341,33 @@ class TestStatsService:
assert count >= 2 # At least VendorA and VendorB, plus test_marketplace_product vendor
assert isinstance(count, int)
def test_get_stock_statistics(self, db, test_stock):
"""Test getting stock statistics"""
# Add additional stock entries
additional_stocks = [
Stock(
def test_get_inventory_statistics(self, db, test_inventory):
"""Test getting inventory statistics"""
# Add additional inventory entries
additional_inventory = [
Inventory(
gtin="1234567890124",
location="LOCATION2",
quantity=25,
reserved_quantity=5,
vendor_id=test_stock.vendor_id,
vendor_id=test_inventory.vendor_id,
),
Stock(
Inventory(
gtin="1234567890125",
location="LOCATION3",
quantity=0, # Out of stock
quantity=0, # Out of inventory
reserved_quantity=0,
vendor_id=test_stock.vendor_id,
vendor_id=test_inventory.vendor_id,
),
]
db.add_all(additional_stocks)
db.add_all(additional_inventory)
db.commit()
stats = self.service.get_stock_statistics(db)
stats = self.service.get_inventory_statistics(db)
assert "total_stock_entries" in stats
assert "total_inventory_entries" in stats
assert "total_inventory_quantity" in stats
assert stats["total_stock_entries"] >= 3 # test_stock + 2 additional
assert stats["total_inventory_entries"] >= 3 # test_inventory + 2 additional
assert stats["total_inventory_quantity"] >= 35 # 10 + 25 + 0 = 35
def test_get_brands_by_marketplace(self, db):
@@ -495,7 +495,7 @@ class TestStatsService:
assert stats["unique_categories"] == 0
assert stats["unique_marketplaces"] == 0
assert stats["unique_vendors"] == 0
assert stats["total_stock_entries"] == 0
assert stats["total_inventory_entries"] == 0
assert stats["total_inventory_quantity"] == 0
def test_marketplace_breakdown_empty_database(self, db):

View File

@@ -1,505 +0,0 @@
# tests/test_stock_service.py
import uuid
import pytest
from app.services.stock_service import StockService
from app.exceptions import (
StockNotFoundException,
InsufficientStockException,
InvalidStockOperationException,
StockValidationException,
NegativeStockException,
InvalidQuantityException,
ValidationException,
)
from models.schemas.stock import StockAdd, StockCreate, StockUpdate
from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock
@pytest.mark.unit
@pytest.mark.stock
class TestStockService:
def setup_method(self):
self.service = StockService()
def test_normalize_gtin_invalid(self):
"""Test GTIN normalization with invalid GTINs."""
# Completely invalid values that should return None
assert self.service._normalize_gtin("invalid") is None
assert self.service._normalize_gtin("abcdef") is None
assert self.service._normalize_gtin("") is None
assert self.service._normalize_gtin(None) is None
assert self.service._normalize_gtin(" ") is None # Only whitespace
assert self.service._normalize_gtin("!@#$%") is None # Only special characters
# Mixed invalid characters that become empty after filtering
assert self.service._normalize_gtin("abc-def-ghi") is None # No digits
def test_normalize_gtin_valid(self):
"""Test GTIN normalization with valid GTINs."""
# Test various valid GTIN formats - these should remain unchanged
assert self.service._normalize_gtin("1234567890123") == "1234567890123" # EAN-13
assert self.service._normalize_gtin("123456789012") == "123456789012" # UPC-A
assert self.service._normalize_gtin("12345678") == "12345678" # EAN-8
assert self.service._normalize_gtin("12345678901234") == "12345678901234" # GTIN-14
# Test with decimal points (should be removed)
assert self.service._normalize_gtin("1234567890123.0") == "1234567890123"
# Test with whitespace (should be trimmed)
assert self.service._normalize_gtin(" 1234567890123 ") == "1234567890123"
# Test short GTINs being padded
assert self.service._normalize_gtin("123") == "0000000000123" # Padded to EAN-13
assert self.service._normalize_gtin("12345") == "0000000012345" # Padded to EAN-13
# Test long GTINs being truncated
assert self.service._normalize_gtin("123456789012345") == "3456789012345" # Truncated to 13
def test_normalize_gtin_edge_cases(self):
"""Test GTIN normalization edge cases."""
# Test numeric inputs
assert self.service._normalize_gtin(1234567890123) == "1234567890123"
assert self.service._normalize_gtin(123) == "0000000000123"
# Test mixed valid/invalid characters
assert self.service._normalize_gtin("123-456-789-012") == "123456789012" # Dashes removed
assert self.service._normalize_gtin("123 456 789 012") == "123456789012" # Spaces removed
assert self.service._normalize_gtin("ABC123456789012DEF") == "123456789012" # Letters removed
def test_set_stock_new_entry_success(self, db):
"""Test setting stock for a new GTIN/location combination successfully."""
unique_id = str(uuid.uuid4())[:8]
stock_data = StockCreate(
gtin="1234567890123", location=f"WAREHOUSE_A_{unique_id}", quantity=100
)
result = self.service.set_stock(db, stock_data)
assert result.gtin == "1234567890123"
assert result.location.upper() == f"WAREHOUSE_A_{unique_id}".upper()
assert result.quantity == 100
def test_set_stock_existing_entry_success(self, db, test_stock):
"""Test setting stock for an existing GTIN/location combination successfully."""
stock_data = StockCreate(
gtin=test_stock.gtin,
location=test_stock.location, # Use exact same location as test_stock
quantity=200,
)
result = self.service.set_stock(db, stock_data)
assert result.gtin == test_stock.gtin
assert result.location == test_stock.location
assert result.quantity == 200 # Should replace the original quantity
def test_set_stock_invalid_gtin_validation_error(self, db):
"""Test setting stock with invalid GTIN returns StockValidationException."""
stock_data = StockCreate(
gtin="invalid_gtin", location="WAREHOUSE_A", quantity=100
)
with pytest.raises(StockValidationException) as exc_info:
self.service.set_stock(db, stock_data)
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
assert exc_info.value.details.get("field") == "gtin"
def test_set_stock_invalid_quantity_error(self, db):
"""Test setting stock with invalid quantity through service validation."""
# Test the service validation directly instead of going through Pydantic schema
# This bypasses the Pydantic validation to test service layer validation
# Create a mock stock data object that bypasses Pydantic validation
class MockStockData:
def __init__(self, gtin, location, quantity):
self.gtin = gtin
self.location = location
self.quantity = quantity
mock_stock_data = MockStockData("1234567890123", "WAREHOUSE_A", -10)
# Test the internal validation method directly
with pytest.raises(InvalidQuantityException) as exc_info:
self.service._validate_quantity(-10, allow_zero=True)
assert exc_info.value.error_code == "INVALID_QUANTITY"
assert "Quantity cannot be negative" in str(exc_info.value)
assert exc_info.value.details.get("quantity") == -10
def test_add_stock_new_entry_success(self, db):
"""Test adding stock for a new GTIN/location combination successfully."""
unique_id = str(uuid.uuid4())[:8]
stock_data = StockAdd(
gtin="1234567890123", location=f"WAREHOUSE_B_{unique_id}", quantity=50
)
result = self.service.add_stock(db, stock_data)
assert result.gtin == "1234567890123"
assert result.location.upper() == f"WAREHOUSE_B_{unique_id}".upper()
assert result.quantity == 50
def test_add_stock_existing_entry_success(self, db, test_stock):
"""Test adding stock to an existing GTIN/location combination successfully."""
original_quantity = test_stock.quantity
stock_data = StockAdd(
gtin=test_stock.gtin,
location=test_stock.location, # Use exact same location as test_stock
quantity=25,
)
result = self.service.add_stock(db, stock_data)
assert result.gtin == test_stock.gtin
assert result.location == test_stock.location
assert result.quantity == original_quantity + 25
def test_add_stock_invalid_gtin_validation_error(self, db):
"""Test adding stock with invalid GTIN returns StockValidationException."""
stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=50)
with pytest.raises(StockValidationException) as exc_info:
self.service.add_stock(db, stock_data)
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
def test_add_stock_invalid_quantity_error(self, db):
"""Test adding stock with invalid quantity through service validation."""
# Test zero quantity which should fail for add_stock (doesn't allow zero)
# This tests the service validation: allow_zero=False for add operations
with pytest.raises(InvalidQuantityException) as exc_info:
self.service._validate_quantity(0, allow_zero=False)
assert exc_info.value.error_code == "INVALID_QUANTITY"
assert "Quantity must be positive" in str(exc_info.value)
def test_remove_stock_success(self, db, test_stock):
"""Test removing stock successfully."""
original_quantity = test_stock.quantity
remove_quantity = min(10, original_quantity) # Ensure we don't remove more than available
stock_data = StockAdd(
gtin=test_stock.gtin,
location=test_stock.location, # Use exact same location as test_stock
quantity=remove_quantity,
)
result = self.service.remove_stock(db, stock_data)
assert result.gtin == test_stock.gtin
assert result.location == test_stock.location
assert result.quantity == original_quantity - remove_quantity
def test_remove_stock_insufficient_stock_error(self, db, test_stock):
"""Test removing more stock than available returns InsufficientStockException."""
stock_data = StockAdd(
gtin=test_stock.gtin,
location=test_stock.location, # Use exact same location as test_stock
quantity=test_stock.quantity + 10, # More than available
)
with pytest.raises(InsufficientStockException) as exc_info:
self.service.remove_stock(db, stock_data)
assert exc_info.value.error_code == "INSUFFICIENT_STOCK"
assert exc_info.value.details["gtin"] == test_stock.gtin
assert exc_info.value.details["location"] == test_stock.location
assert exc_info.value.details["requested_quantity"] == test_stock.quantity + 10
assert exc_info.value.details["available_quantity"] == test_stock.quantity
def test_remove_stock_nonexistent_entry_not_found(self, db):
"""Test removing stock from non-existent GTIN/location returns StockNotFoundException."""
unique_id = str(uuid.uuid4())[:8]
stock_data = StockAdd(
gtin="9999999999999", location=f"NONEXISTENT_{unique_id}", quantity=10
)
with pytest.raises(StockNotFoundException) as exc_info:
self.service.remove_stock(db, stock_data)
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
assert "9999999999999" in str(exc_info.value)
assert exc_info.value.details["resource_type"] == "Stock"
def test_remove_stock_invalid_gtin_validation_error(self, db):
"""Test removing stock with invalid GTIN returns StockValidationException."""
stock_data = StockAdd(gtin="invalid_gtin", location="WAREHOUSE_A", quantity=10)
with pytest.raises(StockValidationException) as exc_info:
self.service.remove_stock(db, stock_data)
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
def test_remove_stock_negative_result_error(self, db, test_stock):
"""Test removing stock that would result in negative quantity returns NegativeStockException."""
# This is handled by InsufficientStockException, but test the logic
stock_data = StockAdd(
gtin=test_stock.gtin,
location=test_stock.location,
quantity=test_stock.quantity + 1, # One more than available
)
with pytest.raises(InsufficientStockException) as exc_info:
self.service.remove_stock(db, stock_data)
# The service prevents negative stock through InsufficientStockException
assert exc_info.value.error_code == "INSUFFICIENT_STOCK"
def test_get_stock_by_gtin_success(self, db, test_stock, test_marketplace_product):
"""Test getting stock summary by GTIN successfully."""
result = self.service.get_stock_by_gtin(db, test_stock.gtin)
assert result.gtin == test_stock.gtin
assert result.total_quantity == test_stock.quantity
assert len(result.locations) == 1
assert result.locations[0].location == test_stock.location
assert result.locations[0].quantity == test_stock.quantity
assert result.product_title == test_marketplace_product.title
def test_get_stock_by_gtin_multiple_locations_success(self, db, test_marketplace_product):
"""Test getting stock summary with multiple locations successfully."""
unique_gtin = test_marketplace_product.gtin
unique_id = str(uuid.uuid4())[:8]
# Create multiple stock entries for the same GTIN with unique locations
stock1 = Stock(gtin=unique_gtin, location=f"WAREHOUSE_A_{unique_id}", quantity=50)
stock2 = Stock(gtin=unique_gtin, location=f"WAREHOUSE_B_{unique_id}", quantity=30)
db.add(stock1)
db.add(stock2)
db.commit()
result = self.service.get_stock_by_gtin(db, unique_gtin)
assert result.gtin == unique_gtin
assert result.total_quantity == 80
assert len(result.locations) == 2
def test_get_stock_by_gtin_not_found_error(self, db):
"""Test getting stock for non-existent GTIN returns StockNotFoundException."""
with pytest.raises(StockNotFoundException) as exc_info:
self.service.get_stock_by_gtin(db, "9999999999999")
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
assert "9999999999999" in str(exc_info.value)
assert exc_info.value.details["resource_type"] == "Stock"
def test_get_stock_by_gtin_invalid_gtin_validation_error(self, db):
"""Test getting stock with invalid GTIN returns StockValidationException."""
with pytest.raises(StockValidationException) as exc_info:
self.service.get_stock_by_gtin(db, "invalid_gtin")
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
def test_get_total_stock_success(self, db, test_stock, test_marketplace_product):
"""Test getting total stock for a GTIN successfully."""
result = self.service.get_total_stock(db, test_stock.gtin)
assert result["gtin"] == test_stock.gtin
assert result["total_quantity"] == test_stock.quantity
assert result["product_title"] == test_marketplace_product.title
assert result["locations_count"] == 1
def test_get_total_stock_invalid_gtin_validation_error(self, db):
"""Test getting total stock with invalid GTIN returns StockValidationException."""
with pytest.raises(StockValidationException) as exc_info:
self.service.get_total_stock(db, "invalid_gtin")
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value)
def test_get_total_stock_not_found_error(self, db):
"""Test getting total stock for non-existent GTIN returns StockNotFoundException."""
with pytest.raises(StockNotFoundException) as exc_info:
self.service.get_total_stock(db, "9999999999999")
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
def test_get_all_stock_no_filters_success(self, db, test_stock):
"""Test getting all stock without filters successfully."""
result = self.service.get_all_stock(db)
assert len(result) >= 1
assert any(stock.gtin == test_stock.gtin for stock in result)
def test_get_all_stock_with_location_filter_success(self, db, test_stock):
"""Test getting all stock with location filter successfully."""
result = self.service.get_all_stock(db, location=test_stock.location)
assert len(result) >= 1
# Check that all returned stocks match the filter (case insensitive)
for stock in result:
assert test_stock.location.upper() in stock.location.upper()
def test_get_all_stock_with_gtin_filter_success(self, db, test_stock):
"""Test getting all stock with GTIN filter successfully."""
result = self.service.get_all_stock(db, gtin=test_stock.gtin)
assert len(result) >= 1
assert all(stock.gtin == test_stock.gtin for stock in result)
def test_get_all_stock_with_pagination_success(self, db):
"""Test getting all stock with pagination successfully."""
unique_prefix = str(uuid.uuid4())[:8]
# Create multiple stock entries with unique GTINs and locations
for i in range(5):
stock = Stock(
gtin=f"1234567890{i:03d}", # Creates valid 13-digit GTINs
location=f"WAREHOUSE_{unique_prefix}_{i}",
quantity=10,
)
db.add(stock)
db.commit()
result = self.service.get_all_stock(db, skip=2, limit=2)
assert len(result) <= 2 # Should be at most 2, might be less if other records exist
def test_update_stock_success(self, db, test_stock):
"""Test updating stock quantity successfully."""
stock_update = StockUpdate(quantity=150)
result = self.service.update_stock(db, test_stock.id, stock_update)
assert result.id == test_stock.id
assert result.quantity == 150
def test_update_stock_not_found_error(self, db):
"""Test updating non-existent stock entry returns StockNotFoundException."""
stock_update = StockUpdate(quantity=150)
with pytest.raises(StockNotFoundException) as exc_info:
self.service.update_stock(db, 99999, stock_update)
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
assert "99999" in str(exc_info.value)
def test_update_stock_invalid_quantity_error(self, db, test_stock):
"""Test updating stock with invalid quantity returns InvalidQuantityException."""
stock_update = StockUpdate(quantity=-10)
with pytest.raises(InvalidQuantityException) as exc_info:
self.service.update_stock(db, test_stock.id, stock_update)
assert exc_info.value.error_code == "INVALID_QUANTITY"
assert "Quantity cannot be negative" in str(exc_info.value)
def test_delete_stock_success(self, db, test_stock):
"""Test deleting stock entry successfully."""
stock_id = test_stock.id
result = self.service.delete_stock(db, stock_id)
assert result is True
# Verify the stock is actually deleted
deleted_stock = db.query(Stock).filter(Stock.id == stock_id).first()
assert deleted_stock is None
def test_delete_stock_not_found_error(self, db):
"""Test deleting non-existent stock entry returns StockNotFoundException."""
with pytest.raises(StockNotFoundException) as exc_info:
self.service.delete_stock(db, 99999)
assert exc_info.value.error_code == "STOCK_NOT_FOUND"
assert "99999" in str(exc_info.value)
def test_get_low_stock_items_success(self, db, test_stock, test_marketplace_product):
"""Test getting low stock items successfully."""
# Set stock to a low value
test_stock.quantity = 5
db.commit()
result = self.service.get_low_stock_items(db, threshold=10)
assert len(result) >= 1
low_stock_item = next((item for item in result if item["gtin"] == test_stock.gtin), None)
assert low_stock_item is not None
assert low_stock_item["current_quantity"] == 5
assert low_stock_item["location"] == test_stock.location
assert low_stock_item["product_title"] == test_marketplace_product.title
def test_get_low_stock_items_invalid_threshold_error(self, db):
"""Test getting low stock items with invalid threshold returns InvalidQuantityException."""
with pytest.raises(InvalidQuantityException) as exc_info:
self.service.get_low_stock_items(db, threshold=-5)
assert exc_info.value.error_code == "INVALID_QUANTITY"
assert "Threshold must be non-negative" in str(exc_info.value)
def test_get_stock_summary_by_location_success(self, db, test_stock):
"""Test getting stock summary by location successfully."""
result = self.service.get_stock_summary_by_location(db, test_stock.location)
assert result["location"] == test_stock.location.upper() # Service normalizes to uppercase
assert result["total_items"] >= 1
assert result["total_quantity"] >= test_stock.quantity
assert result["unique_gtins"] >= 1
def test_get_stock_summary_by_location_empty_result(self, db):
"""Test getting stock summary for location with no stock."""
unique_id = str(uuid.uuid4())[:8]
result = self.service.get_stock_summary_by_location(db, f"EMPTY_LOCATION_{unique_id}")
assert result["total_items"] == 0
assert result["total_quantity"] == 0
assert result["unique_gtins"] == 0
def test_validate_quantity_edge_cases(self, db):
"""Test quantity validation with edge cases."""
# Test zero quantity with allow_zero=True (should succeed)
stock_data = StockCreate(gtin="1234567890123", location="WAREHOUSE_A", quantity=0)
result = self.service.set_stock(db, stock_data)
assert result.quantity == 0
# Test zero quantity with add_stock (should fail - doesn't allow zero)
stock_data_add = StockAdd(gtin="1234567890123", location="WAREHOUSE_B", quantity=0)
with pytest.raises(InvalidQuantityException):
self.service.add_stock(db, stock_data_add)
def test_exception_structure_consistency(self, db):
"""Test that all exceptions follow the consistent LetzShopException structure."""
# Test with a known error case
with pytest.raises(StockNotFoundException) as exc_info:
self.service.get_stock_by_gtin(db, "9999999999999")
exception = exc_info.value
# Verify exception structure matches LetzShopException.to_dict()
assert hasattr(exception, 'error_code')
assert hasattr(exception, 'message')
assert hasattr(exception, 'status_code')
assert hasattr(exception, 'details')
assert isinstance(exception.error_code, str)
assert isinstance(exception.message, str)
assert isinstance(exception.status_code, int)
assert isinstance(exception.details, dict)
@pytest.fixture
def test_product_with_stock(db, test_stock):
"""Create a test product that corresponds to the test stock."""
product = MarketplaceProduct(
marketplace_product_id="STOCK_TEST_001",
title="Stock Test MarketplaceProduct",
gtin=test_stock.gtin,
price="29.99",
brand="TestBrand",
marketplace="Letzshop",
)
db.add(product)
db.commit()
db.refresh(product)
return product

View File

@@ -37,7 +37,7 @@ class TestVendorService:
assert vendor is not None
assert vendor.vendor_code == "NEWVENDOR"
assert vendor.owner_id == test_user.id
assert vendor.owner_user_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):
@@ -51,7 +51,7 @@ class TestVendorService:
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
vendor_code=test_vendor.vendor_code, vendor_name=test_vendor.name
)
with pytest.raises(VendorAlreadyExistsException) as exc_info:
@@ -84,7 +84,7 @@ class TestVendorService:
exception = exc_info.value
assert exception.error_code == "INVALID_VENDOR_DATA"
assert exception.details["field"] == "vendor_name"
assert exception.details["field"] == "name"
def test_create_vendor_invalid_code_format(self, db, test_user):
"""Test vendor creation fails with invalid vendor code format"""

View File

@@ -116,7 +116,7 @@ TEST002,Test MarketplaceProduct 2,15.99,TestMarket"""
"title": ["MarketplaceProduct 1", "MarketplaceProduct 2"],
"price": ["10.99", "15.99"],
"marketplace": ["TestMarket", "TestMarket"],
"vendor_name": ["TestVendor", "TestVendor"],
"name": ["TestVendor", "TestVendor"],
}
)
mock_parse.return_value = mock_df