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