tests.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import string
  2. import uuid
  3. from django.core.exceptions import ImproperlyConfigured
  4. from django.test import SimpleTestCase
  5. from django.test.utils import override_settings
  6. from django.urls import NoReverseMatch, Resolver404, path, resolve, reverse
  7. from django.views import View
  8. from .converters import DynamicConverter
  9. from .views import empty_view
  10. included_kwargs = {'base': b'hello', 'value': b'world'}
  11. converter_test_data = (
  12. # ('url', ('url_name', 'app_name', {kwargs})),
  13. # aGVsbG8= is 'hello' encoded in base64.
  14. ('/base64/aGVsbG8=/', ('base64', '', {'value': b'hello'})),
  15. ('/base64/aGVsbG8=/subpatterns/d29ybGQ=/', ('subpattern-base64', '', included_kwargs)),
  16. ('/base64/aGVsbG8=/namespaced/d29ybGQ=/', ('subpattern-base64', 'namespaced-base64', included_kwargs)),
  17. )
  18. @override_settings(ROOT_URLCONF='urlpatterns.path_urls')
  19. class SimplifiedURLTests(SimpleTestCase):
  20. def test_path_lookup_without_parameters(self):
  21. match = resolve('/articles/2003/')
  22. self.assertEqual(match.url_name, 'articles-2003')
  23. self.assertEqual(match.args, ())
  24. self.assertEqual(match.kwargs, {})
  25. self.assertEqual(match.route, 'articles/2003/')
  26. def test_path_lookup_with_typed_parameters(self):
  27. match = resolve('/articles/2015/')
  28. self.assertEqual(match.url_name, 'articles-year')
  29. self.assertEqual(match.args, ())
  30. self.assertEqual(match.kwargs, {'year': 2015})
  31. self.assertEqual(match.route, 'articles/<int:year>/')
  32. def test_path_lookup_with_multiple_parameters(self):
  33. match = resolve('/articles/2015/04/12/')
  34. self.assertEqual(match.url_name, 'articles-year-month-day')
  35. self.assertEqual(match.args, ())
  36. self.assertEqual(match.kwargs, {'year': 2015, 'month': 4, 'day': 12})
  37. self.assertEqual(match.route, 'articles/<int:year>/<int:month>/<int:day>/')
  38. def test_two_variable_at_start_of_path_pattern(self):
  39. match = resolve('/en/foo/')
  40. self.assertEqual(match.url_name, 'lang-and-path')
  41. self.assertEqual(match.kwargs, {'lang': 'en', 'url': 'foo'})
  42. self.assertEqual(match.route, '<lang>/<path:url>/')
  43. def test_re_path(self):
  44. match = resolve('/regex/1/')
  45. self.assertEqual(match.url_name, 'regex')
  46. self.assertEqual(match.kwargs, {'pk': '1'})
  47. self.assertEqual(match.route, '^regex/(?P<pk>[0-9]+)/$')
  48. def test_re_path_with_optional_parameter(self):
  49. for url, kwargs in (
  50. ('/regex_optional/1/2/', {'arg1': '1', 'arg2': '2'}),
  51. ('/regex_optional/1/', {'arg1': '1'}),
  52. ):
  53. with self.subTest(url=url):
  54. match = resolve(url)
  55. self.assertEqual(match.url_name, 'regex_optional')
  56. self.assertEqual(match.kwargs, kwargs)
  57. self.assertEqual(
  58. match.route,
  59. r'^regex_optional/(?P<arg1>\d+)/(?:(?P<arg2>\d+)/)?',
  60. )
  61. def test_re_path_with_missing_optional_parameter(self):
  62. match = resolve('/regex_only_optional/')
  63. self.assertEqual(match.url_name, 'regex_only_optional')
  64. self.assertEqual(match.kwargs, {})
  65. self.assertEqual(match.args, ())
  66. self.assertEqual(
  67. match.route,
  68. r'^regex_only_optional/(?:(?P<arg1>\d+)/)?',
  69. )
  70. def test_path_lookup_with_inclusion(self):
  71. match = resolve('/included_urls/extra/something/')
  72. self.assertEqual(match.url_name, 'inner-extra')
  73. self.assertEqual(match.route, 'included_urls/extra/<extra>/')
  74. def test_path_lookup_with_empty_string_inclusion(self):
  75. match = resolve('/more/99/')
  76. self.assertEqual(match.url_name, 'inner-more')
  77. self.assertEqual(match.route, r'^more/(?P<extra>\w+)/$')
  78. def test_path_lookup_with_double_inclusion(self):
  79. match = resolve('/included_urls/more/some_value/')
  80. self.assertEqual(match.url_name, 'inner-more')
  81. self.assertEqual(match.route, r'included_urls/more/(?P<extra>\w+)/$')
  82. def test_path_reverse_without_parameter(self):
  83. url = reverse('articles-2003')
  84. self.assertEqual(url, '/articles/2003/')
  85. def test_path_reverse_with_parameter(self):
  86. url = reverse('articles-year-month-day', kwargs={'year': 2015, 'month': 4, 'day': 12})
  87. self.assertEqual(url, '/articles/2015/4/12/')
  88. @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
  89. def test_converter_resolve(self):
  90. for url, (url_name, app_name, kwargs) in converter_test_data:
  91. with self.subTest(url=url):
  92. match = resolve(url)
  93. self.assertEqual(match.url_name, url_name)
  94. self.assertEqual(match.app_name, app_name)
  95. self.assertEqual(match.kwargs, kwargs)
  96. @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
  97. def test_converter_reverse(self):
  98. for expected, (url_name, app_name, kwargs) in converter_test_data:
  99. if app_name:
  100. url_name = '%s:%s' % (app_name, url_name)
  101. with self.subTest(url=url_name):
  102. url = reverse(url_name, kwargs=kwargs)
  103. self.assertEqual(url, expected)
  104. @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
  105. def test_converter_reverse_with_second_layer_instance_namespace(self):
  106. kwargs = included_kwargs.copy()
  107. kwargs['last_value'] = b'world'
  108. url = reverse('instance-ns-base64:subsubpattern-base64', kwargs=kwargs)
  109. self.assertEqual(url, '/base64/aGVsbG8=/subpatterns/d29ybGQ=/d29ybGQ=/')
  110. def test_path_inclusion_is_matchable(self):
  111. match = resolve('/included_urls/extra/something/')
  112. self.assertEqual(match.url_name, 'inner-extra')
  113. self.assertEqual(match.kwargs, {'extra': 'something'})
  114. def test_path_inclusion_is_reversible(self):
  115. url = reverse('inner-extra', kwargs={'extra': 'something'})
  116. self.assertEqual(url, '/included_urls/extra/something/')
  117. def test_invalid_converter(self):
  118. msg = "URL route 'foo/<nonexistent:var>/' uses invalid converter 'nonexistent'."
  119. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  120. path('foo/<nonexistent:var>/', empty_view)
  121. def test_invalid_view(self):
  122. msg = 'view must be a callable or a list/tuple in the case of include().'
  123. with self.assertRaisesMessage(TypeError, msg):
  124. path('articles/', 'invalid_view')
  125. def test_invalid_view_instance(self):
  126. class EmptyCBV(View):
  127. pass
  128. msg = 'view must be a callable, pass EmptyCBV.as_view(), not EmptyCBV().'
  129. with self.assertRaisesMessage(TypeError, msg):
  130. path('foo', EmptyCBV())
  131. def test_whitespace_in_route(self):
  132. msg = (
  133. "URL route 'space/<int:num>/extra/<str:%stest>' cannot contain "
  134. "whitespace in angle brackets <…>"
  135. )
  136. for whitespace in string.whitespace:
  137. with self.subTest(repr(whitespace)):
  138. with self.assertRaisesMessage(ImproperlyConfigured, msg % whitespace):
  139. path('space/<int:num>/extra/<str:%stest>' % whitespace, empty_view)
  140. # Whitespaces are valid in paths.
  141. p = path('space%s/<int:num>/' % string.whitespace, empty_view)
  142. match = p.resolve('space%s/1/' % string.whitespace)
  143. self.assertEqual(match.kwargs, {'num': 1})
  144. @override_settings(ROOT_URLCONF='urlpatterns.converter_urls')
  145. class ConverterTests(SimpleTestCase):
  146. def test_matching_urls(self):
  147. def no_converter(x):
  148. return x
  149. test_data = (
  150. ('int', {'0', '1', '01', 1234567890}, int),
  151. ('str', {'abcxyz'}, no_converter),
  152. ('path', {'allows.ANY*characters'}, no_converter),
  153. ('slug', {'abcxyz-ABCXYZ_01234567890'}, no_converter),
  154. ('uuid', {'39da9369-838e-4750-91a5-f7805cd82839'}, uuid.UUID),
  155. )
  156. for url_name, url_suffixes, converter in test_data:
  157. for url_suffix in url_suffixes:
  158. url = '/%s/%s/' % (url_name, url_suffix)
  159. with self.subTest(url=url):
  160. match = resolve(url)
  161. self.assertEqual(match.url_name, url_name)
  162. self.assertEqual(match.kwargs, {url_name: converter(url_suffix)})
  163. # reverse() works with string parameters.
  164. string_kwargs = {url_name: url_suffix}
  165. self.assertEqual(reverse(url_name, kwargs=string_kwargs), url)
  166. # reverse() also works with native types (int, UUID, etc.).
  167. if converter is not no_converter:
  168. # The converted value might be different for int (a
  169. # leading zero is lost in the conversion).
  170. converted_value = match.kwargs[url_name]
  171. converted_url = '/%s/%s/' % (url_name, converted_value)
  172. self.assertEqual(reverse(url_name, kwargs={url_name: converted_value}), converted_url)
  173. def test_nonmatching_urls(self):
  174. test_data = (
  175. ('int', {'-1', 'letters'}),
  176. ('str', {'', '/'}),
  177. ('path', {''}),
  178. ('slug', {'', 'stars*notallowed'}),
  179. ('uuid', {
  180. '',
  181. '9da9369-838e-4750-91a5-f7805cd82839',
  182. '39da9369-838-4750-91a5-f7805cd82839',
  183. '39da9369-838e-475-91a5-f7805cd82839',
  184. '39da9369-838e-4750-91a-f7805cd82839',
  185. '39da9369-838e-4750-91a5-f7805cd8283',
  186. }),
  187. )
  188. for url_name, url_suffixes in test_data:
  189. for url_suffix in url_suffixes:
  190. url = '/%s/%s/' % (url_name, url_suffix)
  191. with self.subTest(url=url), self.assertRaises(Resolver404):
  192. resolve(url)
  193. @override_settings(ROOT_URLCONF='urlpatterns.path_same_name_urls')
  194. class SameNameTests(SimpleTestCase):
  195. def test_matching_urls_same_name(self):
  196. @DynamicConverter.register_to_url
  197. def requires_tiny_int(value):
  198. if value > 5:
  199. raise ValueError
  200. return value
  201. tests = [
  202. ('number_of_args', [
  203. ([], {}, '0/'),
  204. ([1], {}, '1/1/'),
  205. ]),
  206. ('kwargs_names', [
  207. ([], {'a': 1}, 'a/1/'),
  208. ([], {'b': 1}, 'b/1/'),
  209. ]),
  210. ('converter', [
  211. (['a/b'], {}, 'path/a/b/'),
  212. (['a b'], {}, 'str/a%20b/'),
  213. (['a-b'], {}, 'slug/a-b/'),
  214. (['2'], {}, 'int/2/'),
  215. (
  216. ['39da9369-838e-4750-91a5-f7805cd82839'],
  217. {},
  218. 'uuid/39da9369-838e-4750-91a5-f7805cd82839/'
  219. ),
  220. ]),
  221. ('regex', [
  222. (['ABC'], {}, 'uppercase/ABC/'),
  223. (['abc'], {}, 'lowercase/abc/'),
  224. ]),
  225. ('converter_to_url', [
  226. ([6], {}, 'int/6/'),
  227. ([1], {}, 'tiny_int/1/'),
  228. ]),
  229. ]
  230. for url_name, cases in tests:
  231. for args, kwargs, url_suffix in cases:
  232. expected_url = '/%s/%s' % (url_name, url_suffix)
  233. with self.subTest(url=expected_url):
  234. self.assertEqual(
  235. reverse(url_name, args=args, kwargs=kwargs),
  236. expected_url,
  237. )
  238. class ParameterRestrictionTests(SimpleTestCase):
  239. def test_integer_parameter_name_causes_exception(self):
  240. msg = (
  241. "URL route 'hello/<int:1>/' uses parameter name '1' which isn't "
  242. "a valid Python identifier."
  243. )
  244. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  245. path(r'hello/<int:1>/', lambda r: None)
  246. def test_non_identifier_parameter_name_causes_exception(self):
  247. msg = (
  248. "URL route 'b/<int:book.id>/' uses parameter name 'book.id' which "
  249. "isn't a valid Python identifier."
  250. )
  251. with self.assertRaisesMessage(ImproperlyConfigured, msg):
  252. path(r'b/<int:book.id>/', lambda r: None)
  253. def test_allows_non_ascii_but_valid_identifiers(self):
  254. # \u0394 is "GREEK CAPITAL LETTER DELTA", a valid identifier.
  255. p = path('hello/<str:\u0394>/', lambda r: None)
  256. match = p.resolve('hello/1/')
  257. self.assertEqual(match.kwargs, {'\u0394': '1'})
  258. @override_settings(ROOT_URLCONF='urlpatterns.path_dynamic_urls')
  259. class ConversionExceptionTests(SimpleTestCase):
  260. """How are errors in Converter.to_python() and to_url() handled?"""
  261. def test_resolve_value_error_means_no_match(self):
  262. @DynamicConverter.register_to_python
  263. def raises_value_error(value):
  264. raise ValueError()
  265. with self.assertRaises(Resolver404):
  266. resolve('/dynamic/abc/')
  267. def test_resolve_type_error_propagates(self):
  268. @DynamicConverter.register_to_python
  269. def raises_type_error(value):
  270. raise TypeError('This type error propagates.')
  271. with self.assertRaisesMessage(TypeError, 'This type error propagates.'):
  272. resolve('/dynamic/abc/')
  273. def test_reverse_value_error_means_no_match(self):
  274. @DynamicConverter.register_to_url
  275. def raises_value_error(value):
  276. raise ValueError
  277. with self.assertRaises(NoReverseMatch):
  278. reverse('dynamic', kwargs={'value': object()})
  279. def test_reverse_type_error_propagates(self):
  280. @DynamicConverter.register_to_url
  281. def raises_type_error(value):
  282. raise TypeError('This type error propagates.')
  283. with self.assertRaisesMessage(TypeError, 'This type error propagates.'):
  284. reverse('dynamic', kwargs={'value': object()})