277 lines
No EOL
8.2 KiB
Python
277 lines
No EOL
8.2 KiB
Python
"""Test Pydantic schemas"""
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
from decimal import Decimal
|
|
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 = {
|
|
'name': 'Handcrafted Wooden Bowl',
|
|
'description': 'A beautiful handcrafted bowl',
|
|
'price': 45.99,
|
|
'stock': 10,
|
|
'image_url': 'https://example.com/bowl.jpg'
|
|
}
|
|
|
|
product = ProductCreateRequest(**data)
|
|
assert product.name == data['name']
|
|
assert product.description == data['description']
|
|
assert product.price == Decimal('45.99')
|
|
assert product.stock == 10
|
|
assert product.image_url == data['image_url']
|
|
|
|
@pytest.mark.unit
|
|
def test_minimal_valid_request(self):
|
|
"""Test minimal valid request (only required fields)"""
|
|
data = {
|
|
'name': 'Simple Product',
|
|
'price': 19.99
|
|
}
|
|
|
|
product = ProductCreateRequest(**data)
|
|
assert product.name == 'Simple Product'
|
|
assert product.price == Decimal('19.99')
|
|
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"""
|
|
data = {
|
|
'price': 19.99
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreateRequest(**data)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['loc'] == ('name',) for error in errors)
|
|
|
|
@pytest.mark.unit
|
|
def test_missing_price(self):
|
|
"""Test request with missing price"""
|
|
data = {
|
|
'name': 'Test Product'
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreateRequest(**data)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['loc'] == ('price',) for error in errors)
|
|
|
|
@pytest.mark.unit
|
|
def test_invalid_price_negative(self):
|
|
"""Test request with negative price"""
|
|
data = {
|
|
'name': 'Test Product',
|
|
'price': -10.99
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreateRequest(**data)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['type'] == 'greater_than' for error in errors)
|
|
|
|
@pytest.mark.unit
|
|
def test_invalid_price_zero(self):
|
|
"""Test request with zero price"""
|
|
data = {
|
|
'name': 'Test Product',
|
|
'price': 0.0
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreateRequest(**data)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['type'] == 'greater_than' for error in errors)
|
|
|
|
@pytest.mark.unit
|
|
def test_invalid_price_too_many_decimals(self):
|
|
"""Test request with too many decimal places"""
|
|
data = {
|
|
'name': 'Test Product',
|
|
'price': 10.999
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreateRequest(**data)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any('decimal places' in str(error).lower() for error in errors)
|
|
|
|
@pytest.mark.unit
|
|
def test_invalid_stock_negative(self):
|
|
"""Test request with negative stock"""
|
|
data = {
|
|
'name': 'Test Product',
|
|
'price': 19.99,
|
|
'stock': -5
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreateRequest(**data)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['type'] == 'greater_than_equal' for error in errors)
|
|
|
|
@pytest.mark.unit
|
|
def test_name_too_long(self):
|
|
"""Test request with name exceeding max length"""
|
|
data = {
|
|
'name': 'A' * 201, # Exceeds 200 character limit
|
|
'price': 19.99
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreateRequest(**data)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['loc'] == ('name',) for error in errors)
|
|
|
|
@pytest.mark.unit
|
|
def test_image_url_too_long(self):
|
|
"""Test request with image_url exceeding max length"""
|
|
data = {
|
|
'name': 'Test Product',
|
|
'price': 19.99,
|
|
'image_url': 'A' * 501 # Exceeds 500 character limit
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreateRequest(**data)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any(error['loc'] == ('image_url',) for error in errors)
|
|
|
|
@pytest.mark.unit
|
|
def test_price_string_conversion(self):
|
|
"""Test price string to Decimal conversion"""
|
|
data = {
|
|
'name': 'Test Product',
|
|
'price': '29.99'
|
|
}
|
|
|
|
product = ProductCreateRequest(**data)
|
|
assert product.price == Decimal('29.99')
|
|
|
|
@pytest.mark.unit
|
|
def test_stock_string_conversion(self):
|
|
"""Test stock string to int conversion"""
|
|
data = {
|
|
'name': 'Test Product',
|
|
'price': 19.99,
|
|
'stock': '10'
|
|
}
|
|
|
|
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 = {
|
|
'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'
|
|
}
|
|
|
|
product = ProductResponse(**data)
|
|
assert product.id == 1
|
|
assert product.name == 'Test Product'
|
|
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 = {
|
|
'id': 1,
|
|
'name': 'Test Product',
|
|
'price': 19.99,
|
|
'stock': 0,
|
|
'is_active': True
|
|
}
|
|
|
|
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
|
|
|
|
db_product = Product(
|
|
name='Test Product',
|
|
description='A test product',
|
|
price=Decimal('45.99'),
|
|
stock=10
|
|
)
|
|
db_session.add(db_product)
|
|
db_session.commit()
|
|
|
|
# Validate using model_validate (for SQLAlchemy models)
|
|
response = ProductResponse.model_validate(db_product)
|
|
assert response.name == 'Test Product'
|
|
assert response.price == 45.99
|
|
assert response.stock == 10
|
|
|
|
@pytest.mark.unit
|
|
def test_model_dump(self):
|
|
"""Test model_dump method"""
|
|
data = {
|
|
'id': 1,
|
|
'name': 'Test Product',
|
|
'price': 19.99,
|
|
'stock': 5,
|
|
'is_active': True
|
|
}
|
|
|
|
product = ProductResponse(**data)
|
|
dumped = product.model_dump()
|
|
|
|
assert isinstance(dumped, dict)
|
|
assert dumped['id'] == 1
|
|
assert dumped['name'] == 'Test Product'
|
|
assert dumped['price'] == 19.99
|
|
|
|
@pytest.mark.unit
|
|
def test_model_dump_json(self):
|
|
"""Test model_dump_json method"""
|
|
data = {
|
|
'id': 1,
|
|
'name': 'Test Product',
|
|
'price': 19.99,
|
|
'stock': 5,
|
|
'is_active': True
|
|
}
|
|
|
|
product = ProductResponse(**data)
|
|
json_str = product.model_dump_json()
|
|
|
|
assert isinstance(json_str, str)
|
|
assert 'Test Product' in json_str |