- Replace black, isort, and flake8 with Ruff (all-in-one linter and formatter) - Add comprehensive pyproject.toml configuration - Simplify Makefile code quality targets - Configure exclusions for venv/.venv in pyproject.toml - Auto-fix 1,359 linting issues across codebase Benefits: - Much faster builds (Ruff is written in Rust) - Single tool replaces multiple tools - More comprehensive rule set (UP, B, C4, SIM, PIE, RET, Q) - All configuration centralized in pyproject.toml - Better import sorting and formatting consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
498 lines
18 KiB
Python
498 lines
18 KiB
Python
# 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
|
|
# 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 WizamartException 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 WizamartException.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)
|