Some checks failed
CI / ruff (push) Successful in 9s
CI / architecture (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / audit (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
Prevents .env from being baked into Docker image (was overriding config defaults). Adds env_file directive so containers load host .env properly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
359 lines
12 KiB
Python
359 lines
12 KiB
Python
# tests/unit/models/schema/test_customer.py
|
|
"""Unit tests for customer Pydantic schemas."""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.modules.customers.schemas import (
|
|
CustomerAddressCreate,
|
|
CustomerAddressResponse,
|
|
CustomerAddressUpdate,
|
|
CustomerPreferencesUpdate,
|
|
CustomerRegister,
|
|
CustomerResponse,
|
|
CustomerUpdate,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestCustomerRegisterSchema:
|
|
"""Test CustomerRegister schema validation."""
|
|
|
|
def test_valid_registration(self):
|
|
"""Test valid registration data."""
|
|
customer = CustomerRegister(
|
|
email="customer@example.com",
|
|
password="Password123", # noqa: SEC-001
|
|
first_name="John",
|
|
last_name="Doe",
|
|
)
|
|
assert customer.email == "customer@example.com"
|
|
assert customer.first_name == "John"
|
|
assert customer.last_name == "Doe"
|
|
|
|
def test_email_normalized_to_lowercase(self):
|
|
"""Test email is normalized to lowercase."""
|
|
customer = CustomerRegister(
|
|
email="Customer@Example.COM",
|
|
password="Password123", # noqa: SEC-001
|
|
first_name="John",
|
|
last_name="Doe",
|
|
)
|
|
assert customer.email == "customer@example.com"
|
|
|
|
def test_invalid_email(self):
|
|
"""Test invalid email raises ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
CustomerRegister(
|
|
email="not-an-email",
|
|
password="Password123", # noqa: SEC-001
|
|
first_name="John",
|
|
last_name="Doe",
|
|
)
|
|
assert "email" in str(exc_info.value).lower()
|
|
|
|
def test_password_min_length(self):
|
|
"""Test password must be at least 8 characters."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
CustomerRegister(
|
|
email="customer@example.com",
|
|
password="Pass1", # noqa: SEC-001
|
|
first_name="John",
|
|
last_name="Doe",
|
|
)
|
|
assert "password" in str(exc_info.value).lower()
|
|
|
|
def test_password_requires_digit(self):
|
|
"""Test password must contain at least one digit."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
CustomerRegister(
|
|
email="customer@example.com",
|
|
password="Password", # noqa: SEC-001
|
|
first_name="John",
|
|
last_name="Doe",
|
|
)
|
|
assert "digit" in str(exc_info.value).lower()
|
|
|
|
def test_password_requires_letter(self):
|
|
"""Test password must contain at least one letter."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
CustomerRegister(
|
|
email="customer@example.com",
|
|
password="12345678", # noqa: SEC-001
|
|
first_name="John",
|
|
last_name="Doe",
|
|
)
|
|
assert "letter" in str(exc_info.value).lower()
|
|
|
|
def test_first_name_required(self):
|
|
"""Test first_name is required."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
CustomerRegister(
|
|
email="customer@example.com",
|
|
password="Password123", # noqa: SEC-001
|
|
last_name="Doe",
|
|
)
|
|
assert "first_name" in str(exc_info.value).lower()
|
|
|
|
def test_marketing_consent_default(self):
|
|
"""Test marketing_consent defaults to False."""
|
|
customer = CustomerRegister(
|
|
email="customer@example.com",
|
|
password="Password123", # noqa: SEC-001
|
|
first_name="John",
|
|
last_name="Doe",
|
|
)
|
|
assert customer.marketing_consent is False
|
|
|
|
def test_optional_phone(self):
|
|
"""Test optional phone field."""
|
|
customer = CustomerRegister(
|
|
email="customer@example.com",
|
|
password="Password123", # noqa: SEC-001
|
|
first_name="John",
|
|
last_name="Doe",
|
|
phone="+352 123 456",
|
|
)
|
|
assert customer.phone == "+352 123 456"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestCustomerUpdateSchema:
|
|
"""Test CustomerUpdate schema validation."""
|
|
|
|
def test_partial_update(self):
|
|
"""Test partial update with only some fields."""
|
|
update = CustomerUpdate(first_name="Jane")
|
|
assert update.first_name == "Jane"
|
|
assert update.last_name is None
|
|
assert update.email is None
|
|
|
|
def test_empty_update_is_valid(self):
|
|
"""Test empty update is valid."""
|
|
update = CustomerUpdate()
|
|
assert update.model_dump(exclude_unset=True) == {}
|
|
|
|
def test_email_normalized_to_lowercase(self):
|
|
"""Test email is normalized to lowercase."""
|
|
update = CustomerUpdate(email="NewEmail@Example.COM")
|
|
assert update.email == "newemail@example.com"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestCustomerResponseSchema:
|
|
"""Test CustomerResponse schema."""
|
|
|
|
def test_from_dict(self):
|
|
"""Test creating response from dict."""
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
|
|
data = {
|
|
"id": 1,
|
|
"store_id": 1,
|
|
"email": "customer@example.com",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"phone": None,
|
|
"customer_number": "CUST001",
|
|
"marketing_consent": False,
|
|
"preferred_language": "fr",
|
|
"last_order_date": None,
|
|
"total_orders": 5,
|
|
"total_spent": Decimal("500.00"),
|
|
"is_active": True,
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
}
|
|
response = CustomerResponse(**data)
|
|
assert response.id == 1
|
|
assert response.customer_number == "CUST001"
|
|
assert response.total_orders == 5
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestCustomerAddressCreateSchema:
|
|
"""Test CustomerAddressCreate schema validation."""
|
|
|
|
def test_valid_shipping_address(self):
|
|
"""Test valid shipping address creation."""
|
|
address = CustomerAddressCreate(
|
|
address_type="shipping",
|
|
first_name="John",
|
|
last_name="Doe",
|
|
address_line_1="123 Main St",
|
|
city="Luxembourg",
|
|
postal_code="L-1234",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
)
|
|
assert address.address_type == "shipping"
|
|
assert address.city == "Luxembourg"
|
|
|
|
def test_valid_billing_address(self):
|
|
"""Test valid billing address creation."""
|
|
address = CustomerAddressCreate(
|
|
address_type="billing",
|
|
first_name="John",
|
|
last_name="Doe",
|
|
address_line_1="123 Main St",
|
|
city="Luxembourg",
|
|
postal_code="L-1234",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
)
|
|
assert address.address_type == "billing"
|
|
|
|
def test_invalid_address_type(self):
|
|
"""Test invalid address_type raises ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
CustomerAddressCreate(
|
|
address_type="delivery",
|
|
first_name="John",
|
|
last_name="Doe",
|
|
address_line_1="123 Main St",
|
|
city="Luxembourg",
|
|
postal_code="L-1234",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
)
|
|
assert "address_type" in str(exc_info.value).lower()
|
|
|
|
def test_is_default_defaults_to_false(self):
|
|
"""Test is_default defaults to False."""
|
|
address = CustomerAddressCreate(
|
|
address_type="shipping",
|
|
first_name="John",
|
|
last_name="Doe",
|
|
address_line_1="123 Main St",
|
|
city="Luxembourg",
|
|
postal_code="L-1234",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
)
|
|
assert address.is_default is False
|
|
|
|
def test_optional_address_line_2(self):
|
|
"""Test optional address_line_2 field."""
|
|
address = CustomerAddressCreate(
|
|
address_type="shipping",
|
|
first_name="John",
|
|
last_name="Doe",
|
|
address_line_1="123 Main St",
|
|
address_line_2="Apt 4B",
|
|
city="Luxembourg",
|
|
postal_code="L-1234",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
)
|
|
assert address.address_line_2 == "Apt 4B"
|
|
|
|
def test_required_fields(self):
|
|
"""Test required fields."""
|
|
with pytest.raises(ValidationError):
|
|
CustomerAddressCreate(
|
|
address_type="shipping",
|
|
first_name="John",
|
|
# missing last_name, address_line_1, city, postal_code, country
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestCustomerAddressUpdateSchema:
|
|
"""Test CustomerAddressUpdate schema validation."""
|
|
|
|
def test_partial_update(self):
|
|
"""Test partial update with only some fields."""
|
|
update = CustomerAddressUpdate(city="Esch-sur-Alzette")
|
|
assert update.city == "Esch-sur-Alzette"
|
|
assert update.first_name is None
|
|
|
|
def test_empty_update_is_valid(self):
|
|
"""Test empty update is valid."""
|
|
update = CustomerAddressUpdate()
|
|
assert update.model_dump(exclude_unset=True) == {}
|
|
|
|
def test_address_type_validation(self):
|
|
"""Test address_type validation in update."""
|
|
with pytest.raises(ValidationError):
|
|
CustomerAddressUpdate(address_type="invalid")
|
|
|
|
def test_valid_address_type_update(self):
|
|
"""Test valid address_type values in update."""
|
|
shipping = CustomerAddressUpdate(address_type="shipping")
|
|
billing = CustomerAddressUpdate(address_type="billing")
|
|
assert shipping.address_type == "shipping"
|
|
assert billing.address_type == "billing"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestCustomerAddressResponseSchema:
|
|
"""Test CustomerAddressResponse schema."""
|
|
|
|
def test_from_dict(self):
|
|
"""Test creating response from dict."""
|
|
from datetime import datetime
|
|
|
|
data = {
|
|
"id": 1,
|
|
"store_id": 1,
|
|
"customer_id": 1,
|
|
"address_type": "shipping",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"company": None,
|
|
"address_line_1": "123 Main St",
|
|
"address_line_2": None,
|
|
"city": "Luxembourg",
|
|
"postal_code": "L-1234",
|
|
"country_name": "Luxembourg",
|
|
"country_iso": "LU",
|
|
"is_default": True,
|
|
"created_at": datetime.now(),
|
|
"updated_at": datetime.now(),
|
|
}
|
|
response = CustomerAddressResponse(**data)
|
|
assert response.id == 1
|
|
assert response.is_default is True
|
|
assert response.address_type == "shipping"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.schema
|
|
class TestCustomerPreferencesUpdateSchema:
|
|
"""Test CustomerPreferencesUpdate schema validation."""
|
|
|
|
def test_partial_update(self):
|
|
"""Test partial update with only some fields."""
|
|
update = CustomerPreferencesUpdate(marketing_consent=True)
|
|
assert update.marketing_consent is True
|
|
assert update.preferred_language is None
|
|
|
|
def test_language_update(self):
|
|
"""Test language preference update."""
|
|
update = CustomerPreferencesUpdate(preferred_language="fr")
|
|
assert update.preferred_language == "fr"
|
|
|
|
def test_currency_update(self):
|
|
"""Test currency preference update."""
|
|
update = CustomerPreferencesUpdate(currency="EUR")
|
|
assert update.currency == "EUR"
|
|
|
|
def test_notification_preferences(self):
|
|
"""Test notification preferences dict."""
|
|
update = CustomerPreferencesUpdate(
|
|
notification_preferences={
|
|
"email": True,
|
|
"sms": False,
|
|
"push": True,
|
|
}
|
|
)
|
|
assert update.notification_preferences["email"] is True
|
|
assert update.notification_preferences["sms"] is False
|