Files
old-saburly-wagtail-web/env/lib/python3.10/site-packages/wagtail/test/utils/page_tests.py
2024-08-27 20:33:44 +02:00

458 lines
18 KiB
Python

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(
" {}:\n {}".format(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: # noqa: BLE001
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: # noqa: BLE001
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:
self.client.post(path, data_to_post)
except Exception as e: # noqa: BLE001
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: # noqa: BLE001
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: # noqa: BLE001
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()