import pytest from notifications_utils.recipients import ( InvalidEmailError, InvalidPhoneError, allowed_to_send_to, format_phone_number_human_readable, format_recipient, get_international_phone_info, international_phone_info, is_us_phone_number, show_mangled_number_clues, try_validate_and_format_phone_number, validate_and_format_phone_number, validate_email_address, validate_phone_number, ) valid_us_phone_numbers = [ "1-202-555-0104", "+12025550104", "12025550104", "2025550104", "(202) 555-0104", ] valid_international_phone_numbers = [ "+71234567890", # Russia "+447123456789", # UK "+4407123456789", # UK "+4407123 456789", # UK "+4407123-456-789", # UK "+23051234567", # Mauritius, "+682 12345", # Cook islands "+3312345678", "+9-2345-12345-12345", # 15 digits ] valid_phone_numbers = valid_us_phone_numbers + valid_international_phone_numbers invalid_us_phone_numbers = sum( [ [(phone_number, error) for phone_number in group] for error, group in [ ( "Too many digits", ( "55512345678", "+155512345678", "(555) 1234-5678", ), ), ( "Not enough digits", ( "555123123", "(555) 123-123", "7890x32109", "07123 ☟☜⬇⬆☞☝", "07123☟☜⬇⬆☞☝", ), ), ("Phone number range is not in use", ("1555123123",)), ("Phone number is not possible", ("07123 456789...",)), ( "The string supplied did not seem to be a phone number.", ( '07";DROP TABLE;"', "ALPHANUM3R1C", ), ), ] ], [], ) invalid_phone_numbers = [ ("+80233456789", "Not a valid country prefix"), ("1234567", "Not enough digits"), ("+682 1234", "Not enough digits"), # Cook Islands phone numbers are 5 digits ("+12345 12345 12345 6", "Too many digits"), ] valid_email_addresses = ( "email@domain.com", "email@domain.COM", "firstname.lastname@domain.com", "firstname.o'lastname@domain.com", "email@subdomain.domain.com", "firstname+lastname@domain.com", "1234567890@domain.com", "email@domain-one.com", "_______@domain.com", "email@domain.name", "email@domain.superlongtld", "email@domain.co.jp", "firstname-lastname@domain.com", "info@german-financial-services.vermögensberatung", "info@german-financial-services.reallylongarbitrarytldthatiswaytoohugejustincase", "japanese-info@例え.テスト", "email@double--hyphen.com", ) invalid_email_addresses = ( "email@123.123.123.123", "email@[123.123.123.123]", "plainaddress", "@no-local-part.com", "Outlook Contact ", "no-at.domain.com", "no-tld@domain", ";beginning-semicolon@domain.co.uk", "middle-semicolon@domain.co;uk", "trailing-semicolon@domain.com;", '"email+leading-quotes@domain.com', 'email+middle"-quotes@domain.com', '"quoted-local-part"@domain.com', '"quoted@domain.com"', "lots-of-dots@domain..gov..uk", "two-dots..in-local@domain.com", "multiple@domains@domain.com", "spaces in local@domain.com", "spaces-in-domain@dom ain.com", "underscores-in-domain@dom_ain.com", "pipe-in-domain@example.com|gov.uk", "comma,in-local@gov.uk", "comma-in-domain@domain,gov.uk", "pound-sign-in-local£@domain.com", "local-with-’-apostrophe@domain.com", "local-with-”-quotes@domain.com", "domain-starts-with-a-dot@.domain.com", "brackets(in)local@domain.com", "email-too-long-{}@example.com".format("a" * 320), "incorrect-punycode@xn---something.com", ) @pytest.mark.parametrize("phone_number", valid_international_phone_numbers) def test_detect_international_phone_numbers(phone_number): assert is_us_phone_number(phone_number) is False @pytest.mark.parametrize("phone_number", valid_us_phone_numbers) def test_detect_us_phone_numbers(phone_number): assert is_us_phone_number(phone_number) is True @pytest.mark.parametrize( ("phone_number", "expected_info"), [ ( "+4407900900123", international_phone_info( international=True, country_prefix="44", # UK billable_units=1, ), ), ( "+4407700900123", international_phone_info( international=True, country_prefix="44", # Number in TV range billable_units=1, ), ), ( "+4407700800123", international_phone_info( international=True, country_prefix="44", # UK Crown dependency, so prefix same as UK billable_units=1, ), ), ( # "+20-12-1234-1234", international_phone_info( international=True, country_prefix="20", # Egypt billable_units=1, ), ), ( "+201212341234", international_phone_info( international=True, country_prefix="20", # Egypt billable_units=1, ), ), ( "+1 664-491-3434", international_phone_info( international=True, country_prefix="1664", # Montserrat billable_units=1, ), ), ( "+71234567890", international_phone_info( international=True, country_prefix="7", # Russia billable_units=1, ), ), ( "1-202-555-0104", international_phone_info( international=False, country_prefix="1", # USA billable_units=1, ), ), ( "202-555-0104", international_phone_info( international=False, country_prefix="1", # USA billable_units=1, ), ), ( "+23051234567", international_phone_info( international=True, country_prefix="230", # Mauritius billable_units=1, ), ), ], ) def test_get_international_info(phone_number, expected_info): assert get_international_phone_info(phone_number) == expected_info @pytest.mark.parametrize( "phone_number", [ "+21 4321 0987", "+00997 1234 7890", "+801234-7890", "+8-0-1234-78901", ], ) def test_get_international_info_raises(phone_number): with pytest.raises(InvalidPhoneError) as error: get_international_phone_info(phone_number) assert str(error.value) == "Not a valid country prefix" @pytest.mark.parametrize("phone_number", valid_us_phone_numbers) @pytest.mark.parametrize( "extra_args", [ {}, {"international": False}, ], ) def test_phone_number_accepts_valid_values(extra_args, phone_number): try: validate_phone_number(phone_number, **extra_args) except InvalidPhoneError: pytest.fail("Unexpected InvalidPhoneError") @pytest.mark.parametrize("phone_number", valid_phone_numbers) def test_phone_number_accepts_valid_international_values(phone_number): try: validate_phone_number(phone_number, international=True) except InvalidPhoneError: pytest.fail("Unexpected InvalidPhoneError") @pytest.mark.parametrize("phone_number", valid_us_phone_numbers) def test_valid_us_phone_number_can_be_formatted_consistently(phone_number): assert validate_and_format_phone_number(phone_number) == "+12025550104" @pytest.mark.parametrize( ("phone_number", "expected_formatted"), [ ("+44071234567890", "+4471234567890"), ("1-202-555-0104", "+12025550104"), ("+12025550104", "+12025550104"), ("12025550104", "+12025550104"), ("+23051234567", "+23051234567"), ], ) def test_valid_international_phone_number_can_be_formatted_consistently( phone_number, expected_formatted ): assert ( validate_and_format_phone_number(phone_number, international=True) == expected_formatted ) @pytest.mark.parametrize(("phone_number", "error_message"), invalid_us_phone_numbers) @pytest.mark.parametrize( "extra_args", [ {}, {"international": False}, ], ) def test_phone_number_rejects_invalid_values(extra_args, phone_number, error_message): with pytest.raises(InvalidPhoneError) as e: validate_phone_number(phone_number, **extra_args) assert error_message == str(e.value) @pytest.mark.parametrize(("phone_number", "error_message"), invalid_phone_numbers) def test_phone_number_rejects_invalid_international_values(phone_number, error_message): with pytest.raises(InvalidPhoneError) as e: validate_phone_number(phone_number, international=True) assert error_message == str(e.value) def test_show_mangled_number_clues(): x = show_mangled_number_clues("848!!-202?-2020$$") assert x == "XXX!!-XXX?-XXXX$$" @pytest.mark.parametrize("email_address", valid_email_addresses) def test_validate_email_address_accepts_valid(email_address): try: assert validate_email_address(email_address) == email_address except InvalidEmailError: pytest.fail("Unexpected InvalidEmailError") @pytest.mark.parametrize( "email", [ " email@domain.com ", "\temail@domain.com", "\temail@domain.com\n", "\u200bemail@domain.com\u200b", ], ) def test_validate_email_address_strips_whitespace(email): assert validate_email_address(email) == "email@domain.com" @pytest.mark.parametrize("email_address", invalid_email_addresses) def test_validate_email_address_raises_for_invalid(email_address): with pytest.raises(InvalidEmailError) as e: validate_email_address(email_address) assert str(e.value) == "Not a valid email address" @pytest.mark.parametrize("phone_number", valid_us_phone_numbers) def test_validates_against_guestlist_of_phone_numbers(phone_number): assert allowed_to_send_to( phone_number, ["2025550104", "2025550105", "test@example.com"] ) assert not allowed_to_send_to( phone_number, ["2025550105", "2028675309", "test@example.com"] ) @pytest.mark.parametrize( "recipient_number, allowlist_number", [ ["+4407123-456-789", "+4407123456789"], ["+4407123456789", "+4407123-456-789"], ], ) def test_validates_against_guestlist_of_international_phone_numbers( recipient_number, allowlist_number ): assert allowed_to_send_to(recipient_number, [allowlist_number]) @pytest.mark.parametrize("email_address", valid_email_addresses) def test_validates_against_guestlist_of_email_addresses(email_address): assert not allowed_to_send_to( email_address, ["very_special_and_unique@example.com"] ) # TODO something wrong with formatting Egyptian numbers, doesn't seem # like this would affect sendability need to confirm with AWS simulated numbers. @pytest.mark.parametrize( ("phone_number", "expected_formatted"), [ ("+4407900900123", "+44 7900 900123"), # UK ("+44(0)7900900123", "+44 7900 900123"), # UK ("+447900900123", "+44 7900 900123"), # UK # ("+20-12-1234-1234", "+20 121 234 1234"), # Egypt # ("+201212341234", "+20 121 234 1234"), # Egypt ("+1 664 491-3434", "+1 664-491-3434"), # Montserrat ("+7 499 1231212", "+7 499 123-12-12"), # Moscow (Russia) ("1-202-555-0104", "(202) 555-0104"), # Washington DC (USA) ("+23051234567", "+230 5123 4567"), # Mauritius ("+33(0)1 12345678", "+33 1 12 34 56 78"), # Paris (France) ], ) def test_format_us_and_international_phone_numbers(phone_number, expected_formatted): assert format_phone_number_human_readable(phone_number) == expected_formatted @pytest.mark.parametrize( ("recipient", "expected_formatted"), [ (True, ""), (False, ""), (0, ""), (0.1, ""), (None, ""), ("foo", "foo"), ("TeSt@ExAmPl3.com", "test@exampl3.com"), ("+4407900 900 123", "+447900900123"), ("+1 800 555 5555", "+18005555555"), ], ) def test_format_recipient(recipient, expected_formatted): assert format_recipient(recipient) == expected_formatted def test_try_format_recipient_doesnt_throw(): assert try_validate_and_format_phone_number("ALPHANUM3R1C") == "ALPHANUM3R1C" def test_format_phone_number_human_readable_doenst_throw(): assert format_phone_number_human_readable("ALPHANUM3R1C") == "ALPHANUM3R1C"