Skip to content

Commit c76f61c

Browse files
authored
feat: add domain, URL and email validators (#2167)
* Add domain, URL and email validators * Add docstrings * Address PR comments * Fix `CHANGELOG.rst`
1 parent 6dabe86 commit c76f61c

3 files changed

Lines changed: 413 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Added
1616
- ``QuerySet.union()`` — SQL UNION query support for combining results from multiple QuerySets, including support for union across different models, ``union(all=True)`` for duplicates, ``order_by()``, ``limit()``, and ``count()``.
1717
- ``QuerySet.contains()`` method to check if an object exists in a queryset.
1818
- Added comprehensive EXPLAIN support for MySQL and PostgreSQL.
19+
- Built-in ``DomainNameValidator``, ``URLValidator``, and ``EmailValidator`` classes for common validation patterns. (#2162)
1920

2021
Fixed
2122
^^^^^

tests/test_validators.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44

55
from tests.testmodels import ValidatorModel
66
from tortoise.exceptions import ValidationError
7+
from tortoise.validators import (
8+
DomainNameValidator,
9+
EmailValidator,
10+
InvalidDomainName,
11+
InvalidEmailAddress,
12+
InvalidScheme,
13+
InvalidURL,
14+
URLValidator,
15+
validate_domain_name,
16+
validate_email,
17+
validate_url,
18+
)
719

820

921
@pytest.mark.asyncio
@@ -116,3 +128,154 @@ async def test_update(db):
116128
record.min_value_decimal = Decimal("0.9")
117129
with pytest.raises(ValidationError):
118130
await record.save()
131+
132+
133+
@pytest.mark.parametrize(
134+
"value",
135+
[
136+
"example.com",
137+
"sub.example.com",
138+
"example.co.uk",
139+
"münchen.de",
140+
"sub1.sub2.example.org",
141+
"UPPER-CASE.is.ok.net",
142+
"tortoise.github.io",
143+
"example.space",
144+
"❤️.website",
145+
],
146+
)
147+
def test_domain_name_validator_valid(value):
148+
validate_domain_name(value)
149+
150+
151+
@pytest.mark.parametrize(
152+
"value",
153+
[
154+
"",
155+
"---.com",
156+
"example-.com",
157+
"under_line.com",
158+
"💻.tech",
159+
],
160+
)
161+
def test_domain_name_validator_invalid(value):
162+
with pytest.raises(InvalidDomainName):
163+
validate_domain_name(value)
164+
165+
166+
def test_domain_name_validator_invalid_idn_disabled():
167+
validator = DomainNameValidator(accept_idna=False)
168+
with pytest.raises(InvalidDomainName):
169+
validator("münchen.de")
170+
171+
172+
@pytest.mark.parametrize(
173+
"value",
174+
[
175+
"http://example.com",
176+
"https://www.example.com/path?query=1",
177+
"ftp://ftp.example.com/file.txt",
178+
"http://localhost:8080",
179+
"http://192.168.1.1",
180+
"http://8.8.8.8:8080",
181+
"https://[::1]",
182+
"https://[2001:db8::1]:443",
183+
"http://user:pass@example.com",
184+
"http://example.com#fragment",
185+
],
186+
)
187+
def test_url_validator_valid(value):
188+
validate_url(value)
189+
190+
191+
@pytest.mark.parametrize(
192+
"value",
193+
[
194+
"http://example.com",
195+
"https://example.com",
196+
],
197+
)
198+
def test_url_validator_valid_custom_schemes(value):
199+
validator = URLValidator(allowed_schemes=["http", "https"])
200+
validator(value)
201+
202+
203+
def test_url_validator_invalid_scheme():
204+
validator = URLValidator(allowed_schemes=["http", "https"])
205+
with pytest.raises(InvalidScheme):
206+
validator("ftp://example.com")
207+
208+
209+
@pytest.mark.parametrize(
210+
"value",
211+
[
212+
"",
213+
"not-a-url",
214+
"http://",
215+
"http:// space.com",
216+
"http://[::gggg]",
217+
"http://256.1.1.1",
218+
"http://" + "a" * 254 + ".com",
219+
],
220+
)
221+
def test_url_validator_invalid(value):
222+
with pytest.raises(InvalidURL):
223+
validate_url(value)
224+
225+
226+
def test_url_validator_max_length():
227+
long_url = "http://example.com/" + "a" * 2100
228+
with pytest.raises(InvalidURL):
229+
validate_url(long_url)
230+
231+
232+
@pytest.mark.parametrize(
233+
"value",
234+
[
235+
"user@example.com",
236+
"user.name@example.com",
237+
"user+tag@example.co.uk",
238+
"user@sub.domain.com",
239+
"user@[192.168.1.1]",
240+
"user@[::1]",
241+
"a+b@example.com",
242+
"a-b@example.com",
243+
"a_b@example.com",
244+
"test@test.co.uk",
245+
],
246+
)
247+
def test_email_validator_valid(value):
248+
validate_email(value)
249+
250+
251+
def test_email_validator_valid_allowed_domains():
252+
validator = EmailValidator(allowed_domains=["example.com", "test.com"])
253+
validator("user@example.com")
254+
validator("user@test.com")
255+
256+
257+
def test_email_validator_invalid_allowed_domains():
258+
validator = EmailValidator(allowed_domains=["example.com"])
259+
validator("user@example.com")
260+
with pytest.raises(InvalidEmailAddress):
261+
validator("user@")
262+
with pytest.raises(InvalidEmailAddress):
263+
validator("user@invalid..com")
264+
265+
266+
@pytest.mark.parametrize(
267+
"value",
268+
[
269+
"",
270+
"not-an-email",
271+
"user@",
272+
"@example.com",
273+
"user@.com",
274+
"user@com.",
275+
"user@com..com",
276+
"a" * 330 + "@example.com",
277+
],
278+
)
279+
def test_email_validator_invalid(value):
280+
with pytest.raises(InvalidEmailAddress):
281+
validate_email(value)

0 commit comments

Comments
 (0)