helpers.py 12.4 KB
Newer Older
1 2 3
"""
Helpers for courseware tests.
"""
4 5
import json

6
from django.contrib import messages
7 8
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
9
from django.test import TestCase
10
from django.test.client import Client, RequestFactory
11

12
from courseware.access import has_access
13
from courseware.masquerade import handle_ajax, setup_masquerade
14 15
from edxmako.shortcuts import render_to_string
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
16
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
17
from openedx.core.lib.url_utils import quote_slashes
18
from student.models import Registration
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xblock.field_data import DictFieldData
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.tests import get_test_descriptor_system, get_test_system


class BaseTestXmodule(ModuleStoreTestCase):
    """Base class for testing Xmodules with mongo store.

    This class prepares course and users for tests:
        1. create test course;
        2. create, enroll and login users for this course;

    Any xmodule should overwrite only next parameters for test:
        1. CATEGORY
        2. DATA or METADATA
        3. MODEL_DATA
        4. COURSE_DATA and USER_COUNT if needed

    This class should not contain any tests, because CATEGORY
    should be defined in child class.
    """
    MODULESTORE = TEST_DATA_MONGO_MODULESTORE

    USER_COUNT = 2
    COURSE_DATA = {}

    # Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml
    CATEGORY = "vertical"
    DATA = ''
    # METADATA must be overwritten for every instance that uses it. Otherwise,
    # if we'll change it in the tests, it will be changed for all other instances
    # of parent class.
    METADATA = {}
    MODEL_DATA = {'data': '<some_module></some_module>'}

    def new_module_runtime(self):
        """
        Generate a new ModuleSystem that is minimally set up for testing
        """
        return get_test_system(course_id=self.course.id)

    def new_descriptor_runtime(self):
        runtime = get_test_descriptor_system()
        runtime.get_block = modulestore().get_item
        return runtime

    def initialize_module(self, **kwargs):
        kwargs.update({
            'parent_location': self.section.location,
            'category': self.CATEGORY
        })

        self.item_descriptor = ItemFactory.create(**kwargs)

        self.runtime = self.new_descriptor_runtime()

        field_data = {}
        field_data.update(self.MODEL_DATA)
        student_data = DictFieldData(field_data)
        self.item_descriptor._field_data = LmsFieldData(self.item_descriptor._field_data, student_data)

        self.item_descriptor.xmodule_runtime = self.new_module_runtime()

        self.item_url = unicode(self.item_descriptor.location)

    def setup_course(self):
        self.course = CourseFactory.create(data=self.COURSE_DATA)

        # Turn off cache.
        modulestore().request_cache = None
        modulestore().metadata_inheritance_cache_subsystem = None

        chapter = ItemFactory.create(
            parent_location=self.course.location,
            category="sequential",
        )
        self.section = ItemFactory.create(
            parent_location=chapter.location,
            category="sequential"
        )

        # username = robot{0}, password = 'test'
        self.users = [
            UserFactory.create()
            for dummy0 in range(self.USER_COUNT)
        ]

        for user in self.users:
            CourseEnrollmentFactory.create(user=user, course_id=self.course.id)

        # login all users for acces to Xmodule
        self.clients = {user.username: Client() for user in self.users}
        self.login_statuses = [
            self.clients[user.username].login(
                username=user.username, password='test')
            for user in self.users
        ]

        self.assertTrue(all(self.login_statuses))

    def setUp(self):
        super(BaseTestXmodule, self).setUp()
        self.setup_course()
        self.initialize_module(metadata=self.METADATA, data=self.DATA)

    def get_url(self, dispatch):
        """Return item url with dispatch."""
        return reverse(
            'xblock_handler',
            args=(unicode(self.course.id), quote_slashes(self.item_url), 'xmodule_handler', dispatch)
        )


class XModuleRenderingTestBase(BaseTestXmodule):

    def new_module_runtime(self):
        """
        Create a runtime that actually does html rendering
        """
        runtime = super(XModuleRenderingTestBase, self).new_module_runtime()
        runtime.render_template = render_to_string
        return runtime
144 145 146


class LoginEnrollmentTestCase(TestCase):
147 148 149 150
    """
    Provides support for user creation,
    activation, login, and course enrollment.
    """
151 152
    user = None

153 154 155 156 157 158 159
    def setup_user(self):
        """
        Create a user account, activate, and log in.
        """
        self.email = 'foo@test.com'
        self.password = 'bar'
        self.username = 'test'
160 161 162 163 164
        self.user = self.create_account(
            self.username,
            self.email,
            self.password,
        )
165 166
        # activate_user re-fetches and returns the activated user record
        self.user = self.activate_user(self.email)
167 168
        self.login(self.email, self.password)

169 170 171 172 173 174 175 176 177 178 179 180 181
    def assert_request_status_code(self, status_code, url, method="GET", **kwargs):
        make_request = getattr(self.client, method.lower())
        response = make_request(url, **kwargs)
        self.assertEqual(
            response.status_code, status_code,
            "{method} request to {url} returned status code {actual}, "
            "expected status code {expected}".format(
                method=method, url=url,
                actual=response.status_code, expected=status_code
            )
        )
        return response

182 183 184 185 186 187 188 189
    def assert_account_activated(self, url, method="GET", **kwargs):
        make_request = getattr(self.client, method.lower())
        response = make_request(url, **kwargs)
        message_list = list(messages.get_messages(response.wsgi_request))
        self.assertEqual(len(message_list), 1)
        self.assertIn("success", message_list[0].tags)
        self.assertTrue("You have activated your account." in message_list[0].message)

190 191 192 193 194 195
    # ============ User creation and login ==============

    def login(self, email, password):
        """
        Login, check that the corresponding view's response has a 200 status code.
        """
196 197
        resp = self.client.post(reverse('login'),
                                {'email': email, 'password': password})
198 199 200 201 202 203
        self.assertEqual(resp.status_code, 200)
        data = json.loads(resp.content)
        self.assertTrue(data['success'])

    def logout(self):
        """
204 205
        Logout; check that the HTTP response code indicates redirection
        as expected.
206 207
        """
        # should redirect
208
        self.assert_request_status_code(302, reverse('logout'))
209 210 211 212 213

    def create_account(self, username, email, password):
        """
        Create the account and check that it worked.
        """
214 215
        url = reverse('create_account')
        request_data = {
216 217 218 219 220 221
            'username': username,
            'email': email,
            'password': password,
            'name': 'username',
            'terms_of_service': 'true',
            'honor_code': 'true',
222 223
        }
        resp = self.assert_request_status_code(200, url, method="POST", data=request_data)
224 225 226
        data = json.loads(resp.content)
        self.assertEqual(data['success'], True)
        # Check both that the user is created, and inactive
227 228 229
        user = User.objects.get(email=email)
        self.assertFalse(user.is_active)
        return user
230 231 232 233 234 235 236 237

    def activate_user(self, email):
        """
        Look up the activation key for the user, then hit the activate view.
        No error checking.
        """
        activation_key = Registration.objects.get(user__email=email).activation_key
        # and now we try to activate
238
        url = reverse('activate', kwargs={'key': activation_key})
239
        self.assert_account_activated(url)
240
        # Now make sure that the user is now actually activated
241 242 243 244
        user = User.objects.get(email=email)
        self.assertTrue(user.is_active)
        # And return the user we fetched.
        return user
245 246 247 248

    def enroll(self, course, verify=False):
        """
        Try to enroll and return boolean indicating result.
249
        `course` is an instance of CourseDescriptor.
250
        `verify` is an optional boolean parameter specifying whether we
251 252 253 254 255
        want to verify that the student was successfully enrolled
        in the course.
        """
        resp = self.client.post(reverse('change_enrollment'), {
            'enrollment_action': 'enroll',
256
            'course_id': course.id.to_deprecated_string(),
Julia Hansbrough committed
257
            'check_access': True,
258 259 260 261 262 263 264 265 266
        })
        result = resp.status_code == 200
        if verify:
            self.assertTrue(result)
        return result

    def unenroll(self, course):
        """
        Unenroll the currently logged-in user, and check that it worked.
267
        `course` is an instance of CourseDescriptor.
268
        """
269 270
        url = reverse('change_enrollment')
        request_data = {
271
            'enrollment_action': 'unenroll',
272 273 274
            'course_id': course.id.to_deprecated_string(),
        }
        self.assert_request_status_code(200, url, method="POST", data=request_data)
275 276 277 278 279 280 281 282 283 284 285 286


class CourseAccessTestMixin(TestCase):
    """
    Utility mixin for asserting access (or lack thereof) to courses.
    If relevant, also checks access for courses' corresponding CourseOverviews.
    """

    def assertCanAccessCourse(self, user, action, course):
        """
        Assert that a user has access to the given action for a given course.

287 288
        Test with both the given course and with a CourseOverview of the given
        course.
289 290 291 292 293 294 295

        Arguments:
            user (User): a user.
            action (str): type of access to test.
            course (CourseDescriptor): a course.
        """
        self.assertTrue(has_access(user, action, course))
296
        self.assertTrue(has_access(user, action, CourseOverview.get_from_id(course.id)))
297 298 299 300 301

    def assertCannotAccessCourse(self, user, action, course):
        """
        Assert that a user lacks access to the given action the given course.

302 303
        Test with both the given course and with a CourseOverview of the given
        course.
304 305 306 307 308 309 310 311 312 313 314 315 316

        Arguments:
            user (User): a user.
            action (str): type of access to test.
            course (CourseDescriptor): a course.

        Note:
            It may seem redundant to have one method for testing access
            and another method for testing lack thereof (why not just combine
            them into one method with a boolean flag?), but it makes reading
            stack traces of failed tests easier to understand at a glance.
        """
        self.assertFalse(has_access(user, action, course))
317
        self.assertFalse(has_access(user, action, CourseOverview.get_from_id(course.id)))
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353


def masquerade_as_group_member(user, course, partition_id, group_id):
    """
    Installs a masquerade for the specified user and course, to enable
    the user to masquerade as belonging to the specific partition/group
    combination.

    Arguments:
        user (User): a user.
        course (CourseDescriptor): a course.
        partition_id (int): the integer partition id, referring to partitions already
           configured in the course.
        group_id (int); the integer group id, within the specified partition.

    Returns: the status code for the AJAX response to update the user's masquerade for
        the specified course.
    """
    request = _create_mock_json_request(
        user,
        data={"role": "student", "user_partition_id": partition_id, "group_id": group_id}
    )
    response = handle_ajax(request, unicode(course.id))
    setup_masquerade(request, course.id, True)
    return response.status_code


def _create_mock_json_request(user, data, method='POST'):
    """
    Returns a mock JSON request for the specified user.
    """
    factory = RequestFactory()
    request = factory.generic(method, '/', content_type='application/json', data=json.dumps(data))
    request.user = user
    request.session = {}
    return request