- Add CustomerAddressService with CRUD operations - Add shop API endpoints for address management (GET, POST, PUT, DELETE) - Add set default endpoint for address type - Implement addresses.html with full UI (cards, modals, Alpine.js) - Integrate saved addresses in checkout flow - Address selector dropdowns for shipping/billing - Auto-select default addresses - Save new address checkbox option - Add country_iso field alongside country_name - Add address exceptions (NotFound, LimitExceeded, InvalidType) - Max 10 addresses per customer limit - One default address per type (shipping/billing) - Add unit tests for CustomerAddressService - Add integration tests for shop addresses API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
454 lines
14 KiB
Python
454 lines
14 KiB
Python
# tests/unit/services/test_customer_address_service.py
|
|
"""
|
|
Unit tests for CustomerAddressService.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.exceptions import AddressLimitExceededException, AddressNotFoundException
|
|
from app.services.customer_address_service import CustomerAddressService
|
|
from models.database.customer import CustomerAddress
|
|
from models.schema.customer import CustomerAddressCreate, CustomerAddressUpdate
|
|
|
|
|
|
@pytest.fixture
|
|
def address_service():
|
|
"""Create CustomerAddressService instance."""
|
|
return CustomerAddressService()
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_addresses(db, test_vendor, test_customer):
|
|
"""Create multiple addresses for testing."""
|
|
addresses = []
|
|
for i in range(3):
|
|
address = CustomerAddress(
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_type="shipping" if i < 2 else "billing",
|
|
first_name=f"First{i}",
|
|
last_name=f"Last{i}",
|
|
address_line_1=f"{i+1} Test Street",
|
|
city="Luxembourg",
|
|
postal_code=f"L-{1000+i}",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=(i == 0), # First shipping is default
|
|
)
|
|
db.add(address)
|
|
addresses.append(address)
|
|
|
|
db.commit()
|
|
for a in addresses:
|
|
db.refresh(a)
|
|
|
|
return addresses
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCustomerAddressServiceList:
|
|
"""Tests for list_addresses method."""
|
|
|
|
def test_list_addresses_empty(self, db, address_service, test_vendor, test_customer):
|
|
"""Test listing addresses when none exist."""
|
|
addresses = address_service.list_addresses(
|
|
db, vendor_id=test_vendor.id, customer_id=test_customer.id
|
|
)
|
|
|
|
assert addresses == []
|
|
|
|
def test_list_addresses_basic(
|
|
self, db, address_service, test_vendor, test_customer, test_customer_address
|
|
):
|
|
"""Test basic address listing."""
|
|
addresses = address_service.list_addresses(
|
|
db, vendor_id=test_vendor.id, customer_id=test_customer.id
|
|
)
|
|
|
|
assert len(addresses) == 1
|
|
assert addresses[0].id == test_customer_address.id
|
|
|
|
def test_list_addresses_ordered_by_default(
|
|
self, db, address_service, test_vendor, test_customer, multiple_addresses
|
|
):
|
|
"""Test addresses are ordered by default flag first."""
|
|
addresses = address_service.list_addresses(
|
|
db, vendor_id=test_vendor.id, customer_id=test_customer.id
|
|
)
|
|
|
|
# Default address should be first
|
|
assert addresses[0].is_default is True
|
|
|
|
def test_list_addresses_vendor_isolation(
|
|
self, db, address_service, test_vendor, test_customer, test_customer_address
|
|
):
|
|
"""Test addresses are isolated by vendor."""
|
|
# Query with different vendor ID
|
|
addresses = address_service.list_addresses(
|
|
db, vendor_id=99999, customer_id=test_customer.id
|
|
)
|
|
|
|
assert addresses == []
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCustomerAddressServiceGet:
|
|
"""Tests for get_address method."""
|
|
|
|
def test_get_address_success(
|
|
self, db, address_service, test_vendor, test_customer, test_customer_address
|
|
):
|
|
"""Test getting address by ID."""
|
|
address = address_service.get_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=test_customer_address.id,
|
|
)
|
|
|
|
assert address.id == test_customer_address.id
|
|
assert address.first_name == test_customer_address.first_name
|
|
|
|
def test_get_address_not_found(
|
|
self, db, address_service, test_vendor, test_customer
|
|
):
|
|
"""Test error when address not found."""
|
|
with pytest.raises(AddressNotFoundException):
|
|
address_service.get_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=99999,
|
|
)
|
|
|
|
def test_get_address_wrong_customer(
|
|
self, db, address_service, test_vendor, test_customer, test_customer_address
|
|
):
|
|
"""Test cannot get another customer's address."""
|
|
with pytest.raises(AddressNotFoundException):
|
|
address_service.get_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=99999, # Different customer
|
|
address_id=test_customer_address.id,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCustomerAddressServiceGetDefault:
|
|
"""Tests for get_default_address method."""
|
|
|
|
def test_get_default_address_exists(
|
|
self, db, address_service, test_vendor, test_customer, multiple_addresses
|
|
):
|
|
"""Test getting default shipping address."""
|
|
address = address_service.get_default_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_type="shipping",
|
|
)
|
|
|
|
assert address is not None
|
|
assert address.is_default is True
|
|
assert address.address_type == "shipping"
|
|
|
|
def test_get_default_address_not_set(
|
|
self, db, address_service, test_vendor, test_customer, multiple_addresses
|
|
):
|
|
"""Test getting default billing when none is set."""
|
|
# Remove default from billing (none was set as default)
|
|
address = address_service.get_default_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_type="billing",
|
|
)
|
|
|
|
# The billing address exists but is not default
|
|
assert address is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCustomerAddressServiceCreate:
|
|
"""Tests for create_address method."""
|
|
|
|
def test_create_address_success(
|
|
self, db, address_service, test_vendor, test_customer
|
|
):
|
|
"""Test creating a new address."""
|
|
address_data = CustomerAddressCreate(
|
|
address_type="shipping",
|
|
first_name="John",
|
|
last_name="Doe",
|
|
address_line_1="123 New Street",
|
|
city="Luxembourg",
|
|
postal_code="L-1234",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=False,
|
|
)
|
|
|
|
address = address_service.create_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_data=address_data,
|
|
)
|
|
db.commit()
|
|
|
|
assert address.id is not None
|
|
assert address.first_name == "John"
|
|
assert address.last_name == "Doe"
|
|
assert address.country_iso == "LU"
|
|
assert address.country_name == "Luxembourg"
|
|
|
|
def test_create_address_with_company(
|
|
self, db, address_service, test_vendor, test_customer
|
|
):
|
|
"""Test creating address with company name."""
|
|
address_data = CustomerAddressCreate(
|
|
address_type="billing",
|
|
first_name="Jane",
|
|
last_name="Doe",
|
|
company="Acme Corp",
|
|
address_line_1="456 Business Ave",
|
|
city="Luxembourg",
|
|
postal_code="L-5678",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=False,
|
|
)
|
|
|
|
address = address_service.create_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_data=address_data,
|
|
)
|
|
db.commit()
|
|
|
|
assert address.company == "Acme Corp"
|
|
|
|
def test_create_address_default_clears_others(
|
|
self, db, address_service, test_vendor, test_customer, multiple_addresses
|
|
):
|
|
"""Test creating default address clears other defaults of same type."""
|
|
# First address is default shipping
|
|
assert multiple_addresses[0].is_default is True
|
|
|
|
address_data = CustomerAddressCreate(
|
|
address_type="shipping",
|
|
first_name="New",
|
|
last_name="Default",
|
|
address_line_1="789 Main St",
|
|
city="Luxembourg",
|
|
postal_code="L-9999",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=True,
|
|
)
|
|
|
|
new_address = address_service.create_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_data=address_data,
|
|
)
|
|
db.commit()
|
|
|
|
# New address should be default
|
|
assert new_address.is_default is True
|
|
|
|
# Old default should be cleared
|
|
db.refresh(multiple_addresses[0])
|
|
assert multiple_addresses[0].is_default is False
|
|
|
|
def test_create_address_limit_exceeded(
|
|
self, db, address_service, test_vendor, test_customer
|
|
):
|
|
"""Test error when max addresses reached."""
|
|
# Create 10 addresses (max limit)
|
|
for i in range(10):
|
|
addr = CustomerAddress(
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_type="shipping",
|
|
first_name=f"Test{i}",
|
|
last_name="User",
|
|
address_line_1=f"{i} Street",
|
|
city="City",
|
|
postal_code="12345",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
)
|
|
db.add(addr)
|
|
db.commit()
|
|
|
|
# Try to create 11th address
|
|
address_data = CustomerAddressCreate(
|
|
address_type="shipping",
|
|
first_name="Eleventh",
|
|
last_name="User",
|
|
address_line_1="11 Street",
|
|
city="City",
|
|
postal_code="12345",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=False,
|
|
)
|
|
|
|
with pytest.raises(AddressLimitExceededException):
|
|
address_service.create_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_data=address_data,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCustomerAddressServiceUpdate:
|
|
"""Tests for update_address method."""
|
|
|
|
def test_update_address_success(
|
|
self, db, address_service, test_vendor, test_customer, test_customer_address
|
|
):
|
|
"""Test updating an address."""
|
|
update_data = CustomerAddressUpdate(
|
|
first_name="Updated",
|
|
city="New City",
|
|
)
|
|
|
|
address = address_service.update_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=test_customer_address.id,
|
|
address_data=update_data,
|
|
)
|
|
db.commit()
|
|
|
|
assert address.first_name == "Updated"
|
|
assert address.city == "New City"
|
|
# Unchanged fields should remain
|
|
assert address.last_name == test_customer_address.last_name
|
|
|
|
def test_update_address_set_default(
|
|
self, db, address_service, test_vendor, test_customer, multiple_addresses
|
|
):
|
|
"""Test setting address as default clears others."""
|
|
# Second address is not default
|
|
assert multiple_addresses[1].is_default is False
|
|
|
|
update_data = CustomerAddressUpdate(is_default=True)
|
|
|
|
address = address_service.update_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=multiple_addresses[1].id,
|
|
address_data=update_data,
|
|
)
|
|
db.commit()
|
|
|
|
assert address.is_default is True
|
|
|
|
# Old default should be cleared
|
|
db.refresh(multiple_addresses[0])
|
|
assert multiple_addresses[0].is_default is False
|
|
|
|
def test_update_address_not_found(
|
|
self, db, address_service, test_vendor, test_customer
|
|
):
|
|
"""Test error when address not found."""
|
|
update_data = CustomerAddressUpdate(first_name="Test")
|
|
|
|
with pytest.raises(AddressNotFoundException):
|
|
address_service.update_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=99999,
|
|
address_data=update_data,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCustomerAddressServiceDelete:
|
|
"""Tests for delete_address method."""
|
|
|
|
def test_delete_address_success(
|
|
self, db, address_service, test_vendor, test_customer, test_customer_address
|
|
):
|
|
"""Test deleting an address."""
|
|
address_id = test_customer_address.id
|
|
|
|
address_service.delete_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=address_id,
|
|
)
|
|
db.commit()
|
|
|
|
# Address should be gone
|
|
with pytest.raises(AddressNotFoundException):
|
|
address_service.get_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=address_id,
|
|
)
|
|
|
|
def test_delete_address_not_found(
|
|
self, db, address_service, test_vendor, test_customer
|
|
):
|
|
"""Test error when deleting non-existent address."""
|
|
with pytest.raises(AddressNotFoundException):
|
|
address_service.delete_address(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=99999,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCustomerAddressServiceSetDefault:
|
|
"""Tests for set_default method."""
|
|
|
|
def test_set_default_success(
|
|
self, db, address_service, test_vendor, test_customer, multiple_addresses
|
|
):
|
|
"""Test setting address as default."""
|
|
# Second shipping address is not default
|
|
assert multiple_addresses[1].is_default is False
|
|
assert multiple_addresses[1].address_type == "shipping"
|
|
|
|
address = address_service.set_default(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=multiple_addresses[1].id,
|
|
)
|
|
db.commit()
|
|
|
|
assert address.is_default is True
|
|
|
|
# Old default should be cleared
|
|
db.refresh(multiple_addresses[0])
|
|
assert multiple_addresses[0].is_default is False
|
|
|
|
def test_set_default_not_found(
|
|
self, db, address_service, test_vendor, test_customer
|
|
):
|
|
"""Test error when address not found."""
|
|
with pytest.raises(AddressNotFoundException):
|
|
address_service.set_default(
|
|
db,
|
|
vendor_id=test_vendor.id,
|
|
customer_id=test_customer.id,
|
|
address_id=99999,
|
|
)
|