- Remove |safe from |tojson in HTML attributes (x-data) - quotes must become " for browsers to parse correctly - Update LANG-002 and LANG-003 architecture rules to document correct |tojson usage patterns: - HTML attributes: |tojson (no |safe) - Script blocks: |tojson|safe - Fix validator to warn when |tojson|safe is used in x-data (breaks HTML attribute parsing) - Improve code quality across services, APIs, and tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
300 lines
9.1 KiB
Python
300 lines
9.1 KiB
Python
# tests/unit/models/schema/test_inventory.py
|
|
"""Unit tests for inventory Pydantic schemas."""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from models.schema.inventory import (
|
|
InventoryAdjust,
|
|
InventoryCreate,
|
|
InventoryLocationResponse,
|
|
InventoryReserve,
|
|
InventoryResponse,
|
|
InventoryUpdate,
|
|
ProductInventorySummary,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestInventoryCreateSchema:
|
|
"""Test InventoryCreate schema validation."""
|
|
|
|
def test_valid_inventory_create(self):
|
|
"""Test valid inventory creation data."""
|
|
inventory = InventoryCreate(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=100,
|
|
)
|
|
assert inventory.product_id == 1
|
|
assert inventory.location == "Warehouse A"
|
|
assert inventory.quantity == 100
|
|
|
|
def test_product_id_required(self):
|
|
"""Test product_id is required."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
InventoryCreate(location="Warehouse A", quantity=10)
|
|
assert "product_id" in str(exc_info.value).lower()
|
|
|
|
def test_location_required(self):
|
|
"""Test location is required."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
InventoryCreate(product_id=1, quantity=10)
|
|
assert "location" in str(exc_info.value).lower()
|
|
|
|
def test_quantity_required(self):
|
|
"""Test quantity is required."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
InventoryCreate(product_id=1, location="Warehouse A")
|
|
assert "quantity" in str(exc_info.value).lower()
|
|
|
|
def test_quantity_must_be_non_negative(self):
|
|
"""Test quantity must be >= 0."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
InventoryCreate(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=-5,
|
|
)
|
|
assert "quantity" in str(exc_info.value).lower()
|
|
|
|
def test_quantity_zero_is_valid(self):
|
|
"""Test quantity of 0 is valid."""
|
|
inventory = InventoryCreate(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=0,
|
|
)
|
|
assert inventory.quantity == 0
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestInventoryAdjustSchema:
|
|
"""Test InventoryAdjust schema validation."""
|
|
|
|
def test_positive_adjustment(self):
|
|
"""Test positive quantity adjustment (add stock)."""
|
|
adjustment = InventoryAdjust(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=50,
|
|
)
|
|
assert adjustment.quantity == 50
|
|
|
|
def test_negative_adjustment(self):
|
|
"""Test negative quantity adjustment (remove stock)."""
|
|
adjustment = InventoryAdjust(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=-20,
|
|
)
|
|
assert adjustment.quantity == -20
|
|
|
|
def test_zero_adjustment_is_valid(self):
|
|
"""Test zero adjustment is technically valid."""
|
|
adjustment = InventoryAdjust(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=0,
|
|
)
|
|
assert adjustment.quantity == 0
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestInventoryUpdateSchema:
|
|
"""Test InventoryUpdate schema validation."""
|
|
|
|
def test_partial_update(self):
|
|
"""Test partial update with only some fields."""
|
|
update = InventoryUpdate(quantity=150)
|
|
assert update.quantity == 150
|
|
assert update.reserved_quantity is None
|
|
assert update.location is None
|
|
|
|
def test_empty_update_is_valid(self):
|
|
"""Test empty update is valid."""
|
|
update = InventoryUpdate()
|
|
assert update.model_dump(exclude_unset=True) == {}
|
|
|
|
def test_quantity_must_be_non_negative(self):
|
|
"""Test quantity must be >= 0 in update."""
|
|
with pytest.raises(ValidationError):
|
|
InventoryUpdate(quantity=-10)
|
|
|
|
def test_reserved_quantity_must_be_non_negative(self):
|
|
"""Test reserved_quantity must be >= 0."""
|
|
with pytest.raises(ValidationError):
|
|
InventoryUpdate(reserved_quantity=-5)
|
|
|
|
def test_location_update(self):
|
|
"""Test location can be updated."""
|
|
update = InventoryUpdate(location="Warehouse B")
|
|
assert update.location == "Warehouse B"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestInventoryReserveSchema:
|
|
"""Test InventoryReserve schema validation."""
|
|
|
|
def test_valid_reservation(self):
|
|
"""Test valid inventory reservation."""
|
|
reservation = InventoryReserve(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=10,
|
|
)
|
|
assert reservation.product_id == 1
|
|
assert reservation.quantity == 10
|
|
|
|
def test_quantity_must_be_positive(self):
|
|
"""Test reservation quantity must be > 0."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
InventoryReserve(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=0,
|
|
)
|
|
assert "quantity" in str(exc_info.value).lower()
|
|
|
|
def test_negative_quantity_invalid(self):
|
|
"""Test negative reservation quantity is invalid."""
|
|
with pytest.raises(ValidationError):
|
|
InventoryReserve(
|
|
product_id=1,
|
|
location="Warehouse A",
|
|
quantity=-5,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestInventoryResponseSchema:
|
|
"""Test InventoryResponse schema."""
|
|
|
|
def test_from_dict(self):
|
|
"""Test creating response from dict."""
|
|
from datetime import datetime
|
|
|
|
data = {
|
|
"id": 1,
|
|
"product_id": 1,
|
|
"vendor_id": 1,
|
|
"location": "Warehouse A",
|
|
"quantity": 100,
|
|
"reserved_quantity": 20,
|
|
"gtin": "1234567890123",
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
}
|
|
response = InventoryResponse(**data)
|
|
assert response.id == 1
|
|
assert response.quantity == 100
|
|
assert response.reserved_quantity == 20
|
|
|
|
def test_available_quantity_property(self):
|
|
"""Test available_quantity calculated property."""
|
|
from datetime import datetime
|
|
|
|
data = {
|
|
"id": 1,
|
|
"product_id": 1,
|
|
"vendor_id": 1,
|
|
"location": "Warehouse A",
|
|
"quantity": 100,
|
|
"reserved_quantity": 30,
|
|
"gtin": None,
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
}
|
|
response = InventoryResponse(**data)
|
|
assert response.available_quantity == 70
|
|
|
|
def test_available_quantity_never_negative(self):
|
|
"""Test available_quantity is never negative."""
|
|
from datetime import datetime
|
|
|
|
data = {
|
|
"id": 1,
|
|
"product_id": 1,
|
|
"vendor_id": 1,
|
|
"location": "Warehouse A",
|
|
"quantity": 10,
|
|
"reserved_quantity": 50, # Over-reserved
|
|
"gtin": None,
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
}
|
|
response = InventoryResponse(**data)
|
|
assert response.available_quantity == 0
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestInventoryLocationResponseSchema:
|
|
"""Test InventoryLocationResponse schema."""
|
|
|
|
def test_valid_location_response(self):
|
|
"""Test valid location response."""
|
|
location = InventoryLocationResponse(
|
|
location="Warehouse A",
|
|
quantity=100,
|
|
reserved_quantity=20,
|
|
available_quantity=80,
|
|
)
|
|
assert location.location == "Warehouse A"
|
|
assert location.quantity == 100
|
|
assert location.available_quantity == 80
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestProductInventorySummarySchema:
|
|
"""Test ProductInventorySummary schema."""
|
|
|
|
def test_valid_summary(self):
|
|
"""Test valid inventory summary."""
|
|
summary = ProductInventorySummary(
|
|
product_id=1,
|
|
vendor_id=1,
|
|
product_sku="SKU-001",
|
|
product_title="Test Product",
|
|
total_quantity=200,
|
|
total_reserved=50,
|
|
total_available=150,
|
|
locations=[
|
|
InventoryLocationResponse(
|
|
location="Warehouse A",
|
|
quantity=100,
|
|
reserved_quantity=25,
|
|
available_quantity=75,
|
|
),
|
|
InventoryLocationResponse(
|
|
location="Warehouse B",
|
|
quantity=100,
|
|
reserved_quantity=25,
|
|
available_quantity=75,
|
|
),
|
|
],
|
|
)
|
|
assert summary.product_id == 1
|
|
assert summary.total_quantity == 200
|
|
assert len(summary.locations) == 2
|
|
|
|
def test_empty_locations(self):
|
|
"""Test summary with no locations."""
|
|
summary = ProductInventorySummary(
|
|
product_id=1,
|
|
vendor_id=1,
|
|
product_sku=None,
|
|
product_title="Test Product",
|
|
total_quantity=0,
|
|
total_reserved=0,
|
|
total_available=0,
|
|
locations=[],
|
|
)
|
|
assert summary.locations == []
|