Migrate remaining legacy schemas to their respective modules: Marketplace module (app/modules/marketplace/schemas/): - letzshop.py: Letzshop credentials, orders, fulfillment, sync - onboarding.py: Vendor onboarding wizard schemas Catalog module (app/modules/catalog/schemas/): - product.py: ProductCreate, ProductUpdate, ProductResponse Payments module (app/modules/payments/schemas/): - payment.py: PaymentConfig, Stripe, transactions, balance Delete legacy files: - models/schema/letzshop.py - models/schema/onboarding.py - models/schema/product.py - models/schema/payment.py - models/schema/marketplace_product.py (re-export) - models/schema/marketplace_import_job.py (re-export) - models/schema/search.py (empty) Update imports across 19 files to use canonical locations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
246 lines
7.8 KiB
Python
246 lines
7.8 KiB
Python
# tests/unit/models/schema/test_product.py
|
|
"""Unit tests for product Pydantic schemas."""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.modules.catalog.schemas import (
|
|
ProductCreate,
|
|
ProductListResponse,
|
|
ProductResponse,
|
|
ProductUpdate,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestProductCreateSchema:
|
|
"""Test ProductCreate schema validation."""
|
|
|
|
def test_valid_product_create(self):
|
|
"""Test valid product creation data."""
|
|
product = ProductCreate(
|
|
marketplace_product_id=1,
|
|
vendor_sku="SKU-001",
|
|
price=99.99,
|
|
currency="EUR",
|
|
)
|
|
assert product.marketplace_product_id == 1
|
|
assert product.vendor_sku == "SKU-001"
|
|
assert product.price == 99.99
|
|
|
|
def test_marketplace_product_id_required(self):
|
|
"""Test marketplace_product_id is required."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreate(price=99.99)
|
|
assert "marketplace_product_id" in str(exc_info.value).lower()
|
|
|
|
def test_price_must_be_non_negative(self):
|
|
"""Test price must be >= 0."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreate(
|
|
marketplace_product_id=1,
|
|
price=-10.00,
|
|
)
|
|
assert "price" in str(exc_info.value).lower()
|
|
|
|
def test_price_zero_is_valid(self):
|
|
"""Test price of 0 is valid."""
|
|
product = ProductCreate(
|
|
marketplace_product_id=1,
|
|
price=0,
|
|
)
|
|
assert product.price == 0
|
|
|
|
def test_sale_price_must_be_non_negative(self):
|
|
"""Test sale_price must be >= 0."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreate(
|
|
marketplace_product_id=1,
|
|
sale_price=-5.00,
|
|
)
|
|
assert "sale_price" in str(exc_info.value).lower()
|
|
|
|
def test_min_quantity_default(self):
|
|
"""Test min_quantity defaults to 1."""
|
|
product = ProductCreate(marketplace_product_id=1)
|
|
assert product.min_quantity == 1
|
|
|
|
def test_min_quantity_must_be_at_least_1(self):
|
|
"""Test min_quantity must be >= 1."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreate(
|
|
marketplace_product_id=1,
|
|
min_quantity=0,
|
|
)
|
|
assert "min_quantity" in str(exc_info.value).lower()
|
|
|
|
def test_max_quantity_must_be_at_least_1(self):
|
|
"""Test max_quantity must be >= 1 if provided."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProductCreate(
|
|
marketplace_product_id=1,
|
|
max_quantity=0,
|
|
)
|
|
assert "max_quantity" in str(exc_info.value).lower()
|
|
|
|
def test_is_featured_default(self):
|
|
"""Test is_featured defaults to False."""
|
|
product = ProductCreate(marketplace_product_id=1)
|
|
assert product.is_featured is False
|
|
|
|
def test_all_optional_fields(self):
|
|
"""Test product with all optional fields."""
|
|
product = ProductCreate(
|
|
marketplace_product_id=1,
|
|
vendor_sku="SKU-001",
|
|
price=100.00,
|
|
sale_price=80.00,
|
|
currency="EUR",
|
|
availability="in_stock",
|
|
condition="new",
|
|
is_featured=True,
|
|
min_quantity=2,
|
|
max_quantity=10,
|
|
)
|
|
assert product.sale_price == 80.00
|
|
assert product.availability == "in_stock"
|
|
assert product.condition == "new"
|
|
assert product.is_featured is True
|
|
assert product.max_quantity == 10
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestProductUpdateSchema:
|
|
"""Test ProductUpdate schema validation."""
|
|
|
|
def test_partial_update(self):
|
|
"""Test partial update with only some fields."""
|
|
update = ProductUpdate(price=150.00)
|
|
assert update.price == 150.00
|
|
assert update.vendor_sku is None
|
|
assert update.is_active is None
|
|
|
|
def test_empty_update_is_valid(self):
|
|
"""Test empty update is valid (all fields optional)."""
|
|
update = ProductUpdate()
|
|
assert update.model_dump(exclude_unset=True) == {}
|
|
|
|
def test_price_validation(self):
|
|
"""Test price must be >= 0 in update."""
|
|
with pytest.raises(ValidationError):
|
|
ProductUpdate(price=-10.00)
|
|
|
|
def test_is_active_update(self):
|
|
"""Test is_active can be updated."""
|
|
update = ProductUpdate(is_active=False)
|
|
assert update.is_active is False
|
|
|
|
def test_is_featured_update(self):
|
|
"""Test is_featured can be updated."""
|
|
update = ProductUpdate(is_featured=True)
|
|
assert update.is_featured is True
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestProductResponseSchema:
|
|
"""Test ProductResponse schema."""
|
|
|
|
def test_from_dict(self):
|
|
"""Test creating response from dict."""
|
|
from datetime import datetime
|
|
|
|
data = {
|
|
"id": 1,
|
|
"vendor_id": 1,
|
|
"marketplace_product": {
|
|
"id": 1,
|
|
"marketplace_product_id": "TEST001", # Required field
|
|
"gtin": "1234567890123",
|
|
"title": "Test Product",
|
|
"description": "A test product",
|
|
"brand": "Test Brand",
|
|
"google_product_category": "Electronics",
|
|
"image_link": "https://example.com/image.jpg",
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
},
|
|
"vendor_sku": "SKU-001",
|
|
"price": 99.99,
|
|
"sale_price": None,
|
|
"currency": "EUR",
|
|
"availability": "in_stock",
|
|
"condition": "new",
|
|
"is_featured": False,
|
|
"is_active": True,
|
|
"display_order": 0,
|
|
"min_quantity": 1,
|
|
"max_quantity": None,
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
}
|
|
response = ProductResponse(**data)
|
|
assert response.id == 1
|
|
assert response.vendor_id == 1
|
|
assert response.is_active is True
|
|
|
|
def test_optional_inventory_fields(self):
|
|
"""Test optional inventory summary fields."""
|
|
from datetime import datetime
|
|
|
|
data = {
|
|
"id": 1,
|
|
"vendor_id": 1,
|
|
"marketplace_product": {
|
|
"id": 1,
|
|
"marketplace_product_id": "TEST002", # Required field
|
|
"gtin": "1234567890123",
|
|
"title": "Test Product",
|
|
"description": None,
|
|
"brand": None,
|
|
"google_product_category": None,
|
|
"image_link": None,
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
},
|
|
"vendor_sku": None,
|
|
"price": None,
|
|
"sale_price": None,
|
|
"currency": None,
|
|
"availability": None,
|
|
"condition": None,
|
|
"is_featured": False,
|
|
"is_active": True,
|
|
"display_order": 0,
|
|
"min_quantity": 1,
|
|
"max_quantity": None,
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
"total_inventory": 100,
|
|
"available_inventory": 80,
|
|
}
|
|
response = ProductResponse(**data)
|
|
assert response.total_inventory == 100
|
|
assert response.available_inventory == 80
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestProductListResponseSchema:
|
|
"""Test ProductListResponse schema."""
|
|
|
|
def test_valid_list_response(self):
|
|
"""Test valid list response structure."""
|
|
response = ProductListResponse(
|
|
products=[],
|
|
total=0,
|
|
skip=0,
|
|
limit=10,
|
|
)
|
|
assert response.products == []
|
|
assert response.total == 0
|
|
assert response.skip == 0
|
|
assert response.limit == 10
|