|
@@ -1,18 +1,32 @@
|
|
|
+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 WagtailPageTests(WagtailTestUtils, TestCase):
|
|
|
+class WagtailPageTestCase(WagtailTestUtils, TestCase):
|
|
|
"""
|
|
|
- A set of asserts to help write tests for your own Wagtail site.
|
|
|
+ A set of assertions to help write tests for custom Wagtail page types
|
|
|
"""
|
|
|
|
|
|
- def setUp(self):
|
|
|
- super().setUp()
|
|
|
- self.login()
|
|
|
+ @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()
|
|
@@ -148,3 +162,288 @@ class WagtailPageTests(WagtailTestUtils, TestCase):
|
|
|
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()
|