diff options
Diffstat (limited to 'tests/urlpatterns')
| -rw-r--r-- | tests/urlpatterns/__init__.py | 0 | ||||
| -rw-r--r-- | tests/urlpatterns/converter_urls.py | 8 | ||||
| -rw-r--r-- | tests/urlpatterns/converters.py | 38 | ||||
| -rw-r--r-- | tests/urlpatterns/path_base64_urls.py | 9 | ||||
| -rw-r--r-- | tests/urlpatterns/path_dynamic_urls.py | 9 | ||||
| -rw-r--r-- | tests/urlpatterns/path_urls.py | 15 | ||||
| -rw-r--r-- | tests/urlpatterns/tests.py | 165 | ||||
| -rw-r--r-- | tests/urlpatterns/views.py | 5 |
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('') |
