summaryrefslogtreecommitdiff
path: root/tests/urlpatterns
diff options
context:
space:
mode:
Diffstat (limited to 'tests/urlpatterns')
-rw-r--r--tests/urlpatterns/__init__.py0
-rw-r--r--tests/urlpatterns/converter_urls.py8
-rw-r--r--tests/urlpatterns/converters.py38
-rw-r--r--tests/urlpatterns/path_base64_urls.py9
-rw-r--r--tests/urlpatterns/path_dynamic_urls.py9
-rw-r--r--tests/urlpatterns/path_urls.py15
-rw-r--r--tests/urlpatterns/tests.py165
-rw-r--r--tests/urlpatterns/views.py5
8 files changed, 249 insertions, 0 deletions
diff --git a/tests/urlpatterns/__init__.py b/tests/urlpatterns/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/urlpatterns/__init__.py
diff --git a/tests/urlpatterns/converter_urls.py b/tests/urlpatterns/converter_urls.py
new file mode 100644
index 0000000000..0cc57bd46e
--- /dev/null
+++ b/tests/urlpatterns/converter_urls.py
@@ -0,0 +1,8 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path('{x}/<{x}:{x}>/'.format(x=name), views.empty_view, name=name)
+ for name in ('int', 'path', 'slug', 'str', 'uuid')
+]
diff --git a/tests/urlpatterns/converters.py b/tests/urlpatterns/converters.py
new file mode 100644
index 0000000000..14a65b0483
--- /dev/null
+++ b/tests/urlpatterns/converters.py
@@ -0,0 +1,38 @@
+import base64
+
+
+class Base64Converter:
+ regex = r'[a-zA-Z0-9+/]*={0,2}'
+
+ def to_python(self, value):
+ return base64.b64decode(value)
+
+ def to_url(self, value):
+ return base64.b64encode(value).decode('ascii')
+
+
+class DynamicConverter:
+ _dynamic_to_python = None
+ _dynamic_to_url = None
+
+ @property
+ def regex(self):
+ return r'[0-9a-zA-Z]+'
+
+ @regex.setter
+ def regex(self):
+ raise Exception("You can't modify the regular expression.")
+
+ def to_python(self, value):
+ return type(self)._dynamic_to_python(value)
+
+ def to_url(self, value):
+ return type(self)._dynamic_to_url(value)
+
+ @classmethod
+ def register_to_python(cls, value):
+ cls._dynamic_to_python = value
+
+ @classmethod
+ def register_to_url(cls, value):
+ cls._dynamic_to_url = value
diff --git a/tests/urlpatterns/path_base64_urls.py b/tests/urlpatterns/path_base64_urls.py
new file mode 100644
index 0000000000..872636f06c
--- /dev/null
+++ b/tests/urlpatterns/path_base64_urls.py
@@ -0,0 +1,9 @@
+from django.urls import path, register_converter
+
+from . import converters, views
+
+register_converter(converters.Base64Converter, 'base64')
+
+urlpatterns = [
+ path('base64/<base64:value>/', views.empty_view, name='base64'),
+]
diff --git a/tests/urlpatterns/path_dynamic_urls.py b/tests/urlpatterns/path_dynamic_urls.py
new file mode 100644
index 0000000000..0e5afe1df0
--- /dev/null
+++ b/tests/urlpatterns/path_dynamic_urls.py
@@ -0,0 +1,9 @@
+from django.urls import path, register_converter
+
+from . import converters, views
+
+register_converter(converters.DynamicConverter, 'dynamic')
+
+urlpatterns = [
+ path('dynamic/<dynamic:value>/', views.empty_view, name='dynamic'),
+]
diff --git a/tests/urlpatterns/path_urls.py b/tests/urlpatterns/path_urls.py
new file mode 100644
index 0000000000..097f3cb166
--- /dev/null
+++ b/tests/urlpatterns/path_urls.py
@@ -0,0 +1,15 @@
+from django.conf.urls import include
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path('articles/2003/', views.empty_view, name='articles-2003'),
+ path('articles/<int:year>/', views.empty_view, name='articles-year'),
+ path('articles/<int:year>/<int:month>/', views.empty_view, name='articles-year-month'),
+ path('articles/<int:year>/<int:month>/<int:day>/', views.empty_view, name='articles-year-month-day'),
+ path('users/', views.empty_view, name='users'),
+ path('users/<id>/', views.empty_view, name='user-with-id'),
+ path('included_urls/', include('urlpatterns_reverse.included_urls')),
+ path('<lang>/<path:url>/', views.empty_view, name='lang-and-path'),
+]
diff --git a/tests/urlpatterns/tests.py b/tests/urlpatterns/tests.py
new file mode 100644
index 0000000000..b200aed06d
--- /dev/null
+++ b/tests/urlpatterns/tests.py
@@ -0,0 +1,165 @@
+import uuid
+
+from django.core.exceptions import ImproperlyConfigured
+from django.test import SimpleTestCase
+from django.test.utils import override_settings
+from django.urls import Resolver404, path, resolve, reverse
+
+from .converters import DynamicConverter
+from .views import empty_view
+
+
+@override_settings(ROOT_URLCONF='urlpatterns.path_urls')
+class SimplifiedURLTests(SimpleTestCase):
+
+ def test_path_lookup_without_parameters(self):
+ match = resolve('/articles/2003/')
+ self.assertEqual(match.url_name, 'articles-2003')
+ self.assertEqual(match.args, ())
+ self.assertEqual(match.kwargs, {})
+
+ def test_path_lookup_with_typed_parameters(self):
+ match = resolve('/articles/2015/')
+ self.assertEqual(match.url_name, 'articles-year')
+ self.assertEqual(match.args, ())
+ self.assertEqual(match.kwargs, {'year': 2015})
+
+ def test_path_lookup_with_multiple_paramaters(self):
+ match = resolve('/articles/2015/04/12/')
+ self.assertEqual(match.url_name, 'articles-year-month-day')
+ self.assertEqual(match.args, ())
+ self.assertEqual(match.kwargs, {'year': 2015, 'month': 4, 'day': 12})
+
+ def test_two_variable_at_start_of_path_pattern(self):
+ match = resolve('/en/foo/')
+ self.assertEqual(match.url_name, 'lang-and-path')
+ self.assertEqual(match.kwargs, {'lang': 'en', 'url': 'foo'})
+
+ def test_path_reverse_without_parameter(self):
+ url = reverse('articles-2003')
+ self.assertEqual(url, '/articles/2003/')
+
+ def test_path_reverse_with_parameter(self):
+ url = reverse('articles-year-month-day', kwargs={'year': 2015, 'month': 4, 'day': 12})
+ self.assertEqual(url, '/articles/2015/4/12/')
+
+ @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
+ def test_non_identical_converter_resolve(self):
+ match = resolve('/base64/aGVsbG8=/') # base64 of 'hello'
+ self.assertEqual(match.url_name, 'base64')
+ self.assertEqual(match.kwargs, {'value': b'hello'})
+
+ @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
+ def test_non_identical_converter_reverse(self):
+ url = reverse('base64', kwargs={'value': b'hello'})
+ self.assertEqual(url, '/base64/aGVsbG8=/')
+
+ def test_path_inclusion_is_matchable(self):
+ match = resolve('/included_urls/extra/something/')
+ self.assertEqual(match.url_name, 'inner-extra')
+ self.assertEqual(match.kwargs, {'extra': 'something'})
+
+ def test_path_inclusion_is_reversable(self):
+ url = reverse('inner-extra', kwargs={'extra': 'something'})
+ self.assertEqual(url, '/included_urls/extra/something/')
+
+ def test_invalid_converter(self):
+ msg = "URL route 'foo/<nonexistent:var>/' uses invalid converter 'nonexistent'."
+ with self.assertRaisesMessage(ImproperlyConfigured, msg):
+ path('foo/<nonexistent:var>/', empty_view)
+
+
+@override_settings(ROOT_URLCONF='urlpatterns.converter_urls')
+class ConverterTests(SimpleTestCase):
+
+ def test_matching_urls(self):
+ def no_converter(x):
+ return x
+
+ test_data = (
+ ('int', {'0', '1', '01', 1234567890}, int),
+ ('str', {'abcxyz'}, no_converter),
+ ('path', {'allows.ANY*characters'}, no_converter),
+ ('slug', {'abcxyz-ABCXYZ_01234567890'}, no_converter),
+ ('uuid', {'39da9369-838e-4750-91a5-f7805cd82839'}, uuid.UUID),
+ )
+ for url_name, url_suffixes, converter in test_data:
+ for url_suffix in url_suffixes:
+ url = '/%s/%s/' % (url_name, url_suffix)
+ with self.subTest(url=url):
+ match = resolve(url)
+ self.assertEqual(match.url_name, url_name)
+ self.assertEqual(match.kwargs, {url_name: converter(url_suffix)})
+ # reverse() works with string parameters.
+ string_kwargs = {url_name: url_suffix}
+ self.assertEqual(reverse(url_name, kwargs=string_kwargs), url)
+ # reverse() also works with native types (int, UUID, etc.).
+ if converter is not no_converter:
+ # The converted value might be different for int (a
+ # leading zero is lost in the conversion).
+ converted_value = match.kwargs[url_name]
+ converted_url = '/%s/%s/' % (url_name, converted_value)
+ self.assertEqual(reverse(url_name, kwargs={url_name: converted_value}), converted_url)
+
+ def test_nonmatching_urls(self):
+ test_data = (
+ ('int', {'-1', 'letters'}),
+ ('str', {'', '/'}),
+ ('path', {''}),
+ ('slug', {'', 'stars*notallowed'}),
+ ('uuid', {
+ '',
+ '9da9369-838e-4750-91a5-f7805cd82839',
+ '39da9369-838-4750-91a5-f7805cd82839',
+ '39da9369-838e-475-91a5-f7805cd82839',
+ '39da9369-838e-4750-91a-f7805cd82839',
+ '39da9369-838e-4750-91a5-f7805cd8283',
+ }),
+ )
+ for url_name, url_suffixes in test_data:
+ for url_suffix in url_suffixes:
+ url = '/%s/%s/' % (url_name, url_suffix)
+ with self.subTest(url=url), self.assertRaises(Resolver404):
+ resolve(url)
+
+
+class ParameterRestrictionTests(SimpleTestCase):
+ def test_non_identifier_parameter_name_causes_exception(self):
+ msg = (
+ "URL route 'hello/<int:1>/' uses parameter name '1' which isn't "
+ "a valid Python identifier."
+ )
+ with self.assertRaisesMessage(ImproperlyConfigured, msg):
+ path(r'hello/<int:1>/', lambda r: None)
+
+ def test_allows_non_ascii_but_valid_identifiers(self):
+ # \u0394 is "GREEK CAPITAL LETTER DELTA", a valid identifier.
+ p = path('hello/<str:\u0394>/', lambda r: None)
+ match = p.resolve('hello/1/')
+ self.assertEqual(match.kwargs, {'\u0394': '1'})
+
+
+@override_settings(ROOT_URLCONF='urlpatterns.path_dynamic_urls')
+class ConversionExceptionTests(SimpleTestCase):
+ """How are errors in Converter.to_python() and to_url() handled?"""
+
+ def test_resolve_value_error_means_no_match(self):
+ @DynamicConverter.register_to_python
+ def raises_value_error(value):
+ raise ValueError()
+ with self.assertRaises(Resolver404):
+ resolve('/dynamic/abc/')
+
+ def test_resolve_type_error_propogates(self):
+ @DynamicConverter.register_to_python
+ def raises_type_error(value):
+ raise TypeError('This type error propagates.')
+ with self.assertRaisesMessage(TypeError, 'This type error propagates.'):
+ resolve('/dynamic/abc/')
+
+ def test_reverse_value_error_propagates(self):
+ @DynamicConverter.register_to_url
+ def raises_value_error(value):
+ raise ValueError('This value error propagates.')
+ with self.assertRaisesMessage(ValueError, 'This value error propagates.'):
+ reverse('dynamic', kwargs={'value': object()})
diff --git a/tests/urlpatterns/views.py b/tests/urlpatterns/views.py
new file mode 100644
index 0000000000..50e23d1782
--- /dev/null
+++ b/tests/urlpatterns/views.py
@@ -0,0 +1,5 @@
+from django.http import HttpResponse
+
+
+def empty_view(request, *args, **kwargs):
+ return HttpResponse('')