major refactoring adding vendor and customer features
This commit is contained in:
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user