page_tests.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. from typing import Any, Dict, Optional
  2. from unittest import mock
  3. from django.conf import settings
  4. from django.contrib.auth.base_user import AbstractBaseUser
  5. from django.http import Http404
  6. from django.test import TestCase
  7. from django.urls import reverse
  8. from django.utils.http import urlencode
  9. from django.utils.text import slugify
  10. from wagtail.coreutils import get_dummy_request
  11. from wagtail.models import Page
  12. from .form_data import querydict_from_html
  13. from .wagtail_tests import WagtailTestUtils
  14. AUTH_BACKEND = settings.AUTHENTICATION_BACKENDS[0]
  15. class WagtailPageTestCase(WagtailTestUtils, TestCase):
  16. """
  17. A set of assertions to help write tests for custom Wagtail page types
  18. """
  19. @classmethod
  20. def setUpClass(cls):
  21. super().setUpClass()
  22. cls.dummy_request = get_dummy_request()
  23. def _testCanCreateAt(self, parent_model, child_model):
  24. return child_model in parent_model.allowed_subpage_models()
  25. def assertCanCreateAt(self, parent_model, child_model, msg=None):
  26. """
  27. Assert a particular child Page type can be created under a parent
  28. Page type. ``parent_model`` and ``child_model`` should be the Page
  29. classes being tested.
  30. """
  31. if not self._testCanCreateAt(parent_model, child_model):
  32. msg = self._formatMessage(
  33. msg,
  34. "Can not create a %s.%s under a %s.%s"
  35. % (
  36. child_model._meta.app_label,
  37. child_model._meta.model_name,
  38. parent_model._meta.app_label,
  39. parent_model._meta.model_name,
  40. ),
  41. )
  42. raise self.failureException(msg)
  43. def assertCanNotCreateAt(self, parent_model, child_model, msg=None):
  44. """
  45. Assert a particular child Page type can not be created under a parent
  46. Page type. ``parent_model`` and ``child_model`` should be the Page
  47. classes being tested.
  48. """
  49. if self._testCanCreateAt(parent_model, child_model):
  50. msg = self._formatMessage(
  51. msg,
  52. "Can create a %s.%s under a %s.%s"
  53. % (
  54. child_model._meta.app_label,
  55. child_model._meta.model_name,
  56. parent_model._meta.app_label,
  57. parent_model._meta.model_name,
  58. ),
  59. )
  60. raise self.failureException(msg)
  61. def assertCanCreate(self, parent, child_model, data, msg=None, publish=True):
  62. """
  63. Assert that a child of the given Page type can be created under the
  64. parent, using the supplied POST data.
  65. ``parent`` should be a Page instance, and ``child_model`` should be a
  66. Page subclass. ``data`` should be a dict that will be POSTed at the
  67. Wagtail admin Page creation method.
  68. """
  69. self.assertCanCreateAt(parent.specific_class, child_model)
  70. if "slug" not in data and "title" in data:
  71. data["slug"] = slugify(data["title"])
  72. if publish:
  73. data["action-publish"] = "action-publish"
  74. add_url = reverse(
  75. "wagtailadmin_pages:add",
  76. args=[child_model._meta.app_label, child_model._meta.model_name, parent.pk],
  77. )
  78. response = self.client.post(add_url, data, follow=True)
  79. if response.status_code != 200:
  80. msg = self._formatMessage(
  81. msg,
  82. "Creating a %s.%s returned a %d"
  83. % (
  84. child_model._meta.app_label,
  85. child_model._meta.model_name,
  86. response.status_code,
  87. ),
  88. )
  89. raise self.failureException(msg)
  90. if response.redirect_chain == []:
  91. if "form" not in response.context:
  92. msg = self._formatMessage(msg, "Creating a page failed unusually")
  93. raise self.failureException(msg)
  94. form = response.context["form"]
  95. if not form.errors:
  96. msg = self._formatMessage(
  97. msg, "Creating a page failed for an unknown reason"
  98. )
  99. raise self.failureException(msg)
  100. errors = "\n".join(
  101. " %s:\n %s" % (field, "\n ".join(errors))
  102. for field, errors in sorted(form.errors.items())
  103. )
  104. msg = self._formatMessage(
  105. msg,
  106. "Validation errors found when creating a %s.%s:\n%s"
  107. % (child_model._meta.app_label, child_model._meta.model_name, errors),
  108. )
  109. raise self.failureException(msg)
  110. if publish:
  111. expected_url = reverse("wagtailadmin_explore", args=[parent.pk])
  112. else:
  113. expected_url = reverse(
  114. "wagtailadmin_pages:edit", args=[Page.objects.order_by("pk").last().pk]
  115. )
  116. if response.redirect_chain != [(expected_url, 302)]:
  117. msg = self._formatMessage(
  118. msg,
  119. "Creating a page %s.%s didn't redirect the user to the expected page %s, but to %s"
  120. % (
  121. child_model._meta.app_label,
  122. child_model._meta.model_name,
  123. expected_url,
  124. response.redirect_chain,
  125. ),
  126. )
  127. raise self.failureException(msg)
  128. def assertAllowedSubpageTypes(self, parent_model, child_models, msg=None):
  129. """
  130. Test that the only page types that can be created under
  131. ``parent_model`` are ``child_models``.
  132. The list of allowed child models may differ from those set in
  133. ``Page.subpage_types``, if the child models have set
  134. ``Page.parent_page_types``.
  135. """
  136. self.assertEqual(
  137. set(parent_model.allowed_subpage_models()), set(child_models), msg=msg
  138. )
  139. def assertAllowedParentPageTypes(self, child_model, parent_models, msg=None):
  140. """
  141. Test that the only page types that ``child_model`` can be created under
  142. are ``parent_models``.
  143. The list of allowed parent models may differ from those set in
  144. ``Page.parent_page_types``, if the parent models have set
  145. ``Page.subpage_types``.
  146. """
  147. self.assertEqual(
  148. set(child_model.allowed_parent_page_models()), set(parent_models), msg=msg
  149. )
  150. def assertPageIsRoutable(
  151. self,
  152. page: Page,
  153. route_path: Optional[str] = "/",
  154. msg: Optional[str] = None,
  155. ):
  156. """
  157. Asserts that ``page`` can be routed to without raising a ``Http404`` error.
  158. For page types with multiple routes, you can use ``route_path`` to specify an alternate route to test.
  159. """
  160. path = page.get_url(self.dummy_request)
  161. if route_path != "/":
  162. path = path.rstrip("/") + "/" + route_path.lstrip("/")
  163. site = page.get_site()
  164. if site is None:
  165. msg = self._formatMessage(
  166. msg,
  167. 'Failed to route to "%s" for %s "%s". The page does not belong to any sites.'
  168. % (type(page).__name__, route_path, page),
  169. )
  170. raise self.failureException(msg)
  171. path_components = [component for component in path.split("/") if component]
  172. try:
  173. page, args, kwargs = site.root_page.localized.specific.route(
  174. self.dummy_request, path_components
  175. )
  176. except Http404:
  177. msg = self._formatMessage(
  178. msg,
  179. 'Failed to route to "%(route_path)s" for %(page_type)s "%(page)s". A Http404 was raised for path: "%(full_path)s".'
  180. % {
  181. "route_path": route_path,
  182. "page_type": type(page).__name__,
  183. "page": page,
  184. "full_path": path,
  185. },
  186. )
  187. raise self.failureException(msg)
  188. def assertPageIsRenderable(
  189. self,
  190. page: Page,
  191. route_path: Optional[str] = "/",
  192. query_data: Optional[Dict[str, Any]] = None,
  193. post_data: Optional[Dict[str, Any]] = None,
  194. user: Optional[AbstractBaseUser] = None,
  195. accept_404: Optional[bool] = False,
  196. accept_redirect: Optional[bool] = False,
  197. msg: Optional[str] = None,
  198. ):
  199. """
  200. Asserts that ``page`` can be rendered without raising a fatal error.
  201. For page types with multiple routes, you can use ``route_path`` to specify an alternate route to test.
  202. When ``post_data`` is provided, the test makes a ``POST`` request with ``post_data`` in the request body. Otherwise, a ``GET`` request is made.
  203. When supplied, ``query_data`` is converted to a querystring and added to the request URL (regardless of whether ``post_data`` is provided).
  204. When ``user`` is provided, the test is conducted with them as the active user.
  205. By default, the assertion will fail if the request to the page URL results in a 301, 302 or 404 HTTP response. If you are testing a page/route
  206. where a 404 response is expected, you can use ``accept_404=True`` to indicate this, and the assertion will pass when encountering a 404. Likewise,
  207. if you are testing a page/route where a redirect response is expected, you can use `accept_redirect=True` to indicate this, and the assertion will
  208. pass when encountering 301 or 302.
  209. """
  210. if user:
  211. self.client.force_login(user, AUTH_BACKEND)
  212. path = page.get_url(self.dummy_request)
  213. if route_path != "/":
  214. path = path.rstrip("/") + "/" + route_path.lstrip("/")
  215. post_kwargs = {}
  216. if post_data is not None:
  217. post_kwargs = {"data": post_data}
  218. if query_data:
  219. post_kwargs["QUERYSTRING"] = urlencode(query_data, doseq=True)
  220. try:
  221. if post_data is None:
  222. resp = self.client.get(path, data=query_data)
  223. else:
  224. resp = self.client.post(path, **post_kwargs)
  225. except Exception as e:
  226. msg = self._formatMessage(
  227. msg,
  228. 'Failed to render route "%(route_path)s" for %(page_type)s "%(page)s":\n%(exc)s'
  229. % {
  230. "route_path": route_path,
  231. "page_type": type(page).__name__,
  232. "page": page,
  233. "exc": e,
  234. },
  235. )
  236. raise self.failureException(msg)
  237. finally:
  238. if user:
  239. self.client.logout()
  240. if (
  241. resp.status_code == 200
  242. or (accept_404 and resp.status_code == 404)
  243. or (accept_redirect and resp.status_code in (301, 302))
  244. or isinstance(resp, mock.MagicMock)
  245. ):
  246. return
  247. msg = self._formatMessage(
  248. msg,
  249. 'Failed to render route "%(route_path)s" for %(page_type)s "%(page)s":\nA HTTP %(code)s response was received for path: "%(full_path)s".'
  250. % {
  251. "route_path": route_path,
  252. "page_type": type(page).__name__,
  253. "page": page,
  254. "code": resp.status_code,
  255. "full_path": path,
  256. },
  257. )
  258. raise self.failureException(msg)
  259. def assertPageIsEditable(
  260. self,
  261. page: Page,
  262. post_data: Optional[Dict[str, Any]] = None,
  263. user: Optional[AbstractBaseUser] = None,
  264. msg: Optional[str] = None,
  265. ):
  266. """
  267. Asserts that the page edit view works for ``page`` without raising a fatal error.
  268. When ``user`` is provided, the test is conducted with them as the active user. Otherwise, a superuser is created and used for the test.
  269. After a successful ``GET`` request, a ``POST`` request is made with field data in the request body. If ``post_data`` is provided, that will be used for this purpose. If not, this data will be extracted from the ``GET`` response HTML.
  270. """
  271. if user:
  272. # rule out permission issues early on
  273. if not page.permissions_for_user(user).can_edit():
  274. self._formatMessage(
  275. msg,
  276. 'Failed to load edit view for %(page_type)s "%(page)s":\nUser "%(user)s" have insufficient permissions.'
  277. % {
  278. "page_type": type(page).__name__,
  279. "page": page,
  280. "user": user,
  281. },
  282. )
  283. raise self.failureException(msg)
  284. else:
  285. if not hasattr(self, "_pageiseditable_superuser"):
  286. self._pageiseditable_superuser = self.create_superuser(
  287. "assertpageiseditable"
  288. )
  289. user = self._pageiseditable_superuser
  290. self.client.force_login(user, AUTH_BACKEND)
  291. path = reverse("wagtailadmin_pages:edit", kwargs={"page_id": page.id})
  292. try:
  293. response = self.client.get(path)
  294. except Exception as e:
  295. self.client.logout()
  296. msg = self._formatMessage(
  297. msg,
  298. 'Failed to load edit view via GET for %(page_type)s "%(page)s":\n%(exc)s'
  299. % {"page_type": type(page).__name__, "page": page, "exc": e},
  300. )
  301. raise self.failureException(msg)
  302. if response.status_code != 200:
  303. self.client.logout()
  304. msg = self._formatMessage(
  305. msg,
  306. 'Failed to load edit view via GET for %(page_type)s "%(page)s":\nReceived response with HTTP status code: %(code)s.'
  307. % {
  308. "page_type": type(page).__name__,
  309. "page": page,
  310. "code": response.status_code,
  311. },
  312. )
  313. raise self.failureException(msg)
  314. if post_data is not None:
  315. data_to_post = post_data
  316. else:
  317. data_to_post = querydict_from_html(
  318. response.content.decode(), form_id="page-edit-form"
  319. )
  320. data_to_post["action-publish"] = ""
  321. try:
  322. response = self.client.post(path, data_to_post)
  323. except Exception as e:
  324. msg = self._formatMessage(
  325. msg,
  326. 'Failed to load edit view via POST for %(page_type)s "%(page)s":\n%(exc)s'
  327. % {"page_type": type(page).__name__, "page": page, "exc": e},
  328. )
  329. raise self.failureException(msg)
  330. finally:
  331. page.save() # undo any changes to page
  332. self.client.logout()
  333. def assertPageIsPreviewable(
  334. self,
  335. page: Page,
  336. mode: Optional[str] = "",
  337. post_data: Optional[Dict[str, Any]] = None,
  338. user: Optional[AbstractBaseUser] = None,
  339. msg: Optional[str] = None,
  340. ):
  341. """
  342. Asserts that the page preview view can be loaded for ``page`` without raising a fatal error.
  343. For page types that support multiple preview modes, ``mode`` can be used to specify the preview mode to be tested.
  344. When ``user`` is provided, the test is conducted with them as the active user. Otherwise, a superuser is created and used for the test.
  345. To load the preview, the test client needs to make a ``POST`` request including all required field data in the request body.
  346. If ``post_data`` is provided, that will be used for this purpose. If not, the method will attempt to extract this data from the page edit view.
  347. """
  348. if not user:
  349. if not hasattr(self, "_pageispreviewable_superuser"):
  350. self._pageispreviewable_superuser = self.create_superuser(
  351. "assertpageispreviewable"
  352. )
  353. user = self._pageispreviewable_superuser
  354. self.client.force_login(user, AUTH_BACKEND)
  355. if post_data is None:
  356. edit_path = reverse("wagtailadmin_pages:edit", kwargs={"page_id": page.id})
  357. html = self.client.get(edit_path).content.decode()
  358. post_data = querydict_from_html(html, form_id="page-edit-form")
  359. preview_path = reverse(
  360. "wagtailadmin_pages:preview_on_edit", kwargs={"page_id": page.id}
  361. )
  362. try:
  363. response = self.client.post(
  364. preview_path, data=post_data, QUERYSTRING=f"mode={mode}"
  365. )
  366. self.assertEqual(response.status_code, 200)
  367. self.assertJSONEqual(
  368. response.content.decode(),
  369. {"is_valid": True, "is_available": True},
  370. )
  371. except Exception as e:
  372. self.client.logout()
  373. msg = self._formatMessage(
  374. msg,
  375. 'Failed to load preview for %(page_type)s "%(page)s" with mode="%(mode)s":\n%(exc)s'
  376. % {
  377. "page_type": type(page).__name__,
  378. "page": page,
  379. "mode": mode,
  380. "exc": e,
  381. },
  382. )
  383. raise self.failureException(msg)
  384. try:
  385. self.client.get(preview_path, data={"mode": mode})
  386. except Exception as e:
  387. msg = self._formatMessage(
  388. msg,
  389. 'Failed to load preview for %(page_type)s "%(page)s" with mode="%(mode)s":\n%(exc)s'
  390. % {
  391. "page_type": type(page).__name__,
  392. "page": page,
  393. "mode": mode,
  394. "exc": e,
  395. },
  396. )
  397. raise self.failureException(msg)
  398. finally:
  399. self.client.logout()
  400. class WagtailPageTests(WagtailPageTestCase):
  401. def setUp(self):
  402. super().setUp()
  403. self.login()