major refactoring adding vendor and customer features
This commit is contained in:
@@ -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
42
tests/fixtures/customer_fixtures.py
vendored
Normal 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
|
||||
@@ -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,
|
||||
|
||||
18
tests/fixtures/marketplace_product_fixtures.py
vendored
18
tests/fixtures/marketplace_product_fixtures.py
vendored
@@ -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
|
||||
}
|
||||
|
||||
2
tests/fixtures/testing_fixtures.py
vendored
2
tests/fixtures/testing_fixtures.py
vendored
@@ -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
|
||||
|
||||
113
tests/fixtures/vendor_fixtures.py
vendored
113
tests/fixtures/vendor_fixtures.py
vendored
@@ -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
|
||||
|
||||
|
||||
455
tests/integration/api/v1/test_inventory_endpoints.py
Normal file
455
tests/integration/api/v1/test_inventory_endpoints.py
Normal 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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"""
|
||||
|
||||
505
tests/unit/services/test_inventory_service.py
Normal file
505
tests/unit/services/test_inventory_service.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user