diff --git a/src/validators/domain.py b/src/validators/domain.py index 1ce4a26..6999290 100644 --- a/src/validators/domain.py +++ b/src/validators/domain.py @@ -76,9 +76,6 @@ def domain( if not value: return False - if consider_tld and not _IanaTLD.check(value.rstrip(".").rsplit(".", 1)[-1].upper()): - return False - try: service_record = r"_" if rfc_2782 else "" trailing_dot = r"\.?$" if rfc_1034 else r"$" @@ -88,6 +85,11 @@ def domain( # ref: RFC 1035, presentation form is limited to 253 characters return False + # IANA lists IDN TLDs in their ASCII (punycode) form, + # so look up the encoded TLD, not the raw one. + if consider_tld and not _IanaTLD.check(encoded.rstrip(".").rsplit(".", 1)[-1].upper()): + return False + return not re.search(r"\s|__+", value) and re.match( # First character of the domain rf"^(?:[a-z0-9{service_record}]" diff --git a/tests/test_domain.py b/tests/test_domain.py index d606ad7..562a94e 100644 --- a/tests/test_domain.py +++ b/tests/test_domain.py @@ -47,6 +47,9 @@ def test_returns_true_on_valid_domain(value: str, rfc_1034: bool, rfc_2782: bool ("example_.com", True, False, True), ("somerandomexample.xn--fiqs8s", True, False, False), ("somerandomexample.onion", True, False, False), + ("пример.рф", True, False, False), + ("пример.рф.", True, True, False), + ("例子.中国", True, False, False), ], ) def test_returns_true_on_valid_top_level_domain( @@ -100,6 +103,7 @@ def test_returns_failed_validation_on_invalid_domain(value: str, rfc_1034: bool, ("_example.#13", True, False, True), ("example_.fo-ul", True, False, True), ("somerandomexample.xn-n-fiqs8s", True, False, False), + ("пример.рфы", True, False, False), ], ) def test_returns_failed_validation_invalid_top_level_domain( diff --git a/tests/test_hostname.py b/tests/test_hostname.py index 1fe2468..59c91a3 100644 --- a/tests/test_hostname.py +++ b/tests/test_hostname.py @@ -109,3 +109,29 @@ def test_returns_true_on_hostname_within_rfc_length_limits(value: str): def test_returns_failed_validation_on_hostname_beyond_rfc_limits(value: str): """Test returns failed validation on hostname beyond RFC limits.""" assert isinstance(hostname(value), ValidationError) + + +@pytest.mark.parametrize( + "value", + [ + "example.com:443", + "xn--e1afmkfd.xn--p1ai", + "пример.рф", + "пример.рф:8080", + ], +) +def test_returns_true_on_valid_top_level_domain_hostname(value: str): + """Test returns true on hostname with a TLD allowed by IANA.""" + assert hostname(value, consider_tld=True) + + +@pytest.mark.parametrize( + "value", + [ + "example.flat:443", + "пример.рфы", + ], +) +def test_returns_failed_validation_on_invalid_top_level_domain_hostname(value: str): + """Test returns failed validation on hostname with a TLD not allowed by IANA.""" + assert isinstance(hostname(value, consider_tld=True), ValidationError) diff --git a/tests/test_url.py b/tests/test_url.py index 2649584..38a0cd7 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -232,3 +232,29 @@ def test_returns_failed_validation_on_invalid_private_url(value: str, private: O def test_returns_string_for_valid_file_url(value: str): """Test returns true on valid file url. Ref: RFC 8089.""" assert url(value) + + +@pytest.mark.parametrize( + "value", + [ + "https://example.com/path", + "https://xn--e1afmkfd.xn--p1ai/", + "https://пример.рф/", + "http://例子.中国:8080/index.html", + ], +) +def test_returns_true_on_valid_top_level_domain_url(value: str): + """Test returns true on url with a TLD allowed by IANA.""" + assert url(value, consider_tld=True) + + +@pytest.mark.parametrize( + "value", + [ + "https://example.flat/", + "https://пример.рфы/", + ], +) +def test_returns_failed_validation_on_invalid_top_level_domain_url(value: str): + """Test returns failed validation on url with a TLD not allowed by IANA.""" + assert isinstance(url(value, consider_tld=True), ValidationError)