tests.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import uuid
  2. from django.core.exceptions import ImproperlyConfigured
  3. from django.test import SimpleTestCase
  4. from django.test.utils import override_settings
  5. from django.urls import Resolver404, path, resolve, reverse
  6. from .converters import DynamicConverter
  7. from .views import empty_view
  8. included_kwargs = {'base': b'hello', 'value': b'world'}
  9. converter_test_data = (
  10. # ('url', ('url_name', 'app_name', {kwargs})),
  11. # aGVsbG8= is 'hello' encoded in base64.
  12. ('/base64/aGVsbG8=/', ('base64', '', {'value': b'hello'})),
  13. ('/base64/aGVsbG8=/subpatterns/d29ybGQ=/', ('subpattern-base64', '', included_kwargs)),
  14. ('/base64/aGVsbG8=/namespaced/d29ybGQ=/', ('subpattern-base64', 'namespaced-base64', included_kwargs)),
  15. )
  16. @override_settings(ROOT_URLCONF='urlpatterns.path_urls')
  17. class SimplifiedURLTests(SimpleTestCase):
  18. def test_path_lookup_without_parameters(self):
  19. match = resolve('/articles/2003/')
  20. self.assertEqual(match.url_name, 'articles-2003')
  21. self.assertEqual(match.args, ())
  22. self.assertEqual(match.kwargs, {})
  23. self.assertEqual(match.route, 'articles/2003/')
  24. def test_path_lookup_with_typed_parameters(self):
  25. match = resolve('/articles/2015/')
  26. self.assertEqual(match.url_name, 'articles-year')
  27. self.assertEqual(match.args, ())
  28. self.assertEqual(match.kwargs, {'year': 2015})
  29. self.assertEqual(match.route, 'articles/<int:year>/')
  30. def test_path_lookup_with_multiple_parameters(self):
  31. match = resolve('/articles/2015/04/12/')
  32. self.assertEqual(match.url_name, 'articles-year-month-day')
  33. self.assertEqual(match.args, ())
  34. self.assertEqual(match.kwargs, {'year': 2015, 'month': 4, 'day': 12})
  35. self.assertEqual(match.route, 'articles/<int:year>/<int:month>/<int:day>/')
  36. def test_two_variable_at_start_of_path_pattern(self):
  37. match = resolve('/en/foo/')
  38. self.assertEqual(match.url_name, 'lang-and-path')
  39. self.assertEqual(match.kwargs, {'lang': 'en', 'url': 'foo'})
  40. self.assertEqual(match.route, '<lang>/<path:url>/')
  41. def test_re_path(self):
  42. match = resolve('/regex/1/')
  43. self.assertEqual(match.url_name, 'regex')
  44. self.assertEqual(match.kwargs, {'pk': '1'})
  45. self.assertEqual(match.route, '^regex/(?P<pk>[0-9]+)/$')
  46. def test_re_path_with_optional_parameter(self):
  47. for url, kwargs in (
  48. ('/regex_optional/1/2/', {'arg1': '1', 'arg2': '2'}),
  49. ('/regex_optional/1/', {'arg1': '1'}),
  50. ):
  51. with self.subTest(url=url):
  52. match = resolve(url)
  53. self.assertEqual(match.url_name, 'regex_optional')
  54. self.assertEqual(match.kwargs, kwargs)
  55. self.assertEqual(
  56. match.route,
  57. r'^regex_optional/(?P<arg1>\d+)/(?:(?P<arg2>\d+)/)?',
  58. )
  59. def test_re_path_with_missing_optional_parameter(self):
  60. match = resolve('/regex_only_optional/')
  61. self.assertEqual(match.url_name, 'regex_only_optional')
  62. self.assertEqual(match.kwargs, {})
  63. self.assertEqual(match.args, ())
  64. self.assertEqual(
  65. match.route,
  66. r'^regex_only_optional/(?:(?P<arg1>\d+)/)?',
  67. )
  68. def test_path_lookup_with_inclusion(self):
  69. match = resolve('/included_urls/extra/something/')
  70. self.assertEqual(match.url_name, 'inner-extra')
  71. self.assertEqual(match.route, 'included_urls/extra/<extra>/')
  72. def test_path_lookup_with_empty_string_inclusion(self):
  73. match = resolve('/more/99/')
  74. self.assertEqual(match.url_name, 'inner-more')
  75. self.assertEqual(match.route, r'^more/(?P<extra>\w+)/$')
  76. def test_path_lookup_with_double_inclusion(self):
  77. match = resolve('/included_urls/more/some_value/')
  78. self.assertEqual(match.url_name, 'inner-more')
  79. self.assertEqual(match.route, r'included_urls/more/(?P<extra>\w+)/$')
  80. def test_path_reverse_without_parameter(self):
  81. url = reverse('articles-2003')
  82. self.assertEqual(url, '/articles/2003/')
  83. def test_path_reverse_with_parameter(self):
  84. url = reverse('articles-year-month-day', kwargs={'year': 2015, 'month': 4, 'day': 12})
  85. self.assertEqual(url, '/articles/2015/4/12/')
  86. @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
  87. def test_converter_resolve(self):
  88. for url, (url_name, app_name, kwargs) in converter_test_data:
  89. with self.subTest(url=url):
  90. match = resolve(url)
  91. self.assertEqual(match.url_name, url_name)
  92. self.assertEqual(match.app_name, app_name)
  93. self.assertEqual(match.kwargs, kwargs)
  94. @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
  95. def test_converter_reverse(self):
  96. for expected, (url_name, app_name, kwargs) in converter_test_data:
  97. if app_name:
  98. url_name = '%s:%s' % (app_name, url_name)
  99. with self.subTest(url=url_name):
  100. url = reverse(url_name, kwargs=kwargs)
  101. self.assertEqual(url, expected)
  102. @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
  103. def test_converter_reverse_with_second_layer_instance_namespace(self):
  104. kwargs = included_kwargs.copy()
  105. kwargs['last_value'] = b'world'
  106. url = reverse('instance-ns-base64:subsubpattern-base64', kwargs=kwargs)
  107. self.assertEqual(url, '/base64/aGVsbG8=/subpatterns/d29ybGQ=/d29ybGQ=/')
  108. def test_path_inclusion_is_matchable(self):
  109. match = resolve('/included_urls/extra/something/')
  110. self.assertEqual(match.url_name, 'inner-extra')
  111. self.assertEqual(match.kwargs, {'extra': 'something'})
  112. def test_path_inclusion_is_reversible(self):
  113. url = reverse('inner-extra', kwargs={'extra': 'something'})
  114. self.assertEqual(url, '/included_urls/extra/something/')
  115. def test_invalid_converter(self):
  116. msg = "URL route 'foo/<nonexistent:var>/' uses invalid converter 'nonexistent'."
  117. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  118. path('foo/<nonexistent:var>/', empty_view)
  119. def test_space_in_route(self):
  120. msg = "URL route 'space/<int: num>' cannot contain whitespace."
  121. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  122. path('space/<int: num>', empty_view)
  123. @override_settings(ROOT_URLCONF='urlpatterns.converter_urls')
  124. class ConverterTests(SimpleTestCase):
  125. def test_matching_urls(self):
  126. def no_converter(x):
  127. return x
  128. test_data = (
  129. ('int', {'0', '1', '01', 1234567890}, int),
  130. ('str', {'abcxyz'}, no_converter),
  131. ('path', {'allows.ANY*characters'}, no_converter),
  132. ('slug', {'abcxyz-ABCXYZ_01234567890'}, no_converter),
  133. ('uuid', {'39da9369-838e-4750-91a5-f7805cd82839'}, uuid.UUID),
  134. )
  135. for url_name, url_suffixes, converter in test_data:
  136. for url_suffix in url_suffixes:
  137. url = '/%s/%s/' % (url_name, url_suffix)
  138. with self.subTest(url=url):
  139. match = resolve(url)
  140. self.assertEqual(match.url_name, url_name)
  141. self.assertEqual(match.kwargs, {url_name: converter(url_suffix)})
  142. # reverse() works with string parameters.
  143. string_kwargs = {url_name: url_suffix}
  144. self.assertEqual(reverse(url_name, kwargs=string_kwargs), url)
  145. # reverse() also works with native types (int, UUID, etc.).
  146. if converter is not no_converter:
  147. # The converted value might be different for int (a
  148. # leading zero is lost in the conversion).
  149. converted_value = match.kwargs[url_name]
  150. converted_url = '/%s/%s/' % (url_name, converted_value)
  151. self.assertEqual(reverse(url_name, kwargs={url_name: converted_value}), converted_url)
  152. def test_nonmatching_urls(self):
  153. test_data = (
  154. ('int', {'-1', 'letters'}),
  155. ('str', {'', '/'}),
  156. ('path', {''}),
  157. ('slug', {'', 'stars*notallowed'}),
  158. ('uuid', {
  159. '',
  160. '9da9369-838e-4750-91a5-f7805cd82839',
  161. '39da9369-838-4750-91a5-f7805cd82839',
  162. '39da9369-838e-475-91a5-f7805cd82839',
  163. '39da9369-838e-4750-91a-f7805cd82839',
  164. '39da9369-838e-4750-91a5-f7805cd8283',
  165. }),
  166. )
  167. for url_name, url_suffixes in test_data:
  168. for url_suffix in url_suffixes:
  169. url = '/%s/%s/' % (url_name, url_suffix)
  170. with self.subTest(url=url), self.assertRaises(Resolver404):
  171. resolve(url)
  172. class ParameterRestrictionTests(SimpleTestCase):
  173. def test_non_identifier_parameter_name_causes_exception(self):
  174. msg = (
  175. "URL route 'hello/<int:1>/' uses parameter name '1' which isn't "
  176. "a valid Python identifier."
  177. )
  178. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  179. path(r'hello/<int:1>/', lambda r: None)
  180. def test_allows_non_ascii_but_valid_identifiers(self):
  181. # \u0394 is "GREEK CAPITAL LETTER DELTA", a valid identifier.
  182. p = path('hello/<str:\u0394>/', lambda r: None)
  183. match = p.resolve('hello/1/')
  184. self.assertEqual(match.kwargs, {'\u0394': '1'})
  185. @override_settings(ROOT_URLCONF='urlpatterns.path_dynamic_urls')
  186. class ConversionExceptionTests(SimpleTestCase):
  187. """How are errors in Converter.to_python() and to_url() handled?"""
  188. def test_resolve_value_error_means_no_match(self):
  189. @DynamicConverter.register_to_python
  190. def raises_value_error(value):
  191. raise ValueError()
  192. with self.assertRaises(Resolver404):
  193. resolve('/dynamic/abc/')
  194. def test_resolve_type_error_propagates(self):
  195. @DynamicConverter.register_to_python
  196. def raises_type_error(value):
  197. raise TypeError('This type error propagates.')
  198. with self.assertRaisesMessage(TypeError, 'This type error propagates.'):
  199. resolve('/dynamic/abc/')
  200. def test_reverse_value_error_propagates(self):
  201. @DynamicConverter.register_to_url
  202. def raises_value_error(value):
  203. raise ValueError('This value error propagates.')
  204. with self.assertRaisesMessage(ValueError, 'This value error propagates.'):
  205. reverse('dynamic', kwargs={'value': object()})