testutils.py 8.41 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
from mock import patch
15
from unittest import skip
16

17 18
from django.core.urlresolvers import reverse

19 20
from rest_framework.test import APITestCase

21 22
from opaque_keys.edx.keys import CourseKey

23 24 25 26 27
from courseware.access_response import (
    MobileAvailabilityError,
    StartDateError,
    VisibilityError
)
28
from courseware.tests.factories import UserFactory
29 30 31 32 33
from student import auth
from student.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory

34 35
from mobile_api.test_milestones import MobileAPIMilestonesMixin

36 37 38 39 40 41 42 43 44 45

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()
46
        self.course = CourseFactory.create(mobile_available=True, static_asset_path="needed_for_split")
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
        self.user = UserFactory.create()
        self.password = 'test'
        self.username = self.user.username

    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)

    def api_response(self, reverse_args=None, expected_response_code=200, **kwargs):
        """
        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)
        response = self.url_method(url, **kwargs)
        if expected_response_code is not None:
            self.assertEqual(response.status_code, expected_response_code)
        return response

87
    def reverse_url(self, reverse_args=None, **kwargs):
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
        """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)

    def url_method(self, url, **kwargs):  # pylint: disable=unused-argument
        """Base implementation that returns response from the GET method of the URL."""
        return self.client.get(url)


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


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

    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()
131
        self.api_response(expected_response_code=404, username=other.username)
132 133 134


@ddt.ddt
135
class MobileCourseAccessTestMixin(MobileAPIMilestonesMixin):
136 137 138 139 140
    """
    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.
    """
141
    ALLOW_ACCESS_TO_UNRELEASED_COURSE = False  # pylint: disable=invalid-name
142
    ALLOW_ACCESS_TO_NON_VISIBLE_COURSE = False  # pylint: disable=invalid-name
143

144 145 146 147
    def verify_success(self, response):
        """Base implementation of verifying a successful response."""
        self.assertEqual(response.status_code, 200)

148
    def verify_failure(self, response, error_type=None):
149 150
        """Base implementation of verifying a failed response."""
        self.assertEqual(response.status_code, 404)
151 152
        if error_type:
            self.assertEqual(response.data, error_type.to_json())
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170

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

    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

171
    @skip  # TODO fix this, see MA-1038
172 173 174
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
    def test_unreleased_course(self):
        self.init_course_access()
175
        self._verify_response(self.ALLOW_ACCESS_TO_UNRELEASED_COURSE, StartDateError(self.course.start))
176

177 178 179 180 181 182 183
    # 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)
    )
184 185 186 187 188 189
    @ddt.unpack
    def test_non_mobile_available(self, role, should_succeed):
        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)
190
        self._verify_response(should_succeed, MobileAvailabilityError(), role)
191

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
    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
    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)
207 208
        if self.ALLOW_ACCESS_TO_NON_VISIBLE_COURSE:
            should_succeed = True
209 210 211 212 213 214
        self._verify_response(should_succeed, VisibilityError(), role)

    def _verify_response(self, should_succeed, error_type, role=None):
        """
        Calls API and verifies the response
        """
215 216 217 218 219
        # 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)
220

221 222 223
        if should_succeed:
            self.verify_success(response)
        else:
224
            self.verify_failure(response, error_type)