testutils.py 10.9 KB
Newer Older
1 2 3
"""
Common test utilities for courseware functionality
"""
4
# pylint: disable=attribute-defined-outside-init
5 6

from abc import ABCMeta, abstractmethod
7
from datetime import datetime, timedelta
8 9
from urllib import urlencode

10 11 12
import ddt
from mock import patch

13
from lms.djangoapps.courseware.field_overrides import OverrideModulestoreFieldData
14
from lms.djangoapps.courseware.url_helpers import get_redirect_url
15
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
16
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls


@ddt.ddt
class RenderXBlockTestMixin(object):
    """
    Mixin for testing the courseware.render_xblock function.
    It can be used for testing any higher-level endpoint that calls this method.
    """
    __metaclass__ = ABCMeta

    # DOM elements that appear in the LMS Courseware,
    # but are excluded from the xBlock-only rendering.
    COURSEWARE_CHROME_HTML_ELEMENTS = [
33
        '<ol class="tabs course-tabs"',
34 35
        '<footer id="footer-openedx"',
        '<div class="window-wrap"',
36
        '<div class="preview-menu"',
37
        '<div class="container"',
38 39 40 41 42 43 44 45
    ]

    # DOM elements that appear in an xBlock,
    # but are excluded from the xBlock-only rendering.
    XBLOCK_REMOVED_HTML_ELEMENTS = [
        '<div class="wrap-instructor-info"',
    ]

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
    # DOM elements that appear in the LMS Courseware, but are excluded from the
    # xBlock-only rendering, and are specific to a particular block.
    BLOCK_SPECIFIC_CHROME_HTML_ELEMENTS = {
        # Although bookmarks were removed from all chromeless views of the
        # vertical, it is LTI specifically that must never include them.
        'vertical_block': ['<div class="bookmark-button-wrapper"'],
        'html_block': [],
    }

    def setUp(self):
        """
        Clear out the block to be requested/tested before each test.
        """
        super(RenderXBlockTestMixin, self).setUp()
        # to adjust the block to be tested, update block_name_to_be_tested before calling setup_course.
        self.block_name_to_be_tested = 'html_block'

63
    @abstractmethod
64
    def get_response(self, usage_key, url_encoded_params=None):
65 66
        """
        Abstract method to get the response from the endpoint that is being tested.
67 68

        Arguments:
69 70
            usage_key: The course block usage key. This ensures that the positive and negative tests stay in sync.
            url_encoded_params: URL encoded parameters that should be appended to the requested URL.
71 72 73 74 75 76 77 78 79
        """
        pass   # pragma: no cover

    def login(self):
        """
        Logs in the test user.
        """
        self.client.login(username=self.user.username, password='test')

80 81 82 83 84
    def course_options(self):
        """
        Options to configure the test course. Intended to be overridden by
        subclasses.
        """
85 86 87
        return {
            'start': datetime.now() - timedelta(days=1)
        }
88

89 90 91 92 93 94 95
    def setup_course(self, default_store=None):
        """
        Helper method to create the course.
        """
        if not default_store:
            default_store = self.store.default_modulestore.get_modulestore_type()
        with self.store.default_store(default_store):
96
            self.course = CourseFactory.create(**self.course_options())
97
            chapter = ItemFactory.create(parent=self.course, category='chapter')
98 99 100 101 102 103 104
            self.vertical_block = ItemFactory.create(
                parent_location=chapter.location,
                category='vertical',
                display_name="Vertical"
            )
            self.html_block = ItemFactory.create(
                parent=self.vertical_block,
105 106 107
                category='html',
                data="<p>Test HTML Content<p>"
            )
108
        CourseOverview.load_from_module_store(self.course.id)
109

110 111 112 113 114 115 116
        # block_name_to_be_tested can be `html_block` or `vertical_block`.
        # These attributes help ensure the positive and negative tests are in sync.
        self.block_to_be_tested = getattr(self, self.block_name_to_be_tested)
        self.block_specific_chrome_html_elements = self.BLOCK_SPECIFIC_CHROME_HTML_ELEMENTS[
            self.block_name_to_be_tested
        ]

117 118 119 120
    def setup_user(self, admin=False, enroll=False, login=False):
        """
        Helper method to create the user.
        """
121
        self.user = AdminFactory() if admin else UserFactory()
122 123 124 125 126 127 128

        if enroll:
            CourseEnrollmentFactory(user=self.user, course_id=self.course.id)

        if login:
            self.login()

129
    def verify_response(self, expected_response_code=200, url_params=None):
130 131
        """
        Helper method that calls the endpoint, verifies the expected response code, and returns the response.
132 133 134 135 136

        Arguments:
            expected_response_code: The expected response code.
            url_params: URL parameters that will be encoded and passed to the request.

137
        """
138 139
        if url_params:
            url_params = urlencode(url_params)
140 141

        response = self.get_response(self.block_to_be_tested.location, url_params)
142 143
        if expected_response_code == 200:
            self.assertContains(response, self.html_block.data, status_code=expected_response_code)
144 145 146
            unexpected_elements = self.block_specific_chrome_html_elements
            unexpected_elements += self.COURSEWARE_CHROME_HTML_ELEMENTS + self.XBLOCK_REMOVED_HTML_ELEMENTS
            for chrome_element in unexpected_elements:
147 148 149 150 151 152
                self.assertNotContains(response, chrome_element)
        else:
            self.assertNotContains(response, self.html_block.data, status_code=expected_response_code)
        return response

    @ddt.data(
153
        ('vertical_block', ModuleStoreEnum.Type.mongo, 10),
154
        ('vertical_block', ModuleStoreEnum.Type.split, 6),
155
        ('html_block', ModuleStoreEnum.Type.mongo, 11),
156
        ('html_block', ModuleStoreEnum.Type.split, 6),
157 158
    )
    @ddt.unpack
159
    def test_courseware_html(self, block_name, default_store, mongo_calls):
160 161 162 163 164 165 166 167
        """
        To verify that the removal of courseware chrome elements is working,
        we include this test here to make sure the chrome elements that should
        be removed actually exist in the full courseware page.
        If this test fails, it's probably because the HTML template for courseware
        has changed and COURSEWARE_CHROME_HTML_ELEMENTS needs to be updated.
        """
        with self.store.default_store(default_store):
168
            self.block_name_to_be_tested = block_name
169 170 171 172
            self.setup_course(default_store)
            self.setup_user(admin=True, enroll=True, login=True)

            with check_mongo_calls(mongo_calls):
173
                url = get_redirect_url(self.course.id, self.block_to_be_tested.location)
174
                response = self.client.get(url)
175 176
                expected_elements = self.block_specific_chrome_html_elements + self.COURSEWARE_CHROME_HTML_ELEMENTS
                for chrome_element in expected_elements:
177 178 179 180 181 182 183 184 185
                    self.assertContains(response, chrome_element)

    @ddt.data(
        (ModuleStoreEnum.Type.mongo, 5),
        (ModuleStoreEnum.Type.split, 5),
    )
    @ddt.unpack
    def test_success_enrolled_staff(self, default_store, mongo_calls):
        with self.store.default_store(default_store):
186 187
            if default_store is ModuleStoreEnum.Type.mongo:
                mongo_calls = self.get_success_enrolled_staff_mongo_count()
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
            self.setup_course(default_store)
            self.setup_user(admin=True, enroll=True, login=True)

            # The 5 mongoDB calls include calls for
            # Old Mongo:
            #   (1) fill_in_run
            #   (2) get_course in get_course_with_access
            #   (3) get_item for HTML block in get_module_by_usage_id
            #   (4) get_parent when loading HTML block
            #   (5) edx_notes descriptor call to get_course
            # Split:
            #   (1) course_index - bulk_operation call
            #   (2) structure - get_course_with_access
            #   (3) definition - get_course_with_access
            #   (4) definition - HTML block
            #   (5) definition - edx_notes decorator (original_get_html)
            with check_mongo_calls(mongo_calls):
                self.verify_response()

207 208 209 210 211 212 213
    def get_success_enrolled_staff_mongo_count(self):
        """
        Helper method used by test_success_enrolled_staff because one test
        class using this mixin has an increased number of mongo (only) queries.
        """
        return 5

214 215 216 217 218 219 220 221 222 223
    def test_success_unenrolled_staff(self):
        self.setup_course()
        self.setup_user(admin=True, enroll=False, login=True)
        self.verify_response()

    def test_success_enrolled_student(self):
        self.setup_course()
        self.setup_user(admin=False, enroll=True, login=True)
        self.verify_response()

224
    def test_unauthenticated(self):
225 226
        self.setup_course()
        self.setup_user(admin=False, enroll=True, login=False)
227
        self.verify_response(expected_response_code=404)
228

229
    def test_unenrolled_student(self):
230 231
        self.setup_course()
        self.setup_user(admin=False, enroll=False, login=True)
232
        self.verify_response(expected_response_code=404)
233 234 235 236 237

    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
    def test_fail_block_unreleased(self):
        self.setup_course()
        self.setup_user(admin=False, enroll=True, login=True)
238 239
        self.block_to_be_tested.start = datetime.max
        modulestore().update_item(self.block_to_be_tested, self.user.id)
240 241 242 243 244
        self.verify_response(expected_response_code=404)

    def test_fail_block_nonvisible(self):
        self.setup_course()
        self.setup_user(admin=False, enroll=True, login=True)
245 246
        self.block_to_be_tested.visible_to_staff_only = True
        modulestore().update_item(self.block_to_be_tested, self.user.id)
247
        self.verify_response(expected_response_code=404)
248

249 250 251 252 253 254
    @ddt.data(
        'vertical_block',
        'html_block',
    )
    def test_student_view_param(self, block_name):
        self.block_name_to_be_tested = block_name
255 256 257 258 259 260 261 262
        self.setup_course()
        self.setup_user(admin=False, enroll=True, login=True)
        self.verify_response(url_params={'view': 'student_view'})

    def test_unsupported_view_param(self):
        self.setup_course()
        self.setup_user(admin=False, enroll=True, login=True)
        self.verify_response(url_params={'view': 'author_view'}, expected_response_code=400)
263 264 265 266 267 268 269 270 271 272 273 274 275


class FieldOverrideTestMixin(object):
    """
    A Mixin helper class for classes that test Field Overrides.
    """
    def setUp(self):
        super(FieldOverrideTestMixin, self).setUp()
        OverrideModulestoreFieldData.provider_classes = None

    def tearDown(self):
        super(FieldOverrideTestMixin, self).tearDown()
        OverrideModulestoreFieldData.provider_classes = None