From 7c5d340bce24a62fdb96bcf7febe7b831d718cb2 Mon Sep 17 00:00:00 2001 From: Erick Salas Date: Wed, 14 Oct 2026 11:23:08 -0600 Subject: [PATCH 01/11] added edge cases on test suite --- tests/test_email_control_character.py | 9 +++++++++ tests/test_email_ipv6_literal.py | 9 +++++++++ tests/test_email_quoted_at_sign.py | 7 +++++++ tests/test_ipv4_strict_cidr.py | 8 ++++++++ tests/test_ipv6_brackets.py | 10 ++++++++++ 5 files changed, 43 insertions(+) create mode 100644 tests/test_email_control_character.py create mode 100644 tests/test_email_ipv6_literal.py create mode 100644 tests/test_email_quoted_at_sign.py create mode 100644 tests/test_ipv4_strict_cidr.py create mode 100644 tests/test_ipv6_brackets.py diff --git a/tests/test_email_control_character.py b/tests/test_email_control_character.py new file mode 100644 index 00000000..8023bb14 --- /dev/null +++ b/tests/test_email_control_character.py @@ -0,0 +1,9 @@ +"""HU-02: reject SOH in an address intended for SMTP (RFC 5321).""" +import validators + + +def test_email_rejects_soh_control_character(): + # The Python escape introduces the actual ASCII SOH character. + email = '"user\x01name"@example.com' + result = validators.email(email) + assert isinstance(result, validators.ValidationError), repr(result) diff --git a/tests/test_email_ipv6_literal.py b/tests/test_email_ipv6_literal.py new file mode 100644 index 00000000..a2a0391e --- /dev/null +++ b/tests/test_email_ipv6_literal.py @@ -0,0 +1,9 @@ +"""HU-02: accept the IPv6: prefix in an SMTP address literal.""" +import validators + + +def test_email_accepts_ipv6_literal_with_prefix(): + result = validators.email( + "usuario@[IPv6:2001:db8::1]", ipv6_address=True + ) + assert result is True, repr(result) diff --git a/tests/test_email_quoted_at_sign.py b/tests/test_email_quoted_at_sign.py new file mode 100644 index 00000000..15d28675 --- /dev/null +++ b/tests/test_email_quoted_at_sign.py @@ -0,0 +1,7 @@ +"""HU-02: accept @ inside a quoted local part.""" +import validators + + +def test_email_accepts_at_sign_inside_quoted_local_part(): + result = validators.email('"user@name"@example.com') + assert result is True, repr(result) diff --git a/tests/test_ipv4_strict_cidr.py b/tests/test_ipv4_strict_cidr.py new file mode 100644 index 00000000..317c90df --- /dev/null +++ b/tests/test_ipv4_strict_cidr.py @@ -0,0 +1,8 @@ +"""HU-04: require a prefix length in the documented strict CIDR mode.""" +import validators + + +def test_ipv4_strict_cidr_rejects_dotted_netmask(): + # The network is valid, but its representation is not CIDR prefix notation. + result = validators.ipv4("192.168.1.0/255.255.255.0", strict=True) + assert isinstance(result, validators.ValidationError), repr(result) diff --git a/tests/test_ipv6_brackets.py b/tests/test_ipv6_brackets.py new file mode 100644 index 00000000..5c31fee1 --- /dev/null +++ b/tests/test_ipv6_brackets.py @@ -0,0 +1,10 @@ +"""HU-04: proposed enhancement; brackets are URL host syntax. + +This test expresses a new requirement, not a defect in the current contract. +""" +import validators + + +def test_ipv6_accepts_address_in_brackets(): + result = validators.ipv6("[2001:db8::1]") + assert result is True, repr(result) From a77ed6f86682b287e5f8436faf60dc3e8fb0d369 Mon Sep 17 00:00:00 2001 From: Erick Salas Date: Sun, 18 Oct 2026 08:58:50 -0600 Subject: [PATCH 02/11] added docstrings on 5 tests --- tests/test_email_control_character.py | 15 ++++++++++++--- tests/test_email_ipv6_literal.py | 15 +++++++++++++-- tests/test_email_quoted_at_sign.py | 15 +++++++++++++-- tests/test_ipv4_strict_cidr.py | 16 +++++++++++++--- tests/test_ipv6_brackets.py | 15 +++++++++++++-- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/tests/test_email_control_character.py b/tests/test_email_control_character.py index 8023bb14..542ec6ec 100644 --- a/tests/test_email_control_character.py +++ b/tests/test_email_control_character.py @@ -1,9 +1,18 @@ """HU-02: reject SOH in an address intended for SMTP (RFC 5321).""" + +# external +import pytest + import validators -def test_email_rejects_soh_control_character(): - # The Python escape introduces the actual ASCII SOH character. - email = '"user\x01name"@example.com' +@pytest.mark.parametrize( + ("email",), + [ + ('"user\x01name"@example.com', ), + ], +) +def test_email_rejects_soh_control_character(email: str): + """The Python escape introduces the actual ASCII SOH character.""" result = validators.email(email) assert isinstance(result, validators.ValidationError), repr(result) diff --git a/tests/test_email_ipv6_literal.py b/tests/test_email_ipv6_literal.py index a2a0391e..a4f208a3 100644 --- a/tests/test_email_ipv6_literal.py +++ b/tests/test_email_ipv6_literal.py @@ -1,9 +1,20 @@ """HU-02: accept the IPv6: prefix in an SMTP address literal.""" + +# external +import pytest + import validators -def test_email_accepts_ipv6_literal_with_prefix(): +@pytest.mark.parametrize( + ("email",), + [ + ("usuario@[IPv6:2001:db8::1]", ), + ], +) +def test_email_accepts_ipv6_literal_with_prefix(email: str): + """Email accepts ipv6 literal with prefix.""" result = validators.email( - "usuario@[IPv6:2001:db8::1]", ipv6_address=True + email, ipv6_address=True ) assert result is True, repr(result) diff --git a/tests/test_email_quoted_at_sign.py b/tests/test_email_quoted_at_sign.py index 15d28675..02d7b1f0 100644 --- a/tests/test_email_quoted_at_sign.py +++ b/tests/test_email_quoted_at_sign.py @@ -1,7 +1,18 @@ """HU-02: accept @ inside a quoted local part.""" + +# external +import pytest + import validators -def test_email_accepts_at_sign_inside_quoted_local_part(): - result = validators.email('"user@name"@example.com') +@pytest.mark.parametrize( + ("email",), + [ + ('"user@name"@example.com', ), + ], +) +def test_email_accepts_at_sign_inside_quoted_local_part(email: str): + """The email accepts at sign inside quoted local part character.""" + result = validators.email(email) assert result is True, repr(result) diff --git a/tests/test_ipv4_strict_cidr.py b/tests/test_ipv4_strict_cidr.py index 317c90df..68bdf329 100644 --- a/tests/test_ipv4_strict_cidr.py +++ b/tests/test_ipv4_strict_cidr.py @@ -1,8 +1,18 @@ """HU-04: require a prefix length in the documented strict CIDR mode.""" + +# external +import pytest + import validators -def test_ipv4_strict_cidr_rejects_dotted_netmask(): - # The network is valid, but its representation is not CIDR prefix notation. - result = validators.ipv4("192.168.1.0/255.255.255.0", strict=True) +@pytest.mark.parametrize( + ("ip",), + [ + ("192.168.1.0/255.255.255.0", ), + ], +) +def test_ipv4_strict_cidr_rejects_dotted_netmask(ip: str): + """The network is valid, but its representation is not CIDR prefix notation.""" + result = validators.ipv4(ip, strict=True) assert isinstance(result, validators.ValidationError), repr(result) diff --git a/tests/test_ipv6_brackets.py b/tests/test_ipv6_brackets.py index 5c31fee1..908cdd45 100644 --- a/tests/test_ipv6_brackets.py +++ b/tests/test_ipv6_brackets.py @@ -2,9 +2,20 @@ This test expresses a new requirement, not a defect in the current contract. """ + +# external +import pytest + import validators -def test_ipv6_accepts_address_in_brackets(): - result = validators.ipv6("[2001:db8::1]") +@pytest.mark.parametrize( + ("ip",), + [ + ("[2001:db8::1]", ), + ], +) +def test_ipv6_accepts_address_in_brackets(ip: str): + """Accept iv6 address in brackets.""" + result = validators.ipv6(ip) assert result is True, repr(result) From 9e2e96c55380505dbf780121704ceef0133da5ff Mon Sep 17 00:00:00 2001 From: LisDiana Rodriguez Date: Sun, 18 Oct 2026 13:18:08 -0600 Subject: [PATCH 03/11] test(email): add parametrized ASCII control character cases --- tests/test_email_control_character.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_email_control_character.py b/tests/test_email_control_character.py index 542ec6ec..aebc399f 100644 --- a/tests/test_email_control_character.py +++ b/tests/test_email_control_character.py @@ -1,4 +1,4 @@ -"""HU-02: reject SOH in an address intended for SMTP (RFC 5321).""" +"""HU-02: reject ASCII control characters in quoted SMTP local parts.""" # external import pytest @@ -7,12 +7,14 @@ @pytest.mark.parametrize( - ("email",), - [ - ('"user\x01name"@example.com', ), - ], + "control_code", + [1, 2, 7, 11, 12, 14, 27, 31, 127], ) -def test_email_rejects_soh_control_character(email: str): - """The Python escape introduces the actual ASCII SOH character.""" + +def test_email_rejects_control_characters(control_code: int): + """Reject selected ASCII control characters in quoted local parts.""" + email = f'"user{chr(control_code)}name"@example.com' + result = validators.email(email) - assert isinstance(result, validators.ValidationError), repr(result) + + assert isinstance(result, validators.ValidationError), repr(result) \ No newline at end of file From f0eb665cc605b301c4e17bda6eff2f05b4ea435e Mon Sep 17 00:00:00 2001 From: LisDiana Rodriguez Date: Sun, 18 Oct 2026 13:54:37 -0600 Subject: [PATCH 04/11] fix(email): reject ASCII control characters in quoted local parts --- src/validators/email.py | 4 ++-- tests/test_email_control_character.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/validators/email.py b/src/validators/email.py index 46cc5946..3b59d8ab 100644 --- a/src/validators/email.py +++ b/src/validators/email.py @@ -91,8 +91,8 @@ def email( + r"(\.[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF0-9a-z!#$%&'*+/=?^_`{}|~\-]+)*$" # quoted-string + r'|^"(' - + r"[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF\001-\010\013\014\016-\037" - + r"!#-\[\]-\177]|\\[\011.]" + + r"[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF" + + r"!#-\[\]-~]|\\[\011.]" + r')*")$', username_part, re.IGNORECASE, diff --git a/tests/test_email_control_character.py b/tests/test_email_control_character.py index aebc399f..2373d11b 100644 --- a/tests/test_email_control_character.py +++ b/tests/test_email_control_character.py @@ -17,4 +17,4 @@ def test_email_rejects_control_characters(control_code: int): result = validators.email(email) - assert isinstance(result, validators.ValidationError), repr(result) \ No newline at end of file + assert isinstance(result, validators.ValidationError), repr(result) From 1b612b129650faf5811bfe925e0e6f50c1b34c11 Mon Sep 17 00:00:00 2001 From: Erick Salas Date: Sun, 18 Oct 2026 20:53:28 -0600 Subject: [PATCH 05/11] feat(ipv4): require numeric prefix in strict CIDR mode HU-04: Enforce strict CIDR notation in strict mode by requiring the prefix to be a numeric value (0-32) instead of accepting dotted netmask notation like 255.255.255.0. When strict=True with CIDR enabled: - Accepts: 192.168.1.0/24 - Rejects: 192.168.1.0/255.255.255.0 This change maintains backward compatibility: - Non-strict mode continues to accept both formats - All existing tests continue to pass - Only adds stricter validation when explicitly requested --- src/validators/ip_address.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/validators/ip_address.py b/src/validators/ip_address.py index 94a42c62..110a9c34 100644 --- a/src/validators/ip_address.py +++ b/src/validators/ip_address.py @@ -88,6 +88,10 @@ def ipv4( if cidr: if strict and value.count("/") != 1: raise ValueError("IPv4 address was expected in CIDR notation") + if strict: + _, prefix = value.split("/") + if not prefix.isdigit() or not (0 <= int(prefix) <= 32): + raise ValueError("CIDR prefix must be a number between 0 and 32") return IPv4Network(value, strict=not host_bit) and _check_private_ip(value, private) return IPv4Address(value) and _check_private_ip(value, private) except (ValueError, AddressValueError, NetmaskValueError): From 51563c4c21853104c474a897ea1a8b7f0557c0ae Mon Sep 17 00:00:00 2001 From: Erick Salas Date: Sun, 18 Oct 2026 21:12:30 -0600 Subject: [PATCH 06/11] Add cosmetic comment to demo CI auto-trigger Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/validators/ip_address.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/validators/ip_address.py b/src/validators/ip_address.py index 110a9c34..de888892 100644 --- a/src/validators/ip_address.py +++ b/src/validators/ip_address.py @@ -1,5 +1,6 @@ """IP Address.""" +# cosmetic: no functional change (CI trigger demo) # standard from ipaddress import ( AddressValueError, From 1980ebcebfce29083db5a64bd1142c67ae6a8f70 Mon Sep 17 00:00:00 2001 From: Erick Salas Date: Sun, 18 Oct 2026 21:15:20 -0600 Subject: [PATCH 07/11] Trigger sast and pycqa on release branch too Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pycqa.yaml | 4 ++-- .github/workflows/sast.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pycqa.yaml b/.github/workflows/pycqa.yaml index 1aa6736e..04d01a7a 100644 --- a/.github/workflows/pycqa.yaml +++ b/.github/workflows/pycqa.yaml @@ -3,9 +3,9 @@ name: pycqa on: workflow_dispatch: push: - branches: [master] + branches: [master, release] pull_request: - branches: [master] + branches: [master, release] jobs: tooling: runs-on: ubuntu-latest diff --git a/.github/workflows/sast.yaml b/.github/workflows/sast.yaml index ce70dee1..70314783 100644 --- a/.github/workflows/sast.yaml +++ b/.github/workflows/sast.yaml @@ -4,9 +4,9 @@ name: sast on: workflow_dispatch: push: - branches: ["master"] + branches: ["master", "release"] pull_request: - branches: ["master"] + branches: ["master", "release"] jobs: sast: From 523cfed16a5b2884a22b3cf18059b70eff110694 Mon Sep 17 00:00:00 2001 From: jibarra-cenfo Date: Mon, 19 Oct 2026 11:06:54 -0600 Subject: [PATCH 08/11] JP fix(email): accept IPv6 prefix in address literals HU-02: Strip the IPv6: prefix from bracketed email domain literals before IP validation. Addresses the existing test_email_accepts_ipv6_literal_with_prefix test. --- src/validators/email.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/validators/email.py b/src/validators/email.py index 46cc5946..6490065e 100644 --- a/src/validators/email.py +++ b/src/validators/email.py @@ -68,6 +68,8 @@ def email( if domain_part.startswith("[") and domain_part.endswith("]"): # ref: RFC 5321 domain_part = domain_part.lstrip("[").rstrip("]") + if ipv6_address and domain_part.startswith("IPv6:"): + domain_part = domain_part[5:] else: return False From a0b3bf766c3bdaad90a4319c23c320ca02b698fc Mon Sep 17 00:00:00 2001 From: Erick Salas Date: Mon, 19 Oct 2026 18:00:06 -0600 Subject: [PATCH 09/11] test: add URL validation with UTF-8 query parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test to verify that URL validator correctly handles UTF-8 characters in query parameters, such as special characters like 'ñ' in search queries. --- tests/test_url_utf8_query_parameter.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/test_url_utf8_query_parameter.py diff --git a/tests/test_url_utf8_query_parameter.py b/tests/test_url_utf8_query_parameter.py new file mode 100644 index 00000000..65f579e4 --- /dev/null +++ b/tests/test_url_utf8_query_parameter.py @@ -0,0 +1,11 @@ +"""HA-01. Test URL validation with UTF-8 query parameters.""" + +# local +from validators import url + + +def test_url_with_utf8_query_parameter(): + """Test that URL validator accepts UTF-8 characters in query parameters.""" + result = url("https://domain.com/search?q=caña") + + assert result is True From 128637f504c8d602eb897407315db2b1a82a67e3 Mon Sep 17 00:00:00 2001 From: brauliore Date: Mon, 19 Oct 2026 20:29:58 -0600 Subject: [PATCH 10/11] Add support for bracketed IPv6 addresses Accept IPv6 addresses enclosed in brackets and add test coverage for loopback, compressed, expanded, and IPv4-mapped forms. --- src/validators/ip_address.py | 4 +++- tests/test_ipv6_brackets.py | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/validators/ip_address.py b/src/validators/ip_address.py index de888892..12d3bd82 100644 --- a/src/validators/ip_address.py +++ b/src/validators/ip_address.py @@ -118,7 +118,7 @@ def ipv6(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bo Args: value: - IP address string to validate. + IP address string to validate. It may be enclosed in brackets. cidr: IP address string may contain CIDR annotation. strict: @@ -134,6 +134,8 @@ def ipv6(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bo """ if not value: return False + if value.startswith("[") and value.endswith("]"): + value = value[1:-1] try: if cidr: if strict and value.count("/") != 1: diff --git a/tests/test_ipv6_brackets.py b/tests/test_ipv6_brackets.py index 908cdd45..75700832 100644 --- a/tests/test_ipv6_brackets.py +++ b/tests/test_ipv6_brackets.py @@ -12,10 +12,13 @@ @pytest.mark.parametrize( ("ip",), [ - ("[2001:db8::1]", ), + ("[::1]",), + ("[2001:db8::1]",), + ("[2001:0db8:0000:0000:0000:0000:0000:0001]",), + ("[::ffff:192.0.2.128]",), ], ) def test_ipv6_accepts_address_in_brackets(ip: str): - """Accept iv6 address in brackets.""" + """Accept an IPv6 address enclosed in brackets.""" result = validators.ipv6(ip) assert result is True, repr(result) From 242859447b6842cb8ae33d4829e34201de9d879e Mon Sep 17 00:00:00 2001 From: mvallejosr Date: Mon, 19 Oct 2026 22:57:55 -0600 Subject: [PATCH 11/11] fix(email): complete HU-02 red-green TDD cycle --- src/validators/email.py | 2 +- tests/test_email.py | 1 - tests/test_email_quoted_at_sign.py | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/validators/email.py b/src/validators/email.py index c221c216..51645b06 100644 --- a/src/validators/email.py +++ b/src/validators/email.py @@ -55,7 +55,7 @@ def email( (Literal[True]): If `value` is a valid eMail. (ValidationError): If `value` is an invalid eMail. """ - if not value or value.count("@") != 1: + if not value or "@" not in value: return False username_part, domain_part = value.rsplit("@", 1) diff --git a/tests/test_email.py b/tests/test_email.py index 990ef8ee..c5d11a45 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -45,7 +45,6 @@ def test_returns_true_on_valid_email(value: str): ("example@inv-.alid-.com",), ("example@inv-.-alid.com",), ("john56789.john56789.john56789.john56789.john56789.john56789.john5@example.com",), - ('"test@test"@example.com',), # Quoted-string format (CR not allowed) ('"\\\012"@here.com',), # Non-quoted space/semicolon not allowed diff --git a/tests/test_email_quoted_at_sign.py b/tests/test_email_quoted_at_sign.py index 02d7b1f0..3a2d4443 100644 --- a/tests/test_email_quoted_at_sign.py +++ b/tests/test_email_quoted_at_sign.py @@ -9,7 +9,8 @@ @pytest.mark.parametrize( ("email",), [ - ('"user@name"@example.com', ), + ('"user@name"@example.com',), + ('"test@test"@example.com',), ], ) def test_email_accepts_at_sign_inside_quoted_local_part(email: str):