testutils.py 9.71 KB
Newer Older
1 2 3 4 5 6 7
"""
Test utilities for mobile API tests:

  MobileAPITestCase - Common base class with helper methods and common functionality.
     No tests are implemented in this base class.

  Test Mixins to be included by concrete test classes and provide implementation of common test methods:
8 9
     MobileAuthTestMixin - tests for APIs with mobile_view and is_user=False.
     MobileAuthUserTestMixin - tests for APIs with mobile_view and is_user=True.
10
     MobileCourseAccessTestMixin - tests for APIs with mobile_course_access.
11
"""
12
# pylint: disable=no-member
13 14

import ddt
15
import datetime
16
from django.conf import settings
17
from django.core.urlresolvers import reverse
18 19
from django.utils import timezone
from mock import patch
20
from opaque_keys.edx.keys import CourseKey
21
import pytz
22
from rest_framework.test import APITestCase
23

24
from courseware.access_response import MobileAvailabilityError, StartDateError, VisibilityError
25
from courseware.tests.factories import UserFactory
26
from mobile_api.models import IgnoreMobileAvailableFlagConfig
27
from mobile_api.tests.test_milestones import MobileAPIMilestonesMixin
28 29 30 31
from student import auth
from student.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
32

33 34 35 36 37 38 39 40 41 42

class MobileAPITestCase(ModuleStoreTestCase, APITestCase):
    """
    Base class for testing Mobile APIs.
    Subclasses are expected to define REVERSE_INFO to be used for django reverse URL, of the form:
       REVERSE_INFO = {'name': <django reverse name>, 'params': [<list of params in the URL>]}
    They may also override any of the methods defined in this class to control the behavior of the TestMixins.
    """
    def setUp(self):
        super(MobileAPITestCase, self).setUp()
43 44 45
        self.course = CourseFactory.create(
            mobile_available=True,
            static_asset_path="needed_for_split",
46 47 48
            end=datetime.datetime.now(pytz.UTC),
            certificate_available_date=datetime.datetime.now(pytz.UTC)
        )
49 50 51
        self.user = UserFactory.create()
        self.password = 'test'
        self.username = self.user.username
52
        IgnoreMobileAvailableFlagConfig(enabled=False).save()
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

    def tearDown(self):
        super(MobileAPITestCase, self).tearDown()
        self.logout()

    def login(self):
        """Login test user."""
        self.client.login(username=self.username, password=self.password)

    def logout(self):
        """Logout test user."""
        self.client.logout()

    def enroll(self, course_id=None):
        """Enroll test user in test course."""
        CourseEnrollment.enroll(self.user, course_id or self.course.id)

    def unenroll(self, course_id=None):
        """Unenroll test user in test course."""
        CourseEnrollment.unenroll(self.user, course_id or self.course.id)

    def login_and_enroll(self, course_id=None):
        """Shortcut for both login and enrollment of the user."""
        self.login()
        self.enroll(course_id)

79
    def api_response(self, reverse_args=None, expected_response_code=200, data=None, **kwargs):
80 81 82 83 84
        """
        Helper method for calling endpoint, verifying and returning response.
        If expected_response_code is None, doesn't verify the response' status_code.
        """
        url = self.reverse_url(reverse_args, **kwargs)
85
        response = self.url_method(url, data=data, **kwargs)
86 87 88 89
        if expected_response_code is not None:
            self.assertEqual(response.status_code, expected_response_code)
        return response

90
    def reverse_url(self, reverse_args=None, **kwargs):
91 92 93 94 95 96 97 98
        """Base implementation that returns URL for endpoint that's being tested."""
        reverse_args = reverse_args or {}
        if 'course_id' in self.REVERSE_INFO['params']:
            reverse_args.update({'course_id': unicode(kwargs.get('course_id', self.course.id))})
        if 'username' in self.REVERSE_INFO['params']:
            reverse_args.update({'username': kwargs.get('username', self.user.username)})
        return reverse(self.REVERSE_INFO['name'], kwargs=reverse_args)

99
    def url_method(self, url, data=None, **kwargs):  # pylint: disable=unused-argument
100
        """Base implementation that returns response from the GET method of the URL."""
101
        return self.client.get(url, data=data)
102 103 104 105


class MobileAuthTestMixin(object):
    """
106
    Test Mixin for testing APIs decorated with mobile_view.
107 108 109 110 111 112 113 114
    """
    def test_no_auth(self):
        self.logout()
        self.api_response(expected_response_code=401)


class MobileAuthUserTestMixin(MobileAuthTestMixin):
    """
115
    Test Mixin for testing APIs related to users: mobile_view with is_user=True.
116 117 118
    """
    def test_invalid_user(self):
        self.login_and_enroll()
119
        self.api_response(expected_response_code=404, username='no_user')
120 121 122 123 124 125 126 127 128 129 130 131 132 133

    def test_other_user(self):
        # login and enroll as the test user
        self.login_and_enroll()
        self.logout()

        # login and enroll as another user
        other = UserFactory.create()
        self.client.login(username=other.username, password='test')
        self.enroll()
        self.logout()

        # now login and call the API as the test user
        self.login()
134
        self.api_response(expected_response_code=404, username=other.username)
135 136 137


@ddt.ddt
138
class MobileCourseAccessTestMixin(MobileAPIMilestonesMixin):
139 140 141 142 143
    """
    Test Mixin for testing APIs marked with mobile_course_access.
    Subclasses are expected to inherit from MobileAPITestCase.
    Subclasses can override verify_success, verify_failure, and init_course_access methods.
    """
144
    ALLOW_ACCESS_TO_UNRELEASED_COURSE = False  # pylint: disable=invalid-name
145
    ALLOW_ACCESS_TO_NON_VISIBLE_COURSE = False  # pylint: disable=invalid-name
146

147 148 149 150
    def verify_success(self, response):
        """Base implementation of verifying a successful response."""
        self.assertEqual(response.status_code, 200)

151
    def verify_failure(self, response, error_type=None):
152 153
        """Base implementation of verifying a failed response."""
        self.assertEqual(response.status_code, 404)
154 155
        if error_type:
            self.assertEqual(response.data, error_type.to_json())
156 157 158 159 160

    def init_course_access(self, course_id=None):
        """Base implementation of initializing the user for each test."""
        self.login_and_enroll(course_id)

161
    @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
162 163 164 165 166 167 168 169 170 171 172 173 174
    def test_success(self):
        self.init_course_access()

        response = self.api_response(expected_response_code=None)
        self.verify_success(response)  # allow subclasses to override verification

    def test_course_not_found(self):
        non_existent_course_id = CourseKey.from_string('a/b/c')
        self.init_course_access(course_id=non_existent_course_id)

        response = self.api_response(expected_response_code=None, course_id=non_existent_course_id)
        self.verify_failure(response)  # allow subclasses to override verification

175
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False, 'ENABLE_MKTG_SITE': True})
176
    def test_unreleased_course(self):
Sanford Student committed
177 178 179 180
        # ensure the course always starts in the future
        # pylint: disable=attribute-defined-outside-init
        self.course = CourseFactory.create(mobile_available=True, static_asset_path="needed_for_split")
        # pylint: disable=attribute-defined-outside-init
181
        self.course.start = timezone.now() + datetime.timedelta(days=365)
182
        self.init_course_access()
183
        self._verify_response(self.ALLOW_ACCESS_TO_UNRELEASED_COURSE, StartDateError(self.course.start))
184

185 186 187 188 189 190 191
    # A tuple of Role Types and Boolean values that indicate whether access should be given to that role.
    @ddt.data(
        (auth.CourseBetaTesterRole, True),
        (auth.CourseStaffRole, True),
        (auth.CourseInstructorRole, True),
        (None, False)
    )
192
    @ddt.unpack
193
    @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
194
    def test_non_mobile_available(self, role, should_succeed):
195 196 197 198 199 200
        """
        Tests that the MobileAvailabilityError() is raised for certain user
        roles when trying to access course content. Also verifies that if
        the IgnoreMobileAvailableFlagConfig is enabled,
        MobileAvailabilityError() will not be raised for all user roles.
        """
201 202 203 204
        self.init_course_access()
        # set mobile_available to False for the test course
        self.course.mobile_available = False
        self.store.update_item(self.course, self.user.id)
205
        self._verify_response(should_succeed, MobileAvailabilityError(), role)
206

207 208 209
        IgnoreMobileAvailableFlagConfig(enabled=True).save()
        self._verify_response(True, MobileAvailabilityError(), role)

210 211 212 213 214 215 216 217 218 219 220
    def test_unenrolled_user(self):
        self.login()
        self.unenroll()
        response = self.api_response(expected_response_code=None)
        self.verify_failure(response)

    @ddt.data(
        (auth.CourseStaffRole, True),
        (None, False)
    )
    @ddt.unpack
221
    @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
222 223 224 225
    def test_visible_to_staff_only_course(self, role, should_succeed):
        self.init_course_access()
        self.course.visible_to_staff_only = True
        self.store.update_item(self.course, self.user.id)
226 227
        if self.ALLOW_ACCESS_TO_NON_VISIBLE_COURSE:
            should_succeed = True
228 229 230 231 232 233
        self._verify_response(should_succeed, VisibilityError(), role)

    def _verify_response(self, should_succeed, error_type, role=None):
        """
        Calls API and verifies the response
        """
234 235 236 237 238
        # set user's role in the course
        if role:
            role(self.course.id).add_users(self.user)

        response = self.api_response(expected_response_code=None)
239

240 241 242
        if should_succeed:
            self.verify_success(response)
        else:
243
            self.verify_failure(response, error_type)