test_access.py 35 KB
Newer Older
1 2 3 4
# -*- coding: utf-8 -*-
"""
Test the access control framework
"""
5
import datetime
6
import itertools
7

8 9
import ddt
import pytz
10
from ccx_keys.locator import CCXLocator
11
from django.contrib.auth.models import User
12 13
from django.core.urlresolvers import reverse
from django.test import TestCase
14 15
from django.test.client import RequestFactory
from milestones.tests.utils import MilestonesTestCaseMixin
16 17
from mock import Mock, patch
from nose.plugins.attrib import attr
18
from opaque_keys.edx.locator import CourseLocator
19

20
import courseware.access as access
21
import courseware.access_response as access_response
22
from courseware.masquerade import CourseMasquerade
23 24 25 26 27
from courseware.tests.factories import (
    BetaTesterFactory,
    GlobalStaffFactory,
    InstructorFactory,
    StaffFactory,
28
    UserFactory
29
)
30
from courseware.tests.helpers import LoginEnrollmentTestCase, masquerade_as_group_member
31
from lms.djangoapps.ccx.models import CustomCourseForEdX
32
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
33
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
34
from student.models import CourseEnrollment
35
from student.roles import CourseCcxCoachRole, CourseStaffRole
36
from student.tests.factories import (
37
    AdminFactory,
38 39
    AnonymousUserFactory,
    CourseEnrollmentAllowedFactory,
40
    CourseEnrollmentFactory
41
)
42
from util.milestones_helpers import fulfill_course_milestone, set_prerequisite_courses
43
from xmodule.course_module import (
44
    CATALOG_VISIBILITY_ABOUT,
45 46
    CATALOG_VISIBILITY_CATALOG_AND_ABOUT,
    CATALOG_VISIBILITY_NONE
47
)
48 49
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
50
from xmodule.modulestore.tests.django_utils import (
51
    TEST_DATA_SPLIT_MODULESTORE,
52
    ModuleStoreTestCase,
53
    SharedModuleStoreTestCase
54
)
55 56
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import MINIMUM_STATIC_PARTITION_ID, Group, UserPartition
57

58
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
59

60
# pylint: disable=protected-access
Julia Hansbrough committed
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
class CoachAccessTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
    """
    Test if user is coach on ccx.
    """
    MODULESTORE = TEST_DATA_SPLIT_MODULESTORE

    @classmethod
    def setUpClass(cls):
        """
        Set up course for tests
        """
        super(CoachAccessTestCaseCCX, cls).setUpClass()
        cls.course = CourseFactory.create()

    def setUp(self):
        """
        Set up tests
        """
        super(CoachAccessTestCaseCCX, self).setUp()

        # Create ccx coach account
        self.coach = AdminFactory.create(password="test")
        self.client.login(username=self.coach.username, password="test")

        # assign role to coach
        role = CourseCcxCoachRole(self.course.id)
        role.add_users(self.coach)
        self.request_factory = RequestFactory()

    def make_ccx(self):
        """
        create ccx
        """
        ccx = CustomCourseForEdX(
            course_id=self.course.id,
            coach=self.coach,
            display_name="Test CCX"
        )
        ccx.save()

        ccx_locator = CCXLocator.from_course_locator(self.course.id, unicode(ccx.id))
        role = CourseCcxCoachRole(ccx_locator)
        role.add_users(self.coach)
        CourseEnrollment.enroll(self.coach, ccx_locator)
        return ccx_locator

    def test_has_ccx_coach_role(self):
        """
        Assert that user has coach access on ccx.
        """
        ccx_locator = self.make_ccx()

        # user have access as coach on ccx
        self.assertTrue(access.has_ccx_coach_role(self.coach, ccx_locator))

        # user dont have access as coach on ccx
        self.setup_user()
        self.assertFalse(access.has_ccx_coach_role(self.user, ccx_locator))

122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
    def test_ccx_coach_has_staff_role(self):
        """
        Assert that user has staff access on ccx.
        """
        ccx_locator = self.make_ccx()

        # coach user has access as staff on ccx
        self.assertTrue(access.has_access(self.coach, 'staff', ccx_locator))

        # basic user doesn't have staff access on ccx..
        self.setup_user()
        self.assertFalse(access.has_access(self.user, 'staff', ccx_locator))

        # until we give her a staff role.
        CourseStaffRole(ccx_locator).add_users(self.user)
        self.assertTrue(access.has_access(self.user, 'staff', ccx_locator))

139 140 141 142 143 144 145 146 147 148 149
    def test_access_student_progress_ccx(self):
        """
        Assert that only a coach can see progress of student.
        """
        ccx_locator = self.make_ccx()
        student = UserFactory()

        # Enroll user
        CourseEnrollment.enroll(student, ccx_locator)

        # Test for access of a coach
150
        resp = self.client.get(reverse('student_progress', args=[unicode(ccx_locator), student.id]))
151 152 153
        self.assertEqual(resp.status_code, 200)

        # Assert access of a student
154 155 156
        self.client.login(username=student.username, password='test')
        resp = self.client.get(reverse('student_progress', args=[unicode(ccx_locator), self.coach.id]))
        self.assertEqual(resp.status_code, 404)
157 158


159
@attr(shard=1)
160
@ddt.ddt
161
class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTestCaseMixin):
162 163 164
    """
    Tests for the various access controls on the student dashboard
    """
Jeremy Bowman committed
165 166
    TOMORROW = 'tomorrow'
    YESTERDAY = 'yesterday'
167
    MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
Jeremy Bowman committed
168 169 170 171 172
    DATES = {
        TOMORROW: datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1),
        YESTERDAY: datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1),
        None: None,
    }
173

174
    def setUp(self):
175
        super(AccessTestCase, self).setUp()
176
        self.course = CourseFactory.create(org='edX', course='toy', run='test_run')
177
        self.anonymous_user = AnonymousUserFactory()
178
        self.beta_user = BetaTesterFactory(course_key=self.course.id)
179 180
        self.student = UserFactory()
        self.global_staff = UserFactory(is_staff=True)
181 182
        self.course_staff = StaffFactory(course_key=self.course.id)
        self.course_instructor = InstructorFactory(course_key=self.course.id)
183
        self.staff = GlobalStaffFactory()
184

185
    def verify_access(self, mock_unit, student_should_have_access, expected_error_type=None):
186
        """ Verify the expected result from _has_access_descriptor """
187
        response = access._has_access_descriptor(self.anonymous_user, 'load', mock_unit, course_key=self.course.id)
188 189 190 191 192 193
        self.assertEqual(student_should_have_access, bool(response))

        if expected_error_type is not None:
            self.assertIsInstance(response, expected_error_type)
            self.assertIsNotNone(response.to_json()['error_code'])

194
        self.assertTrue(
195
            access._has_access_descriptor(self.course_staff, 'load', mock_unit, course_key=self.course.id)
196 197
        )

198 199
    def test_has_staff_access_to_preview_mode(self):
        """
200
        Test that preview mode is only accessible by staff users.
201 202
        """
        course_key = self.course.id
203
        CourseEnrollmentFactory(user=self.student, course_id=self.course.id)
204

205 206
        for user in [self.global_staff, self.course_staff, self.course_instructor]:
            self.assertTrue(access.has_staff_access_to_preview_mode(user, course_key))
207

208
        self.assertFalse(access.has_staff_access_to_preview_mode(self.student, course_key))
209

210 211
        # we don't want to restrict a staff user, masquerading as student,
        # to access preview mode.
212

213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
        # Note that self.student now have access to preview mode,
        # `is_masquerading_as_student == True` means user is staff and is
        # masquerading as a student.
        with patch('courseware.access.is_masquerading_as_student') as mock_masquerade:
            mock_masquerade.return_value = True
            for user in [self.global_staff, self.course_staff, self.course_instructor, self.student]:
                self.assertTrue(access.has_staff_access_to_preview_mode(user, course_key))

    def test_administrative_accesses_to_course_for_user(self):
        """
        Test types of admin accesses to a course
        """
        course_key = self.course.id

        # `administrative_accesses_to_course_for_user` returns accesses in tuple as
        # (`global_staff`, `course_staff`, `course_instructor`).
        # Order matters here, for example `True` at first index in tuple essentially means
        # given user is a global staff.
        for count, user in enumerate([self.global_staff, self.course_staff, self.course_instructor]):
            self.assertTrue(access.administrative_accesses_to_course_for_user(user, course_key)[count])

        self.assertFalse(any(access.administrative_accesses_to_course_for_user(self.student, course_key)))
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268

    def test_student_has_access(self):
        """
        Tests course student have right access to content w/o preview.
        """
        course_key = self.course.id
        chapter = ItemFactory.create(category="chapter", parent_location=self.course.location)
        overview = CourseOverview.get_from_id(course_key)

        # Enroll student to the course
        CourseEnrollmentFactory(user=self.student, course_id=self.course.id)

        modules = [
            self.course,
            overview,
            chapter,
        ]
        with patch('courseware.access.in_preview_mode') as mock_preview:
            mock_preview.return_value = False
            for obj in modules:
                self.assertTrue(bool(access.has_access(self.student, 'load', obj, course_key=self.course.id)))

        with patch('courseware.access.in_preview_mode') as mock_preview:
            mock_preview.return_value = True
            for obj in modules:
                self.assertFalse(bool(access.has_access(self.student, 'load', obj, course_key=self.course.id)))

    @patch('courseware.access.in_preview_mode', Mock(return_value=True))
    def test_has_access_with_preview_mode(self):
        """
        Tests particular user's can access content via has_access in preview mode.
        """
        self.assertTrue(bool(access.has_access(self.global_staff, 'staff', self.course, course_key=self.course.id)))
        self.assertTrue(bool(access.has_access(self.course_staff, 'staff', self.course, course_key=self.course.id)))
Usman Khalid committed
269 270 271
        self.assertTrue(bool(access.has_access(
            self.course_instructor, 'staff', self.course, course_key=self.course.id
        )))
272 273 274
        self.assertFalse(bool(access.has_access(self.student, 'staff', self.course, course_key=self.course.id)))
        self.assertFalse(bool(access.has_access(self.student, 'load', self.course, course_key=self.course.id)))

275
        # When masquerading is true, user should not be able to access staff content
276 277
        with patch('courseware.access.is_masquerading_as_student') as mock_masquerade:
            mock_masquerade.return_value = True
278
            self.assertFalse(
279 280 281 282 283 284
                bool(access.has_access(self.global_staff, 'staff', self.course, course_key=self.course.id))
            )
            self.assertFalse(
                bool(access.has_access(self.student, 'staff', self.course, course_key=self.course.id))
            )

285
    @patch('courseware.access_utils.in_preview_mode', Mock(return_value=True))
286 287 288 289
    def test_has_access_in_preview_mode_with_group(self):
        """
        Test that a user masquerading as a member of a group sees appropriate content in preview mode.
        """
290 291 292 293 294
        # Note about UserPartition and UserPartition Group IDs: these must not conflict with IDs used
        # by dynamic user partitions.
        partition_id = MINIMUM_STATIC_PARTITION_ID
        group_0_id = MINIMUM_STATIC_PARTITION_ID + 1
        group_1_id = MINIMUM_STATIC_PARTITION_ID + 2
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
        user_partition = UserPartition(
            partition_id, 'Test User Partition', '',
            [Group(group_0_id, 'Group 1'), Group(group_1_id, 'Group 2')],
            scheme_id='cohort'
        )
        self.course.user_partitions.append(user_partition)
        self.course.cohort_config = {'cohorted': True}

        chapter = ItemFactory.create(category="chapter", parent_location=self.course.location)
        chapter.group_access = {partition_id: [group_0_id]}

        modulestore().update_item(self.course, ModuleStoreEnum.UserID.test)

        # User should not be able to preview when masquerading as student (and not in the group above).
        with patch('courseware.access.get_user_role') as mock_user_role:
            mock_user_role.return_value = 'student'
            self.assertFalse(
                bool(access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id))
            )

        # Should be able to preview when in staff or instructor role.
        for mocked_role in ['staff', 'instructor']:
            with patch('courseware.access.get_user_role') as mock_user_role:
                mock_user_role.return_value = mocked_role
                self.assertTrue(
                    bool(access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id))
                )

        # Now install masquerade group and set staff as a member of that.
        self.assertEqual(200, masquerade_as_group_member(self.global_staff, self.course, partition_id, group_0_id))
        # Can load the chapter since user is in the group.
        self.assertTrue(
            bool(access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id))
        )

        # Move the user to be a part of the second group.
        self.assertEqual(200, masquerade_as_group_member(self.global_staff, self.course, partition_id, group_1_id))
        # Cannot load the chapter since user is in a different group.
        self.assertFalse(
            bool(access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id))
        )

337 338
    def test_has_access_to_course(self):
        self.assertFalse(access._has_access_to_course(
339
            None, 'staff', self.course.id
340 341
        ))

342
        self.assertFalse(access._has_access_to_course(
343
            self.anonymous_user, 'staff', self.course.id
344
        ))
345
        self.assertFalse(access._has_access_to_course(
346
            self.anonymous_user, 'instructor', self.course.id
347 348
        ))

349
        self.assertTrue(access._has_access_to_course(
350
            self.global_staff, 'staff', self.course.id
351
        ))
352
        self.assertTrue(access._has_access_to_course(
353
            self.global_staff, 'instructor', self.course.id
354
        ))
355 356

        # A user has staff access if they are in the staff group
357
        self.assertTrue(access._has_access_to_course(
358
            self.course_staff, 'staff', self.course.id
359
        ))
360
        self.assertFalse(access._has_access_to_course(
361
            self.course_staff, 'instructor', self.course.id
362
        ))
363

364
        # A user has staff and instructor access if they are in the instructor group
365
        self.assertTrue(access._has_access_to_course(
366
            self.course_instructor, 'staff', self.course.id
367
        ))
368
        self.assertTrue(access._has_access_to_course(
369
            self.course_instructor, 'instructor', self.course.id
370
        ))
371

372
        # A user does not have staff or instructor access if they are
373
        # not in either the staff or the the instructor group
374
        self.assertFalse(access._has_access_to_course(
375
            self.student, 'staff', self.course.id
376
        ))
377
        self.assertFalse(access._has_access_to_course(
378
            self.student, 'instructor', self.course.id
379
        ))
380

381
        self.assertFalse(access._has_access_to_course(
382
            self.student, 'not_staff_or_instructor', self.course.id
383 384
        ))

385
    def test__has_access_string(self):
386
        user = Mock(is_staff=True)
stv committed
387
        self.assertFalse(access._has_access_string(user, 'staff', 'not_global'))
388

389
        user._has_global_staff_access.return_value = True
stv committed
390
        self.assertTrue(access._has_access_string(user, 'staff', 'global'))
391

stv committed
392
        self.assertRaises(ValueError, access._has_access_string, user, 'not_staff', 'global')
393

394 395 396 397 398 399 400
    @ddt.data(
        ('load', False, True, True),
        ('staff', False, True, True),
        ('instructor', False, False, True)
    )
    @ddt.unpack
    def test__has_access_error_desc(self, action, expected_student, expected_staff, expected_instructor):
401 402
        descriptor = Mock()

403 404 405 406 407 408
        for (user, expected_response) in (
                (self.student, expected_student),
                (self.course_staff, expected_staff),
                (self.course_instructor, expected_instructor)
        ):
            self.assertEquals(
409
                bool(access._has_access_error_desc(user, action, descriptor, self.course.id)),
410 411
                expected_response
            )
412 413

        with self.assertRaises(ValueError):
414
            access._has_access_error_desc(self.course_instructor, 'not_load_or_staff', descriptor, self.course.id)
415

416
    def test__has_access_descriptor(self):
417
        # TODO: override DISABLE_START_DATES and test the start date branch of the method
418
        user = Mock()
419
        descriptor = Mock(user_partitions=[])
420
        descriptor._class_tags = {}
421
        descriptor.merged_group_access = {}
422 423

        # Always returns true because DISABLE_START_DATES is set in test.py
424 425
        self.assertTrue(access._has_access_descriptor(user, 'load', descriptor))
        self.assertTrue(access._has_access_descriptor(user, 'instructor', descriptor))
426
        with self.assertRaises(ValueError):
427
            access._has_access_descriptor(user, 'not_load_or_staff', descriptor)
428

429 430 431 432 433 434 435 436 437
    @ddt.data(
        (True, None, access_response.VisibilityError),
        (False, None),
        (True, YESTERDAY, access_response.VisibilityError),
        (False, YESTERDAY),
        (True, TOMORROW, access_response.VisibilityError),
        (False, TOMORROW, access_response.StartDateError)
    )
    @ddt.unpack
438
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
439
    def test__has_access_descriptor_staff_lock(self, visible_to_staff_only, start, expected_error_type=None):
440 441 442
        """
        Tests that "visible_to_staff_only" overrides start date.
        """
443
        expected_access = expected_error_type is None
444
        mock_unit = Mock(location=self.course.location, user_partitions=[])
445
        mock_unit._class_tags = {}  # Needed for detached check in _has_access_descriptor
446
        mock_unit.visible_to_staff_only = visible_to_staff_only
Jeremy Bowman committed
447
        mock_unit.start = self.DATES[start]
448 449
        mock_unit.merged_group_access = {}

450
        self.verify_access(mock_unit, expected_access, expected_error_type)
451

452 453 454 455
    def test__has_access_descriptor_beta_user(self):
        mock_unit = Mock(user_partitions=[])
        mock_unit._class_tags = {}
        mock_unit.days_early_for_beta = 2
Jeremy Bowman committed
456
        mock_unit.start = self.DATES[self.TOMORROW]
457
        mock_unit.visible_to_staff_only = False
458
        mock_unit.merged_group_access = {}
459

460
        self.assertTrue(bool(access._has_access_descriptor(
461
            self.beta_user, 'load', mock_unit, course_key=self.course.id)))
462

463
    @ddt.data(None, YESTERDAY, TOMORROW)
464
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
465
    @patch('courseware.access_utils.get_current_request_hostname', Mock(return_value='preview.localhost'))
466
    def test__has_access_descriptor_in_preview_mode(self, start):
467 468 469
        """
        Tests that descriptor has access in preview mode.
        """
470
        mock_unit = Mock(location=self.course.location, user_partitions=[])
471 472
        mock_unit._class_tags = {}  # Needed for detached check in _has_access_descriptor
        mock_unit.visible_to_staff_only = False
Jeremy Bowman committed
473
        mock_unit.start = self.DATES[start]
474 475
        mock_unit.merged_group_access = {}

476 477
        self.verify_access(mock_unit, True)

478 479 480 481 482 483
    @ddt.data(
        (TOMORROW, access_response.StartDateError),
        (None, None),
        (YESTERDAY, None)
    )  # ddt throws an error if I don't put the None argument there
    @ddt.unpack
484
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
485
    @patch('courseware.access_utils.get_current_request_hostname', Mock(return_value='localhost'))
486
    def test__has_access_descriptor_when_not_in_preview_mode(self, start, expected_error_type):
487 488 489
        """
        Tests that descriptor has no access when start date in future & without preview.
        """
490
        expected_access = expected_error_type is None
491
        mock_unit = Mock(location=self.course.location, user_partitions=[])
492 493
        mock_unit._class_tags = {}  # Needed for detached check in _has_access_descriptor
        mock_unit.visible_to_staff_only = False
Jeremy Bowman committed
494
        mock_unit.start = self.DATES[start]
495 496
        mock_unit.merged_group_access = {}

497
        self.verify_access(mock_unit, expected_access, expected_error_type)
498

499
    def test__has_access_course_can_enroll(self):
500 501
        yesterday = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1)
        tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
502

503
        # Non-staff can enroll if authenticated and specifically allowed for that course
504
        # even outside the open enrollment period
505
        user = UserFactory.create()
506 507
        course = Mock(
            enrollment_start=tomorrow, enrollment_end=tomorrow,
508
            id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain=''
509 510
        )
        CourseEnrollmentAllowedFactory(email=user.email, course_id=course.id)
511
        self.assertTrue(access._has_access_course(user, 'enroll', course))
512 513

        # Staff can always enroll even outside the open enrollment period
514
        user = StaffFactory.create(course_key=course.id)
515
        self.assertTrue(access._has_access_course(user, 'enroll', course))
516

517 518
        # Non-staff cannot enroll if it is between the start and end dates and invitation only
        # and not specifically allowed
519
        course = Mock(
520
            enrollment_start=yesterday, enrollment_end=tomorrow,
521
            id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain='',
522 523 524
            invitation_only=True
        )
        user = UserFactory.create()
525
        self.assertFalse(access._has_access_course(user, 'enroll', course))
526 527 528 529

        # Non-staff can enroll if it is between the start and end dates and not invitation only
        course = Mock(
            enrollment_start=yesterday, enrollment_end=tomorrow,
530
            id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain='',
531
            invitation_only=False
532
        )
533
        self.assertTrue(access._has_access_course(user, 'enroll', course))
534 535

        # Non-staff cannot enroll outside the open enrollment period if not specifically allowed
536 537
        course = Mock(
            enrollment_start=tomorrow, enrollment_end=tomorrow,
538
            id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain='',
539 540
            invitation_only=False
        )
541
        self.assertFalse(access._has_access_course(user, 'enroll', course))
542 543 544

    def test__user_passed_as_none(self):
        """Ensure has_access handles a user being passed as null"""
545 546
        access.has_access(None, 'staff', 'global', None)

547 548 549 550 551
    def test__catalog_visibility(self):
        """
        Tests the catalog visibility tri-states
        """
        user = UserFactory.create()
552
        course_id = CourseLocator('edX', 'test', '2012_Fall')
553 554 555 556 557 558
        staff = StaffFactory.create(course_key=course_id)

        course = Mock(
            id=course_id,
            catalog_visibility=CATALOG_VISIBILITY_CATALOG_AND_ABOUT
        )
559 560 561 562
        self.assertTrue(access._has_access_course(user, 'see_in_catalog', course))
        self.assertTrue(access._has_access_course(user, 'see_about_page', course))
        self.assertTrue(access._has_access_course(staff, 'see_in_catalog', course))
        self.assertTrue(access._has_access_course(staff, 'see_about_page', course))
563 564 565

        # Now set visibility to just about page
        course = Mock(
566
            id=CourseLocator('edX', 'test', '2012_Fall'),
567 568
            catalog_visibility=CATALOG_VISIBILITY_ABOUT
        )
569 570 571 572
        self.assertFalse(access._has_access_course(user, 'see_in_catalog', course))
        self.assertTrue(access._has_access_course(user, 'see_about_page', course))
        self.assertTrue(access._has_access_course(staff, 'see_in_catalog', course))
        self.assertTrue(access._has_access_course(staff, 'see_about_page', course))
573 574 575

        # Now set visibility to none, which means neither in catalog nor about pages
        course = Mock(
576
            id=CourseLocator('edX', 'test', '2012_Fall'),
577 578
            catalog_visibility=CATALOG_VISIBILITY_NONE
        )
579 580 581 582
        self.assertFalse(access._has_access_course(user, 'see_in_catalog', course))
        self.assertFalse(access._has_access_course(user, 'see_about_page', course))
        self.assertTrue(access._has_access_course(staff, 'see_in_catalog', course))
        self.assertTrue(access._has_access_course(staff, 'see_about_page', course))
583

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
    @patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
    def test_access_on_course_with_pre_requisites(self):
        """
        Test course access when a course has pre-requisite course yet to be completed
        """
        user = UserFactory.create()

        pre_requisite_course = CourseFactory.create(
            org='test_org', number='788', run='test_run'
        )

        pre_requisite_courses = [unicode(pre_requisite_course.id)]
        course = CourseFactory.create(
            org='test_org', number='786', run='test_run', pre_requisite_courses=pre_requisite_courses
        )
        set_prerequisite_courses(course.id, pre_requisite_courses)

601
        # user should not be able to load course even if enrolled
602
        CourseEnrollmentFactory(user=user, course_id=course.id)
603
        response = access._has_access_course(user, 'load', course)
604
        self.assertFalse(response)
605
        self.assertIsInstance(response, access_response.MilestoneAccessError)
606 607
        # Staff can always access course
        staff = StaffFactory.create(course_key=course.id)
608
        self.assertTrue(access._has_access_course(staff, 'load', course))
609 610 611

        # User should be able access after completing required course
        fulfill_course_milestone(pre_requisite_course.id, user)
612
        self.assertTrue(access._has_access_course(user, 'load', course))
613

614 615 616 617 618 619 620 621 622
    @ddt.data(
        (True, True, True),
        (False, False, True)
    )
    @ddt.unpack
    def test__access_on_mobile(self, mobile_available, student_expected, staff_expected):
        """
        Test course access on mobile for staff and students.
        """
623
        descriptor = CourseFactory()
624 625 626 627
        descriptor.visible_to_staff_only = False
        descriptor.mobile_available = mobile_available

        self.assertEqual(
628
            bool(access._has_access_course(self.student, 'load_mobile', descriptor)),
629 630
            student_expected
        )
631
        self.assertEqual(bool(access._has_access_course(self.staff, 'load_mobile', descriptor)), staff_expected)
632

633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
    @patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
    def test_courseware_page_unfulfilled_prereqs(self):
        """
        Test courseware access when a course has pre-requisite course yet to be completed
        """
        pre_requisite_course = CourseFactory.create(
            org='edX',
            course='900',
            run='test_run',
        )

        pre_requisite_courses = [unicode(pre_requisite_course.id)]
        course = CourseFactory.create(
            org='edX',
            course='1000',
            run='test_run',
            pre_requisite_courses=pre_requisite_courses,
        )
        set_prerequisite_courses(course.id, pre_requisite_courses)

        test_password = 't3stp4ss.!'
        user = UserFactory.create()
        user.set_password(test_password)
        user.save()
        self.login(user.email, test_password)
        CourseEnrollmentFactory(user=user, course_id=course.id)

        url = reverse('courseware', args=[unicode(course.id)])
        response = self.client.get(url)
        self.assertRedirects(
            response,
            reverse(
                'dashboard'
            )
        )
        self.assertEqual(response.status_code, 302)

        fulfill_course_milestone(pre_requisite_course.id, user)
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

674

675
@attr(shard=1)
676 677 678 679
class UserRoleTestCase(TestCase):
    """
    Tests for user roles.
    """
680

681
    def setUp(self):
682
        super(UserRoleTestCase, self).setUp()
683
        self.course_key = CourseLocator('edX', 'toy', '2012_Fall')
684 685 686
        self.anonymous_user = AnonymousUserFactory()
        self.student = UserFactory()
        self.global_staff = UserFactory(is_staff=True)
687 688
        self.course_staff = StaffFactory(course_key=self.course_key)
        self.course_instructor = InstructorFactory(course_key=self.course_key)
689

690 691 692 693 694 695 696 697
    def _install_masquerade(self, user, role='student'):
        """
        Installs a masquerade for the specified user.
        """
        user.masquerade_settings = {
            self.course_key: CourseMasquerade(self.course_key, role=role)
        }

698 699 700 701
    def test_user_role_staff(self):
        """Ensure that user role is student for staff masqueraded as student."""
        self.assertEqual(
            'staff',
702
            access.get_user_role(self.course_staff, self.course_key)
703 704
        )
        # Masquerade staff
705
        self._install_masquerade(self.course_staff)
706 707
        self.assertEqual(
            'student',
708
            access.get_user_role(self.course_staff, self.course_key)
709 710 711 712 713 714
        )

    def test_user_role_instructor(self):
        """Ensure that user role is student for instructor masqueraded as student."""
        self.assertEqual(
            'instructor',
715
            access.get_user_role(self.course_instructor, self.course_key)
716 717
        )
        # Masquerade instructor
718
        self._install_masquerade(self.course_instructor)
719 720
        self.assertEqual(
            'student',
721
            access.get_user_role(self.course_instructor, self.course_key)
722 723 724 725 726 727
        )

    def test_user_role_anonymous(self):
        """Ensure that user role is student for anonymous user."""
        self.assertEqual(
            'student',
728
            access.get_user_role(self.anonymous_user, self.course_key)
729
        )
730 731


732
@attr(shard=3)
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
@ddt.ddt
class CourseOverviewAccessTestCase(ModuleStoreTestCase):
    """
    Tests confirming that has_access works equally on CourseDescriptors and
    CourseOverviews.
    """

    def setUp(self):
        super(CourseOverviewAccessTestCase, self).setUp()

        today = datetime.datetime.now(pytz.UTC)
        last_week = today - datetime.timedelta(days=7)
        next_week = today + datetime.timedelta(days=7)

        self.course_default = CourseFactory.create()
        self.course_started = CourseFactory.create(start=last_week)
        self.course_not_started = CourseFactory.create(start=next_week, days_early_for_beta=10)
        self.course_staff_only = CourseFactory.create(visible_to_staff_only=True)
        self.course_mobile_available = CourseFactory.create(mobile_available=True)
        self.course_with_pre_requisite = CourseFactory.create(
            pre_requisite_courses=[str(self.course_started.id)]
        )
        self.course_with_pre_requisites = CourseFactory.create(
            pre_requisite_courses=[str(self.course_started.id), str(self.course_not_started.id)]
        )

        self.user_normal = UserFactory.create()
        self.user_beta_tester = BetaTesterFactory.create(course_key=self.course_not_started.id)
761
        self.user_completed_pre_requisite = UserFactory.create()
762
        fulfill_course_milestone(self.course_started.id, self.user_completed_pre_requisite)
763 764 765
        self.user_staff = UserFactory.create(is_staff=True)
        self.user_anonymous = AnonymousUserFactory.create()

766
    COURSE_TEST_DATA = list(itertools.product(
767
        ['user_normal', 'user_staff', 'user_anonymous'],
768
        ['enroll', 'load', 'staff', 'instructor', 'see_exists', 'see_in_catalog', 'see_about_page'],
769 770 771 772 773 774 775 776 777 778 779
        ['course_default', 'course_started', 'course_not_started', 'course_staff_only'],
    ))

    LOAD_MOBILE_TEST_DATA = list(itertools.product(
        ['user_normal', 'user_staff'],
        ['load_mobile'],
        ['course_default', 'course_mobile_available'],
    ))

    PREREQUISITES_TEST_DATA = list(itertools.product(
        ['user_normal', 'user_completed_pre_requisite', 'user_staff', 'user_anonymous'],
780
        ['load'],
781 782 783
        ['course_default', 'course_with_pre_requisite', 'course_with_pre_requisites'],
    ))

784
    @ddt.data(*(COURSE_TEST_DATA + LOAD_MOBILE_TEST_DATA + PREREQUISITES_TEST_DATA))
785
    @ddt.unpack
786
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
    def test_course_overview_access(self, user_attr_name, action, course_attr_name):
        """
        Check that a user's access to a course is equal to the user's access to
        the corresponding course overview.

        Instead of taking a user and course directly as arguments, we have to
        take their attribute names, as ddt doesn't allow us to reference self.

        Arguments:
            user_attr_name (str): the name of the attribute on self that is the
                User to test with.
            action (str): action to test with.
            course_attr_name (str): the name of the attribute on self that is
                the CourseDescriptor to test with.
        """
        user = getattr(self, user_attr_name)
        course = getattr(self, course_attr_name)

        course_overview = CourseOverview.get_from_id(course.id)
        self.assertEqual(
807 808
            bool(access.has_access(user, action, course, course_key=course.id)),
            bool(access.has_access(user, action, course_overview, course_key=course.id))
809 810
        )

811
    def test_course_overview_unsupported_action(self):
812 813 814 815 816 817 818
        """
        Check that calling has_access with an unsupported action raises a
        ValueError.
        """
        overview = CourseOverview.get_from_id(self.course_default.id)
        with self.assertRaises(ValueError):
            access.has_access(self.user, '_non_existent_action', overview)
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838

    @ddt.data(
        *itertools.product(
            ['user_normal', 'user_staff', 'user_anonymous'],
            ['see_exists', 'see_in_catalog', 'see_about_page'],
            ['course_default', 'course_started', 'course_not_started'],
        )
    )
    @ddt.unpack
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
    def test_course_catalog_access_num_queries(self, user_attr_name, action, course_attr_name):
        course = getattr(self, course_attr_name)

        # get a fresh user object that won't have any cached role information
        if user_attr_name == 'user_anonymous':
            user = AnonymousUserFactory()
        else:
            user = getattr(self, user_attr_name)
            user = User.objects.get(id=user.id)

839 840 841 842
        if (user_attr_name == 'user_staff' and
            action == 'see_exists' and
            course_attr_name in
                ['course_default', 'course_not_started']):
843 844 845 846 847 848 849 850 851
            # checks staff role
            num_queries = 1
        elif user_attr_name == 'user_normal' and action == 'see_exists' and course_attr_name != 'course_started':
            # checks staff role and enrollment data
            num_queries = 2
        else:
            num_queries = 0

        course_overview = CourseOverview.get_from_id(course.id)
852
        with self.assertNumQueries(num_queries, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
853
            bool(access.has_access(user, action, course_overview, course_key=course.id))