diff --git a/src/validators/domain.py b/src/validators/domain.py index 24f46180..ee6db4e7 100644 --- a/src/validators/domain.py +++ b/src/validators/domain.py @@ -94,9 +94,6 @@ def domain( if not value: return False - if consider_tld and not _IanaTLD.check(value.rstrip(".").rsplit(".", 1)[-1].upper()): - return False - try: if _relies_on_idna_mapping(value): return False @@ -114,6 +111,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 e61d1952..533dde97 100644 --- a/tests/test_domain.py +++ b/tests/test_domain.py @@ -49,6 +49,9 @@ def test_returns_true_on_valid_domain(value: str, rfc_1034: bool, rfc_2782: bool ("somerandomexample.onion", True, False, False), # delegated 2026-07-23 ("somerandomexample.web", True, False, False), + ("пример.рф", True, False, False), + ("пример.рф.", True, True, False), + ("例子.中国", True, False, False), ], ) def test_returns_true_on_valid_top_level_domain( @@ -104,6 +107,7 @@ def test_returns_failed_validation_on_invalid_domain(value: str, rfc_1034: bool, ("somerandomexample.xn-n-fiqs8s", True, False, False), # retired from the root zone ("somerandomexample.goo", 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 f62dced7..83f00212 100644 --- a/tests/test_hostname.py +++ b/tests/test_hostname.py @@ -145,3 +145,29 @@ def test_returns_failed_validation_on_idna_mapped_lookalike_hostname(value: str) def test_returns_failed_validation_on_ipv4_lookalike_hostname(value: str): """Test returns failed validation on dotted names whose last label is non-ASCII digits.""" 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 9b5e2d77..d48a03ec 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -273,3 +273,29 @@ def test_returns_failed_validation_on_idna_mapped_lookalike_url(value: str): def test_returns_failed_validation_on_ipv4_lookalike_url(value: str, private: Optional[bool]): """Test returns failed validation on hosts that only look like an IPv4 address.""" assert isinstance(url(value, private=private), ValidationError) + + +@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)