"""Unit tests for validation logic.""" import pytest from src.validation import ( ValidationError, validate_email, validate_phone, validate_member_type_requirements, ) class TestEmailValidation: """Tests for validate_email function.""" def test_valid_email(self): """Test that valid email passes validation.""" validate_email("test@example.com") # No exception = pass def test_valid_email_with_subdomain(self): """Test that email with subdomain passes validation.""" validate_email("user@mail.example.com") def test_valid_email_with_plus(self): """Test that email with plus sign passes validation.""" validate_email("user+tag@example.com") def test_invalid_email_no_at(self): """Test that email without @ raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid email format"): validate_email("notanemail") def test_invalid_email_no_domain(self): """Test that email without domain raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid email format"): validate_email("user@") def test_invalid_email_no_tld(self): """Test that email without TLD raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid email format"): validate_email("user@example") def test_none_is_allowed(self): """Test that None passes validation (optional field).""" validate_email(None) # No exception = pass def test_empty_string_is_allowed(self): """Test that empty string passes validation (optional field).""" validate_email("") # No exception = pass class TestPhoneValidation: """Tests for validate_phone function.""" def test_valid_phone_with_plus(self): """Test that valid E.164 phone with + passes validation.""" validate_phone("+41791234567") def test_valid_phone_without_plus(self): """Test that valid E.164 phone without + passes validation.""" validate_phone("41791234567") def test_valid_us_phone(self): """Test that valid US phone passes validation.""" validate_phone("+14155551234") def test_invalid_phone_with_invalid_chars(self): """Test that phone with invalid characters raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid phone format"): validate_phone("abc123") def test_invalid_phone_too_long(self): """Test that phone too long raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid phone format"): validate_phone("+123456789012345678") # More than 15 digits def test_invalid_phone_starts_with_zero(self): """Test that phone starting with 0 raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid phone format"): validate_phone("+0791234567") def test_invalid_phone_has_letters(self): """Test that phone with letters raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid phone format"): validate_phone("+41abc1234567") def test_none_is_allowed(self): """Test that None passes validation (optional field).""" validate_phone(None) # No exception = pass def test_empty_string_is_allowed(self): """Test that empty string passes validation (optional field).""" validate_phone("") # No exception = pass class TestMemberTypeRequirements: """Tests for validate_member_type_requirements function.""" def test_individual_with_first_name_valid(self): """Test INDIVIDUAL with firstName passes.""" validate_member_type_requirements( member_type="INDIVIDUAL", first_name="John", company_name=None ) def test_individual_without_first_name_invalid(self): """Test INDIVIDUAL without firstName fails.""" with pytest.raises(ValidationError, match="firstName is required for individual members"): validate_member_type_requirements( member_type="INDIVIDUAL", first_name=None, company_name=None ) def test_individual_with_empty_first_name_invalid(self): """Test INDIVIDUAL with empty firstName fails.""" with pytest.raises(ValidationError, match="firstName is required for individual members"): validate_member_type_requirements( member_type="INDIVIDUAL", first_name="", company_name=None ) def test_individual_with_whitespace_first_name_invalid(self): """Test INDIVIDUAL with whitespace-only firstName fails.""" with pytest.raises(ValidationError, match="firstName is required for individual members"): validate_member_type_requirements( member_type="INDIVIDUAL", first_name=" ", company_name=None ) def test_organization_with_company_name_valid(self): """Test ORGANIZATION with companyName passes.""" validate_member_type_requirements( member_type="ORGANIZATION", first_name=None, company_name="Acme Corp" ) def test_organization_without_company_name_invalid(self): """Test ORGANIZATION without companyName fails.""" with pytest.raises(ValidationError, match="companyName is required for organization members"): validate_member_type_requirements( member_type="ORGANIZATION", first_name=None, company_name=None ) def test_organization_with_empty_company_name_invalid(self): """Test ORGANIZATION with empty companyName fails.""" with pytest.raises(ValidationError, match="companyName is required for organization members"): validate_member_type_requirements( member_type="ORGANIZATION", first_name=None, company_name="" ) def test_organization_with_whitespace_company_name_invalid(self): """Test ORGANIZATION with whitespace-only companyName fails.""" with pytest.raises(ValidationError, match="companyName is required for organization members"): validate_member_type_requirements( member_type="ORGANIZATION", first_name=None, company_name=" " ) def test_invalid_member_type_raises_error(self): """Test invalid member type raises ValidationError.""" with pytest.raises(ValidationError, match="Invalid member type"): validate_member_type_requirements( member_type="INVALID", first_name="John", company_name=None ) def test_organization_can_have_first_name_optional(self): """Test ORGANIZATION can optionally have firstName.""" validate_member_type_requirements( member_type="ORGANIZATION", first_name="John Doe", company_name="Acme Corp" ) def test_individual_can_have_company_name_optional(self): """Test INDIVIDUAL can optionally have companyName (edge case).""" validate_member_type_requirements( member_type="INDIVIDUAL", first_name="John", company_name="Acme Corp" )