test_enrollment.py 8.7 KB
Newer Older
1 2 3 4 5
"""
Tests for student enrollment.
"""
import ddt
import unittest
6
from mock import patch
7 8 9

from django.conf import settings
from django.core.urlresolvers import reverse
10
from course_modes.models import CourseMode
11
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
12
from xmodule.modulestore.tests.factories import CourseFactory
13 14
from util.testing import UrlResetMixin
from embargo.test_utils import restrict_course
15 16 17 18 19 20
from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment


@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
21
class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
22 23 24
    """
    Test student enrollment, especially with different course modes.
    """
25

26 27 28 29
    USERNAME = "Bob"
    EMAIL = "bob@example.com"
    PASSWORD = "edx"

30
    @patch.dict(settings.FEATURES, {'EMBARGO': True})
31 32
    def setUp(self):
        """ Create a course and user, then log in. """
33
        super(EnrollmentTest, self).setUp('embargo')
34
        self.course = CourseFactory.create()
35 36
        self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
        self.client.login(username=self.USERNAME, password=self.PASSWORD)
37 38 39 40 41 42 43 44

        self.urls = [
            reverse('course_modes_choose', kwargs={'course_id': unicode(self.course.id)})
        ]

    @ddt.data(
        # Default (no course modes in the database)
        # Expect that we're redirected to the dashboard
45 46
        # and automatically enrolled
        ([], '', CourseMode.DEFAULT_MODE_SLUG),
47

48
        # Audit / Verified
49
        # We should always go to the "choose your course" page.
50
        # We should also be enrolled as the default mode.
51 52 53 54 55 56 57 58
        (['verified', 'audit'], 'course_modes_choose', CourseMode.DEFAULT_MODE_SLUG),

        # Audit / Verified / Honor
        # We should always go to the "choose your course" page.
        # We should also be enrolled as the honor mode.
        # Since honor and audit are currently offered together this precedence must
        # be maintained.
        (['honor', 'verified', 'audit'], 'course_modes_choose', CourseMode.HONOR),
59 60 61 62

        # Professional ed
        # Expect that we're sent to the "choose your track" page
        # (which will, in turn, redirect us to a page where we can verify/pay)
Will Daly committed
63
        # We should NOT be auto-enrolled, because that would be giving
64 65
        # away an expensive course for free :)
        (['professional'], 'course_modes_choose', None),
66
        (['no-id-professional'], 'course_modes_choose', None),
67 68
    )
    @ddt.unpack
69
    def test_enroll(self, course_modes, next_url, enrollment_mode):
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
        # Create the course modes (if any) required for this test case
        for mode_slug in course_modes:
            CourseModeFactory.create(
                course_id=self.course.id,
                mode_slug=mode_slug,
                mode_display_name=mode_slug,
            )

        # Reverse the expected next URL, if one is provided
        # (otherwise, use an empty string, which the JavaScript client
        # interprets as a redirect to the dashboard)
        full_url = (
            reverse(next_url, kwargs={'course_id': unicode(self.course.id)})
            if next_url else next_url
        )

        # Enroll in the course and verify the URL we get sent to
87
        resp = self._change_enrollment('enroll')
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
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, full_url)

        # If we're not expecting to be enrolled, verify that this is the case
        if enrollment_mode is None:
            self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))

        # Otherwise, verify that we're enrolled with the expected course mode
        else:
            self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
            course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
            self.assertTrue(is_active)
            self.assertEqual(course_mode, enrollment_mode)

    def test_unenroll(self):
        # Enroll the student in the course
        CourseEnrollment.enroll(self.user, self.course.id, mode="honor")

        # Attempt to unenroll the student
        resp = self._change_enrollment('unenroll')
        self.assertEqual(resp.status_code, 200)

        # Expect that we're no longer enrolled
        self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))

113
    @patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
114
    @patch('openedx.core.djangoapps.user_api.preferences.api.update_email_opt_in')
115 116 117
    @ddt.data(
        ([], 'true'),
        ([], 'false'),
118
        ([], None),
119 120
        (['honor', 'verified'], 'true'),
        (['honor', 'verified'], 'false'),
121
        (['honor', 'verified'], None),
122 123
        (['professional'], 'true'),
        (['professional'], 'false'),
124
        (['professional'], None),
125 126 127
        (['no-id-professional'], 'true'),
        (['no-id-professional'], 'false'),
        (['no-id-professional'], None),
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
    )
    @ddt.unpack
    def test_enroll_with_email_opt_in(self, course_modes, email_opt_in, mock_update_email_opt_in):
        # Create the course modes (if any) required for this test case
        for mode_slug in course_modes:
            CourseModeFactory.create(
                course_id=self.course.id,
                mode_slug=mode_slug,
                mode_display_name=mode_slug,
            )

        # Enroll in the course
        self._change_enrollment('enroll', email_opt_in=email_opt_in)

        # Verify that the profile API has been called as expected
143 144
        if email_opt_in is not None:
            opt_in = email_opt_in == 'true'
145
            mock_update_email_opt_in.assert_called_once_with(self.user, self.course.org, opt_in)
146 147
        else:
            self.assertFalse(mock_update_email_opt_in.called)
148

149
    @patch.dict(settings.FEATURES, {'EMBARGO': True})
150 151 152 153 154 155 156 157 158 159 160 161
    def test_embargo_restrict(self):
        # When accessing the course from an embargoed country,
        # we should be blocked.
        with restrict_course(self.course.id) as redirect_url:
            response = self._change_enrollment('enroll')
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.content, redirect_url)

        # Verify that we weren't enrolled
        is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
        self.assertFalse(is_enrolled)

162
    @patch.dict(settings.FEATURES, {'EMBARGO': True})
163 164 165 166 167 168 169 170 171
    def test_embargo_allow(self):
        response = self._change_enrollment('enroll')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.content, '')

        # Verify that we were enrolled
        is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
        self.assertTrue(is_enrolled)

172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
    def test_user_not_authenticated(self):
        # Log out, so we're no longer authenticated
        self.client.logout()

        # Try to enroll, expecting a forbidden response
        resp = self._change_enrollment('enroll')
        self.assertEqual(resp.status_code, 403)

    def test_missing_course_id_param(self):
        resp = self.client.post(
            reverse('change_enrollment'),
            {'enrollment_action': 'enroll'}
        )
        self.assertEqual(resp.status_code, 400)

    def test_unenroll_not_enrolled_in_course(self):
        # Try unenroll without first enrolling in the course
        resp = self._change_enrollment('unenroll')
        self.assertEqual(resp.status_code, 400)

    def test_invalid_enrollment_action(self):
        resp = self._change_enrollment('not_an_action')
        self.assertEqual(resp.status_code, 400)
195 196 197 198 199

    def test_with_invalid_course_id(self):
        CourseEnrollment.enroll(self.user, self.course.id, mode="honor")
        resp = self._change_enrollment('unenroll', course_id="edx/")
        self.assertEqual(resp.status_code, 400)
200

201
    def _change_enrollment(self, action, course_id=None, email_opt_in=None):
202
        """Change the student's enrollment status in a course.
203 204

        Args:
205
            action (str): The action to perform (either "enroll" or "unenroll")
206 207 208 209

        Keyword Args:
            course_id (unicode): If provided, use this course ID.  Otherwise, use the
                course ID created in the setup for this test.
210 211
            email_opt_in (unicode): If provided, pass this value along as
                an additional GET parameter.
212 213 214 215 216 217 218 219 220 221 222 223

        Returns:
            Response

        """
        if course_id is None:
            course_id = unicode(self.course.id)

        params = {
            'enrollment_action': action,
            'course_id': course_id
        }
224 225 226 227

        if email_opt_in:
            params['email_opt_in'] = email_opt_in

228
        return self.client.post(reverse('change_enrollment'), params)