|
1 | 1 | from django.core.exceptions import ValidationError |
| 2 | +from django.core.validators import URLValidator |
2 | 3 | from django.forms import URLField |
3 | 4 | from django.test import SimpleTestCase |
4 | 5 |
|
@@ -72,6 +73,16 @@ def test_urlfield_clean(self): |
72 | 73 | # IPv6. |
73 | 74 | ("http://[12:34::3a53]/", "http://[12:34::3a53]/"), |
74 | 75 | ("http://[a34:9238::]:8080/", "http://[a34:9238::]:8080/"), |
| 76 | + # IPv6 without scheme. |
| 77 | + ("[12:34::3a53]/", "https://[12:34::3a53]/"), |
| 78 | + # IDN domain without scheme but with port. |
| 79 | + ("ñandú.es:8080/", "https://ñandú.es:8080/"), |
| 80 | + # Scheme-relative. |
| 81 | + ("//example.com", "https://example.com"), |
| 82 | + ("//example.com/path", "https://example.com/path"), |
| 83 | + # Whitespace stripped. |
| 84 | + ("\t\n//example.com \n\t\n", "https://example.com"), |
| 85 | + ("\t\nhttp://example.com \n\t\n", "http://example.com"), |
75 | 86 | ] |
76 | 87 | for url, expected in tests: |
77 | 88 | with self.subTest(url=url): |
@@ -102,10 +113,19 @@ def test_urlfield_clean_invalid(self): |
102 | 113 | # even on domains that don't fail the domain label length check in |
103 | 114 | # the regex. |
104 | 115 | "http://%s" % ("X" * 200,), |
105 | | - # urlsplit() raises ValueError. |
| 116 | + # Scheme prepend yields a structurally invalid URL. |
106 | 117 | "////]@N.AN", |
107 | | - # Empty hostname. |
| 118 | + # Scheme prepend yields an empty hostname. |
108 | 119 | "#@A.bO", |
| 120 | + # Known problematic unicode chars. |
| 121 | + "http://" + "¾" * 200, |
| 122 | + # Non-ASCII character before the first colon. |
| 123 | + "¾:example.com", |
| 124 | + # ASCII digit before the first colon. |
| 125 | + "1http://example.com", |
| 126 | + # Empty scheme. |
| 127 | + "://example.com", |
| 128 | + ":example.com", |
109 | 129 | ] |
110 | 130 | msg = "'Enter a valid URL.'" |
111 | 131 | for value in tests: |
@@ -143,3 +163,64 @@ def test_urlfield_assume_scheme(self): |
143 | 163 | self.assertEqual(f.clean("example.com"), "http://example.com") |
144 | 164 | f = URLField(assume_scheme="https") |
145 | 165 | self.assertEqual(f.clean("example.com"), "https://example.com") |
| 166 | + |
| 167 | + def test_urlfield_assume_scheme_when_colons(self): |
| 168 | + f = URLField() |
| 169 | + tests = [ |
| 170 | + # Port number. |
| 171 | + ("http://example.com:8080/", "http://example.com:8080/"), |
| 172 | + ("https://example.com:443/path", "https://example.com:443/path"), |
| 173 | + # Userinfo with password. |
| 174 | + ("http://user:pass@example.com", "http://user:pass@example.com"), |
| 175 | + ( |
| 176 | + "http://user:pass@example.com:8080/", |
| 177 | + "http://user:pass@example.com:8080/", |
| 178 | + ), |
| 179 | + # Colon in path segment. |
| 180 | + ("http://example.com/path:segment", "http://example.com/path:segment"), |
| 181 | + ("http://example.com/a:b/c:d", "http://example.com/a:b/c:d"), |
| 182 | + # Colon in query string. |
| 183 | + ("http://example.com/?key=val:ue", "http://example.com/?key=val:ue"), |
| 184 | + # Colon in fragment. |
| 185 | + ("http://example.com/#section:1", "http://example.com/#section:1"), |
| 186 | + # IPv6 -- multiple colons in host. |
| 187 | + ("http://[::1]/", "http://[::1]/"), |
| 188 | + ("http://[2001:db8::1]/", "http://[2001:db8::1]/"), |
| 189 | + ("http://[2001:db8::1]:8080/", "http://[2001:db8::1]:8080/"), |
| 190 | + # Colons across multiple components. |
| 191 | + ( |
| 192 | + "http://user:pass@example.com:8080/path:x?q=a:b#id:1", |
| 193 | + "http://user:pass@example.com:8080/path:x?q=a:b#id:1", |
| 194 | + ), |
| 195 | + # FTP with port and userinfo. |
| 196 | + ( |
| 197 | + "ftp://user:pass@ftp.example.com:21/file", |
| 198 | + "ftp://user:pass@ftp.example.com:21/file", |
| 199 | + ), |
| 200 | + ( |
| 201 | + "ftps://user:pass@ftp.example.com:990/", |
| 202 | + "ftps://user:pass@ftp.example.com:990/", |
| 203 | + ), |
| 204 | + # Scheme-relative URLs, starts with "//". |
| 205 | + ("//example.com:8080/path", "https://example.com:8080/path"), |
| 206 | + ("//user:pass@example.com/", "https://user:pass@example.com/"), |
| 207 | + ] |
| 208 | + for value, expected in tests: |
| 209 | + with self.subTest(value=value): |
| 210 | + self.assertEqual(f.clean(value), expected) |
| 211 | + |
| 212 | + def test_custom_validator_longer_max_length(self): |
| 213 | + |
| 214 | + class CustomLongURLValidator(URLValidator): |
| 215 | + max_length = 4096 |
| 216 | + |
| 217 | + class CustomURLField(URLField): |
| 218 | + default_validators = [CustomLongURLValidator()] |
| 219 | + |
| 220 | + field = CustomURLField() |
| 221 | + # A URL with 4096 chars is valid given the custom validator. |
| 222 | + prefix = "https://example.com/" |
| 223 | + url = prefix + "a" * (4096 - len(prefix)) |
| 224 | + self.assertEqual(len(url), 4096) |
| 225 | + # No ValidationError is raised. |
| 226 | + field.clean(url) |
0 commit comments