# 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