Skip to content
Snippets Groups Projects
tests.py 12.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • from typing import List
    
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
    from django.contrib.auth import get_user_model
    
    from django.contrib.messages import get_messages
    from django.contrib.messages.storage.base import Message
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
    from django.test import TestCase
    
    from django.urls import reverse_lazy, reverse
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
    
    from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \
        ConstraintViolation, DefaultSlot
    
    
    class BasicViewTests:
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
        """
        Parent class for "standard" tests of views
    
        Provided with a list of views and arguments (if necessary), this will test that views
        - render correctly without errors
        - are only reachable with the correct rights (neither too freely nor too restricted)
    
        To do this, the test creates sample users, fixtures are loaded automatically by the django test framework.
        It also provides helper functions, e.g., to check for correct messages to the user or more simply generate
        the URLs to test
    
        In this class, methods from :class:`TestCase` will be called at multiple places event though TestCase is not a
        parent of this class but has to be included as parent in concrete implementations of this class seperately.
        It however still makes sense to treat this class as some kind of mixin and not implement it as a child of TestCase,
        since the test framework does not understand the concept of abstract test definitions and would handle this class
        as real test case otherwise, distorting the test results.
        """
        # pylint: disable=no-member
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
        VIEWS = []
        APP_NAME = ''
        VIEWS_STAFF_ONLY = []
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
        def setUp(self):  # pylint: disable=invalid-name
            """
            Setup testing by creating sample users
            """
            user_model = get_user_model()
            self.staff_user = user_model.objects.create(
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                username='Test Staff User', email='teststaff@example.com', password='staffpw',
                is_staff=True, is_active=True
            )
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            self.admin_user = user_model.objects.create(
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                username='Test Admin User', email='testadmin@example.com', password='adminpw',
                is_staff=True, is_superuser=True, is_active=True
            )
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            self.deactivated_user = user_model.objects.create(
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw',
                is_staff=True, is_active=False
            )
    
        def _name_and_url(self, view_name):
            """
            Get full view name (with prefix if there is one) and url from raw view definition
    
            :param view_name: raw definition of a view
            :type view_name: (str, dict)
            :return: full view name with prefix if applicable, url of the view
            :rtype: str, str
            """
            view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
    
            url = reverse(view_name_with_prefix, kwargs=view_name[1])
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            return view_name_with_prefix, url
    
    
        def _assert_message(self, response, expected_message, msg_prefix=""):
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            """
            Assert that the correct message is shown and cause test to fail if not
    
            :param response: response to check
            :param expected_message: message that should be shown
            :param msg_prefix: prefix for the error message when test fails
            """
    
            messages:List[Message] = list(get_messages(response.wsgi_request))
    
            msg_count = "No message shown to user"
            msg_content = "Wrong message, expected '{expected_message}'"
            if msg_prefix != "":
                msg_count = f"{msg_prefix}: {msg_count}"
                msg_content = f"{msg_prefix}: {msg_content}"
    
            # Check that the last message correctly reports the issue
            # (there might be more messages from previous calls that were not already rendered)
            self.assertGreater(len(messages), 0, msg=msg_count)
            self.assertEqual(messages[-1].message, expected_message, msg=msg_content)
    
    
        def test_views_for_200(self):
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            """
            Test the list of public views (as specified in "VIEWS") for error-free rendering
            """
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            for view_name in self.VIEWS:
                view_name_with_prefix, url = self._name_and_url(view_name)
    
                try:
                    response = self.client.get(url)
                    self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken")
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                except Exception: # pylint: disable=broad-exception-caught
                    self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
                              f"\n\n{traceback.format_exc()}")
    
        def test_access_control_staff_only(self):
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            """
            Test whether internal views (as specified in "VIEWS_STAFF_ONLY" are visible to staff users and staff users only
            """
            # Not logged in? Views should not be visible
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            self.client.logout()
    
            for view_name_info in self.VIEWS_STAFF_ONLY:
                expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
                view_name_with_prefix, url = self._name_and_url(view_name_info)
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                response = self.client.get(url)
    
                self.assertEqual(response.status_code, expected_response_code,
                                 msg=f"{view_name_with_prefix} ({url}) accessible by non-staff")
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            # Logged in? Views should be visible
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            self.client.force_login(self.staff_user)
    
            for view_name_info in self.VIEWS_STAFF_ONLY:
                view_name_with_prefix, url = self._name_and_url(view_name_info)
    
                try:
                    response = self.client.get(url)
                    self.assertEqual(response.status_code, 200,
                                     msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)")
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                except Exception:  # pylint: disable=broad-exception-caught
                    self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
                              f"\n\n{traceback.format_exc()}")
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            # Disabled user? Views should not be visible
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            self.client.force_login(self.deactivated_user)
    
            for view_name_info in self.VIEWS_STAFF_ONLY:
                expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
                view_name_with_prefix, url = self._name_and_url(view_name_info)
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                response = self.client.get(url)
    
                self.assertEqual(response.status_code, expected_response_code,
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                                 msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user")
    
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
        def _to_sendable_value(self, val):
    
            """
            Create representation sendable via POST from form data
    
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            Needed to automatically check create, update and delete views
    
            :param val: value to prepare
            :type val: any
    
            :return: prepared value (normally either raw value or primary key of complex object)
            """
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            if isinstance(val, list):
                return [e.pk for e in val]
            if type(val) == "RelatedManager":  # pylint: disable=unidiomatic-typecheck
                return [e.pk for e in val.all()]
            return val
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            Test edit forms (as specified in "EDIT_TESTCASES") in the most simple way (sending them again unchanged)
    
            """
            for testcase in self.EDIT_TESTCASES:
                self._test_submit_edit_form(testcase)
    
        def _test_submit_edit_form(self, testcase):
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            """
            Test a single edit form by rendering and sending it again unchanged
    
            This will test for correct rendering, dispatching/redirecting, messages and access control handling
    
            :param testcase: details of the form to test
            """
    
            name, url = self._name_and_url((testcase["view"], testcase["kwargs"]))
            form_name = testcase.get("form_name", "form")
            expected_code = testcase.get("expected_code", 302)
            if "target_view" in testcase.keys():
                kwargs = testcase.get("target_kwargs", testcase["kwargs"])
                _, target_url = self._name_and_url((testcase["target_view"], kwargs))
            else:
                target_url = url
            expected_message = testcase.get("expected_message", "")
            admin_user = testcase.get("admin", False)
    
            if admin_user:
                self.client.force_login(self.admin_user)
            else:
                self.client.logout()
    
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200, msg=f"{name}: Could not load edit form via GET ({url})")
    
            form = response.context[form_name]
            data = {k:self._to_sendable_value(v) for k,v in form.initial.items()}
    
            response = self.client.post(url, data=data)
            if expected_code == 200:
                self.assertEqual(response.status_code, 200, msg=f"{name}: Did not return 200 ({url}")
            elif expected_code == 302:
                self.assertRedirects(response, target_url, msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}")
            if expected_message != "":
                self._assert_message(response, expected_message, msg_prefix=f"{name}")
    
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
    
    class ModelViewTests(BasicViewTests, TestCase):
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
        """
        Basic view test cases for views from AKModel plus some custom tests
        """
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
        fixtures = ['model.json']
    
        ADMIN_MODELS = [
            (Event, 'event'), (AKOwner, 'akowner'), (AKCategory, 'akcategory'), (AKTrack, 'aktrack'),
            (AKRequirement, 'akrequirement'), (AK, 'ak'), (Room, 'room'), (AKSlot, 'akslot'),
            (AKOrgaMessage, 'akorgamessage'), (ConstraintViolation, 'constraintviolation'),
            (DefaultSlot, 'defaultslot')
        ]
    
    
            ('admin:event_status', {'event_slug': 'kif42'}),
    
            ('admin:event_requirement_overview', {'event_slug': 'kif42'}),
            ('admin:ak_csv_export', {'event_slug': 'kif42'}),
            ('admin:ak_wiki_export', {'slug': 'kif42'}),
            ('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}),
            ('admin:ak_slide_export', {'event_slug': 'kif42'}),
            ('admin:default-slots-editor', {'event_slug': 'kif42'}),
            ('admin:room-import', {'event_slug': 'kif42'}),
            ('admin:new_event_wizard_start', {}),
    
        EDIT_TESTCASES = [
            {'view': 'admin:default-slots-editor', 'kwargs': {'event_slug': 'kif42'}, "admin": True},
        ]
    
    
        def test_admin(self):
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            """
            Test basic admin functionality (displaying and interacting with model instances)
            """
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            self.client.force_login(self.admin_user)
            for model in self.ADMIN_MODELS:
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                # Special treatment for a subset of views (where we exchanged default functionality, e.g., create views)
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                if model[1] == "event":
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                    _, url = self._name_and_url(('admin:new_event_wizard_start', {}))
    
                elif model[1] == "room":
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                    _, url = self._name_and_url(('admin:room-new', {}))
                # Otherwise, just call the creation form view
    
                else:
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                    _, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {}))
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                response = self.client.get(url)
                self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken")
    
            for model in self.ADMIN_MODELS:
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                # Test the update view using the first existing instance of each model
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                m = model[0].objects.first()
                if m is not None:
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                    _, url = self._name_and_url(
                        (f'admin:AKModel_{model[1]}_change', {'object_id': m.pk})
                    )
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                    response = self.client.get(url)
                    self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken")
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            """
            Test wiki export
            This will test whether the view renders at all and whether the export list contains the correct AKs
            """
    
            self.client.force_login(self.admin_user)
    
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'})
    
            response = self.client.get(export_url)
            self.assertEqual(response.status_code, 200, "Export not working at all")
    
            export_count = 0
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
            for _, aks in response.context["categories_with_aks"]:
    
    Benjamin Hättasch's avatar
    Benjamin Hättasch committed
                    self.assertEqual(ak.include_in_export, True,
                                     f"AK with export flag set to False (pk={ak.pk}) included in export")
    
                    self.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) included in export")
                    export_count += 1
    
            self.assertEqual(export_count, AK.objects.filter(event_id=2, include_in_export=True).count(),
                             "Wiki export contained the wrong number of AKs")