fix(billing): complete billing module — fix tier change, platform support, merchant portal
- Fix admin tier change: resolve tier_code→tier_id in update_subscription(), delegate to billing_service.change_tier() for Stripe-connected subs - Add platform support to admin tiers page: platform column, filter dropdown, platform selector in create/edit modal, platform_name in tier API response - Filter used platforms in create subscription modal on merchant detail page - Enrich merchant portal API responses with tier code, tier_name, platform_name - Add eager-load of platform relationship in get_merchant_subscription() - Remove stale store_name/store_code references from merchant templates - Add merchant tier change endpoint (POST /change-tier) and tier selector UI replacing broken requestUpgrade() button - Fix subscription detail link to use platform_id instead of sub.id Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,271 +0,0 @@
|
||||
# tests/unit/models/database/test_admin_platform.py
|
||||
"""
|
||||
Unit tests for AdminPlatform model.
|
||||
|
||||
Tests the admin-platform junction table model and its relationships.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.modules.tenancy.models import AdminPlatform
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
@pytest.mark.admin
|
||||
class TestAdminPlatformModel:
|
||||
"""Test AdminPlatform model creation and constraints."""
|
||||
|
||||
def test_create_admin_platform_assignment(
|
||||
self, db, test_platform_admin, test_platform, test_super_admin
|
||||
):
|
||||
"""Test creating an admin platform assignment."""
|
||||
assignment = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
db.refresh(assignment)
|
||||
|
||||
assert assignment.id is not None
|
||||
assert assignment.user_id == test_platform_admin.id
|
||||
assert assignment.platform_id == test_platform.id
|
||||
assert assignment.is_active is True
|
||||
assert assignment.assigned_by_user_id == test_super_admin.id
|
||||
assert assignment.assigned_at is not None
|
||||
|
||||
def test_admin_platform_unique_constraint(
|
||||
self, db, test_platform_admin, test_platform, test_super_admin
|
||||
):
|
||||
"""Test that an admin can only be assigned to a platform once."""
|
||||
# Create first assignment
|
||||
assignment1 = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment1)
|
||||
db.commit()
|
||||
|
||||
# Try to create duplicate assignment
|
||||
assignment2 = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment2)
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
db.commit()
|
||||
|
||||
def test_admin_platform_cascade_delete_user(
|
||||
self, db, auth_manager, test_platform, test_super_admin
|
||||
):
|
||||
"""Test that deleting user cascades to admin platform assignments."""
|
||||
from app.modules.tenancy.models import User
|
||||
|
||||
# Create a temporary admin
|
||||
temp_admin = User(
|
||||
email="temp_admin@example.com",
|
||||
username="temp_admin",
|
||||
hashed_password=auth_manager.hash_password("temppass"),
|
||||
role="admin",
|
||||
is_active=True,
|
||||
is_super_admin=False,
|
||||
)
|
||||
db.add(temp_admin)
|
||||
db.flush()
|
||||
|
||||
# Create assignment
|
||||
assignment = AdminPlatform(
|
||||
user_id=temp_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
|
||||
assignment_id = assignment.id
|
||||
|
||||
# Delete user - should cascade to assignment
|
||||
db.delete(temp_admin)
|
||||
db.commit()
|
||||
|
||||
# Verify assignment is gone
|
||||
remaining = db.query(AdminPlatform).filter(AdminPlatform.id == assignment_id).first()
|
||||
assert remaining is None
|
||||
|
||||
def test_admin_platform_relationships(
|
||||
self, db, test_platform_admin, test_platform, test_super_admin
|
||||
):
|
||||
"""Test AdminPlatform relationships are loaded correctly."""
|
||||
assignment = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
db.refresh(assignment)
|
||||
|
||||
# Test relationships
|
||||
assert assignment.user is not None
|
||||
assert assignment.user.id == test_platform_admin.id
|
||||
assert assignment.platform is not None
|
||||
assert assignment.platform.id == test_platform.id
|
||||
assert assignment.assigned_by is not None
|
||||
assert assignment.assigned_by.id == test_super_admin.id
|
||||
|
||||
def test_admin_platform_properties(
|
||||
self, db, test_platform_admin, test_platform, test_super_admin
|
||||
):
|
||||
"""Test AdminPlatform computed properties."""
|
||||
assignment = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
db.refresh(assignment)
|
||||
|
||||
# Test properties
|
||||
assert assignment.platform_code == test_platform.code
|
||||
assert assignment.platform_name == test_platform.name
|
||||
|
||||
def test_admin_platform_repr(
|
||||
self, db, test_platform_admin, test_platform, test_super_admin
|
||||
):
|
||||
"""Test AdminPlatform string representation."""
|
||||
assignment = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
db.refresh(assignment)
|
||||
|
||||
repr_str = repr(assignment)
|
||||
assert "AdminPlatform" in repr_str
|
||||
assert str(test_platform_admin.id) in repr_str
|
||||
assert str(test_platform.id) in repr_str
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
@pytest.mark.admin
|
||||
class TestUserAdminMethods:
|
||||
"""Test User model admin-related methods."""
|
||||
|
||||
def test_is_super_admin_user_true(self, db, test_super_admin):
|
||||
"""Test is_super_admin_user property for super admin."""
|
||||
assert test_super_admin.is_super_admin_user is True
|
||||
|
||||
def test_is_super_admin_user_false_for_platform_admin(self, db, test_platform_admin):
|
||||
"""Test is_super_admin_user property for platform admin."""
|
||||
assert test_platform_admin.is_super_admin_user is False
|
||||
|
||||
def test_is_platform_admin_true(self, db, test_platform_admin):
|
||||
"""Test is_platform_admin property for platform admin."""
|
||||
assert test_platform_admin.is_platform_admin is True
|
||||
|
||||
def test_is_platform_admin_false_for_super_admin(self, db, test_super_admin):
|
||||
"""Test is_platform_admin property for super admin."""
|
||||
assert test_super_admin.is_platform_admin is False
|
||||
|
||||
def test_can_access_platform_super_admin(self, db, test_super_admin, test_platform):
|
||||
"""Test that super admin can access any platform."""
|
||||
assert test_super_admin.can_access_platform(test_platform.id) is True
|
||||
|
||||
def test_can_access_platform_assigned(
|
||||
self, db, test_platform_admin, test_platform, test_super_admin
|
||||
):
|
||||
"""Test that platform admin can access assigned platform."""
|
||||
# Create assignment
|
||||
assignment = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
db.refresh(test_platform_admin)
|
||||
|
||||
assert test_platform_admin.can_access_platform(test_platform.id) is True
|
||||
|
||||
def test_can_access_platform_not_assigned(
|
||||
self, db, test_platform_admin, test_platform
|
||||
):
|
||||
"""Test that platform admin cannot access unassigned platform."""
|
||||
# No assignment created
|
||||
assert test_platform_admin.can_access_platform(test_platform.id) is False
|
||||
|
||||
def test_can_access_platform_inactive_assignment(
|
||||
self, db, test_platform_admin, test_platform, test_super_admin
|
||||
):
|
||||
"""Test that platform admin cannot access platform with inactive assignment."""
|
||||
# Create inactive assignment
|
||||
assignment = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=False, # Inactive
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
db.refresh(test_platform_admin)
|
||||
|
||||
assert test_platform_admin.can_access_platform(test_platform.id) is False
|
||||
|
||||
def test_get_accessible_platform_ids_super_admin(self, db, test_super_admin):
|
||||
"""Test get_accessible_platform_ids returns None for super admin."""
|
||||
result = test_super_admin.get_accessible_platform_ids()
|
||||
assert result is None # None means all platforms
|
||||
|
||||
def test_get_accessible_platform_ids_platform_admin(
|
||||
self, db, test_platform_admin, test_platform, another_platform, test_super_admin
|
||||
):
|
||||
"""Test get_accessible_platform_ids returns correct list for platform admin."""
|
||||
# Create assignments for both platforms
|
||||
assignment1 = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=test_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
assignment2 = AdminPlatform(
|
||||
user_id=test_platform_admin.id,
|
||||
platform_id=another_platform.id,
|
||||
is_active=True,
|
||||
assigned_by_user_id=test_super_admin.id,
|
||||
)
|
||||
db.add_all([assignment1, assignment2])
|
||||
db.commit()
|
||||
db.refresh(test_platform_admin)
|
||||
|
||||
result = test_platform_admin.get_accessible_platform_ids()
|
||||
assert len(result) == 2
|
||||
assert test_platform.id in result
|
||||
assert another_platform.id in result
|
||||
|
||||
def test_get_accessible_platform_ids_no_assignments(self, db, test_platform_admin):
|
||||
"""Test get_accessible_platform_ids returns empty list when no assignments."""
|
||||
result = test_platform_admin.get_accessible_platform_ids()
|
||||
assert result == []
|
||||
|
||||
def test_get_accessible_platform_ids_store_user(self, db, test_store_user):
|
||||
"""Test get_accessible_platform_ids returns empty list for non-admin."""
|
||||
result = test_store_user.get_accessible_platform_ids()
|
||||
assert result == []
|
||||
@@ -1,247 +0,0 @@
|
||||
# tests/unit/models/database/test_customer.py
|
||||
"""Unit tests for Customer and CustomerAddress database models."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.customers.models.customer import Customer, CustomerAddress
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestCustomerModel:
|
||||
"""Test Customer model."""
|
||||
|
||||
def test_customer_creation(self, db, test_store):
|
||||
"""Test Customer model with store isolation."""
|
||||
customer = Customer(
|
||||
store_id=test_store.id,
|
||||
email="customer@example.com",
|
||||
hashed_password="hashed_password",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
customer_number="CUST001",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
assert customer.id is not None
|
||||
assert customer.store_id == test_store.id
|
||||
assert customer.email == "customer@example.com"
|
||||
assert customer.customer_number == "CUST001"
|
||||
assert customer.first_name == "John"
|
||||
assert customer.last_name == "Doe"
|
||||
assert customer.store.store_code == test_store.store_code
|
||||
|
||||
def test_customer_default_values(self, db, test_store):
|
||||
"""Test Customer model default values."""
|
||||
customer = Customer(
|
||||
store_id=test_store.id,
|
||||
email="defaults@example.com",
|
||||
hashed_password="hash",
|
||||
customer_number="CUST_DEFAULTS",
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
assert customer.is_active is True # Default
|
||||
assert customer.marketing_consent is False # Default
|
||||
assert customer.total_orders == 0 # Default
|
||||
assert customer.total_spent == 0 # Default
|
||||
|
||||
def test_customer_full_name_property(self, db, test_store):
|
||||
"""Test Customer full_name computed property."""
|
||||
customer = Customer(
|
||||
store_id=test_store.id,
|
||||
email="fullname@example.com",
|
||||
hashed_password="hash",
|
||||
customer_number="CUST_FULLNAME",
|
||||
first_name="Jane",
|
||||
last_name="Smith",
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
assert customer.full_name == "Jane Smith"
|
||||
|
||||
def test_customer_full_name_fallback_to_email(self, db, test_store):
|
||||
"""Test Customer full_name falls back to email when names not set."""
|
||||
customer = Customer(
|
||||
store_id=test_store.id,
|
||||
email="noname@example.com",
|
||||
hashed_password="hash",
|
||||
customer_number="CUST_NONAME",
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
assert customer.full_name == "noname@example.com"
|
||||
|
||||
def test_customer_optional_fields(self, db, test_store):
|
||||
"""Test Customer with optional fields."""
|
||||
customer = Customer(
|
||||
store_id=test_store.id,
|
||||
email="optional@example.com",
|
||||
hashed_password="hash",
|
||||
customer_number="CUST_OPT",
|
||||
phone="+352123456789",
|
||||
preferences={"language": "en", "currency": "EUR"},
|
||||
marketing_consent=True,
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
assert customer.phone == "+352123456789"
|
||||
assert customer.preferences == {"language": "en", "currency": "EUR"}
|
||||
assert customer.marketing_consent is True
|
||||
|
||||
def test_customer_store_relationship(self, db, test_store):
|
||||
"""Test Customer-Store relationship."""
|
||||
customer = Customer(
|
||||
store_id=test_store.id,
|
||||
email="relationship@example.com",
|
||||
hashed_password="hash",
|
||||
customer_number="CUST_REL",
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
assert customer.store is not None
|
||||
assert customer.store.id == test_store.id
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestCustomerAddressModel:
|
||||
"""Test CustomerAddress model."""
|
||||
|
||||
def test_customer_address_creation(self, db, test_store, test_customer):
|
||||
"""Test CustomerAddress model."""
|
||||
address = CustomerAddress(
|
||||
store_id=test_store.id,
|
||||
customer_id=test_customer.id,
|
||||
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",
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
db.add(address)
|
||||
db.commit()
|
||||
db.refresh(address)
|
||||
|
||||
assert address.id is not None
|
||||
assert address.store_id == test_store.id
|
||||
assert address.customer_id == test_customer.id
|
||||
assert address.address_type == "shipping"
|
||||
assert address.is_default is True
|
||||
|
||||
def test_customer_address_types(self, db, test_store, test_customer):
|
||||
"""Test CustomerAddress with different address types."""
|
||||
shipping_address = CustomerAddress(
|
||||
store_id=test_store.id,
|
||||
customer_id=test_customer.id,
|
||||
address_type="shipping",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Shipping St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_name="Luxembourg",
|
||||
country_iso="LU",
|
||||
)
|
||||
db.add(shipping_address)
|
||||
|
||||
billing_address = CustomerAddress(
|
||||
store_id=test_store.id,
|
||||
customer_id=test_customer.id,
|
||||
address_type="billing",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="456 Billing Ave",
|
||||
city="Luxembourg",
|
||||
postal_code="L-5678",
|
||||
country_name="Luxembourg",
|
||||
country_iso="LU",
|
||||
)
|
||||
db.add(billing_address)
|
||||
db.commit()
|
||||
|
||||
assert shipping_address.address_type == "shipping"
|
||||
assert billing_address.address_type == "billing"
|
||||
|
||||
def test_customer_address_optional_fields(self, db, test_store, test_customer):
|
||||
"""Test CustomerAddress with optional fields."""
|
||||
address = CustomerAddress(
|
||||
store_id=test_store.id,
|
||||
customer_id=test_customer.id,
|
||||
address_type="shipping",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
company="ACME Corp",
|
||||
address_line_1="123 Main St",
|
||||
address_line_2="Suite 100",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_name="Luxembourg",
|
||||
country_iso="LU",
|
||||
)
|
||||
db.add(address)
|
||||
db.commit()
|
||||
db.refresh(address)
|
||||
|
||||
assert address.company == "ACME Corp"
|
||||
assert address.address_line_2 == "Suite 100"
|
||||
|
||||
def test_customer_address_default_values(self, db, test_store, test_customer):
|
||||
"""Test CustomerAddress default values."""
|
||||
address = CustomerAddress(
|
||||
store_id=test_store.id,
|
||||
customer_id=test_customer.id,
|
||||
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",
|
||||
)
|
||||
db.add(address)
|
||||
db.commit()
|
||||
db.refresh(address)
|
||||
|
||||
assert address.is_default is False # Default
|
||||
|
||||
def test_customer_address_relationships(self, db, test_store, test_customer):
|
||||
"""Test CustomerAddress relationships."""
|
||||
address = CustomerAddress(
|
||||
store_id=test_store.id,
|
||||
customer_id=test_customer.id,
|
||||
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",
|
||||
)
|
||||
db.add(address)
|
||||
db.commit()
|
||||
db.refresh(address)
|
||||
|
||||
assert address.customer is not None
|
||||
assert address.customer.id == test_customer.id
|
||||
@@ -1,165 +0,0 @@
|
||||
# tests/unit/models/database/test_inventory.py
|
||||
"""Unit tests for Inventory database model."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.modules.inventory.models import Inventory
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestInventoryModel:
|
||||
"""Test Inventory model."""
|
||||
|
||||
def test_inventory_creation_with_product(self, db, test_store, test_product):
|
||||
"""Test Inventory model linked to product."""
|
||||
inventory = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="SA-10-01",
|
||||
location="WAREHOUSE_A",
|
||||
quantity=150,
|
||||
reserved_quantity=10,
|
||||
gtin=test_product.marketplace_product.gtin,
|
||||
)
|
||||
|
||||
db.add(inventory)
|
||||
db.commit()
|
||||
db.refresh(inventory)
|
||||
|
||||
assert inventory.id is not None
|
||||
assert inventory.product_id == test_product.id
|
||||
assert inventory.store_id == test_store.id
|
||||
assert inventory.location == "WAREHOUSE_A"
|
||||
assert inventory.bin_location == "SA-10-01"
|
||||
assert inventory.quantity == 150
|
||||
assert inventory.reserved_quantity == 10
|
||||
assert inventory.available_quantity == 140 # 150 - 10
|
||||
|
||||
def test_inventory_unique_product_location(self, db, test_store, test_product):
|
||||
"""Test unique constraint on product_id + warehouse + bin_location."""
|
||||
inventory1 = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="SA-10-01",
|
||||
location="WAREHOUSE_A",
|
||||
quantity=100,
|
||||
)
|
||||
db.add(inventory1)
|
||||
db.commit()
|
||||
|
||||
# Same product + warehouse + bin_location should fail
|
||||
with pytest.raises(IntegrityError):
|
||||
inventory2 = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="SA-10-01",
|
||||
location="WAREHOUSE_A",
|
||||
quantity=50,
|
||||
)
|
||||
db.add(inventory2)
|
||||
db.commit()
|
||||
|
||||
def test_inventory_same_product_different_location(
|
||||
self, db, test_store, test_product
|
||||
):
|
||||
"""Test same product can have inventory in different locations."""
|
||||
inventory1 = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="SA-10-01",
|
||||
location="WAREHOUSE_A",
|
||||
quantity=100,
|
||||
)
|
||||
db.add(inventory1)
|
||||
db.commit()
|
||||
|
||||
# Same product in different bin_location should succeed
|
||||
inventory2 = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="SA-10-02",
|
||||
location="WAREHOUSE_B",
|
||||
quantity=50,
|
||||
)
|
||||
db.add(inventory2)
|
||||
db.commit()
|
||||
db.refresh(inventory2)
|
||||
|
||||
assert inventory2.id is not None
|
||||
assert inventory2.bin_location == "SA-10-02"
|
||||
|
||||
def test_inventory_default_values(self, db, test_store, test_product):
|
||||
"""Test Inventory model default values."""
|
||||
inventory = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="DEF-01-01",
|
||||
location="DEFAULT_LOC",
|
||||
quantity=100,
|
||||
)
|
||||
db.add(inventory)
|
||||
db.commit()
|
||||
db.refresh(inventory)
|
||||
|
||||
assert inventory.reserved_quantity == 0 # Default
|
||||
assert inventory.available_quantity == 100 # quantity - reserved
|
||||
|
||||
def test_inventory_available_quantity_property(self, db, test_store, test_product):
|
||||
"""Test available_quantity computed property."""
|
||||
inventory = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="PROP-01-01",
|
||||
location="PROP_TEST",
|
||||
quantity=200,
|
||||
reserved_quantity=50,
|
||||
)
|
||||
db.add(inventory)
|
||||
db.commit()
|
||||
db.refresh(inventory)
|
||||
|
||||
assert inventory.available_quantity == 150 # 200 - 50
|
||||
|
||||
def test_inventory_relationships(self, db, test_store, test_product):
|
||||
"""Test Inventory relationships."""
|
||||
inventory = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="REL-01-01",
|
||||
location="REL_TEST",
|
||||
quantity=100,
|
||||
)
|
||||
db.add(inventory)
|
||||
db.commit()
|
||||
db.refresh(inventory)
|
||||
|
||||
assert inventory.product is not None
|
||||
assert inventory.store is not None
|
||||
assert inventory.product.id == test_product.id
|
||||
assert inventory.store.id == test_store.id
|
||||
|
||||
def test_inventory_without_gtin(self, db, test_store, test_product):
|
||||
"""Test Inventory can be created without GTIN."""
|
||||
inventory = Inventory(
|
||||
product_id=test_product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="NOGTIN-01-01",
|
||||
location="NO_GTIN",
|
||||
quantity=100,
|
||||
)
|
||||
db.add(inventory)
|
||||
db.commit()
|
||||
db.refresh(inventory)
|
||||
|
||||
assert inventory.gtin is None
|
||||
@@ -1,136 +0,0 @@
|
||||
# tests/unit/models/database/test_marketplace_import_job.py
|
||||
"""Unit tests for MarketplaceImportJob database model."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.marketplace.models import MarketplaceImportJob
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestMarketplaceImportJobModel:
|
||||
"""Test MarketplaceImportJob model."""
|
||||
|
||||
def test_import_job_creation(self, db, test_user, test_store):
|
||||
"""Test MarketplaceImportJob model with relationships."""
|
||||
import_job = MarketplaceImportJob(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
marketplace="Letzshop",
|
||||
source_url="https://example.com/feed.csv",
|
||||
status="pending",
|
||||
imported_count=0,
|
||||
updated_count=0,
|
||||
error_count=0,
|
||||
total_processed=0,
|
||||
)
|
||||
|
||||
db.add(import_job)
|
||||
db.commit()
|
||||
db.refresh(import_job)
|
||||
|
||||
assert import_job.id is not None
|
||||
assert import_job.store_id == test_store.id
|
||||
assert import_job.user_id == test_user.id
|
||||
assert import_job.marketplace == "Letzshop"
|
||||
assert import_job.source_url == "https://example.com/feed.csv"
|
||||
assert import_job.status == "pending"
|
||||
assert import_job.store.store_code == test_store.store_code
|
||||
assert import_job.user.username == test_user.username
|
||||
|
||||
def test_import_job_default_values(self, db, test_user, test_store):
|
||||
"""Test MarketplaceImportJob default values."""
|
||||
import_job = MarketplaceImportJob(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
source_url="https://example.com/feed.csv",
|
||||
)
|
||||
|
||||
db.add(import_job)
|
||||
db.commit()
|
||||
db.refresh(import_job)
|
||||
|
||||
assert import_job.marketplace == "Letzshop" # Default
|
||||
assert import_job.status == "pending" # Default
|
||||
assert import_job.imported_count == 0 # Default
|
||||
assert import_job.updated_count == 0 # Default
|
||||
assert import_job.error_count == 0 # Default
|
||||
assert import_job.total_processed == 0 # Default
|
||||
|
||||
def test_import_job_status_values(self, db, test_user, test_store):
|
||||
"""Test MarketplaceImportJob with different status values."""
|
||||
statuses = [
|
||||
"pending",
|
||||
"processing",
|
||||
"completed",
|
||||
"failed",
|
||||
"completed_with_errors",
|
||||
]
|
||||
|
||||
for i, status in enumerate(statuses):
|
||||
import_job = MarketplaceImportJob(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
source_url=f"https://example.com/feed_{i}.csv",
|
||||
status=status,
|
||||
)
|
||||
db.add(import_job)
|
||||
db.commit()
|
||||
db.refresh(import_job)
|
||||
|
||||
assert import_job.status == status
|
||||
|
||||
def test_import_job_counts(self, db, test_user, test_store):
|
||||
"""Test MarketplaceImportJob count fields."""
|
||||
import_job = MarketplaceImportJob(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
source_url="https://example.com/feed.csv",
|
||||
status="completed",
|
||||
imported_count=100,
|
||||
updated_count=50,
|
||||
error_count=5,
|
||||
total_processed=155,
|
||||
)
|
||||
|
||||
db.add(import_job)
|
||||
db.commit()
|
||||
db.refresh(import_job)
|
||||
|
||||
assert import_job.imported_count == 100
|
||||
assert import_job.updated_count == 50
|
||||
assert import_job.error_count == 5
|
||||
assert import_job.total_processed == 155
|
||||
|
||||
def test_import_job_error_message(self, db, test_user, test_store):
|
||||
"""Test MarketplaceImportJob with error message."""
|
||||
import_job = MarketplaceImportJob(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
source_url="https://example.com/feed.csv",
|
||||
status="failed",
|
||||
error_message="Connection timeout while fetching CSV",
|
||||
)
|
||||
|
||||
db.add(import_job)
|
||||
db.commit()
|
||||
db.refresh(import_job)
|
||||
|
||||
assert import_job.error_message == "Connection timeout while fetching CSV"
|
||||
|
||||
def test_import_job_relationships(self, db, test_user, test_store):
|
||||
"""Test MarketplaceImportJob relationships."""
|
||||
import_job = MarketplaceImportJob(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
source_url="https://example.com/feed.csv",
|
||||
)
|
||||
|
||||
db.add(import_job)
|
||||
db.commit()
|
||||
db.refresh(import_job)
|
||||
|
||||
assert import_job.store is not None
|
||||
assert import_job.user is not None
|
||||
assert import_job.store.id == test_store.id
|
||||
assert import_job.user.id == test_user.id
|
||||
@@ -1,244 +0,0 @@
|
||||
# tests/unit/models/database/test_marketplace_product.py
|
||||
"""Unit tests for MarketplaceProduct database model."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.modules.marketplace.models import (
|
||||
MarketplaceProduct,
|
||||
MarketplaceProductTranslation,
|
||||
)
|
||||
|
||||
|
||||
def _create_with_translation(db, marketplace_product_id, title, **kwargs):
|
||||
"""Helper to create MarketplaceProduct with translation."""
|
||||
mp = MarketplaceProduct(marketplace_product_id=marketplace_product_id, **kwargs)
|
||||
db.add(mp)
|
||||
db.flush()
|
||||
|
||||
translation = MarketplaceProductTranslation(
|
||||
marketplace_product_id=mp.id,
|
||||
language="en",
|
||||
title=title,
|
||||
)
|
||||
db.add(translation)
|
||||
db.commit()
|
||||
db.refresh(mp)
|
||||
return mp
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestMarketplaceProductModel:
|
||||
"""Test MarketplaceProduct model."""
|
||||
|
||||
def test_marketplace_product_creation(self, db):
|
||||
"""Test MarketplaceProduct model creation."""
|
||||
marketplace_product = _create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="DB_TEST_001",
|
||||
title="Database Test Product",
|
||||
price="25.99",
|
||||
currency="USD",
|
||||
brand="DBTest",
|
||||
gtin="1234567890123",
|
||||
availability="in stock",
|
||||
marketplace="Letzshop",
|
||||
store_name="Test Store",
|
||||
)
|
||||
|
||||
assert marketplace_product.id is not None
|
||||
assert marketplace_product.marketplace_product_id == "DB_TEST_001"
|
||||
assert marketplace_product.get_title("en") == "Database Test Product"
|
||||
assert marketplace_product.marketplace == "Letzshop"
|
||||
assert marketplace_product.created_at is not None
|
||||
|
||||
def test_marketplace_product_id_uniqueness(self, db):
|
||||
"""Test unique marketplace_product_id constraint."""
|
||||
_create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="UNIQUE_001",
|
||||
title="Product 1",
|
||||
marketplace="Letzshop",
|
||||
)
|
||||
|
||||
# Duplicate marketplace_product_id should raise error
|
||||
with pytest.raises(IntegrityError):
|
||||
_create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="UNIQUE_001",
|
||||
title="Product 2",
|
||||
marketplace="Letzshop",
|
||||
)
|
||||
|
||||
def test_marketplace_product_all_fields(self, db):
|
||||
"""Test MarketplaceProduct with all optional fields."""
|
||||
marketplace_product = _create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="FULL_001",
|
||||
title="Full Product",
|
||||
link="https://example.com/product",
|
||||
image_link="https://example.com/image.jpg",
|
||||
availability="in stock",
|
||||
price="99.99",
|
||||
brand="TestBrand",
|
||||
gtin="9876543210123",
|
||||
mpn="MPN123",
|
||||
condition="new",
|
||||
adult="no",
|
||||
age_group="adult",
|
||||
color="blue",
|
||||
gender="unisex",
|
||||
material="cotton",
|
||||
pattern="solid",
|
||||
size="M",
|
||||
google_product_category="Apparel & Accessories",
|
||||
product_type_raw="Clothing",
|
||||
currency="EUR",
|
||||
marketplace="Letzshop",
|
||||
store_name="Full Store",
|
||||
)
|
||||
|
||||
assert marketplace_product.brand == "TestBrand"
|
||||
assert marketplace_product.gtin == "9876543210123"
|
||||
assert marketplace_product.color == "blue"
|
||||
assert marketplace_product.size == "M"
|
||||
|
||||
def test_marketplace_product_custom_labels(self, db):
|
||||
"""Test MarketplaceProduct with custom labels."""
|
||||
marketplace_product = _create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="LABELS_001",
|
||||
title="Labeled Product",
|
||||
marketplace="Letzshop",
|
||||
custom_label_0="Label0",
|
||||
custom_label_1="Label1",
|
||||
custom_label_2="Label2",
|
||||
custom_label_3="Label3",
|
||||
custom_label_4="Label4",
|
||||
)
|
||||
|
||||
assert marketplace_product.custom_label_0 == "Label0"
|
||||
assert marketplace_product.custom_label_4 == "Label4"
|
||||
|
||||
def test_marketplace_product_minimal_fields(self, db):
|
||||
"""Test MarketplaceProduct with only required fields."""
|
||||
marketplace_product = _create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="MINIMAL_001",
|
||||
title="Minimal Product",
|
||||
)
|
||||
|
||||
assert marketplace_product.id is not None
|
||||
assert marketplace_product.marketplace_product_id == "MINIMAL_001"
|
||||
assert marketplace_product.get_title("en") == "Minimal Product"
|
||||
assert marketplace_product.get_description("en") is None
|
||||
assert marketplace_product.price is None
|
||||
|
||||
def test_marketplace_product_digital_fields(self, db):
|
||||
"""Test MarketplaceProduct with digital product fields."""
|
||||
marketplace_product = _create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="DIGITAL_001",
|
||||
title="Digital Product",
|
||||
product_type_enum="digital",
|
||||
is_digital=True,
|
||||
digital_delivery_method="license_key",
|
||||
platform="steam",
|
||||
region_restrictions=["EU", "US"],
|
||||
license_type="single_use",
|
||||
)
|
||||
|
||||
assert marketplace_product.product_type_enum == "digital"
|
||||
assert marketplace_product.is_digital is True
|
||||
assert marketplace_product.digital_delivery_method == "license_key"
|
||||
assert marketplace_product.platform == "steam"
|
||||
assert marketplace_product.region_restrictions == ["EU", "US"]
|
||||
assert marketplace_product.license_type == "single_use"
|
||||
|
||||
def test_marketplace_product_translation_methods(self, db):
|
||||
"""Test translation helper methods."""
|
||||
mp = MarketplaceProduct(marketplace_product_id="TRANS_001")
|
||||
db.add(mp)
|
||||
db.flush()
|
||||
|
||||
# Add English translation
|
||||
en_trans = MarketplaceProductTranslation(
|
||||
marketplace_product_id=mp.id,
|
||||
language="en",
|
||||
title="English Title",
|
||||
description="English Description",
|
||||
)
|
||||
db.add(en_trans)
|
||||
|
||||
# Add French translation
|
||||
fr_trans = MarketplaceProductTranslation(
|
||||
marketplace_product_id=mp.id,
|
||||
language="fr",
|
||||
title="Titre Français",
|
||||
description="Description Française",
|
||||
)
|
||||
db.add(fr_trans)
|
||||
db.commit()
|
||||
db.refresh(mp)
|
||||
|
||||
assert mp.get_title("en") == "English Title"
|
||||
assert mp.get_title("fr") == "Titre Français"
|
||||
assert mp.get_description("en") == "English Description"
|
||||
assert mp.get_description("fr") == "Description Française"
|
||||
|
||||
# Test fallback to English for unknown language
|
||||
assert mp.get_title("de") == "English Title" # Falls back to 'en'
|
||||
assert mp.get_description("de") == "English Description"
|
||||
|
||||
def test_marketplace_product_numeric_prices(self, db):
|
||||
"""Test numeric price fields."""
|
||||
marketplace_product = _create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="PRICES_001",
|
||||
title="Priced Product",
|
||||
price="99.99 EUR",
|
||||
price_numeric=99.99,
|
||||
sale_price="79.99 EUR",
|
||||
sale_price_numeric=79.99,
|
||||
)
|
||||
|
||||
assert marketplace_product.price == "99.99 EUR"
|
||||
assert marketplace_product.price_numeric == 99.99
|
||||
assert marketplace_product.sale_price_numeric == 79.99
|
||||
assert marketplace_product.effective_price == 99.99
|
||||
assert marketplace_product.effective_sale_price == 79.99
|
||||
|
||||
def test_marketplace_product_attributes_json(self, db):
|
||||
"""Test flexible attributes JSON field."""
|
||||
marketplace_product = _create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="ATTRS_001",
|
||||
title="Product with Attributes",
|
||||
attributes={
|
||||
"custom_field": "custom_value",
|
||||
"nested": {"key": "value"},
|
||||
},
|
||||
)
|
||||
|
||||
assert marketplace_product.attributes["custom_field"] == "custom_value"
|
||||
assert marketplace_product.attributes["nested"]["key"] == "value"
|
||||
|
||||
def test_marketplace_product_all_images_property(self, db):
|
||||
"""Test all_images property."""
|
||||
marketplace_product = _create_with_translation(
|
||||
db,
|
||||
marketplace_product_id="IMAGES_001",
|
||||
title="Product with Images",
|
||||
image_link="https://example.com/main.jpg",
|
||||
additional_images=[
|
||||
"https://example.com/img1.jpg",
|
||||
"https://example.com/img2.jpg",
|
||||
],
|
||||
)
|
||||
|
||||
images = marketplace_product.all_images
|
||||
assert len(images) == 3
|
||||
assert images[0] == "https://example.com/main.jpg"
|
||||
assert "https://example.com/img1.jpg" in images
|
||||
assert "https://example.com/img2.jpg" in images
|
||||
@@ -1,256 +0,0 @@
|
||||
# tests/unit/models/database/test_order.py
|
||||
"""Unit tests for Order and OrderItem database models."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.modules.orders.models import Order, OrderItem
|
||||
|
||||
|
||||
def create_order_with_snapshots(
|
||||
db,
|
||||
store,
|
||||
customer,
|
||||
customer_address,
|
||||
order_number,
|
||||
status="pending",
|
||||
subtotal=99.99,
|
||||
total_amount=99.99,
|
||||
**kwargs
|
||||
):
|
||||
"""Helper to create Order with required address snapshots."""
|
||||
# Remove channel from kwargs if present (we set it explicitly)
|
||||
channel = kwargs.pop("channel", "direct")
|
||||
|
||||
order = Order(
|
||||
store_id=store.id,
|
||||
customer_id=customer.id,
|
||||
order_number=order_number,
|
||||
status=status,
|
||||
channel=channel,
|
||||
subtotal=subtotal,
|
||||
total_amount=total_amount,
|
||||
currency="EUR",
|
||||
order_date=datetime.now(timezone.utc),
|
||||
# Customer snapshot
|
||||
customer_first_name=customer.first_name,
|
||||
customer_last_name=customer.last_name,
|
||||
customer_email=customer.email,
|
||||
# Shipping address snapshot
|
||||
ship_first_name=customer_address.first_name,
|
||||
ship_last_name=customer_address.last_name,
|
||||
ship_address_line_1=customer_address.address_line_1,
|
||||
ship_city=customer_address.city,
|
||||
ship_postal_code=customer_address.postal_code,
|
||||
ship_country_iso="LU",
|
||||
# Billing address snapshot
|
||||
bill_first_name=customer_address.first_name,
|
||||
bill_last_name=customer_address.last_name,
|
||||
bill_address_line_1=customer_address.address_line_1,
|
||||
bill_city=customer_address.city,
|
||||
bill_postal_code=customer_address.postal_code,
|
||||
bill_country_iso="LU",
|
||||
**kwargs
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
return order
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestOrderModel:
|
||||
"""Test Order model."""
|
||||
|
||||
def test_order_creation(
|
||||
self, db, test_store, test_customer, test_customer_address
|
||||
):
|
||||
"""Test Order model with customer relationship."""
|
||||
order = create_order_with_snapshots(
|
||||
db, test_store, test_customer, test_customer_address,
|
||||
order_number="ORD-001",
|
||||
)
|
||||
|
||||
assert order.id is not None
|
||||
assert order.store_id == test_store.id
|
||||
assert order.customer_id == test_customer.id
|
||||
assert order.order_number == "ORD-001"
|
||||
assert order.status == "pending"
|
||||
assert float(order.total_amount) == 99.99
|
||||
# Verify snapshots
|
||||
assert order.customer_first_name == test_customer.first_name
|
||||
assert order.ship_city == test_customer_address.city
|
||||
assert order.ship_country_iso == "LU"
|
||||
|
||||
def test_order_number_uniqueness(
|
||||
self, db, test_store, test_customer, test_customer_address
|
||||
):
|
||||
"""Test order_number unique constraint."""
|
||||
create_order_with_snapshots(
|
||||
db, test_store, test_customer, test_customer_address,
|
||||
order_number="UNIQUE-ORD-001",
|
||||
)
|
||||
|
||||
# Duplicate order number should fail
|
||||
with pytest.raises(IntegrityError):
|
||||
create_order_with_snapshots(
|
||||
db, test_store, test_customer, test_customer_address,
|
||||
order_number="UNIQUE-ORD-001",
|
||||
)
|
||||
|
||||
def test_order_status_values(
|
||||
self, db, test_store, test_customer, test_customer_address
|
||||
):
|
||||
"""Test Order with different status values."""
|
||||
statuses = [
|
||||
"pending",
|
||||
"processing",
|
||||
"shipped",
|
||||
"delivered",
|
||||
"cancelled",
|
||||
"refunded",
|
||||
]
|
||||
|
||||
for i, status in enumerate(statuses):
|
||||
order = create_order_with_snapshots(
|
||||
db, test_store, test_customer, test_customer_address,
|
||||
order_number=f"STATUS-ORD-{i:03d}",
|
||||
status=status,
|
||||
)
|
||||
assert order.status == status
|
||||
|
||||
def test_order_amounts(self, db, test_store, test_customer, test_customer_address):
|
||||
"""Test Order amount fields."""
|
||||
order = create_order_with_snapshots(
|
||||
db, test_store, test_customer, test_customer_address,
|
||||
order_number="AMOUNTS-ORD-001",
|
||||
subtotal=100.00,
|
||||
tax_amount=20.00,
|
||||
shipping_amount=10.00,
|
||||
discount_amount=5.00,
|
||||
total_amount=125.00,
|
||||
)
|
||||
|
||||
assert float(order.subtotal) == 100.00
|
||||
assert float(order.tax_amount) == 20.00
|
||||
assert float(order.shipping_amount) == 10.00
|
||||
assert float(order.discount_amount) == 5.00
|
||||
assert float(order.total_amount) == 125.00
|
||||
|
||||
def test_order_relationships(
|
||||
self, db, test_store, test_customer, test_customer_address
|
||||
):
|
||||
"""Test Order relationships."""
|
||||
order = create_order_with_snapshots(
|
||||
db, test_store, test_customer, test_customer_address,
|
||||
order_number="REL-ORD-001",
|
||||
)
|
||||
|
||||
assert order.store is not None
|
||||
assert order.customer is not None
|
||||
assert order.store.id == test_store.id
|
||||
assert order.customer.id == test_customer.id
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestOrderItemModel:
|
||||
"""Test OrderItem model."""
|
||||
|
||||
def test_order_item_creation(self, db, test_order, test_product):
|
||||
"""Test OrderItem model."""
|
||||
# Get title from translation
|
||||
product_title = test_product.marketplace_product.get_title("en")
|
||||
|
||||
order_item = OrderItem(
|
||||
order_id=test_order.id,
|
||||
product_id=test_product.id,
|
||||
product_name=product_title,
|
||||
product_sku=test_product.store_sku or "SKU001",
|
||||
quantity=2,
|
||||
unit_price=49.99,
|
||||
total_price=99.98,
|
||||
)
|
||||
|
||||
db.add(order_item)
|
||||
db.commit()
|
||||
db.refresh(order_item)
|
||||
|
||||
assert order_item.id is not None
|
||||
assert order_item.order_id == test_order.id
|
||||
assert order_item.product_id == test_product.id
|
||||
assert order_item.quantity == 2
|
||||
assert float(order_item.unit_price) == 49.99
|
||||
assert float(order_item.total_price) == 99.98
|
||||
|
||||
def test_order_item_stores_product_snapshot(self, db, test_order, test_product):
|
||||
"""Test OrderItem stores product name and SKU as snapshot."""
|
||||
order_item = OrderItem(
|
||||
order_id=test_order.id,
|
||||
product_id=test_product.id,
|
||||
product_name="Snapshot Product Name",
|
||||
product_sku="SNAPSHOT-SKU-001",
|
||||
quantity=1,
|
||||
unit_price=25.00,
|
||||
total_price=25.00,
|
||||
)
|
||||
|
||||
db.add(order_item)
|
||||
db.commit()
|
||||
db.refresh(order_item)
|
||||
|
||||
assert order_item.id is not None
|
||||
assert order_item.product_name == "Snapshot Product Name"
|
||||
assert order_item.product_sku == "SNAPSHOT-SKU-001"
|
||||
|
||||
def test_order_item_relationships(self, db, test_order, test_product):
|
||||
"""Test OrderItem relationships."""
|
||||
order_item = OrderItem(
|
||||
order_id=test_order.id,
|
||||
product_id=test_product.id,
|
||||
product_name="Test Product",
|
||||
product_sku="SKU001",
|
||||
quantity=1,
|
||||
unit_price=50.00,
|
||||
total_price=50.00,
|
||||
)
|
||||
|
||||
db.add(order_item)
|
||||
db.commit()
|
||||
db.refresh(order_item)
|
||||
|
||||
assert order_item.order is not None
|
||||
assert order_item.order.id == test_order.id
|
||||
|
||||
def test_multiple_items_per_order(self, db, test_order, test_product):
|
||||
"""Test multiple OrderItems can belong to same Order."""
|
||||
# Create two order items for the same product (different quantities)
|
||||
item1 = OrderItem(
|
||||
order_id=test_order.id,
|
||||
product_id=test_product.id,
|
||||
product_name="Product - Size M",
|
||||
product_sku="SKU001-M",
|
||||
quantity=1,
|
||||
unit_price=25.00,
|
||||
total_price=25.00,
|
||||
)
|
||||
item2 = OrderItem(
|
||||
order_id=test_order.id,
|
||||
product_id=test_product.id,
|
||||
product_name="Product - Size L",
|
||||
product_sku="SKU001-L",
|
||||
quantity=2,
|
||||
unit_price=30.00,
|
||||
total_price=60.00,
|
||||
)
|
||||
|
||||
db.add_all([item1, item2])
|
||||
db.commit()
|
||||
|
||||
assert item1.order_id == item2.order_id
|
||||
assert item1.id != item2.id
|
||||
assert item1.product_id == item2.product_id # Same product, different items
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
# tests/unit/models/database/test_order_item_exception.py
|
||||
"""Unit tests for OrderItemException database model."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.modules.orders.models import OrderItemException
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestOrderItemExceptionModel:
|
||||
"""Test OrderItemException model."""
|
||||
|
||||
def test_exception_creation(self, db, test_order_item, test_store):
|
||||
"""Test OrderItemException model creation."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin="4006381333931",
|
||||
original_product_name="Test Missing Product",
|
||||
original_sku="MISSING-SKU-001",
|
||||
exception_type="product_not_found",
|
||||
status="pending",
|
||||
)
|
||||
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.id is not None
|
||||
assert exception.order_item_id == test_order_item.id
|
||||
assert exception.store_id == test_store.id
|
||||
assert exception.original_gtin == "4006381333931"
|
||||
assert exception.original_product_name == "Test Missing Product"
|
||||
assert exception.original_sku == "MISSING-SKU-001"
|
||||
assert exception.exception_type == "product_not_found"
|
||||
assert exception.status == "pending"
|
||||
assert exception.created_at is not None
|
||||
|
||||
def test_exception_unique_order_item(self, db, test_order_item, test_store):
|
||||
"""Test that only one exception can exist per order item."""
|
||||
exception1 = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception1)
|
||||
db.commit()
|
||||
|
||||
# Second exception for the same order item should fail
|
||||
with pytest.raises(IntegrityError):
|
||||
exception2 = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin="4006381333932",
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception2)
|
||||
db.commit()
|
||||
|
||||
def test_exception_types(self, db, test_order_item, test_store):
|
||||
"""Test different exception types."""
|
||||
exception_types = ["product_not_found", "gtin_mismatch", "duplicate_gtin"]
|
||||
|
||||
# Create new order items for each test to avoid unique constraint
|
||||
from app.modules.orders.models import OrderItem
|
||||
|
||||
for i, exc_type in enumerate(exception_types):
|
||||
order_item = OrderItem(
|
||||
order_id=test_order_item.order_id,
|
||||
product_id=test_order_item.product_id,
|
||||
product_name=f"Product {i}",
|
||||
product_sku=f"SKU-{i}",
|
||||
quantity=1,
|
||||
unit_price=10.00,
|
||||
total_price=10.00,
|
||||
)
|
||||
db.add(order_item)
|
||||
db.commit()
|
||||
db.refresh(order_item)
|
||||
|
||||
exception = OrderItemException(
|
||||
order_item_id=order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin=f"400638133393{i}",
|
||||
exception_type=exc_type,
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.exception_type == exc_type
|
||||
|
||||
def test_exception_status_values(self, db, test_order_item, test_store):
|
||||
"""Test different status values."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
status="pending",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
|
||||
# Test pending status
|
||||
assert exception.status == "pending"
|
||||
assert exception.is_pending is True
|
||||
assert exception.is_resolved is False
|
||||
assert exception.is_ignored is False
|
||||
assert exception.blocks_confirmation is True
|
||||
|
||||
# Test resolved status
|
||||
exception.status = "resolved"
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.is_pending is False
|
||||
assert exception.is_resolved is True
|
||||
assert exception.is_ignored is False
|
||||
assert exception.blocks_confirmation is False
|
||||
|
||||
# Test ignored status
|
||||
exception.status = "ignored"
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.is_pending is False
|
||||
assert exception.is_resolved is False
|
||||
assert exception.is_ignored is True
|
||||
assert exception.blocks_confirmation is True # Ignored still blocks
|
||||
|
||||
def test_exception_nullable_fields(self, db, test_order_item, test_store):
|
||||
"""Test that GTIN and other fields can be null."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin=None, # Can be null for vouchers etc.
|
||||
original_product_name="Gift Voucher",
|
||||
original_sku=None,
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.id is not None
|
||||
assert exception.original_gtin is None
|
||||
assert exception.original_sku is None
|
||||
assert exception.original_product_name == "Gift Voucher"
|
||||
|
||||
def test_exception_resolution(self, db, test_order_item, test_store, test_product, test_user):
|
||||
"""Test resolving an exception with a product."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
status="pending",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
|
||||
# Resolve the exception
|
||||
now = datetime.now(timezone.utc)
|
||||
exception.status = "resolved"
|
||||
exception.resolved_product_id = test_product.id
|
||||
exception.resolved_at = now
|
||||
exception.resolved_by = test_user.id
|
||||
exception.resolution_notes = "Matched to existing product"
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.status == "resolved"
|
||||
assert exception.resolved_product_id == test_product.id
|
||||
assert exception.resolved_at is not None
|
||||
assert exception.resolved_by == test_user.id
|
||||
assert exception.resolution_notes == "Matched to existing product"
|
||||
|
||||
def test_exception_relationships(self, db, test_order_item, test_store):
|
||||
"""Test OrderItemException relationships."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
assert exception.order_item is not None
|
||||
assert exception.order_item.id == test_order_item.id
|
||||
assert exception.store is not None
|
||||
assert exception.store.id == test_store.id
|
||||
|
||||
def test_exception_repr(self, db, test_order_item, test_store):
|
||||
"""Test OrderItemException __repr__ method."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
status="pending",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
db.refresh(exception)
|
||||
|
||||
repr_str = repr(exception)
|
||||
assert "OrderItemException" in repr_str
|
||||
assert str(exception.id) in repr_str
|
||||
assert "4006381333931" in repr_str
|
||||
assert "pending" in repr_str
|
||||
|
||||
def test_exception_cascade_delete(self, db, test_order_item, test_store):
|
||||
"""Test that exception is deleted when order item is deleted."""
|
||||
exception = OrderItemException(
|
||||
order_item_id=test_order_item.id,
|
||||
store_id=test_store.id,
|
||||
original_gtin="4006381333931",
|
||||
exception_type="product_not_found",
|
||||
)
|
||||
db.add(exception)
|
||||
db.commit()
|
||||
exception_id = exception.id
|
||||
|
||||
# Delete the order item
|
||||
db.delete(test_order_item)
|
||||
db.commit()
|
||||
|
||||
# Exception should be cascade deleted
|
||||
deleted_exception = (
|
||||
db.query(OrderItemException)
|
||||
.filter(OrderItemException.id == exception_id)
|
||||
.first()
|
||||
)
|
||||
assert deleted_exception is None
|
||||
@@ -1,402 +0,0 @@
|
||||
# tests/unit/models/database/test_product.py
|
||||
"""Unit tests for Product (store catalog) database model."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.modules.catalog.models import Product
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestProductModel:
|
||||
"""Test Product (store catalog) model."""
|
||||
|
||||
def test_product_creation(self, db, test_store, test_marketplace_product):
|
||||
"""Test Product model linking store catalog to marketplace product."""
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
store_sku="STORE_PROD_001",
|
||||
price=89.99,
|
||||
currency="EUR",
|
||||
availability="in stock",
|
||||
is_featured=True,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.id is not None
|
||||
assert product.store_id == test_store.id
|
||||
assert product.marketplace_product_id == test_marketplace_product.id
|
||||
assert product.price == 89.99
|
||||
assert product.is_featured is True
|
||||
assert product.store.store_code == test_store.store_code
|
||||
# Use get_title() method instead of .title attribute
|
||||
assert product.marketplace_product.get_title(
|
||||
"en"
|
||||
) == test_marketplace_product.get_title("en")
|
||||
|
||||
def test_product_unique_per_store(self, db, test_store, test_marketplace_product):
|
||||
"""Test that same marketplace product can't be added twice to store catalog."""
|
||||
product1 = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(product1)
|
||||
db.commit()
|
||||
|
||||
# Same marketplace product to same store should fail
|
||||
with pytest.raises(IntegrityError):
|
||||
product2 = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(product2)
|
||||
db.commit()
|
||||
|
||||
def test_product_default_values(self, db, test_store, test_marketplace_product):
|
||||
"""Test Product model default values."""
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.is_active is True # Default
|
||||
assert product.is_featured is False # Default
|
||||
assert product.is_digital is False # Default
|
||||
assert product.product_type == "physical" # Default
|
||||
assert product.min_quantity == 1 # Default
|
||||
assert product.display_order == 0 # Default
|
||||
|
||||
def test_product_store_override_fields(
|
||||
self, db, test_store, test_marketplace_product
|
||||
):
|
||||
"""Test Product model store-specific override fields."""
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
store_sku="CUSTOM_SKU_001",
|
||||
price=49.99,
|
||||
sale_price=39.99,
|
||||
currency="USD",
|
||||
availability="limited",
|
||||
condition="new",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.store_sku == "CUSTOM_SKU_001"
|
||||
assert product.price == 49.99
|
||||
assert product.sale_price == 39.99
|
||||
assert product.currency == "USD"
|
||||
assert product.availability == "limited"
|
||||
|
||||
def test_product_inventory_settings(
|
||||
self, db, test_store, test_marketplace_product
|
||||
):
|
||||
"""Test Product model inventory settings."""
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
min_quantity=2,
|
||||
max_quantity=10,
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.min_quantity == 2
|
||||
assert product.max_quantity == 10
|
||||
|
||||
def test_product_relationships(self, db, test_store, test_marketplace_product):
|
||||
"""Test Product relationships."""
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.store is not None
|
||||
assert product.marketplace_product is not None
|
||||
assert product.inventory_entries == [] # No inventory yet
|
||||
|
||||
def test_product_get_source_comparison_info(
|
||||
self, db, test_store, test_marketplace_product
|
||||
):
|
||||
"""Test get_source_comparison_info method for 'view original source' feature.
|
||||
|
||||
Products are independent entities with all fields populated at creation.
|
||||
Source values are kept for comparison only, not inheritance.
|
||||
"""
|
||||
# Set up marketplace product values
|
||||
test_marketplace_product.price_cents = 10000 # €100.00
|
||||
test_marketplace_product.brand = "SourceBrand"
|
||||
db.commit()
|
||||
|
||||
# Create product with its own values (independent copy pattern)
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
price_cents=8999, # €89.99 - store's price
|
||||
brand="StoreBrand", # Store's brand
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
info = product.get_source_comparison_info()
|
||||
|
||||
# Product has its own price
|
||||
assert info["price"] == 89.99
|
||||
assert info["price_cents"] == 8999
|
||||
assert info["price_source"] == 100.00 # Original marketplace price
|
||||
|
||||
# Product has its own brand
|
||||
assert info["brand"] == "StoreBrand"
|
||||
assert info["brand_source"] == "SourceBrand" # Original marketplace brand
|
||||
|
||||
# No more *_overridden keys in the pattern
|
||||
assert "price_overridden" not in info
|
||||
assert "brand_overridden" not in info
|
||||
|
||||
def test_product_fields_are_independent(
|
||||
self, db, test_store, test_marketplace_product
|
||||
):
|
||||
"""Test that product fields don't inherit from marketplace product.
|
||||
|
||||
Products are independent entities - NULL fields stay NULL,
|
||||
no inheritance/fallback logic.
|
||||
"""
|
||||
# Set up marketplace product values
|
||||
test_marketplace_product.price_cents = 10000
|
||||
test_marketplace_product.brand = "SourceBrand"
|
||||
db.commit()
|
||||
|
||||
# Create product without copying values
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
# Not copying price_cents or brand
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
# Fields should be NULL (not inherited)
|
||||
assert product.price_cents is None
|
||||
assert product.price is None
|
||||
assert product.brand is None
|
||||
|
||||
# But we can still see the source values for comparison
|
||||
info = product.get_source_comparison_info()
|
||||
assert info["price_source"] == 100.00
|
||||
assert info["brand_source"] == "SourceBrand"
|
||||
|
||||
def test_product_direct_creation_without_marketplace(self, db, test_store):
|
||||
"""Test creating a product directly without a marketplace source.
|
||||
|
||||
Products can be created directly without a marketplace_product_id,
|
||||
making them fully independent store products.
|
||||
"""
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
marketplace_product_id=None, # No marketplace source
|
||||
store_sku="DIRECT_001",
|
||||
brand="DirectBrand",
|
||||
price=59.99,
|
||||
currency="EUR",
|
||||
is_digital=True,
|
||||
product_type="digital",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.id is not None
|
||||
assert product.marketplace_product_id is None
|
||||
assert product.marketplace_product is None
|
||||
assert product.store_sku == "DIRECT_001"
|
||||
assert product.brand == "DirectBrand"
|
||||
assert product.is_digital is True
|
||||
assert product.product_type == "digital"
|
||||
|
||||
def test_product_is_digital_column(self, db, test_store):
|
||||
"""Test is_digital is an independent column, not derived from marketplace."""
|
||||
# Create digital product without marketplace source
|
||||
digital_product = Product(
|
||||
store_id=test_store.id,
|
||||
store_sku="DIGITAL_001",
|
||||
is_digital=True,
|
||||
product_type="digital",
|
||||
)
|
||||
db.add(digital_product)
|
||||
db.commit()
|
||||
db.refresh(digital_product)
|
||||
|
||||
assert digital_product.is_digital is True
|
||||
assert digital_product.product_type == "digital"
|
||||
|
||||
# Create physical product without marketplace source
|
||||
physical_product = Product(
|
||||
store_id=test_store.id,
|
||||
store_sku="PHYSICAL_001",
|
||||
is_digital=False,
|
||||
product_type="physical",
|
||||
)
|
||||
db.add(physical_product)
|
||||
db.commit()
|
||||
db.refresh(physical_product)
|
||||
|
||||
assert physical_product.is_digital is False
|
||||
assert physical_product.product_type == "physical"
|
||||
|
||||
def test_product_type_values(self, db, test_store):
|
||||
"""Test product_type can be set to various values."""
|
||||
product_types = ["physical", "digital", "service", "subscription"]
|
||||
|
||||
for ptype in product_types:
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
store_sku=f"TYPE_{ptype.upper()}",
|
||||
product_type=ptype,
|
||||
is_digital=(ptype == "digital"),
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.product_type == ptype
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
@pytest.mark.inventory
|
||||
class TestProductInventoryProperties:
|
||||
"""Test Product inventory properties including digital product handling."""
|
||||
|
||||
def test_physical_product_no_inventory_returns_zero(self, db, test_store):
|
||||
"""Test physical product with no inventory entries returns 0."""
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
store_sku="PHYS_INV_001",
|
||||
is_digital=False,
|
||||
product_type="physical",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.is_digital is False
|
||||
assert product.has_unlimited_inventory is False
|
||||
assert product.total_inventory == 0
|
||||
assert product.available_inventory == 0
|
||||
|
||||
def test_physical_product_with_inventory(self, db, test_store):
|
||||
"""Test physical product calculates inventory from entries."""
|
||||
from app.modules.inventory.models import Inventory
|
||||
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
store_sku="PHYS_INV_002",
|
||||
is_digital=False,
|
||||
product_type="physical",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
# Add inventory entries
|
||||
inv1 = Inventory(
|
||||
product_id=product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="SA-01-01",
|
||||
location="WAREHOUSE_A",
|
||||
quantity=100,
|
||||
reserved_quantity=10,
|
||||
)
|
||||
inv2 = Inventory(
|
||||
product_id=product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="SA-01-02",
|
||||
location="WAREHOUSE_B",
|
||||
quantity=50,
|
||||
reserved_quantity=5,
|
||||
)
|
||||
db.add_all([inv1, inv2])
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.has_unlimited_inventory is False
|
||||
assert product.total_inventory == 150 # 100 + 50
|
||||
assert product.available_inventory == 135 # (100-10) + (50-5)
|
||||
|
||||
def test_digital_product_has_unlimited_inventory(self, db, test_store):
|
||||
"""Test digital product returns unlimited inventory."""
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
store_sku="DIG_INV_001",
|
||||
is_digital=True,
|
||||
product_type="digital",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
assert product.is_digital is True
|
||||
assert product.has_unlimited_inventory is True
|
||||
assert product.total_inventory == Product.UNLIMITED_INVENTORY
|
||||
assert product.available_inventory == Product.UNLIMITED_INVENTORY
|
||||
|
||||
def test_digital_product_ignores_inventory_entries(self, db, test_store):
|
||||
"""Test digital product returns unlimited even with inventory entries."""
|
||||
from app.modules.inventory.models import Inventory
|
||||
|
||||
product = Product(
|
||||
store_id=test_store.id,
|
||||
store_sku="DIG_INV_002",
|
||||
is_digital=True,
|
||||
product_type="digital",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
# Add inventory entries (e.g., for license keys)
|
||||
inv = Inventory(
|
||||
product_id=product.id,
|
||||
store_id=test_store.id,
|
||||
warehouse="strassen",
|
||||
bin_location="DIG-01-01",
|
||||
location="DIGITAL_LICENSES",
|
||||
quantity=10,
|
||||
reserved_quantity=2,
|
||||
)
|
||||
db.add(inv)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
# Digital product should still return unlimited
|
||||
assert product.has_unlimited_inventory is True
|
||||
assert product.total_inventory == Product.UNLIMITED_INVENTORY
|
||||
assert product.available_inventory == Product.UNLIMITED_INVENTORY
|
||||
|
||||
def test_unlimited_inventory_constant(self):
|
||||
"""Test UNLIMITED_INVENTORY constant value."""
|
||||
assert Product.UNLIMITED_INVENTORY == 999999
|
||||
# Should be large enough to never cause "insufficient inventory"
|
||||
assert Product.UNLIMITED_INVENTORY > 100000
|
||||
@@ -1,138 +0,0 @@
|
||||
# tests/unit/models/database/test_store.py
|
||||
"""Unit tests for Store database model."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.modules.tenancy.models import Store
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestStoreModel:
|
||||
"""Test Store model."""
|
||||
|
||||
def test_store_creation(self, db, test_merchant):
|
||||
"""Test Store model creation with merchant relationship."""
|
||||
store = Store(
|
||||
merchant_id=test_merchant.id,
|
||||
store_code="DBTEST",
|
||||
subdomain="dbtest",
|
||||
name="Database Test Store",
|
||||
description="Testing store model",
|
||||
contact_email="contact@dbtest.com",
|
||||
contact_phone="+1234567890",
|
||||
business_address="123 Test Street",
|
||||
is_active=True,
|
||||
is_verified=False,
|
||||
)
|
||||
|
||||
db.add(store)
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
|
||||
assert store.id is not None
|
||||
assert store.store_code == "DBTEST"
|
||||
assert store.subdomain == "dbtest"
|
||||
assert store.name == "Database Test Store"
|
||||
assert store.merchant_id == test_merchant.id
|
||||
assert store.contact_email == "contact@dbtest.com"
|
||||
assert store.is_active is True
|
||||
assert store.is_verified is False
|
||||
assert store.created_at is not None
|
||||
|
||||
def test_store_with_letzshop_urls(self, db, test_merchant):
|
||||
"""Test Store model with multi-language Letzshop URLs."""
|
||||
store = Store(
|
||||
merchant_id=test_merchant.id,
|
||||
store_code="MULTILANG",
|
||||
subdomain="multilang",
|
||||
name="Multi-Language Store",
|
||||
letzshop_csv_url_fr="https://example.com/feed_fr.csv",
|
||||
letzshop_csv_url_en="https://example.com/feed_en.csv",
|
||||
letzshop_csv_url_de="https://example.com/feed_de.csv",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
db.add(store)
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
|
||||
assert store.letzshop_csv_url_fr == "https://example.com/feed_fr.csv"
|
||||
assert store.letzshop_csv_url_en == "https://example.com/feed_en.csv"
|
||||
assert store.letzshop_csv_url_de == "https://example.com/feed_de.csv"
|
||||
|
||||
def test_store_code_uniqueness(self, db, test_merchant):
|
||||
"""Test store_code unique constraint."""
|
||||
store1 = Store(
|
||||
merchant_id=test_merchant.id,
|
||||
store_code="UNIQUE",
|
||||
subdomain="unique1",
|
||||
name="Unique Store 1",
|
||||
)
|
||||
db.add(store1)
|
||||
db.commit()
|
||||
|
||||
# Duplicate store_code should raise error
|
||||
with pytest.raises(IntegrityError):
|
||||
store2 = Store(
|
||||
merchant_id=test_merchant.id,
|
||||
store_code="UNIQUE",
|
||||
subdomain="unique2",
|
||||
name="Unique Store 2",
|
||||
)
|
||||
db.add(store2)
|
||||
db.commit()
|
||||
|
||||
def test_subdomain_uniqueness(self, db, test_merchant):
|
||||
"""Test subdomain unique constraint."""
|
||||
store1 = Store(
|
||||
merchant_id=test_merchant.id,
|
||||
store_code="STORE1",
|
||||
subdomain="testsubdomain",
|
||||
name="Store 1",
|
||||
)
|
||||
db.add(store1)
|
||||
db.commit()
|
||||
|
||||
# Duplicate subdomain should raise error
|
||||
with pytest.raises(IntegrityError):
|
||||
store2 = Store(
|
||||
merchant_id=test_merchant.id,
|
||||
store_code="STORE2",
|
||||
subdomain="testsubdomain",
|
||||
name="Store 2",
|
||||
)
|
||||
db.add(store2)
|
||||
db.commit()
|
||||
|
||||
def test_store_default_values(self, db, test_merchant):
|
||||
"""Test Store model default values."""
|
||||
store = Store(
|
||||
merchant_id=test_merchant.id,
|
||||
store_code="DEFAULTS",
|
||||
subdomain="defaults",
|
||||
name="Default Store",
|
||||
)
|
||||
db.add(store)
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
|
||||
assert store.is_active is True # Default
|
||||
assert store.is_verified is False # Default
|
||||
|
||||
def test_store_merchant_relationship(self, db, test_merchant):
|
||||
"""Test Store-Merchant relationship."""
|
||||
store = Store(
|
||||
merchant_id=test_merchant.id,
|
||||
store_code="RELTEST",
|
||||
subdomain="reltest",
|
||||
name="Relationship Test Store",
|
||||
)
|
||||
db.add(store)
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
|
||||
assert store.merchant is not None
|
||||
assert store.merchant.id == test_merchant.id
|
||||
assert store.merchant.name == test_merchant.name
|
||||
@@ -1,138 +0,0 @@
|
||||
# tests/unit/models/database/test_team.py
|
||||
"""Unit tests for StoreUser and Role database models."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.tenancy.models import Role, Store, StoreUser
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestRoleModel:
|
||||
"""Test Role model."""
|
||||
|
||||
def test_role_creation(self, db, test_store):
|
||||
"""Test Role model creation."""
|
||||
role = Role(
|
||||
store_id=test_store.id,
|
||||
name="Manager",
|
||||
permissions=["products.create", "orders.view"],
|
||||
)
|
||||
db.add(role)
|
||||
db.commit()
|
||||
db.refresh(role)
|
||||
|
||||
assert role.id is not None
|
||||
assert role.store_id == test_store.id
|
||||
assert role.name == "Manager"
|
||||
assert "products.create" in role.permissions
|
||||
assert "orders.view" in role.permissions
|
||||
|
||||
def test_role_default_permissions(self, db, test_store):
|
||||
"""Test Role model with default empty permissions."""
|
||||
role = Role(
|
||||
store_id=test_store.id,
|
||||
name="Viewer",
|
||||
)
|
||||
db.add(role)
|
||||
db.commit()
|
||||
db.refresh(role)
|
||||
|
||||
assert role.permissions == [] or role.permissions is None
|
||||
|
||||
def test_role_store_relationship(self, db, test_store):
|
||||
"""Test Role-Store relationship."""
|
||||
role = Role(
|
||||
store_id=test_store.id,
|
||||
name="Admin",
|
||||
permissions=["*"],
|
||||
)
|
||||
db.add(role)
|
||||
db.commit()
|
||||
db.refresh(role)
|
||||
|
||||
assert role.store is not None
|
||||
assert role.store.id == test_store.id
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestStoreUserModel:
|
||||
"""Test StoreUser model."""
|
||||
|
||||
def test_store_user_creation(self, db, test_store, test_user):
|
||||
"""Test StoreUser model for team management."""
|
||||
# Create a role
|
||||
role = Role(
|
||||
store_id=test_store.id,
|
||||
name="Manager",
|
||||
permissions=["products.create", "orders.view"],
|
||||
)
|
||||
db.add(role)
|
||||
db.commit()
|
||||
|
||||
# Create store user
|
||||
store_user = StoreUser(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
role_id=role.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(store_user)
|
||||
db.commit()
|
||||
db.refresh(store_user)
|
||||
|
||||
assert store_user.id is not None
|
||||
assert store_user.store_id == test_store.id
|
||||
assert store_user.user_id == test_user.id
|
||||
assert store_user.role.name == "Manager"
|
||||
assert "products.create" in store_user.role.permissions
|
||||
|
||||
def test_store_user_relationships(self, db, test_store, test_user):
|
||||
"""Test StoreUser relationships."""
|
||||
role = Role(
|
||||
store_id=test_store.id,
|
||||
name="Staff",
|
||||
permissions=["orders.view"],
|
||||
)
|
||||
db.add(role)
|
||||
db.commit()
|
||||
|
||||
store_user = StoreUser(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
role_id=role.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(store_user)
|
||||
db.commit()
|
||||
db.refresh(store_user)
|
||||
|
||||
assert store_user.store is not None
|
||||
assert store_user.user is not None
|
||||
assert store_user.role is not None
|
||||
assert store_user.store.store_code == test_store.store_code
|
||||
assert store_user.user.email == test_user.email
|
||||
|
||||
def test_store_user_with_active_flag(self, db, test_store, test_user):
|
||||
"""Test StoreUser is_active field."""
|
||||
role = Role(
|
||||
store_id=test_store.id,
|
||||
name="Default",
|
||||
permissions=[],
|
||||
)
|
||||
db.add(role)
|
||||
db.commit()
|
||||
|
||||
# Create with explicit is_active=True
|
||||
store_user = StoreUser(
|
||||
store_id=test_store.id,
|
||||
user_id=test_user.id,
|
||||
role_id=role.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(store_user)
|
||||
db.commit()
|
||||
db.refresh(store_user)
|
||||
|
||||
assert store_user.is_active is True
|
||||
@@ -1,105 +0,0 @@
|
||||
# tests/unit/models/database/test_user.py
|
||||
"""Unit tests for User database model."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.modules.tenancy.models import User
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.database
|
||||
class TestUserModel:
|
||||
"""Test User model."""
|
||||
|
||||
def test_user_creation(self, db):
|
||||
"""Test User model creation and relationships."""
|
||||
user = User(
|
||||
email="db_test@example.com",
|
||||
username="dbtest",
|
||||
hashed_password="hashed_password_123",
|
||||
role="user",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
assert user.id is not None
|
||||
assert user.email == "db_test@example.com"
|
||||
assert user.username == "dbtest"
|
||||
assert user.role == "user"
|
||||
assert user.is_active is True
|
||||
assert user.created_at is not None
|
||||
assert user.updated_at is not None
|
||||
|
||||
def test_user_email_uniqueness(self, db):
|
||||
"""Test email unique constraint."""
|
||||
user1 = User(
|
||||
email="unique@example.com",
|
||||
username="user1",
|
||||
hashed_password="hash1",
|
||||
)
|
||||
db.add(user1)
|
||||
db.commit()
|
||||
|
||||
# Duplicate email should raise error
|
||||
with pytest.raises(IntegrityError):
|
||||
user2 = User(
|
||||
email="unique@example.com",
|
||||
username="user2",
|
||||
hashed_password="hash2",
|
||||
)
|
||||
db.add(user2)
|
||||
db.commit()
|
||||
|
||||
def test_user_username_uniqueness(self, db):
|
||||
"""Test username unique constraint."""
|
||||
user1 = User(
|
||||
email="user1@example.com",
|
||||
username="sameusername",
|
||||
hashed_password="hash1",
|
||||
)
|
||||
db.add(user1)
|
||||
db.commit()
|
||||
|
||||
# Duplicate username should raise error
|
||||
with pytest.raises(IntegrityError):
|
||||
user2 = User(
|
||||
email="user2@example.com",
|
||||
username="sameusername",
|
||||
hashed_password="hash2",
|
||||
)
|
||||
db.add(user2)
|
||||
db.commit()
|
||||
|
||||
def test_user_default_values(self, db):
|
||||
"""Test User model default values."""
|
||||
user = User(
|
||||
email="defaults@example.com",
|
||||
username="defaultuser",
|
||||
hashed_password="hash",
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
assert user.is_active is True # Default
|
||||
assert user.role == "store" # Default (UserRole.STORE)
|
||||
|
||||
def test_user_optional_fields(self, db):
|
||||
"""Test User model with optional fields."""
|
||||
user = User(
|
||||
email="optional@example.com",
|
||||
username="optionaluser",
|
||||
hashed_password="hash",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
assert user.first_name == "John"
|
||||
assert user.last_name == "Doe"
|
||||
@@ -1,358 +0,0 @@
|
||||
# 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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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
|
||||
@@ -1,299 +0,0 @@
|
||||
# tests/unit/models/schema/test_inventory.py
|
||||
"""Unit tests for inventory Pydantic schemas."""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.modules.inventory.schemas 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,
|
||||
"store_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,
|
||||
"store_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,
|
||||
"store_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,
|
||||
store_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,
|
||||
store_id=1,
|
||||
product_sku=None,
|
||||
product_title="Test Product",
|
||||
total_quantity=0,
|
||||
total_reserved=0,
|
||||
total_available=0,
|
||||
locations=[],
|
||||
)
|
||||
assert summary.locations == []
|
||||
@@ -1,240 +0,0 @@
|
||||
# tests/unit/models/schema/test_marketplace_import_job.py
|
||||
"""Unit tests for marketplace import job Pydantic schemas."""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.modules.marketplace.schemas import (
|
||||
MarketplaceImportJobListResponse,
|
||||
MarketplaceImportJobRequest,
|
||||
MarketplaceImportJobResponse,
|
||||
MarketplaceImportJobStatusUpdate,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestMarketplaceImportJobRequestSchema:
|
||||
"""Test MarketplaceImportJobRequest schema validation."""
|
||||
|
||||
def test_valid_request(self):
|
||||
"""Test valid import job request."""
|
||||
request = MarketplaceImportJobRequest(
|
||||
source_url="https://example.com/products.csv",
|
||||
marketplace="Letzshop",
|
||||
)
|
||||
assert request.source_url == "https://example.com/products.csv"
|
||||
assert request.marketplace == "Letzshop"
|
||||
|
||||
def test_source_url_required(self):
|
||||
"""Test source_url is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MarketplaceImportJobRequest(marketplace="Letzshop")
|
||||
assert "source_url" in str(exc_info.value).lower()
|
||||
|
||||
def test_source_url_must_be_http_or_https(self):
|
||||
"""Test source_url must start with http:// or https://."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MarketplaceImportJobRequest(
|
||||
source_url="ftp://example.com/products.csv",
|
||||
)
|
||||
assert "url" in str(exc_info.value).lower()
|
||||
|
||||
def test_source_url_http_is_valid(self):
|
||||
"""Test http:// URLs are valid."""
|
||||
request = MarketplaceImportJobRequest(
|
||||
source_url="http://example.com/products.csv",
|
||||
)
|
||||
assert request.source_url == "http://example.com/products.csv"
|
||||
|
||||
def test_source_url_with_leading_whitespace_invalid(self):
|
||||
"""Test source_url with leading whitespace is invalid (validation before strip)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MarketplaceImportJobRequest(
|
||||
source_url=" https://example.com/products.csv",
|
||||
)
|
||||
assert "url" in str(exc_info.value).lower()
|
||||
|
||||
def test_marketplace_default(self):
|
||||
"""Test marketplace defaults to Letzshop."""
|
||||
request = MarketplaceImportJobRequest(
|
||||
source_url="https://example.com/products.csv",
|
||||
)
|
||||
assert request.marketplace == "Letzshop"
|
||||
|
||||
def test_marketplace_stripped(self):
|
||||
"""Test marketplace is stripped of whitespace."""
|
||||
request = MarketplaceImportJobRequest(
|
||||
source_url="https://example.com/products.csv",
|
||||
marketplace=" CustomMarket ",
|
||||
)
|
||||
assert request.marketplace == "CustomMarket"
|
||||
|
||||
def test_batch_size_default(self):
|
||||
"""Test batch_size defaults to 1000."""
|
||||
request = MarketplaceImportJobRequest(
|
||||
source_url="https://example.com/products.csv",
|
||||
)
|
||||
assert request.batch_size == 1000
|
||||
|
||||
def test_batch_size_minimum(self):
|
||||
"""Test batch_size must be >= 100."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MarketplaceImportJobRequest(
|
||||
source_url="https://example.com/products.csv",
|
||||
batch_size=50,
|
||||
)
|
||||
assert "batch_size" in str(exc_info.value).lower()
|
||||
|
||||
def test_batch_size_maximum(self):
|
||||
"""Test batch_size must be <= 10000."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MarketplaceImportJobRequest(
|
||||
source_url="https://example.com/products.csv",
|
||||
batch_size=15000,
|
||||
)
|
||||
assert "batch_size" in str(exc_info.value).lower()
|
||||
|
||||
def test_batch_size_valid_range(self):
|
||||
"""Test batch_size in valid range."""
|
||||
request = MarketplaceImportJobRequest(
|
||||
source_url="https://example.com/products.csv",
|
||||
batch_size=5000,
|
||||
)
|
||||
assert request.batch_size == 5000
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestMarketplaceImportJobResponseSchema:
|
||||
"""Test MarketplaceImportJobResponse schema."""
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating response from dict."""
|
||||
from datetime import datetime
|
||||
|
||||
data = {
|
||||
"job_id": 1,
|
||||
"store_id": 1,
|
||||
"store_code": "TEST_STORE",
|
||||
"store_name": "Test Store",
|
||||
"marketplace": "Letzshop",
|
||||
"source_url": "https://example.com/products.csv",
|
||||
"status": "pending",
|
||||
"created_at": datetime.now(),
|
||||
}
|
||||
response = MarketplaceImportJobResponse(**data)
|
||||
assert response.job_id == 1
|
||||
assert response.store_code == "TEST_STORE"
|
||||
assert response.status == "pending"
|
||||
|
||||
def test_default_counts(self):
|
||||
"""Test count fields default to 0."""
|
||||
from datetime import datetime
|
||||
|
||||
data = {
|
||||
"job_id": 1,
|
||||
"store_id": 1,
|
||||
"store_code": "TEST_STORE",
|
||||
"store_name": "Test Store",
|
||||
"marketplace": "Letzshop",
|
||||
"source_url": "https://example.com/products.csv",
|
||||
"status": "completed",
|
||||
"created_at": datetime.now(),
|
||||
}
|
||||
response = MarketplaceImportJobResponse(**data)
|
||||
assert response.imported == 0
|
||||
assert response.updated == 0
|
||||
assert response.total_processed == 0
|
||||
assert response.error_count == 0
|
||||
|
||||
def test_optional_timestamps(self):
|
||||
"""Test optional timestamp fields."""
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now()
|
||||
data = {
|
||||
"job_id": 1,
|
||||
"store_id": 1,
|
||||
"store_code": "TEST_STORE",
|
||||
"store_name": "Test Store",
|
||||
"marketplace": "Letzshop",
|
||||
"source_url": "https://example.com/products.csv",
|
||||
"status": "completed",
|
||||
"created_at": now,
|
||||
"started_at": now,
|
||||
"completed_at": now,
|
||||
}
|
||||
response = MarketplaceImportJobResponse(**data)
|
||||
assert response.started_at == now
|
||||
assert response.completed_at == now
|
||||
|
||||
def test_error_message_optional(self):
|
||||
"""Test error_message is optional."""
|
||||
from datetime import datetime
|
||||
|
||||
data = {
|
||||
"job_id": 1,
|
||||
"store_id": 1,
|
||||
"store_code": "TEST_STORE",
|
||||
"store_name": "Test Store",
|
||||
"marketplace": "Letzshop",
|
||||
"source_url": "https://example.com/products.csv",
|
||||
"status": "failed",
|
||||
"created_at": datetime.now(),
|
||||
"error_message": "Connection timeout",
|
||||
}
|
||||
response = MarketplaceImportJobResponse(**data)
|
||||
assert response.error_message == "Connection timeout"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestMarketplaceImportJobListResponseSchema:
|
||||
"""Test MarketplaceImportJobListResponse schema."""
|
||||
|
||||
def test_valid_list_response(self):
|
||||
"""Test valid list response structure."""
|
||||
response = MarketplaceImportJobListResponse(
|
||||
jobs=[],
|
||||
total=0,
|
||||
skip=0,
|
||||
limit=10,
|
||||
)
|
||||
assert response.jobs == []
|
||||
assert response.total == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestMarketplaceImportJobStatusUpdateSchema:
|
||||
"""Test MarketplaceImportJobStatusUpdate schema."""
|
||||
|
||||
def test_status_only_update(self):
|
||||
"""Test update with only status."""
|
||||
update = MarketplaceImportJobStatusUpdate(status="processing")
|
||||
assert update.status == "processing"
|
||||
assert update.imported_count is None
|
||||
|
||||
def test_full_update(self):
|
||||
"""Test update with all fields."""
|
||||
update = MarketplaceImportJobStatusUpdate(
|
||||
status="completed",
|
||||
imported_count=100,
|
||||
updated_count=50,
|
||||
error_count=2,
|
||||
total_processed=152,
|
||||
)
|
||||
assert update.imported_count == 100
|
||||
assert update.updated_count == 50
|
||||
assert update.error_count == 2
|
||||
assert update.total_processed == 152
|
||||
|
||||
def test_error_update(self):
|
||||
"""Test update with error message."""
|
||||
update = MarketplaceImportJobStatusUpdate(
|
||||
status="failed",
|
||||
error_message="File not found",
|
||||
)
|
||||
assert update.status == "failed"
|
||||
assert update.error_message == "File not found"
|
||||
@@ -1,578 +0,0 @@
|
||||
# tests/unit/models/schema/test_order.py
|
||||
"""Unit tests for order Pydantic schemas."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.modules.orders.schemas import (
|
||||
AddressSnapshot,
|
||||
AddressSnapshotResponse,
|
||||
CustomerSnapshot,
|
||||
CustomerSnapshotResponse,
|
||||
OrderCreate,
|
||||
OrderItemCreate,
|
||||
OrderItemResponse,
|
||||
OrderListResponse,
|
||||
OrderResponse,
|
||||
OrderUpdate,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestOrderItemCreateSchema:
|
||||
"""Test OrderItemCreate schema validation."""
|
||||
|
||||
def test_valid_order_item(self):
|
||||
"""Test valid order item creation."""
|
||||
item = OrderItemCreate(
|
||||
product_id=1,
|
||||
quantity=2,
|
||||
)
|
||||
assert item.product_id == 1
|
||||
assert item.quantity == 2
|
||||
|
||||
def test_product_id_required(self):
|
||||
"""Test product_id is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderItemCreate(quantity=2)
|
||||
assert "product_id" in str(exc_info.value).lower()
|
||||
|
||||
def test_quantity_required(self):
|
||||
"""Test quantity is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderItemCreate(product_id=1)
|
||||
assert "quantity" in str(exc_info.value).lower()
|
||||
|
||||
def test_quantity_must_be_at_least_1(self):
|
||||
"""Test quantity must be >= 1."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderItemCreate(
|
||||
product_id=1,
|
||||
quantity=0,
|
||||
)
|
||||
assert "quantity" in str(exc_info.value).lower()
|
||||
|
||||
def test_negative_quantity_invalid(self):
|
||||
"""Test negative quantity is invalid."""
|
||||
with pytest.raises(ValidationError):
|
||||
OrderItemCreate(
|
||||
product_id=1,
|
||||
quantity=-1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestAddressSnapshotSchema:
|
||||
"""Test AddressSnapshot schema validation."""
|
||||
|
||||
def test_valid_address(self):
|
||||
"""Test valid address creation."""
|
||||
address = AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
)
|
||||
assert address.first_name == "John"
|
||||
assert address.city == "Luxembourg"
|
||||
assert address.country_iso == "LU"
|
||||
|
||||
def test_required_fields(self):
|
||||
"""Test required fields validation."""
|
||||
with pytest.raises(ValidationError):
|
||||
AddressSnapshot(
|
||||
first_name="John",
|
||||
# missing required fields
|
||||
)
|
||||
|
||||
def test_optional_address_line_2(self):
|
||||
"""Test optional address_line_2 field."""
|
||||
address = AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
address_line_2="Suite 500",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
)
|
||||
assert address.address_line_2 == "Suite 500"
|
||||
|
||||
def test_first_name_min_length(self):
|
||||
"""Test first_name minimum length."""
|
||||
with pytest.raises(ValidationError):
|
||||
AddressSnapshot(
|
||||
first_name="",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
)
|
||||
|
||||
def test_country_iso_min_length(self):
|
||||
"""Test country_iso minimum length (2)."""
|
||||
with pytest.raises(ValidationError):
|
||||
AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="L", # Too short
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestCustomerSnapshotSchema:
|
||||
"""Test CustomerSnapshot schema validation."""
|
||||
|
||||
def test_valid_customer(self):
|
||||
"""Test valid customer snapshot."""
|
||||
customer = CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
phone="+352123456",
|
||||
locale="en",
|
||||
)
|
||||
assert customer.first_name == "John"
|
||||
assert customer.email == "john@example.com"
|
||||
|
||||
def test_optional_phone(self):
|
||||
"""Test phone is optional."""
|
||||
customer = CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
assert customer.phone is None
|
||||
|
||||
def test_optional_locale(self):
|
||||
"""Test locale is optional."""
|
||||
customer = CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
assert customer.locale is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestCustomerSnapshotResponseSchema:
|
||||
"""Test CustomerSnapshotResponse schema."""
|
||||
|
||||
def test_full_name_property(self):
|
||||
"""Test full_name property."""
|
||||
response = CustomerSnapshotResponse(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
phone=None,
|
||||
locale=None,
|
||||
)
|
||||
assert response.full_name == "John Doe"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestOrderCreateSchema:
|
||||
"""Test OrderCreate schema validation."""
|
||||
|
||||
def test_valid_order(self):
|
||||
"""Test valid order creation."""
|
||||
order = OrderCreate(
|
||||
items=[
|
||||
OrderItemCreate(product_id=1, quantity=2),
|
||||
OrderItemCreate(product_id=2, quantity=1),
|
||||
],
|
||||
customer=CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
),
|
||||
shipping_address=AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
),
|
||||
)
|
||||
assert len(order.items) == 2
|
||||
assert order.customer_id is None # Optional for guest checkout
|
||||
|
||||
def test_items_required(self):
|
||||
"""Test items are required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderCreate(
|
||||
customer=CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
),
|
||||
shipping_address=AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
),
|
||||
)
|
||||
assert "items" in str(exc_info.value).lower()
|
||||
|
||||
def test_items_must_not_be_empty(self):
|
||||
"""Test items list must have at least 1 item."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderCreate(
|
||||
items=[],
|
||||
customer=CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
),
|
||||
shipping_address=AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
),
|
||||
)
|
||||
assert "items" in str(exc_info.value).lower()
|
||||
|
||||
def test_customer_required(self):
|
||||
"""Test customer is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderCreate(
|
||||
items=[OrderItemCreate(product_id=1, quantity=1)],
|
||||
shipping_address=AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
),
|
||||
)
|
||||
assert "customer" in str(exc_info.value).lower()
|
||||
|
||||
def test_shipping_address_required(self):
|
||||
"""Test shipping_address is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderCreate(
|
||||
items=[OrderItemCreate(product_id=1, quantity=1)],
|
||||
customer=CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
),
|
||||
)
|
||||
assert "shipping_address" in str(exc_info.value).lower()
|
||||
|
||||
def test_optional_billing_address(self):
|
||||
"""Test billing_address is optional."""
|
||||
order = OrderCreate(
|
||||
items=[OrderItemCreate(product_id=1, quantity=1)],
|
||||
customer=CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
),
|
||||
shipping_address=AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
),
|
||||
billing_address=AddressSnapshot(
|
||||
first_name="Jane",
|
||||
last_name="Doe",
|
||||
address_line_1="456 Other St",
|
||||
city="Esch",
|
||||
postal_code="L-4321",
|
||||
country_iso="LU",
|
||||
),
|
||||
)
|
||||
assert order.billing_address is not None
|
||||
assert order.billing_address.first_name == "Jane"
|
||||
|
||||
def test_optional_customer_notes(self):
|
||||
"""Test optional customer_notes."""
|
||||
order = OrderCreate(
|
||||
items=[OrderItemCreate(product_id=1, quantity=1)],
|
||||
customer=CustomerSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
),
|
||||
shipping_address=AddressSnapshot(
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
address_line_1="123 Main St",
|
||||
city="Luxembourg",
|
||||
postal_code="L-1234",
|
||||
country_iso="LU",
|
||||
),
|
||||
customer_notes="Please leave at door",
|
||||
)
|
||||
assert order.customer_notes == "Please leave at door"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestOrderUpdateSchema:
|
||||
"""Test OrderUpdate schema validation."""
|
||||
|
||||
def test_status_update(self):
|
||||
"""Test valid status update."""
|
||||
update = OrderUpdate(status="processing")
|
||||
assert update.status == "processing"
|
||||
|
||||
def test_valid_status_values(self):
|
||||
"""Test all valid status values."""
|
||||
valid_statuses = [
|
||||
"pending",
|
||||
"processing",
|
||||
"shipped",
|
||||
"delivered",
|
||||
"cancelled",
|
||||
"refunded",
|
||||
]
|
||||
for status in valid_statuses:
|
||||
update = OrderUpdate(status=status)
|
||||
assert update.status == status
|
||||
|
||||
def test_invalid_status(self):
|
||||
"""Test invalid status raises ValidationError."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrderUpdate(status="invalid_status")
|
||||
assert "status" in str(exc_info.value).lower()
|
||||
|
||||
def test_tracking_number_update(self):
|
||||
"""Test tracking number update."""
|
||||
update = OrderUpdate(tracking_number="TRACK123456")
|
||||
assert update.tracking_number == "TRACK123456"
|
||||
|
||||
def test_internal_notes_update(self):
|
||||
"""Test internal notes update."""
|
||||
update = OrderUpdate(internal_notes="Customer requested expedited shipping")
|
||||
assert update.internal_notes == "Customer requested expedited shipping"
|
||||
|
||||
def test_empty_update_is_valid(self):
|
||||
"""Test empty update is valid."""
|
||||
update = OrderUpdate()
|
||||
assert update.model_dump(exclude_unset=True) == {}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestOrderResponseSchema:
|
||||
"""Test OrderResponse schema."""
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating response from dict."""
|
||||
now = datetime.now(timezone.utc)
|
||||
data = {
|
||||
"id": 1,
|
||||
"store_id": 1,
|
||||
"customer_id": 1,
|
||||
"order_number": "ORD-001",
|
||||
"channel": "direct",
|
||||
"status": "pending",
|
||||
"subtotal": 100.00,
|
||||
"tax_amount": 20.00,
|
||||
"shipping_amount": 10.00,
|
||||
"discount_amount": 5.00,
|
||||
"total_amount": 125.00,
|
||||
"currency": "EUR",
|
||||
# Customer snapshot
|
||||
"customer_first_name": "John",
|
||||
"customer_last_name": "Doe",
|
||||
"customer_email": "john@example.com",
|
||||
"customer_phone": None,
|
||||
"customer_locale": "en",
|
||||
# Ship address snapshot
|
||||
"ship_first_name": "John",
|
||||
"ship_last_name": "Doe",
|
||||
"ship_company": None,
|
||||
"ship_address_line_1": "123 Main St",
|
||||
"ship_address_line_2": None,
|
||||
"ship_city": "Luxembourg",
|
||||
"ship_postal_code": "L-1234",
|
||||
"ship_country_iso": "LU",
|
||||
# Bill address snapshot
|
||||
"bill_first_name": "John",
|
||||
"bill_last_name": "Doe",
|
||||
"bill_company": None,
|
||||
"bill_address_line_1": "123 Main St",
|
||||
"bill_address_line_2": None,
|
||||
"bill_city": "Luxembourg",
|
||||
"bill_postal_code": "L-1234",
|
||||
"bill_country_iso": "LU",
|
||||
# Tracking
|
||||
"shipping_method": "standard",
|
||||
"tracking_number": None,
|
||||
"tracking_provider": None,
|
||||
# Notes
|
||||
"customer_notes": None,
|
||||
"internal_notes": None,
|
||||
# Timestamps
|
||||
"order_date": now,
|
||||
"confirmed_at": None,
|
||||
"shipped_at": None,
|
||||
"delivered_at": None,
|
||||
"cancelled_at": None,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
response = OrderResponse(**data)
|
||||
assert response.id == 1
|
||||
assert response.order_number == "ORD-001"
|
||||
assert response.total_amount == 125.00
|
||||
assert response.channel == "direct"
|
||||
assert response.customer_full_name == "John Doe"
|
||||
|
||||
def test_is_marketplace_order(self):
|
||||
"""Test is_marketplace_order property."""
|
||||
now = datetime.now(timezone.utc)
|
||||
# Direct order
|
||||
direct_order = OrderResponse(
|
||||
id=1, store_id=1, customer_id=1, order_number="ORD-001",
|
||||
channel="direct", status="pending",
|
||||
subtotal=100.0, tax_amount=0.0, shipping_amount=0.0, discount_amount=0.0,
|
||||
total_amount=100.0, currency="EUR",
|
||||
customer_first_name="John", customer_last_name="Doe",
|
||||
customer_email="john@example.com", customer_phone=None, customer_locale=None,
|
||||
ship_first_name="John", ship_last_name="Doe", ship_company=None,
|
||||
ship_address_line_1="123 Main", ship_address_line_2=None,
|
||||
ship_city="Luxembourg", ship_postal_code="L-1234", ship_country_iso="LU",
|
||||
bill_first_name="John", bill_last_name="Doe", bill_company=None,
|
||||
bill_address_line_1="123 Main", bill_address_line_2=None,
|
||||
bill_city="Luxembourg", bill_postal_code="L-1234", bill_country_iso="LU",
|
||||
shipping_method=None, tracking_number=None, tracking_provider=None,
|
||||
customer_notes=None, internal_notes=None,
|
||||
order_date=now, confirmed_at=None, shipped_at=None,
|
||||
delivered_at=None, cancelled_at=None, created_at=now, updated_at=now,
|
||||
)
|
||||
assert direct_order.is_marketplace_order is False
|
||||
|
||||
# Marketplace order
|
||||
marketplace_order = OrderResponse(
|
||||
id=2, store_id=1, customer_id=1, order_number="LS-001",
|
||||
channel="letzshop", status="pending",
|
||||
subtotal=100.0, tax_amount=0.0, shipping_amount=0.0, discount_amount=0.0,
|
||||
total_amount=100.0, currency="EUR",
|
||||
customer_first_name="John", customer_last_name="Doe",
|
||||
customer_email="john@example.com", customer_phone=None, customer_locale=None,
|
||||
ship_first_name="John", ship_last_name="Doe", ship_company=None,
|
||||
ship_address_line_1="123 Main", ship_address_line_2=None,
|
||||
ship_city="Luxembourg", ship_postal_code="L-1234", ship_country_iso="LU",
|
||||
bill_first_name="John", bill_last_name="Doe", bill_company=None,
|
||||
bill_address_line_1="123 Main", bill_address_line_2=None,
|
||||
bill_city="Luxembourg", bill_postal_code="L-1234", bill_country_iso="LU",
|
||||
shipping_method=None, tracking_number=None, tracking_provider=None,
|
||||
customer_notes=None, internal_notes=None,
|
||||
order_date=now, confirmed_at=None, shipped_at=None,
|
||||
delivered_at=None, cancelled_at=None, created_at=now, updated_at=now,
|
||||
)
|
||||
assert marketplace_order.is_marketplace_order is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestOrderItemResponseSchema:
|
||||
"""Test OrderItemResponse schema."""
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating response from dict."""
|
||||
now = datetime.now(timezone.utc)
|
||||
data = {
|
||||
"id": 1,
|
||||
"order_id": 1,
|
||||
"product_id": 1,
|
||||
"product_name": "Test Product",
|
||||
"product_sku": "SKU-001",
|
||||
"gtin": "4006381333931",
|
||||
"gtin_type": "EAN13",
|
||||
"quantity": 2,
|
||||
"unit_price": 50.00,
|
||||
"total_price": 100.00,
|
||||
"inventory_reserved": True,
|
||||
"inventory_fulfilled": False,
|
||||
"needs_product_match": False,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
response = OrderItemResponse(**data)
|
||||
assert response.id == 1
|
||||
assert response.quantity == 2
|
||||
assert response.total_price == 100.00
|
||||
assert response.gtin == "4006381333931"
|
||||
|
||||
def test_has_unresolved_exception(self):
|
||||
"""Test has_unresolved_exception property."""
|
||||
now = datetime.now(timezone.utc)
|
||||
base_data = {
|
||||
"id": 1, "order_id": 1, "product_id": 1,
|
||||
"product_name": "Test", "product_sku": "SKU-001",
|
||||
"gtin": None, "gtin_type": None,
|
||||
"quantity": 1, "unit_price": 10.0, "total_price": 10.0,
|
||||
"inventory_reserved": False, "inventory_fulfilled": False,
|
||||
"created_at": now, "updated_at": now,
|
||||
}
|
||||
|
||||
# No exception
|
||||
response = OrderItemResponse(**base_data, needs_product_match=False, exception=None)
|
||||
assert response.has_unresolved_exception is False
|
||||
|
||||
# Pending exception
|
||||
from app.modules.orders.schemas import OrderItemExceptionBrief
|
||||
pending_exc = OrderItemExceptionBrief(
|
||||
id=1, original_gtin="123", original_product_name="Test",
|
||||
exception_type="product_not_found", status="pending",
|
||||
resolved_product_id=None,
|
||||
)
|
||||
response = OrderItemResponse(**base_data, needs_product_match=True, exception=pending_exc)
|
||||
assert response.has_unresolved_exception is True
|
||||
|
||||
# Resolved exception
|
||||
resolved_exc = OrderItemExceptionBrief(
|
||||
id=1, original_gtin="123", original_product_name="Test",
|
||||
exception_type="product_not_found", status="resolved",
|
||||
resolved_product_id=5,
|
||||
)
|
||||
response = OrderItemResponse(**base_data, needs_product_match=False, exception=resolved_exc)
|
||||
assert response.has_unresolved_exception is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestOrderListResponseSchema:
|
||||
"""Test OrderListResponse schema."""
|
||||
|
||||
def test_valid_list_response(self):
|
||||
"""Test valid list response structure."""
|
||||
response = OrderListResponse(
|
||||
orders=[],
|
||||
total=0,
|
||||
skip=0,
|
||||
limit=10,
|
||||
)
|
||||
assert response.orders == []
|
||||
assert response.total == 0
|
||||
assert response.skip == 0
|
||||
assert response.limit == 10
|
||||
@@ -1,245 +0,0 @@
|
||||
# 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,
|
||||
store_sku="SKU-001",
|
||||
price=99.99,
|
||||
currency="EUR",
|
||||
)
|
||||
assert product.marketplace_product_id == 1
|
||||
assert product.store_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,
|
||||
store_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.store_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,
|
||||
"store_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(),
|
||||
},
|
||||
"store_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.store_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,
|
||||
"store_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(),
|
||||
},
|
||||
"store_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
|
||||
@@ -1,316 +0,0 @@
|
||||
# tests/unit/models/schema/test_store.py
|
||||
"""Unit tests for store Pydantic schemas."""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.modules.tenancy.schemas.store import (
|
||||
StoreCreate,
|
||||
StoreDetailResponse,
|
||||
StoreListResponse,
|
||||
StoreResponse,
|
||||
StoreSummary,
|
||||
StoreUpdate,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestStoreCreateSchema:
|
||||
"""Test StoreCreate schema validation."""
|
||||
|
||||
def test_valid_store_create(self):
|
||||
"""Test valid store creation data."""
|
||||
store = StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="TECHSTORE",
|
||||
subdomain="techstore",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert store.merchant_id == 1
|
||||
assert store.store_code == "TECHSTORE"
|
||||
assert store.subdomain == "techstore"
|
||||
assert store.name == "Tech Store"
|
||||
|
||||
def test_merchant_id_required(self):
|
||||
"""Test merchant_id is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
store_code="TECHSTORE",
|
||||
subdomain="techstore",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert "merchant_id" in str(exc_info.value).lower()
|
||||
|
||||
def test_merchant_id_must_be_positive(self):
|
||||
"""Test merchant_id must be > 0."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
merchant_id=0,
|
||||
store_code="TECHSTORE",
|
||||
subdomain="techstore",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert "merchant_id" in str(exc_info.value).lower()
|
||||
|
||||
def test_store_code_required(self):
|
||||
"""Test store_code is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
merchant_id=1,
|
||||
subdomain="techstore",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert "store_code" in str(exc_info.value).lower()
|
||||
|
||||
def test_store_code_uppercase_normalized(self):
|
||||
"""Test store_code is normalized to uppercase."""
|
||||
store = StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="techstore",
|
||||
subdomain="techstore",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert store.store_code == "TECHSTORE"
|
||||
|
||||
def test_store_code_min_length(self):
|
||||
"""Test store_code minimum length (2)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="T",
|
||||
subdomain="techstore",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert "store_code" in str(exc_info.value).lower()
|
||||
|
||||
def test_subdomain_required(self):
|
||||
"""Test subdomain is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="TECHSTORE",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert "subdomain" in str(exc_info.value).lower()
|
||||
|
||||
def test_subdomain_uppercase_invalid(self):
|
||||
"""Test subdomain with uppercase is invalid (validated before normalization)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="TECHSTORE",
|
||||
subdomain="TechStore",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert "subdomain" in str(exc_info.value).lower()
|
||||
|
||||
def test_subdomain_valid_format(self):
|
||||
"""Test subdomain with valid format."""
|
||||
store = StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="TECHSTORE",
|
||||
subdomain="tech-store-123",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert store.subdomain == "tech-store-123"
|
||||
|
||||
def test_subdomain_invalid_format(self):
|
||||
"""Test subdomain with invalid characters raises ValidationError."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="TECHSTORE",
|
||||
subdomain="tech_store!",
|
||||
name="Tech Store",
|
||||
)
|
||||
assert "subdomain" in str(exc_info.value).lower()
|
||||
|
||||
def test_name_required(self):
|
||||
"""Test name is required."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="TECHSTORE",
|
||||
subdomain="techstore",
|
||||
)
|
||||
assert "name" in str(exc_info.value).lower()
|
||||
|
||||
def test_name_min_length(self):
|
||||
"""Test name minimum length (2)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="TECHSTORE",
|
||||
subdomain="techstore",
|
||||
name="T",
|
||||
)
|
||||
assert "name" in str(exc_info.value).lower()
|
||||
|
||||
def test_optional_fields(self):
|
||||
"""Test optional fields."""
|
||||
store = StoreCreate(
|
||||
merchant_id=1,
|
||||
store_code="TECHSTORE",
|
||||
subdomain="techstore",
|
||||
name="Tech Store",
|
||||
description="Best tech store",
|
||||
letzshop_csv_url_fr="https://example.com/fr.csv",
|
||||
contact_email="contact@techstore.com",
|
||||
website="https://techstore.com",
|
||||
)
|
||||
assert store.description == "Best tech store"
|
||||
assert store.letzshop_csv_url_fr == "https://example.com/fr.csv"
|
||||
assert store.contact_email == "contact@techstore.com"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestStoreUpdateSchema:
|
||||
"""Test StoreUpdate schema validation."""
|
||||
|
||||
def test_partial_update(self):
|
||||
"""Test partial update with only some fields."""
|
||||
update = StoreUpdate(name="New Tech Store")
|
||||
assert update.name == "New Tech Store"
|
||||
assert update.subdomain is None
|
||||
assert update.is_active is None
|
||||
|
||||
def test_empty_update_is_valid(self):
|
||||
"""Test empty update is valid."""
|
||||
update = StoreUpdate()
|
||||
assert update.model_dump(exclude_unset=True) == {}
|
||||
|
||||
def test_subdomain_normalized_to_lowercase(self):
|
||||
"""Test subdomain is normalized to lowercase."""
|
||||
update = StoreUpdate(subdomain="NewSubdomain")
|
||||
assert update.subdomain == "newsubdomain"
|
||||
|
||||
def test_subdomain_stripped(self):
|
||||
"""Test subdomain is stripped of whitespace."""
|
||||
update = StoreUpdate(subdomain=" newsubdomain ")
|
||||
assert update.subdomain == "newsubdomain"
|
||||
|
||||
def test_name_min_length(self):
|
||||
"""Test name minimum length (2)."""
|
||||
with pytest.raises(ValidationError):
|
||||
StoreUpdate(name="X")
|
||||
|
||||
def test_is_active_update(self):
|
||||
"""Test is_active can be updated."""
|
||||
update = StoreUpdate(is_active=False)
|
||||
assert update.is_active is False
|
||||
|
||||
def test_is_verified_update(self):
|
||||
"""Test is_verified can be updated."""
|
||||
update = StoreUpdate(is_verified=True)
|
||||
assert update.is_verified is True
|
||||
|
||||
def test_reset_contact_to_merchant_flag(self):
|
||||
"""Test reset_contact_to_merchant flag."""
|
||||
update = StoreUpdate(reset_contact_to_merchant=True)
|
||||
assert update.reset_contact_to_merchant is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestStoreResponseSchema:
|
||||
"""Test StoreResponse schema."""
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating response from dict."""
|
||||
from datetime import datetime
|
||||
|
||||
data = {
|
||||
"id": 1,
|
||||
"store_code": "TECHSTORE",
|
||||
"subdomain": "techstore",
|
||||
"name": "Tech Store",
|
||||
"description": "Best tech store",
|
||||
"merchant_id": 1,
|
||||
"letzshop_csv_url_fr": None,
|
||||
"letzshop_csv_url_en": None,
|
||||
"letzshop_csv_url_de": None,
|
||||
"is_active": True,
|
||||
"is_verified": False,
|
||||
"created_at": datetime.now(),
|
||||
"updated_at": datetime.now(),
|
||||
}
|
||||
response = StoreResponse(**data)
|
||||
assert response.id == 1
|
||||
assert response.store_code == "TECHSTORE"
|
||||
assert response.is_active is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestStoreDetailResponseSchema:
|
||||
"""Test StoreDetailResponse schema."""
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating detail response from dict."""
|
||||
from datetime import datetime
|
||||
|
||||
data = {
|
||||
"id": 1,
|
||||
"store_code": "TECHSTORE",
|
||||
"subdomain": "techstore",
|
||||
"name": "Tech Store",
|
||||
"description": None,
|
||||
"merchant_id": 1,
|
||||
"letzshop_csv_url_fr": None,
|
||||
"letzshop_csv_url_en": None,
|
||||
"letzshop_csv_url_de": None,
|
||||
"is_active": True,
|
||||
"is_verified": True,
|
||||
"created_at": datetime.now(),
|
||||
"updated_at": datetime.now(),
|
||||
# Additional detail fields
|
||||
"merchant_name": "Tech Corp",
|
||||
"owner_email": "owner@techcorp.com",
|
||||
"owner_username": "owner",
|
||||
"contact_email": "contact@techstore.com",
|
||||
"contact_email_inherited": False,
|
||||
}
|
||||
response = StoreDetailResponse(**data)
|
||||
assert response.merchant_name == "Tech Corp"
|
||||
assert response.owner_email == "owner@techcorp.com"
|
||||
assert response.contact_email_inherited is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestStoreListResponseSchema:
|
||||
"""Test StoreListResponse schema."""
|
||||
|
||||
def test_valid_list_response(self):
|
||||
"""Test valid list response structure."""
|
||||
response = StoreListResponse(
|
||||
stores=[],
|
||||
total=0,
|
||||
skip=0,
|
||||
limit=10,
|
||||
)
|
||||
assert response.stores == []
|
||||
assert response.total == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.schema
|
||||
class TestStoreSummarySchema:
|
||||
"""Test StoreSummary schema."""
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating summary from dict."""
|
||||
data = {
|
||||
"id": 1,
|
||||
"store_code": "TECHSTORE",
|
||||
"subdomain": "techstore",
|
||||
"name": "Tech Store",
|
||||
"merchant_id": 1,
|
||||
"is_active": True,
|
||||
}
|
||||
summary = StoreSummary(**data)
|
||||
assert summary.id == 1
|
||||
assert summary.store_code == "TECHSTORE"
|
||||
assert summary.is_active is True
|
||||
@@ -1,275 +0,0 @@
|
||||
# tests/unit/models/test_merchant_domain.py
|
||||
"""Unit tests for MerchantDomain model and related model properties."""
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.tenancy.models.merchant_domain import MerchantDomain
|
||||
from app.modules.tenancy.models.store_domain import StoreDomain
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MODEL TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.tenancy
|
||||
class TestMerchantDomainModel:
|
||||
"""Test suite for MerchantDomain model."""
|
||||
|
||||
def test_create_merchant_domain(self, db, test_merchant):
|
||||
"""Test creating a MerchantDomain with required fields."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"test{unique_id}.example.com",
|
||||
is_primary=True,
|
||||
verification_token=f"token_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(domain)
|
||||
|
||||
assert domain.id is not None
|
||||
assert domain.merchant_id == test_merchant.id
|
||||
assert domain.is_primary is True
|
||||
assert domain.is_active is True # default
|
||||
assert domain.is_verified is False # default
|
||||
assert domain.ssl_status == "pending" # default
|
||||
assert domain.verified_at is None
|
||||
assert domain.platform_id is None
|
||||
|
||||
def test_merchant_domain_defaults(self, db, test_merchant):
|
||||
"""Test default values for MerchantDomain."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"defaults{unique_id}.example.com",
|
||||
verification_token=f"dtoken_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(domain)
|
||||
|
||||
assert domain.is_primary is True
|
||||
assert domain.is_active is True
|
||||
assert domain.is_verified is False
|
||||
assert domain.ssl_status == "pending"
|
||||
|
||||
def test_merchant_domain_repr(self, db, test_merchant):
|
||||
"""Test string representation."""
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain="repr.example.com",
|
||||
)
|
||||
assert "repr.example.com" in repr(domain)
|
||||
assert str(test_merchant.id) in repr(domain)
|
||||
|
||||
def test_merchant_domain_full_url(self):
|
||||
"""Test full_url property."""
|
||||
domain = MerchantDomain(domain="test.example.com")
|
||||
assert domain.full_url == "https://test.example.com"
|
||||
|
||||
def test_normalize_domain_removes_protocol(self):
|
||||
"""Test normalize_domain strips protocols."""
|
||||
assert MerchantDomain.normalize_domain("https://example.com") == "example.com"
|
||||
assert MerchantDomain.normalize_domain("http://example.com") == "example.com"
|
||||
|
||||
def test_normalize_domain_removes_trailing_slash(self):
|
||||
"""Test normalize_domain strips trailing slashes."""
|
||||
assert MerchantDomain.normalize_domain("example.com/") == "example.com"
|
||||
|
||||
def test_normalize_domain_lowercases(self):
|
||||
"""Test normalize_domain converts to lowercase."""
|
||||
assert MerchantDomain.normalize_domain("EXAMPLE.COM") == "example.com"
|
||||
|
||||
def test_unique_domain_constraint(self, db, test_merchant):
|
||||
"""Test that domain must be unique across all merchant domains."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain_name = f"unique{unique_id}.example.com"
|
||||
|
||||
domain1 = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=domain_name,
|
||||
verification_token=f"t1_{unique_id}",
|
||||
)
|
||||
db.add(domain1)
|
||||
db.commit()
|
||||
|
||||
domain2 = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=domain_name,
|
||||
verification_token=f"t2_{unique_id}",
|
||||
)
|
||||
db.add(domain2)
|
||||
with pytest.raises(Exception): # IntegrityError
|
||||
db.commit()
|
||||
db.rollback()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MERCHANT.primary_domain PROPERTY TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.tenancy
|
||||
class TestMerchantPrimaryDomain:
|
||||
"""Test Merchant.primary_domain property."""
|
||||
|
||||
def test_primary_domain_returns_active_verified_primary(self, db, test_merchant):
|
||||
"""Test primary_domain returns domain when active, verified, and primary."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"primary{unique_id}.example.com",
|
||||
is_primary=True,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
verified_at=datetime.now(UTC),
|
||||
verification_token=f"pt_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(test_merchant)
|
||||
|
||||
assert test_merchant.primary_domain == f"primary{unique_id}.example.com"
|
||||
|
||||
def test_primary_domain_returns_none_when_no_domains(self, db, test_merchant):
|
||||
"""Test primary_domain returns None when merchant has no domains."""
|
||||
db.refresh(test_merchant)
|
||||
# Fresh merchant without any domains added in this test
|
||||
# Need to check if it may have domains from other fixtures
|
||||
# Just verify the property works without error
|
||||
result = test_merchant.primary_domain
|
||||
assert result is None or isinstance(result, str)
|
||||
|
||||
def test_primary_domain_returns_none_when_inactive(self, db, test_merchant):
|
||||
"""Test primary_domain returns None when domain is inactive."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"inactive{unique_id}.example.com",
|
||||
is_primary=True,
|
||||
is_active=False,
|
||||
is_verified=True,
|
||||
verified_at=datetime.now(UTC),
|
||||
verification_token=f"it_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(test_merchant)
|
||||
|
||||
assert test_merchant.primary_domain is None
|
||||
|
||||
def test_primary_domain_returns_none_when_unverified(self, db, test_merchant):
|
||||
"""Test primary_domain returns None when domain is unverified."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"unverified{unique_id}.example.com",
|
||||
is_primary=True,
|
||||
is_active=True,
|
||||
is_verified=False,
|
||||
verification_token=f"ut_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(test_merchant)
|
||||
|
||||
assert test_merchant.primary_domain is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# STORE.effective_domain PROPERTY TESTS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.tenancy
|
||||
class TestStoreEffectiveDomain:
|
||||
"""Test Store.effective_domain inheritance chain."""
|
||||
|
||||
def test_effective_domain_returns_store_domain_when_present(self, db, test_store):
|
||||
"""Test effective_domain returns store's own custom domain (highest priority)."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
store_domain = StoreDomain(
|
||||
store_id=test_store.id,
|
||||
domain=f"storeover{unique_id}.example.com",
|
||||
is_primary=True,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
verified_at=datetime.now(UTC),
|
||||
verification_token=f"sd_{unique_id}",
|
||||
)
|
||||
db.add(store_domain)
|
||||
db.commit()
|
||||
db.refresh(test_store)
|
||||
|
||||
assert test_store.effective_domain == f"storeover{unique_id}.example.com"
|
||||
|
||||
def test_effective_domain_returns_merchant_domain_when_no_store_domain(
|
||||
self, db, test_store, test_merchant
|
||||
):
|
||||
"""Test effective_domain returns merchant domain when no store domain."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
merchant_domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"merchant{unique_id}.example.com",
|
||||
is_primary=True,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
verified_at=datetime.now(UTC),
|
||||
verification_token=f"md_{unique_id}",
|
||||
)
|
||||
db.add(merchant_domain)
|
||||
db.commit()
|
||||
db.refresh(test_store)
|
||||
db.refresh(test_merchant)
|
||||
|
||||
assert test_store.effective_domain == f"merchant{unique_id}.example.com"
|
||||
|
||||
def test_effective_domain_returns_subdomain_fallback(self, db, test_store):
|
||||
"""Test effective_domain returns subdomain fallback when no custom domains."""
|
||||
db.refresh(test_store)
|
||||
# With no store or merchant domains, should fall back to subdomain
|
||||
result = test_store.effective_domain
|
||||
assert test_store.subdomain in result
|
||||
|
||||
def test_effective_domain_store_domain_overrides_merchant_domain(
|
||||
self, db, test_store, test_merchant
|
||||
):
|
||||
"""Test that store domain takes priority over merchant domain."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Add merchant domain
|
||||
merchant_domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"merchantpri{unique_id}.example.com",
|
||||
is_primary=True,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
verified_at=datetime.now(UTC),
|
||||
verification_token=f"mpri_{unique_id}",
|
||||
)
|
||||
db.add(merchant_domain)
|
||||
|
||||
# Add store domain (should take priority)
|
||||
store_domain = StoreDomain(
|
||||
store_id=test_store.id,
|
||||
domain=f"storepri{unique_id}.example.com",
|
||||
is_primary=True,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
verified_at=datetime.now(UTC),
|
||||
verification_token=f"spri_{unique_id}",
|
||||
)
|
||||
db.add(store_domain)
|
||||
db.commit()
|
||||
db.refresh(test_store)
|
||||
db.refresh(test_merchant)
|
||||
|
||||
assert test_store.effective_domain == f"storepri{unique_id}.example.com"
|
||||
Reference in New Issue
Block a user