2026-02-24 14:36:31 +00:00
|
|
|
"""Test Pydantic schemas"""
|
2026-02-24 16:19:15 +00:00
|
|
|
from decimal import Decimal
|
|
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
import pytest
|
|
|
|
|
from pydantic import ValidationError
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
from app.schemas import ProductCreateRequest, ProductResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestProductCreateRequestSchema:
|
|
|
|
|
"""Test ProductCreateRequest schema"""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_valid_product_request(self):
|
|
|
|
|
"""Test valid product creation request"""
|
|
|
|
|
data = {
|
2026-02-24 16:19:15 +00:00
|
|
|
"name": "Handcrafted Wooden Bowl",
|
|
|
|
|
"description": "A beautiful handcrafted bowl",
|
|
|
|
|
"price": 45.99,
|
|
|
|
|
"stock": 10,
|
|
|
|
|
"image_url": "https://example.com/bowl.jpg",
|
2026-02-24 14:36:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
product = ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
assert product.name == data["name"]
|
|
|
|
|
assert product.description == data["description"]
|
|
|
|
|
assert product.price == Decimal("45.99")
|
2026-02-24 14:36:31 +00:00
|
|
|
assert product.stock == 10
|
2026-02-24 16:19:15 +00:00
|
|
|
assert product.image_url == data["image_url"]
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_minimal_valid_request(self):
|
|
|
|
|
"""Test minimal valid request (only required fields)"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "Simple Product", "price": 19.99}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
product = ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
assert product.name == "Simple Product"
|
|
|
|
|
assert product.price == Decimal("19.99")
|
2026-02-24 14:36:31 +00:00
|
|
|
assert product.stock == 0
|
|
|
|
|
assert product.description is None
|
|
|
|
|
assert product.image_url is None
|
|
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_missing_name(self):
|
|
|
|
|
"""Test request with missing name"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"price": 19.99}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
|
|
|
ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
errors = exc_info.value.errors()
|
2026-02-24 16:19:15 +00:00
|
|
|
assert any(error["loc"] == ("name",) for error in errors)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_missing_price(self):
|
|
|
|
|
"""Test request with missing price"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "Test Product"}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
|
|
|
ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
errors = exc_info.value.errors()
|
2026-02-24 16:19:15 +00:00
|
|
|
assert any(error["loc"] == ("price",) for error in errors)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_invalid_price_negative(self):
|
|
|
|
|
"""Test request with negative price"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "Test Product", "price": -10.99}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
|
|
|
ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
errors = exc_info.value.errors()
|
2026-02-24 16:19:15 +00:00
|
|
|
assert any(error["type"] == "greater_than" for error in errors)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_invalid_price_zero(self):
|
|
|
|
|
"""Test request with zero price"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "Test Product", "price": 0.0}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
|
|
|
ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
errors = exc_info.value.errors()
|
2026-02-24 16:19:15 +00:00
|
|
|
assert any(error["type"] == "greater_than" for error in errors)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_invalid_price_too_many_decimals(self):
|
|
|
|
|
"""Test request with too many decimal places"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "Test Product", "price": 10.999}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
|
|
|
ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
errors = exc_info.value.errors()
|
2026-02-24 16:19:15 +00:00
|
|
|
assert any("decimal places" in str(error).lower() for error in errors)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_invalid_stock_negative(self):
|
|
|
|
|
"""Test request with negative stock"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "Test Product", "price": 19.99, "stock": -5}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
|
|
|
ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
errors = exc_info.value.errors()
|
2026-02-24 16:19:15 +00:00
|
|
|
assert any(error["type"] == "greater_than_equal" for error in errors)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_name_too_long(self):
|
|
|
|
|
"""Test request with name exceeding max length"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "A" * 201, "price": 19.99} # Exceeds 200 character limit
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
|
|
|
ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
errors = exc_info.value.errors()
|
2026-02-24 16:19:15 +00:00
|
|
|
assert any(error["loc"] == ("name",) for error in errors)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_image_url_too_long(self):
|
|
|
|
|
"""Test request with image_url exceeding max length"""
|
|
|
|
|
data = {
|
2026-02-24 16:19:15 +00:00
|
|
|
"name": "Test Product",
|
|
|
|
|
"price": 19.99,
|
|
|
|
|
"image_url": "A" * 501, # Exceeds 500 character limit
|
2026-02-24 14:36:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
|
|
|
ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
errors = exc_info.value.errors()
|
2026-02-24 16:19:15 +00:00
|
|
|
assert any(error["loc"] == ("image_url",) for error in errors)
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_price_string_conversion(self):
|
|
|
|
|
"""Test price string to Decimal conversion"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "Test Product", "price": "29.99"}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
product = ProductCreateRequest(**data)
|
2026-02-24 16:19:15 +00:00
|
|
|
assert product.price == Decimal("29.99")
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_stock_string_conversion(self):
|
|
|
|
|
"""Test stock string to int conversion"""
|
2026-02-24 16:19:15 +00:00
|
|
|
data = {"name": "Test Product", "price": 19.99, "stock": "10"}
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
product = ProductCreateRequest(**data)
|
|
|
|
|
assert product.stock == 10
|
|
|
|
|
assert isinstance(product.stock, int)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestProductResponseSchema:
|
|
|
|
|
"""Test ProductResponse schema"""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_valid_product_response(self):
|
|
|
|
|
"""Test valid product response"""
|
|
|
|
|
data = {
|
2026-02-24 16:19:15 +00:00
|
|
|
"id": 1,
|
|
|
|
|
"name": "Test Product",
|
|
|
|
|
"description": "A test product",
|
|
|
|
|
"price": 45.99,
|
|
|
|
|
"stock": 10,
|
|
|
|
|
"image_url": "https://example.com/product.jpg",
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"created_at": "2024-01-15T10:30:00",
|
|
|
|
|
"updated_at": "2024-01-15T10:30:00",
|
2026-02-24 14:36:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
product = ProductResponse(**data)
|
|
|
|
|
assert product.id == 1
|
2026-02-24 16:19:15 +00:00
|
|
|
assert product.name == "Test Product"
|
2026-02-24 14:36:31 +00:00
|
|
|
assert product.price == 45.99
|
|
|
|
|
assert product.stock == 10
|
|
|
|
|
assert product.is_active is True
|
|
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_product_response_with_none_fields(self):
|
|
|
|
|
"""Test product response with optional None fields"""
|
|
|
|
|
data = {
|
2026-02-24 16:19:15 +00:00
|
|
|
"id": 1,
|
|
|
|
|
"name": "Test Product",
|
|
|
|
|
"price": 19.99,
|
|
|
|
|
"stock": 0,
|
|
|
|
|
"is_active": True,
|
2026-02-24 14:36:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
product = ProductResponse(**data)
|
|
|
|
|
assert product.description is None
|
|
|
|
|
assert product.image_url is None
|
|
|
|
|
assert product.created_at is None
|
|
|
|
|
assert product.updated_at is None
|
|
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_model_validate_from_sqlalchemy(self, db_session):
|
|
|
|
|
"""Test validating SQLAlchemy model to Pydantic schema"""
|
|
|
|
|
from app.models import Product
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
db_product = Product(
|
2026-02-24 16:19:15 +00:00
|
|
|
name="Test Product",
|
|
|
|
|
description="A test product",
|
|
|
|
|
price=Decimal("45.99"),
|
|
|
|
|
stock=10,
|
2026-02-24 14:36:31 +00:00
|
|
|
)
|
|
|
|
|
db_session.add(db_product)
|
|
|
|
|
db_session.commit()
|
|
|
|
|
|
|
|
|
|
# Validate using model_validate (for SQLAlchemy models)
|
|
|
|
|
response = ProductResponse.model_validate(db_product)
|
2026-02-24 16:19:15 +00:00
|
|
|
assert response.name == "Test Product"
|
2026-02-24 14:36:31 +00:00
|
|
|
assert response.price == 45.99
|
|
|
|
|
assert response.stock == 10
|
|
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_model_dump(self):
|
|
|
|
|
"""Test model_dump method"""
|
|
|
|
|
data = {
|
2026-02-24 16:19:15 +00:00
|
|
|
"id": 1,
|
|
|
|
|
"name": "Test Product",
|
|
|
|
|
"price": 19.99,
|
|
|
|
|
"stock": 5,
|
|
|
|
|
"is_active": True,
|
2026-02-24 14:36:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
product = ProductResponse(**data)
|
|
|
|
|
dumped = product.model_dump()
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
assert isinstance(dumped, dict)
|
2026-02-24 16:19:15 +00:00
|
|
|
assert dumped["id"] == 1
|
|
|
|
|
assert dumped["name"] == "Test Product"
|
|
|
|
|
assert dumped["price"] == 19.99
|
2026-02-24 14:36:31 +00:00
|
|
|
|
|
|
|
|
@pytest.mark.unit
|
|
|
|
|
def test_model_dump_json(self):
|
|
|
|
|
"""Test model_dump_json method"""
|
|
|
|
|
data = {
|
2026-02-24 16:19:15 +00:00
|
|
|
"id": 1,
|
|
|
|
|
"name": "Test Product",
|
|
|
|
|
"price": 19.99,
|
|
|
|
|
"stock": 5,
|
|
|
|
|
"is_active": True,
|
2026-02-24 14:36:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
product = ProductResponse(**data)
|
|
|
|
|
json_str = product.model_dump_json()
|
2026-02-24 16:19:15 +00:00
|
|
|
|
2026-02-24 14:36:31 +00:00
|
|
|
assert isinstance(json_str, str)
|
2026-02-24 16:19:15 +00:00
|
|
|
assert "Test Product" in json_str
|