# 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)