123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- from typing import Any, Dict, Optional
- from unittest import mock
- from django.conf import settings
- from django.contrib.auth.base_user import AbstractBaseUser
- from django.http import Http404
- from django.test import TestCase
- from django.urls import reverse
- from django.utils.http import urlencode
- from django.utils.text import slugify
- from wagtail.coreutils import get_dummy_request
- from wagtail.models import Page
- from .form_data import querydict_from_html
- from .wagtail_tests import WagtailTestUtils
- AUTH_BACKEND = settings.AUTHENTICATION_BACKENDS[0]
- class WagtailPageTestCase(WagtailTestUtils, TestCase):
- """
- A set of assertions to help write tests for custom Wagtail page types
- """
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- cls.dummy_request = get_dummy_request()
- def _testCanCreateAt(self, parent_model, child_model):
- return child_model in parent_model.allowed_subpage_models()
- def assertCanCreateAt(self, parent_model, child_model, msg=None):
- """
- Assert a particular child Page type can be created under a parent
- Page type. ``parent_model`` and ``child_model`` should be the Page
- classes being tested.
- """
- if not self._testCanCreateAt(parent_model, child_model):
- msg = self._formatMessage(
- msg,
- "Can not create a %s.%s under a %s.%s"
- % (
- child_model._meta.app_label,
- child_model._meta.model_name,
- parent_model._meta.app_label,
- parent_model._meta.model_name,
- ),
- )
- raise self.failureException(msg)
- def assertCanNotCreateAt(self, parent_model, child_model, msg=None):
- """
- Assert a particular child Page type can not be created under a parent
- Page type. ``parent_model`` and ``child_model`` should be the Page
- classes being tested.
- """
- if self._testCanCreateAt(parent_model, child_model):
- msg = self._formatMessage(
- msg,
- "Can create a %s.%s under a %s.%s"
- % (
- child_model._meta.app_label,
- child_model._meta.model_name,
- parent_model._meta.app_label,
- parent_model._meta.model_name,
- ),
- )
- raise self.failureException(msg)
- def assertCanCreate(self, parent, child_model, data, msg=None, publish=True):
- """
- Assert that a child of the given Page type can be created under the
- parent, using the supplied POST data.
- ``parent`` should be a Page instance, and ``child_model`` should be a
- Page subclass. ``data`` should be a dict that will be POSTed at the
- Wagtail admin Page creation method.
- """
- self.assertCanCreateAt(parent.specific_class, child_model)
- if "slug" not in data and "title" in data:
- data["slug"] = slugify(data["title"])
- if publish:
- data["action-publish"] = "action-publish"
- add_url = reverse(
- "wagtailadmin_pages:add",
- args=[child_model._meta.app_label, child_model._meta.model_name, parent.pk],
- )
- response = self.client.post(add_url, data, follow=True)
- if response.status_code != 200:
- msg = self._formatMessage(
- msg,
- "Creating a %s.%s returned a %d"
- % (
- child_model._meta.app_label,
- child_model._meta.model_name,
- response.status_code,
- ),
- )
- raise self.failureException(msg)
- if response.redirect_chain == []:
- if "form" not in response.context:
- msg = self._formatMessage(msg, "Creating a page failed unusually")
- raise self.failureException(msg)
- form = response.context["form"]
- if not form.errors:
- msg = self._formatMessage(
- msg, "Creating a page failed for an unknown reason"
- )
- raise self.failureException(msg)
- errors = "\n".join(
- " %s:\n %s" % (field, "\n ".join(errors))
- for field, errors in sorted(form.errors.items())
- )
- msg = self._formatMessage(
- msg,
- "Validation errors found when creating a %s.%s:\n%s"
- % (child_model._meta.app_label, child_model._meta.model_name, errors),
- )
- raise self.failureException(msg)
- if publish:
- expected_url = reverse("wagtailadmin_explore", args=[parent.pk])
- else:
- expected_url = reverse(
- "wagtailadmin_pages:edit", args=[Page.objects.order_by("pk").last().pk]
- )
- if response.redirect_chain != [(expected_url, 302)]:
- msg = self._formatMessage(
- msg,
- "Creating a page %s.%s didn't redirect the user to the expected page %s, but to %s"
- % (
- child_model._meta.app_label,
- child_model._meta.model_name,
- expected_url,
- response.redirect_chain,
- ),
- )
- raise self.failureException(msg)
- def assertAllowedSubpageTypes(self, parent_model, child_models, msg=None):
- """
- Test that the only page types that can be created under
- ``parent_model`` are ``child_models``.
- The list of allowed child models may differ from those set in
- ``Page.subpage_types``, if the child models have set
- ``Page.parent_page_types``.
- """
- self.assertEqual(
- set(parent_model.allowed_subpage_models()), set(child_models), msg=msg
- )
- def assertAllowedParentPageTypes(self, child_model, parent_models, msg=None):
- """
- Test that the only page types that ``child_model`` can be created under
- are ``parent_models``.
- The list of allowed parent models may differ from those set in
- ``Page.parent_page_types``, if the parent models have set
- ``Page.subpage_types``.
- """
- self.assertEqual(
- set(child_model.allowed_parent_page_models()), set(parent_models), msg=msg
- )
- def assertPageIsRoutable(
- self,
- page: Page,
- route_path: Optional[str] = "/",
- msg: Optional[str] = None,
- ):
- """
- Asserts that ``page`` can be routed to without raising a ``Http404`` error.
- For page types with multiple routes, you can use ``route_path`` to specify an alternate route to test.
- """
- path = page.get_url(self.dummy_request)
- if route_path != "/":
- path = path.rstrip("/") + "/" + route_path.lstrip("/")
- site = page.get_site()
- if site is None:
- msg = self._formatMessage(
- msg,
- 'Failed to route to "%s" for %s "%s". The page does not belong to any sites.'
- % (type(page).__name__, route_path, page),
- )
- raise self.failureException(msg)
- path_components = [component for component in path.split("/") if component]
- try:
- page, args, kwargs = site.root_page.localized.specific.route(
- self.dummy_request, path_components
- )
- except Http404:
- msg = self._formatMessage(
- msg,
- 'Failed to route to "%(route_path)s" for %(page_type)s "%(page)s". A Http404 was raised for path: "%(full_path)s".'
- % {
- "route_path": route_path,
- "page_type": type(page).__name__,
- "page": page,
- "full_path": path,
- },
- )
- raise self.failureException(msg)
- def assertPageIsRenderable(
- self,
- page: Page,
- route_path: Optional[str] = "/",
- query_data: Optional[Dict[str, Any]] = None,
- post_data: Optional[Dict[str, Any]] = None,
- user: Optional[AbstractBaseUser] = None,
- accept_404: Optional[bool] = False,
- accept_redirect: Optional[bool] = False,
- msg: Optional[str] = None,
- ):
- """
- Asserts that ``page`` can be rendered without raising a fatal error.
- For page types with multiple routes, you can use ``route_path`` to specify an alternate route to test.
- When ``post_data`` is provided, the test makes a ``POST`` request with ``post_data`` in the request body. Otherwise, a ``GET`` request is made.
- When supplied, ``query_data`` is converted to a querystring and added to the request URL (regardless of whether ``post_data`` is provided).
- When ``user`` is provided, the test is conducted with them as the active user.
- 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
- 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,
- 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
- pass when encountering 301 or 302.
- """
- if user:
- self.client.force_login(user, AUTH_BACKEND)
- path = page.get_url(self.dummy_request)
- if route_path != "/":
- path = path.rstrip("/") + "/" + route_path.lstrip("/")
- post_kwargs = {}
- if post_data is not None:
- post_kwargs = {"data": post_data}
- if query_data:
- post_kwargs["QUERYSTRING"] = urlencode(query_data, doseq=True)
- try:
- if post_data is None:
- resp = self.client.get(path, data=query_data)
- else:
- resp = self.client.post(path, **post_kwargs)
- except Exception as e:
- msg = self._formatMessage(
- msg,
- 'Failed to render route "%(route_path)s" for %(page_type)s "%(page)s":\n%(exc)s'
- % {
- "route_path": route_path,
- "page_type": type(page).__name__,
- "page": page,
- "exc": e,
- },
- )
- raise self.failureException(msg)
- finally:
- if user:
- self.client.logout()
- if (
- resp.status_code == 200
- or (accept_404 and resp.status_code == 404)
- or (accept_redirect and resp.status_code in (301, 302))
- or isinstance(resp, mock.MagicMock)
- ):
- return
- msg = self._formatMessage(
- msg,
- '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".'
- % {
- "route_path": route_path,
- "page_type": type(page).__name__,
- "page": page,
- "code": resp.status_code,
- "full_path": path,
- },
- )
- raise self.failureException(msg)
- def assertPageIsEditable(
- self,
- page: Page,
- post_data: Optional[Dict[str, Any]] = None,
- user: Optional[AbstractBaseUser] = None,
- msg: Optional[str] = None,
- ):
- """
- Asserts that the page edit view works for ``page`` without raising a fatal error.
- When ``user`` is provided, the test is conducted with them as the active user. Otherwise, a superuser is created and used for the test.
- 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.
- """
- if user:
- # rule out permission issues early on
- if not page.permissions_for_user(user).can_edit():
- self._formatMessage(
- msg,
- 'Failed to load edit view for %(page_type)s "%(page)s":\nUser "%(user)s" have insufficient permissions.'
- % {
- "page_type": type(page).__name__,
- "page": page,
- "user": user,
- },
- )
- raise self.failureException(msg)
- else:
- if not hasattr(self, "_pageiseditable_superuser"):
- self._pageiseditable_superuser = self.create_superuser(
- "assertpageiseditable"
- )
- user = self._pageiseditable_superuser
- self.client.force_login(user, AUTH_BACKEND)
- path = reverse("wagtailadmin_pages:edit", kwargs={"page_id": page.id})
- try:
- response = self.client.get(path)
- except Exception as e:
- self.client.logout()
- msg = self._formatMessage(
- msg,
- 'Failed to load edit view via GET for %(page_type)s "%(page)s":\n%(exc)s'
- % {"page_type": type(page).__name__, "page": page, "exc": e},
- )
- raise self.failureException(msg)
- if response.status_code != 200:
- self.client.logout()
- msg = self._formatMessage(
- msg,
- 'Failed to load edit view via GET for %(page_type)s "%(page)s":\nReceived response with HTTP status code: %(code)s.'
- % {
- "page_type": type(page).__name__,
- "page": page,
- "code": response.status_code,
- },
- )
- raise self.failureException(msg)
- if post_data is not None:
- data_to_post = post_data
- else:
- data_to_post = querydict_from_html(
- response.content.decode(), form_id="page-edit-form"
- )
- data_to_post["action-publish"] = ""
- try:
- response = self.client.post(path, data_to_post)
- except Exception as e:
- msg = self._formatMessage(
- msg,
- 'Failed to load edit view via POST for %(page_type)s "%(page)s":\n%(exc)s'
- % {"page_type": type(page).__name__, "page": page, "exc": e},
- )
- raise self.failureException(msg)
- finally:
- page.save() # undo any changes to page
- self.client.logout()
- def assertPageIsPreviewable(
- self,
- page: Page,
- mode: Optional[str] = "",
- post_data: Optional[Dict[str, Any]] = None,
- user: Optional[AbstractBaseUser] = None,
- msg: Optional[str] = None,
- ):
- """
- Asserts that the page preview view can be loaded for ``page`` without raising a fatal error.
- For page types that support multiple preview modes, ``mode`` can be used to specify the preview mode to be tested.
- When ``user`` is provided, the test is conducted with them as the active user. Otherwise, a superuser is created and used for the test.
- To load the preview, the test client needs to make a ``POST`` request including all required field data in the request body.
- 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.
- """
- if not user:
- if not hasattr(self, "_pageispreviewable_superuser"):
- self._pageispreviewable_superuser = self.create_superuser(
- "assertpageispreviewable"
- )
- user = self._pageispreviewable_superuser
- self.client.force_login(user, AUTH_BACKEND)
- if post_data is None:
- edit_path = reverse("wagtailadmin_pages:edit", kwargs={"page_id": page.id})
- html = self.client.get(edit_path).content.decode()
- post_data = querydict_from_html(html, form_id="page-edit-form")
- preview_path = reverse(
- "wagtailadmin_pages:preview_on_edit", kwargs={"page_id": page.id}
- )
- try:
- response = self.client.post(
- preview_path, data=post_data, QUERYSTRING=f"mode={mode}"
- )
- self.assertEqual(response.status_code, 200)
- self.assertJSONEqual(
- response.content.decode(),
- {"is_valid": True, "is_available": True},
- )
- except Exception as e:
- self.client.logout()
- msg = self._formatMessage(
- msg,
- 'Failed to load preview for %(page_type)s "%(page)s" with mode="%(mode)s":\n%(exc)s'
- % {
- "page_type": type(page).__name__,
- "page": page,
- "mode": mode,
- "exc": e,
- },
- )
- raise self.failureException(msg)
- try:
- self.client.get(preview_path, data={"mode": mode})
- except Exception as e:
- msg = self._formatMessage(
- msg,
- 'Failed to load preview for %(page_type)s "%(page)s" with mode="%(mode)s":\n%(exc)s'
- % {
- "page_type": type(page).__name__,
- "page": page,
- "mode": mode,
- "exc": e,
- },
- )
- raise self.failureException(msg)
- finally:
- self.client.logout()
- class WagtailPageTests(WagtailPageTestCase):
- def setUp(self):
- super().setUp()
- self.login()
|