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