tests.py 17 KB

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