Rename "shop" to "storefront" as not all platforms sell items - storefront is a more accurate term for the customer-facing interface. Changes: - Rename app/api/v1/shop/ → app/api/v1/storefront/ - Rename app/routes/shop_pages.py → app/routes/storefront_pages.py - Rename app/modules/cms/routes/api/shop.py → storefront.py - Rename tests/integration/api/v1/shop/ → storefront/ - Update API prefix from /api/v1/shop to /api/v1/storefront - Update route tags from shop-* to storefront-* - Rename get_shop_context() → get_storefront_context() - Update architecture rules to reference storefront paths - Update all test API endpoint paths This is Phase 2 of the storefront module restructure plan. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
622 lines
21 KiB
Python
622 lines
21 KiB
Python
# tests/integration/api/v1/storefront/test_addresses.py
|
|
"""Integration tests for shop addresses API endpoints.
|
|
|
|
Tests the /api/v1/storefront/addresses/* endpoints.
|
|
All endpoints require customer JWT authentication with vendor context.
|
|
"""
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from jose import jwt
|
|
|
|
from app.modules.customers.models.customer import Customer, CustomerAddress
|
|
|
|
|
|
@pytest.fixture
|
|
def shop_customer(db, test_vendor):
|
|
"""Create a test customer for shop API tests."""
|
|
from middleware.auth import AuthManager
|
|
|
|
auth_manager = AuthManager()
|
|
customer = Customer(
|
|
vendor_id=test_vendor.id,
|
|
email="shopcustomer@example.com",
|
|
hashed_password=auth_manager.hash_password("testpass123"),
|
|
first_name="Shop",
|
|
last_name="Customer",
|
|
customer_number="SHOP001",
|
|
is_active=True,
|
|
)
|
|
db.add(customer)
|
|
db.commit()
|
|
db.refresh(customer)
|
|
return customer
|
|
|
|
|
|
@pytest.fixture
|
|
def shop_customer_token(shop_customer, test_vendor):
|
|
"""Create JWT token for shop customer."""
|
|
from middleware.auth import AuthManager
|
|
|
|
auth_manager = AuthManager()
|
|
|
|
expires_delta = timedelta(minutes=auth_manager.token_expire_minutes)
|
|
expire = datetime.now(UTC) + expires_delta
|
|
|
|
payload = {
|
|
"sub": str(shop_customer.id),
|
|
"email": shop_customer.email,
|
|
"vendor_id": test_vendor.id,
|
|
"type": "customer",
|
|
"exp": expire,
|
|
"iat": datetime.now(UTC),
|
|
}
|
|
|
|
token = jwt.encode(
|
|
payload, auth_manager.secret_key, algorithm=auth_manager.algorithm
|
|
)
|
|
return token
|
|
|
|
|
|
@pytest.fixture
|
|
def shop_customer_headers(shop_customer_token):
|
|
"""Get authentication headers for shop customer."""
|
|
return {"Authorization": f"Bearer {shop_customer_token}"}
|
|
|
|
|
|
@pytest.fixture
|
|
def customer_address(db, test_vendor, shop_customer):
|
|
"""Create a test address for shop customer."""
|
|
address = CustomerAddress(
|
|
vendor_id=test_vendor.id,
|
|
customer_id=shop_customer.id,
|
|
address_type="shipping",
|
|
first_name="Ship",
|
|
last_name="Address",
|
|
address_line_1="123 Shipping St",
|
|
city="Luxembourg",
|
|
postal_code="L-1234",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=True,
|
|
)
|
|
db.add(address)
|
|
db.commit()
|
|
db.refresh(address)
|
|
return address
|
|
|
|
|
|
@pytest.fixture
|
|
def customer_billing_address(db, test_vendor, shop_customer):
|
|
"""Create a billing address for shop customer."""
|
|
address = CustomerAddress(
|
|
vendor_id=test_vendor.id,
|
|
customer_id=shop_customer.id,
|
|
address_type="billing",
|
|
first_name="Bill",
|
|
last_name="Address",
|
|
company="Test Company",
|
|
address_line_1="456 Billing Ave",
|
|
city="Esch-sur-Alzette",
|
|
postal_code="L-5678",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=True,
|
|
)
|
|
db.add(address)
|
|
db.commit()
|
|
db.refresh(address)
|
|
return address
|
|
|
|
|
|
@pytest.fixture
|
|
def other_customer(db, test_vendor):
|
|
"""Create another customer for testing access controls."""
|
|
from middleware.auth import AuthManager
|
|
|
|
auth_manager = AuthManager()
|
|
customer = Customer(
|
|
vendor_id=test_vendor.id,
|
|
email="othercustomer@example.com",
|
|
hashed_password=auth_manager.hash_password("otherpass123"),
|
|
first_name="Other",
|
|
last_name="Customer",
|
|
customer_number="OTHER001",
|
|
is_active=True,
|
|
)
|
|
db.add(customer)
|
|
db.commit()
|
|
db.refresh(customer)
|
|
return customer
|
|
|
|
|
|
@pytest.fixture
|
|
def other_customer_address(db, test_vendor, other_customer):
|
|
"""Create an address for another customer."""
|
|
address = CustomerAddress(
|
|
vendor_id=test_vendor.id,
|
|
customer_id=other_customer.id,
|
|
address_type="shipping",
|
|
first_name="Other",
|
|
last_name="Address",
|
|
address_line_1="999 Other St",
|
|
city="Differdange",
|
|
postal_code="L-9999",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=True,
|
|
)
|
|
db.add(address)
|
|
db.commit()
|
|
db.refresh(address)
|
|
return address
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
@pytest.mark.shop
|
|
class TestShopAddressesListAPI:
|
|
"""Test shop addresses list endpoint at /api/v1/storefront/addresses."""
|
|
|
|
def test_list_addresses_requires_authentication(self, client, test_vendor):
|
|
"""Test that listing addresses requires authentication."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
response = client.get("/api/v1/storefront/addresses")
|
|
assert response.status_code in [401, 403]
|
|
|
|
def test_list_addresses_success(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test listing customer addresses successfully."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.get(
|
|
"/api/v1/storefront/addresses",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "addresses" in data
|
|
assert "total" in data
|
|
assert data["total"] == 1
|
|
assert data["addresses"][0]["first_name"] == "Ship"
|
|
|
|
def test_list_addresses_empty(
|
|
self, client, shop_customer_headers, test_vendor, shop_customer
|
|
):
|
|
"""Test listing addresses when customer has none."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.get(
|
|
"/api/v1/storefront/addresses",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 0
|
|
assert data["addresses"] == []
|
|
|
|
def test_list_addresses_multiple_types(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
customer_address,
|
|
customer_billing_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test listing addresses includes both shipping and billing."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.get(
|
|
"/api/v1/storefront/addresses",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 2
|
|
|
|
types = {addr["address_type"] for addr in data["addresses"]}
|
|
assert "shipping" in types
|
|
assert "billing" in types
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
@pytest.mark.shop
|
|
class TestShopAddressDetailAPI:
|
|
"""Test shop address detail endpoint at /api/v1/storefront/addresses/{address_id}."""
|
|
|
|
def test_get_address_success(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test getting address details successfully."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.get(
|
|
f"/api/v1/storefront/addresses/{customer_address.id}",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == customer_address.id
|
|
assert data["first_name"] == "Ship"
|
|
assert data["country_iso"] == "LU"
|
|
assert data["country_name"] == "Luxembourg"
|
|
|
|
def test_get_address_not_found(
|
|
self, client, shop_customer_headers, test_vendor, shop_customer
|
|
):
|
|
"""Test getting non-existent address returns 404."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.get(
|
|
"/api/v1/storefront/addresses/99999",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_get_address_other_customer(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
other_customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test cannot access another customer's address."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.get(
|
|
f"/api/v1/storefront/addresses/{other_customer_address.id}",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
# Should return 404 to prevent enumeration
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
@pytest.mark.shop
|
|
class TestShopAddressCreateAPI:
|
|
"""Test shop address creation at POST /api/v1/storefront/addresses."""
|
|
|
|
def test_create_address_success(
|
|
self, client, shop_customer_headers, test_vendor, shop_customer
|
|
):
|
|
"""Test creating a new address."""
|
|
address_data = {
|
|
"address_type": "shipping",
|
|
"first_name": "New",
|
|
"last_name": "Address",
|
|
"address_line_1": "789 New St",
|
|
"city": "Luxembourg",
|
|
"postal_code": "L-1111",
|
|
"country_name": "Luxembourg",
|
|
"country_iso": "LU",
|
|
"is_default": False,
|
|
}
|
|
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.post(
|
|
"/api/v1/storefront/addresses",
|
|
headers=shop_customer_headers,
|
|
json=address_data,
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["first_name"] == "New"
|
|
assert data["last_name"] == "Address"
|
|
assert data["country_iso"] == "LU"
|
|
assert "id" in data
|
|
|
|
def test_create_address_with_company(
|
|
self, client, shop_customer_headers, test_vendor, shop_customer
|
|
):
|
|
"""Test creating address with company name."""
|
|
address_data = {
|
|
"address_type": "billing",
|
|
"first_name": "Business",
|
|
"last_name": "Address",
|
|
"company": "Acme Corp",
|
|
"address_line_1": "100 Business Park",
|
|
"city": "Luxembourg",
|
|
"postal_code": "L-2222",
|
|
"country_name": "Luxembourg",
|
|
"country_iso": "LU",
|
|
"is_default": True,
|
|
}
|
|
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.post(
|
|
"/api/v1/storefront/addresses",
|
|
headers=shop_customer_headers,
|
|
json=address_data,
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["company"] == "Acme Corp"
|
|
|
|
def test_create_address_validation_error(
|
|
self, client, shop_customer_headers, test_vendor, shop_customer
|
|
):
|
|
"""Test validation error for missing required fields."""
|
|
address_data = {
|
|
"address_type": "shipping",
|
|
"first_name": "Test",
|
|
# Missing required fields
|
|
}
|
|
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.post(
|
|
"/api/v1/storefront/addresses",
|
|
headers=shop_customer_headers,
|
|
json=address_data,
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
@pytest.mark.shop
|
|
class TestShopAddressUpdateAPI:
|
|
"""Test shop address update at PUT /api/v1/storefront/addresses/{address_id}."""
|
|
|
|
def test_update_address_success(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test updating an address."""
|
|
update_data = {
|
|
"first_name": "Updated",
|
|
"city": "Esch-sur-Alzette",
|
|
}
|
|
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.put(
|
|
f"/api/v1/storefront/addresses/{customer_address.id}",
|
|
headers=shop_customer_headers,
|
|
json=update_data,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["first_name"] == "Updated"
|
|
assert data["city"] == "Esch-sur-Alzette"
|
|
|
|
def test_update_address_not_found(
|
|
self, client, shop_customer_headers, test_vendor, shop_customer
|
|
):
|
|
"""Test updating non-existent address returns 404."""
|
|
update_data = {"first_name": "Test"}
|
|
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.put(
|
|
"/api/v1/storefront/addresses/99999",
|
|
headers=shop_customer_headers,
|
|
json=update_data,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_update_address_other_customer(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
other_customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test cannot update another customer's address."""
|
|
update_data = {"first_name": "Hacked"}
|
|
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.put(
|
|
f"/api/v1/storefront/addresses/{other_customer_address.id}",
|
|
headers=shop_customer_headers,
|
|
json=update_data,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
@pytest.mark.shop
|
|
class TestShopAddressDeleteAPI:
|
|
"""Test shop address deletion at DELETE /api/v1/storefront/addresses/{address_id}."""
|
|
|
|
def test_delete_address_success(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test deleting an address."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.delete(
|
|
f"/api/v1/storefront/addresses/{customer_address.id}",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 204
|
|
|
|
def test_delete_address_not_found(
|
|
self, client, shop_customer_headers, test_vendor, shop_customer
|
|
):
|
|
"""Test deleting non-existent address returns 404."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.delete(
|
|
"/api/v1/storefront/addresses/99999",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_delete_address_other_customer(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
other_customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test cannot delete another customer's address."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.delete(
|
|
f"/api/v1/storefront/addresses/{other_customer_address.id}",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.api
|
|
@pytest.mark.shop
|
|
class TestShopAddressSetDefaultAPI:
|
|
"""Test set address as default at PUT /api/v1/storefront/addresses/{address_id}/default."""
|
|
|
|
def test_set_default_success(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
db,
|
|
):
|
|
"""Test setting address as default."""
|
|
# Create a second non-default address
|
|
second_address = CustomerAddress(
|
|
vendor_id=test_vendor.id,
|
|
customer_id=shop_customer.id,
|
|
address_type="shipping",
|
|
first_name="Second",
|
|
last_name="Address",
|
|
address_line_1="222 Second St",
|
|
city="Dudelange",
|
|
postal_code="L-3333",
|
|
country_name="Luxembourg",
|
|
country_iso="LU",
|
|
is_default=False,
|
|
)
|
|
db.add(second_address)
|
|
db.commit()
|
|
db.refresh(second_address)
|
|
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.put(
|
|
f"/api/v1/storefront/addresses/{second_address.id}/default",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["is_default"] is True
|
|
|
|
def test_set_default_not_found(
|
|
self, client, shop_customer_headers, test_vendor, shop_customer
|
|
):
|
|
"""Test setting default on non-existent address returns 404."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.put(
|
|
"/api/v1/storefront/addresses/99999/default",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_set_default_other_customer(
|
|
self,
|
|
client,
|
|
shop_customer_headers,
|
|
other_customer_address,
|
|
test_vendor,
|
|
shop_customer,
|
|
):
|
|
"""Test cannot set default on another customer's address."""
|
|
with patch("app.api.v1.shop.addresses.getattr") as mock_getattr:
|
|
mock_getattr.return_value = test_vendor
|
|
with patch("app.api.deps._validate_customer_token") as mock_validate:
|
|
mock_validate.return_value = shop_customer
|
|
response = client.put(
|
|
f"/api/v1/storefront/addresses/{other_customer_address.id}/default",
|
|
headers=shop_customer_headers,
|
|
)
|
|
|
|
assert response.status_code == 404
|