test_view_authentication.py 17.6 KB
Newer Older
1 2
import datetime

3
import pytz
4
from django.core.urlresolvers import reverse
5
from mock import patch
6
from nose.plugins.attrib import attr
7

8
from courseware.access import has_access
9 10 11 12 13
from courseware.tests.factories import (
    BetaTesterFactory,
    GlobalStaffFactory,
    InstructorFactory,
    OrgInstructorFactory,
14 15
    OrgStaffFactory,
    StaffFactory
16
)
17 18
from courseware.tests.helpers import CourseAccessTestMixin, LoginEnrollmentTestCase
from student.tests.factories import CourseEnrollmentFactory, UserFactory
19
from xmodule.modulestore.django import modulestore
20 21
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
22 23


24
@attr(shard=1)
25 26 27 28
class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
    """
    Check that view authentication works properly.
    """
29

30
    ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')]
31

32 33 34 35
    @staticmethod
    def _reverse_urls(names, course):
        """
        Reverse a list of course urls.
36 37 38 39 40 41 42

        `names` is a list of URL names that correspond to sections in a course.

        `course` is the instance of CourseDescriptor whose section URLs are to be returned.

        Returns a list URLs corresponding to section in the passed in course.

43
        """
44
        return [reverse(name, kwargs={'course_id': course.id.to_deprecated_string()})
45
                for name in names]
46

47 48 49 50 51 52
    def _check_non_staff_light(self, course):
        """
        Check that non-staff have access to light urls.

        `course` is an instance of CourseDescriptor.
        """
53 54
        urls = [reverse('about_course', kwargs={'course_id': course.id.to_deprecated_string()}),
                reverse('courses')]
55
        for url in urls:
56
            self.assert_request_status_code(200, url)
57 58 59 60 61 62 63 64 65

    def _check_non_staff_dark(self, course):
        """
        Check that non-staff don't have access to dark urls.
        """

        names = ['courseware', 'instructor_dashboard', 'progress']
        urls = self._reverse_urls(names, course)
        urls.extend([
66
            reverse('book', kwargs={'course_id': course.id.to_deprecated_string(),
67
                                    'book_index': index})
68
            for index, __ in enumerate(course.textbooks)
69 70
        ])
        for url in urls:
71
            self.assert_request_status_code(404, url)
72 73 74 75 76 77 78 79

    def _check_staff(self, course):
        """
        Check that access is right for staff in course.
        """
        names = ['about_course', 'instructor_dashboard', 'progress']
        urls = self._reverse_urls(names, course)
        urls.extend([
80
            reverse('book', kwargs={'course_id': course.id.to_deprecated_string(),
81
                                    'book_index': index})
82
            for index in xrange(len(course.textbooks))
83 84
        ])
        for url in urls:
85
            self.assert_request_status_code(200, url)
86 87 88

        # The student progress tab is not accessible to a student
        # before launch, so the instructor view-as-student feature
89
        # should return a 404.
90 91 92
        # TODO (vshnayder): If this is not the behavior we want, will need
        # to make access checking smarter and understand both the effective
        # user (the student), and the requesting user (the prof)
93 94 95 96 97 98 99
        url = reverse(
            'student_progress',
            kwargs={
                'course_id': course.id.to_deprecated_string(),
                'student_id': self.enrolled_user.id,
            }
        )
100
        self.assert_request_status_code(404, url)
101 102 103

        # The courseware url should redirect, not 200
        url = self._reverse_urls(['courseware'], course)[0]
104
        self.assert_request_status_code(302, url)
105

106 107 108
    def login(self, user):
        return super(TestViewAuth, self).login(user.email, 'test')

109
    def setUp(self):
110
        super(TestViewAuth, self).setUp()
111

112
        self.course = CourseFactory.create(number='999', display_name='Robot_Super_Course')
113
        self.courseware_chapter = ItemFactory.create(display_name='courseware')
114 115 116 117 118 119 120 121 122 123 124 125
        self.overview_chapter = ItemFactory.create(
            parent_location=self.course.location,
            display_name='Super Overview'
        )
        self.welcome_section = ItemFactory.create(
            parent_location=self.overview_chapter.location,
            display_name='Super Welcome'
        )
        self.welcome_unit = ItemFactory.create(
            parent_location=self.welcome_section.location,
            display_name='Super Unit'
        )
126
        self.course = modulestore().get_course(self.course.id)
127

128
        self.test_course = CourseFactory.create(org=self.course.id.org)
129 130
        self.other_org_course = CourseFactory.create(org='Other_Org_Course')
        self.sub_courseware_chapter = ItemFactory.create(
131 132
            parent_location=self.test_course.location,
            display_name='courseware'
133 134 135 136 137
        )
        self.sub_overview_chapter = ItemFactory.create(
            parent_location=self.sub_courseware_chapter.location,
            display_name='Overview'
        )
138 139
        self.sub_welcome_section = ItemFactory.create(
            parent_location=self.sub_overview_chapter.location,
140 141
            display_name='Welcome'
        )
142 143 144 145
        self.sub_welcome_unit = ItemFactory.create(
            parent_location=self.sub_welcome_section.location,
            display_name='New Unit'
        )
146
        self.test_course = modulestore().get_course(self.test_course.id)
147

148
        self.global_staff_user = GlobalStaffFactory()
149 150 151 152 153 154
        self.unenrolled_user = UserFactory(last_name="Unenrolled")

        self.enrolled_user = UserFactory(last_name="Enrolled")
        CourseEnrollmentFactory(user=self.enrolled_user, course_id=self.course.id)
        CourseEnrollmentFactory(user=self.enrolled_user, course_id=self.test_course.id)

155 156 157 158
        self.staff_user = StaffFactory(course_key=self.course.id)
        self.instructor_user = InstructorFactory(course_key=self.course.id)
        self.org_staff_user = OrgStaffFactory(course_key=self.course.id)
        self.org_instructor_user = OrgInstructorFactory(course_key=self.course.id)
159 160 161 162 163 164

    def test_redirection_unenrolled(self):
        """
        Verify unenrolled student is redirected to the 'about' section of the chapter
        instead of the 'Welcome' section after clicking on the courseware tab.
        """
165
        self.login(self.unenrolled_user)
166
        response = self.client.get(reverse('courseware',
167
                                           kwargs={'course_id': self.course.id.to_deprecated_string()}))
168 169 170 171 172 173 174
        self.assertRedirects(
            response,
            reverse(
                'about_course',
                args=[self.course.id.to_deprecated_string()]
            )
        )
175 176 177 178 179 180

    def test_redirection_enrolled(self):
        """
        Verify enrolled student is redirected to the 'Welcome' section of
        the chapter after clicking on the courseware tab.
        """
181
        self.login(self.enrolled_user)
182

183 184 185 186 187 188
        response = self.client.get(
            reverse(
                'courseware',
                kwargs={'course_id': self.course.id.to_deprecated_string()}
            )
        )
189

190 191 192 193 194 195 196 197 198
        self.assertRedirects(
            response,
            reverse(
                'courseware_section',
                kwargs={'course_id': self.course.id.to_deprecated_string(),
                        'chapter': self.overview_chapter.url_name,
                        'section': self.welcome_section.url_name}
            )
        )
199

200
    @patch('openedx.features.enterprise_support.api.get_enterprise_consent_url')
201
    def test_redirection_missing_enterprise_consent(self, mock_get_url):
202 203 204 205 206 207 208
        """
        Verify that enrolled students are redirected to the Enterprise consent
        URL if a linked Enterprise Customer requires data sharing consent
        and it has not yet been provided.
        """
        mock_get_url.return_value = reverse('dashboard')
        self.login(self.enrolled_user)
209 210 211
        url = reverse(
            'courseware',
            kwargs={'course_id': self.course.id.to_deprecated_string()}
212
        )
213
        response = self.client.get(url)
214 215 216 217
        self.assertRedirects(
            response,
            reverse('dashboard')
        )
218 219 220 221
        mock_get_url.assert_called_once()
        mock_get_url.return_value = None
        response = self.client.get(url)
        self.assertNotIn("You are not currently enrolled in this course", response.content)
222

223 224 225 226 227
    def test_instructor_page_access_nonstaff(self):
        """
        Verify non-staff cannot load the instructor
        dashboard, the grade views, and student profile pages.
        """
228
        self.login(self.enrolled_user)
229

230 231
        urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}),
                reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})]
232 233

        # Shouldn't be able to get to the instructor pages
234
        for url in urls:
235
            self.assert_request_status_code(404, url)
236

237
    def test_staff_course_access(self):
238
        """
239
        Verify staff can load the staff dashboard, the grade views,
240 241
        and student profile pages for their course.
        """
242
        self.login(self.staff_user)
243

244
        # Now should be able to get to self.course, but not  self.test_course
245
        url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
246
        self.assert_request_status_code(200, url)
247

248
        url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})
249
        self.assert_request_status_code(404, url)
250

251 252 253 254 255 256
    def test_instructor_course_access(self):
        """
        Verify instructor can load the instructor dashboard, the grade views,
        and student profile pages for their course.
        """
        self.login(self.instructor_user)
257

258
        # Now should be able to get to self.course, but not  self.test_course
259
        url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
260
        self.assert_request_status_code(200, url)
261

262
        url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})
263
        self.assert_request_status_code(404, url)
264

265 266 267 268 269 270
    def test_org_staff_access(self):
        """
        Verify org staff can load the instructor dashboard, the grade views,
        and student profile pages for course in their org.
        """
        self.login(self.org_staff_user)
271
        url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
272
        self.assert_request_status_code(200, url)
273

274
        url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})
275
        self.assert_request_status_code(200, url)
276

277
        url = reverse('instructor_dashboard', kwargs={'course_id': self.other_org_course.id.to_deprecated_string()})
278
        self.assert_request_status_code(404, url)
279 280 281 282 283 284 285

    def test_org_instructor_access(self):
        """
        Verify org instructor can load the instructor dashboard, the grade views,
        and student profile pages for course in their org.
        """
        self.login(self.org_instructor_user)
286
        url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
287
        self.assert_request_status_code(200, url)
288

289
        url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})
290
        self.assert_request_status_code(200, url)
291

292
        url = reverse('instructor_dashboard', kwargs={'course_id': self.other_org_course.id.to_deprecated_string()})
293
        self.assert_request_status_code(404, url)
294

295
    def test_global_staff_access(self):
296
        """
297
        Verify the global staff user can access any course.
298
        """
299
        self.login(self.global_staff_user)
300 301

        # and now should be able to load both
302 303
        urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}),
                reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id.to_deprecated_string()})]
304

305
        for url in urls:
306
            self.assert_request_status_code(200, url)
307

308
    @patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
309
    def test_dark_launch_enrolled_student(self):
310
        """
311 312
        Make sure that before course start, students can't access course
        pages.
313
        """
314 315

        # Make courses start in the future
316 317
        now = datetime.datetime.now(pytz.UTC)
        tomorrow = now + datetime.timedelta(days=1)
318 319
        self.course.start = tomorrow
        self.test_course.start = tomorrow
320 321
        self.course = self.update_course(self.course, self.user.id)
        self.test_course = self.update_course(self.test_course, self.user.id)
322 323

        self.assertFalse(self.course.has_started())
324
        self.assertFalse(self.test_course.has_started())
325 326

        # First, try with an enrolled student
327
        self.login(self.enrolled_user)
328 329

        # shouldn't be able to get to anything except the light pages
330 331
        self._check_non_staff_light(self.course)
        self._check_non_staff_dark(self.course)
332 333
        self._check_non_staff_light(self.test_course)
        self._check_non_staff_dark(self.test_course)
334

335
    @patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
336 337 338 339 340
    def test_dark_launch_instructor(self):
        """
        Make sure that before course start instructors can access the
        page for their course.
        """
341 342
        now = datetime.datetime.now(pytz.UTC)
        tomorrow = now + datetime.timedelta(days=1)
343 344
        self.course.start = tomorrow
        self.test_course.start = tomorrow
345 346
        self.course = self.update_course(self.course, self.user.id)
        self.test_course = self.update_course(self.test_course, self.user.id)
347

348
        self.login(self.instructor_user)
349
        # Enroll in the classes---can't see courseware otherwise.
350
        self.enroll(self.course, True)
351
        self.enroll(self.test_course, True)
352 353

        # should now be able to get to everything for self.course
354 355
        self._check_non_staff_light(self.test_course)
        self._check_non_staff_dark(self.test_course)
356 357
        self._check_staff(self.course)

358
    @patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
359
    def test_dark_launch_global_staff(self):
360 361 362 363
        """
        Make sure that before course start staff can access
        course pages.
        """
364 365
        now = datetime.datetime.now(pytz.UTC)
        tomorrow = now + datetime.timedelta(days=1)
366

367 368
        self.course.start = tomorrow
        self.test_course.start = tomorrow
369 370
        self.course = self.update_course(self.course, self.user.id)
        self.test_course = self.update_course(self.test_course, self.user.id)
371

372
        self.login(self.global_staff_user)
373
        self.enroll(self.course, True)
374
        self.enroll(self.test_course, True)
375 376

        # and now should be able to load both
377
        self._check_staff(self.course)
378
        self._check_staff(self.test_course)
379

380
    @patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
381
    def test_enrollment_period(self):
382
        """
383
        Check that enrollment periods work.
384
        """
385 386 387 388 389 390 391
        # Make courses start in the future
        now = datetime.datetime.now(pytz.UTC)
        tomorrow = now + datetime.timedelta(days=1)
        nextday = tomorrow + datetime.timedelta(days=1)
        yesterday = now - datetime.timedelta(days=1)

        # self.course's enrollment period hasn't started
392 393
        self.course.enrollment_start = tomorrow
        self.course.enrollment_end = nextday
394
        # test_course course's has
395 396
        self.test_course.enrollment_start = yesterday
        self.test_course.enrollment_end = tomorrow
397 398
        self.course = self.update_course(self.course, self.user.id)
        self.test_course = self.update_course(self.test_course, self.user.id)
399 400

        # First, try with an enrolled student
401
        self.login(self.unenrolled_user)
402
        self.assertFalse(self.enroll(self.course))
403
        self.assertTrue(self.enroll(self.test_course))
404

405
        # Then, try as an instructor
406
        self.logout()
407
        self.login(self.instructor_user)
408
        self.assertTrue(self.enroll(self.course))
409

410 411
        # Then, try as global staff
        self.logout()
412
        self.login(self.global_staff_user)
413
        self.assertTrue(self.enroll(self.course))
414

415

416
@attr(shard=1)
417
class TestBetatesterAccess(ModuleStoreTestCase, CourseAccessTestMixin):
418 419 420
    """
    Tests for the beta tester feature
    """
421
    def setUp(self):
422
        super(TestBetatesterAccess, self).setUp()
423

424 425
        now = datetime.datetime.now(pytz.UTC)
        tomorrow = now + datetime.timedelta(days=1)
426

427 428 429 430
        self.course = CourseFactory(days_early_for_beta=2, start=tomorrow)
        self.content = ItemFactory(parent=self.course)

        self.normal_student = UserFactory()
431
        self.beta_tester = BetaTesterFactory(course_key=self.course.id)
432

433
    @patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
434 435 436 437 438
    def test_course_beta_period(self):
        """
        Check that beta-test access works for courses.
        """
        self.assertFalse(self.course.has_started())
439 440
        self.assertCannotAccessCourse(self.normal_student, 'load', self.course)
        self.assertCanAccessCourse(self.beta_tester, 'load', self.course)
441

442
    @patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
443 444 445 446 447
    def test_content_beta_period(self):
        """
        Check that beta-test access works for content.
        """
        # student user shouldn't see it
448
        self.assertFalse(has_access(self.normal_student, 'load', self.content, self.course.id))
449 450

        # now the student should see it
451
        self.assertTrue(has_access(self.beta_tester, 'load', self.content, self.course.id))