"""Tests for CustomerService functionality.""" import pytest import pytest_asyncio from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from alpine_bits_python.customer_service import CustomerService from alpine_bits_python.db import Base, Customer, HashedCustomer @pytest_asyncio.fixture async def async_session(): """Create an async session for testing.""" engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async_session_maker = async_sessionmaker(engine, expire_on_commit=False) async with async_session_maker() as session: yield session await engine.dispose() @pytest.mark.asyncio async def test_create_customer_creates_hashed_version(async_session: AsyncSession): """Test that creating a customer automatically creates hashed version.""" service = CustomerService(async_session) customer_data = { "given_name": "John", "surname": "Doe", "contact_id": "test123", "email_address": "john@example.com", "phone": "+1234567890", } customer = await service.create_customer(customer_data) assert customer.id is not None assert customer.given_name == "John" # Check that hashed version was created hashed = await service.get_hashed_customer(customer.id) assert hashed is not None assert hashed.customer_id == customer.id assert hashed.hashed_email is not None assert hashed.hashed_phone is not None assert hashed.hashed_given_name is not None assert hashed.hashed_surname is not None @pytest.mark.asyncio async def test_update_customer_updates_hashed_version(async_session: AsyncSession): """Test that updating a customer updates the hashed version.""" service = CustomerService(async_session) # Create initial customer customer_data = { "given_name": "John", "surname": "Doe", "contact_id": "test123", "email_address": "john@example.com", } customer = await service.create_customer(customer_data) # Get initial hashed email hashed = await service.get_hashed_customer(customer.id) original_hashed_email = hashed.hashed_email # Update customer email update_data = {"email_address": "newemail@example.com"} updated_customer = await service.update_customer(customer, update_data) # Check that hashed version was updated updated_hashed = await service.get_hashed_customer(updated_customer.id) assert updated_hashed.hashed_email != original_hashed_email @pytest.mark.asyncio async def test_get_or_create_customer_creates_new(async_session: AsyncSession): """Test get_or_create creates new customer when not found.""" service = CustomerService(async_session) customer_data = { "given_name": "Jane", "surname": "Smith", "contact_id": "new123", "email_address": "jane@example.com", } customer = await service.get_or_create_customer(customer_data) assert customer.id is not None assert customer.contact_id == "new123" # Verify hashed version exists hashed = await service.get_hashed_customer(customer.id) assert hashed is not None @pytest.mark.asyncio async def test_get_or_create_customer_updates_existing(async_session: AsyncSession): """Test get_or_create updates existing customer when found.""" service = CustomerService(async_session) # Create initial customer customer_data = { "given_name": "Jane", "surname": "Smith", "contact_id": "existing123", "email_address": "jane@example.com", } original_customer = await service.create_customer(customer_data) # Try to create again with same contact_id but different data updated_data = { "given_name": "Janet", "surname": "Smith", "contact_id": "existing123", "email_address": "janet@example.com", } customer = await service.get_or_create_customer(updated_data) # Should be same customer ID but updated data assert customer.id == original_customer.id assert customer.given_name == "Janet" assert customer.email_address == "janet@example.com" @pytest.mark.asyncio async def test_hash_existing_customers_backfills(async_session: AsyncSession): """Test that hash_existing_customers backfills missing hashed data.""" # Create a customer directly in DB without using service customer = Customer( given_name="Bob", surname="Builder", contact_id="bob123", email_address="bob@example.com", phone="+9876543210", ) async_session.add(customer) await async_session.commit() await async_session.refresh(customer) # Verify no hashed version exists result = await async_session.execute( select(HashedCustomer).where(HashedCustomer.customer_id == customer.id) ) hashed = result.scalar_one_or_none() assert hashed is None # Run backfill service = CustomerService(async_session) count = await service.hash_existing_customers() assert count == 1 # Verify hashed version now exists result = await async_session.execute( select(HashedCustomer).where(HashedCustomer.customer_id == customer.id) ) hashed = result.scalar_one_or_none() assert hashed is not None assert hashed.hashed_email is not None @pytest.mark.asyncio async def test_hash_existing_customers_skips_already_hashed( async_session: AsyncSession, ): """Test that hash_existing_customers skips customers already hashed.""" service = CustomerService(async_session) # Create customer with service (creates hashed version) customer_data = { "given_name": "Alice", "surname": "Wonder", "contact_id": "alice123", "email_address": "alice@example.com", } await service.create_customer(customer_data) # Run backfill - should return 0 since customer is already hashed count = await service.hash_existing_customers() assert count == 0 @pytest.mark.asyncio async def test_hashing_normalization(async_session: AsyncSession): """Test that hashing properly normalizes data.""" service = CustomerService(async_session) # Create customer with mixed case and spaces customer_data = { "given_name": " John ", "surname": "DOE", "contact_id": "test123", "email_address": " John.Doe@Example.COM ", "phone": "+1 (234) 567-8900", } customer = await service.create_customer(customer_data) hashed = await service.get_hashed_customer(customer.id) # Verify hashes exist (normalization should have occurred) assert hashed.hashed_email is not None assert hashed.hashed_phone is not None # Hash should be consistent for same normalized value from alpine_bits_python.db import Customer as CustomerModel normalized_email_hash = CustomerModel._normalize_and_hash( "john.doe@example.com" ) assert hashed.hashed_email == normalized_email_hash @pytest.mark.asyncio async def test_hash_existing_customers_normalizes_country_code( async_session: AsyncSession, ): """Test that hash_existing_customers normalizes invalid country codes. Specifically tests the case where a customer was saved with a full country name (e.g., "Italy") instead of the ISO 3166-1 alpha-2 code (e.g., "IT"). The validation should convert "Italy" to "IT", which is then hashed. """ # Create a customer directly in DB with invalid country code "Italy" # This simulates a customer that was saved before validation was implemented customer = Customer( given_name="Marco", surname="Rossi", contact_id="marco123", email_address="marco@example.com", phone="+39123456789", country_code="Italy", # Invalid - should be "IT" city_name="Rome", postal_code="00100", ) async_session.add(customer) await async_session.commit() await async_session.refresh(customer) # Verify no hashed version exists yet result = await async_session.execute( select(HashedCustomer).where(HashedCustomer.customer_id == customer.id) ) hashed = result.scalar_one_or_none() assert hashed is None # Verify the customer has the invalid country code stored in the DB assert customer.country_code == "Italy" # Run hash_existing_customers - this should normalize "Italy" to "IT" # during validation and successfully create a hashed customer service = CustomerService(async_session) count = await service.hash_existing_customers() # Should successfully hash this customer (country code normalized during validation) assert count == 1 # Verify hashed version was created await async_session.refresh(customer) result = await async_session.execute( select(HashedCustomer).where(HashedCustomer.customer_id == customer.id) ) hashed = result.scalar_one_or_none() assert hashed is not None # The hashed customer should have the hashed version of normalized country code "IT" # "IT" -> lowercase "it" -> sha256 hash expected_hash = "2ad8a7049d7c5511ac254f5f51fe70a046ebd884729056f0fe57f5160d467153" assert hashed.hashed_country_code == expected_hash # Note: The original customer's country_code in DB remains "Italy" unchanged # Now let's test the case where we have a valid 2-letter code # but in the wrong case (should be normalized to uppercase) customer2 = Customer( given_name="Maria", surname="Bianchi", contact_id="maria123", email_address="maria@example.com", phone="+39987654321", country_code="it", # Valid but lowercase - should be normalized to "IT" city_name="Milan", postal_code="20100", ) async_session.add(customer2) await async_session.commit() await async_session.refresh(customer2) # Run hash_existing_customers again count = await service.hash_existing_customers() # Should hash only the second customer (first one already hashed) assert count == 1 # Verify the customer's country_code was normalized to uppercase await async_session.refresh(customer2) assert customer2.country_code == "IT" # Verify hashed version was created with correct hash result = await async_session.execute( select(HashedCustomer).where(HashedCustomer.customer_id == customer2.id) ) hashed = result.scalar_one_or_none() assert hashed is not None # Verify the hashed country code matches the expected hash # "IT" -> lowercase "it" -> sha256 hash expected_hash = "2ad8a7049d7c5511ac254f5f51fe70a046ebd884729056f0fe57f5160d467153" assert hashed.hashed_country_code == expected_hash # For comparison, verify that "italy" would produce a different hash italy_hash = "93ff074ca77b5f7e61e63320b615081149b818863599189b0e356ec3889d51f7" assert hashed.hashed_country_code != italy_hash