Files
edx-platform/lms/djangoapps/courseware/tests/test_access.py
Michael Terry cb1bb7fa64 test: switch default test store to the split store
It's long past time that the default test modulestore was Split,
instead of Old Mongo. This commit switches the default store and
fixes some tests that now fail:
- Tests that didn't expect MFE to be enabled (because we don't
  enable MFE for Old Mongo) - opt out of MFE for those
- Tests that hardcoded old key string formats
- Lots of other random little differences

In many places, I didn't spend much time trying to figure out how to
properly fix the test, and instead just set the modulestore to Old
Mongo.

For those tests that I didn't spend time investigating, I've set
the modulestore to TEST_DATA_MONGO_AMNESTY_MODULESTORE - search for
that string to find further work.
2022-02-04 14:32:50 -05:00

843 lines
37 KiB
Python

"""
Test the access control framework
"""
import datetime
import itertools
from unittest.mock import Mock, patch
import pytest
import ddt
import pytz
from ccx_keys.locator import CCXLocator
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from milestones.tests.utils import MilestonesTestCaseMixin
from opaque_keys.edx.locator import CourseLocator
import lms.djangoapps.courseware.access as access
import lms.djangoapps.courseware.access_response as access_response
from lms.djangoapps.courseware.masquerade import CourseMasquerade
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase, masquerade_as_group_member
from lms.djangoapps.ccx.models import CustomCourseForEdX
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseCcxCoachRole, CourseStaffRole
from common.djangoapps.student.tests.factories import (
AdminFactory,
AnonymousUserFactory,
CourseEnrollmentAllowedFactory,
CourseEnrollmentFactory
)
from common.djangoapps.student.tests.factories import BetaTesterFactory
from common.djangoapps.student.tests.factories import GlobalStaffFactory
from common.djangoapps.student.tests.factories import InstructorFactory
from common.djangoapps.student.tests.factories import StaffFactory
from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.util.milestones_helpers import fulfill_course_milestone, set_prerequisite_courses
from xmodule.course_module import ( # lint-amnesty, pylint: disable=wrong-import-order
CATALOG_VISIBILITY_ABOUT,
CATALOG_VISIBILITY_CATALOG_AND_ABOUT,
CATALOG_VISIBILITY_NONE
)
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import ( # lint-amnesty, pylint: disable=wrong-import-order
ModuleStoreTestCase,
SharedModuleStoreTestCase
)
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.partitions.partitions import MINIMUM_STATIC_PARTITION_ID, Group, UserPartition # lint-amnesty, pylint: disable=wrong-import-order
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
# pylint: disable=protected-access
class CoachAccessTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Test if user is coach on ccx.
"""
@classmethod
def setUpClass(cls):
"""
Set up course for tests
"""
super().setUpClass()
cls.course = CourseFactory.create()
def setUp(self):
"""
Set up tests
"""
super().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, str(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
assert access.has_ccx_coach_role(self.coach, ccx_locator)
# user dont have access as coach on ccx
self.setup_user()
assert not access.has_ccx_coach_role(self.user, ccx_locator)
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
assert access.has_access(self.coach, 'staff', ccx_locator)
# basic user doesn't have staff access on ccx..
self.setup_user()
assert not access.has_access(self.user, 'staff', ccx_locator)
# until we give her a staff role.
CourseStaffRole(ccx_locator).add_users(self.user)
assert access.has_access(self.user, 'staff', ccx_locator)
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
resp = self.client.get(reverse('student_progress', args=[str(ccx_locator), student.id]))
assert resp.status_code == 200
# Assert access of a student
self.client.login(username=student.username, password='test')
resp = self.client.get(reverse('student_progress', args=[str(ccx_locator), self.coach.id]))
assert resp.status_code == 404
@ddt.ddt
class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTestCaseMixin):
"""
Tests for the various access controls on the student dashboard
"""
TOMORROW = 'tomorrow'
YESTERDAY = 'yesterday'
DATES = {
TOMORROW: datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1),
YESTERDAY: datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1),
None: None,
}
def setUp(self):
super().setUp()
self.course = CourseFactory.create(org='edX', course='toy', run='test_run')
self.anonymous_user = AnonymousUserFactory()
self.beta_user = BetaTesterFactory(course_key=self.course.id)
self.student = UserFactory()
self.global_staff = UserFactory(is_staff=True)
self.course_staff = StaffFactory(course_key=self.course.id)
self.course_instructor = InstructorFactory(course_key=self.course.id)
self.staff = GlobalStaffFactory()
def verify_access(self, mock_unit, student_should_have_access, expected_error_type=None):
""" Verify the expected result from _has_access_descriptor """
response = access._has_access_descriptor(self.anonymous_user, 'load', mock_unit, course_key=self.course.id)
assert student_should_have_access == bool(response)
if expected_error_type is not None:
assert isinstance(response, expected_error_type)
assert response.to_json()['error_code'] is not None
assert access._has_access_descriptor(self.course_staff, 'load', mock_unit, course_key=self.course.id)
def test_has_staff_access_to_preview_mode(self):
"""
Test that preview mode is only accessible by staff users.
"""
course_key = self.course.id
CourseEnrollmentFactory(user=self.student, course_id=self.course.id)
for user in [self.global_staff, self.course_staff, self.course_instructor]:
assert access.has_staff_access_to_preview_mode(user, course_key)
assert not access.has_staff_access_to_preview_mode(self.student, course_key)
# we don't want to restrict a staff user, masquerading as student,
# to access preview mode.
# 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('lms.djangoapps.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]:
assert 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]):
assert access.administrative_accesses_to_course_for_user(user, course_key)[count]
assert not any(access.administrative_accesses_to_course_for_user(self.student, course_key))
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('lms.djangoapps.courseware.access.in_preview_mode') as mock_preview:
mock_preview.return_value = False
for obj in modules:
assert bool(access.has_access(self.student, 'load', obj, course_key=self.course.id))
with patch('lms.djangoapps.courseware.access.in_preview_mode') as mock_preview:
mock_preview.return_value = True
for obj in modules:
assert not bool(access.has_access(self.student, 'load', obj, course_key=self.course.id))
@patch('lms.djangoapps.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.
"""
assert bool(access.has_access(self.global_staff, 'staff', self.course, course_key=self.course.id))
assert bool(access.has_access(self.course_staff, 'staff', self.course, course_key=self.course.id))
assert bool(access.has_access(self.course_instructor, 'staff', self.course, course_key=self.course.id))
assert not bool(access.has_access(self.student, 'staff', self.course, course_key=self.course.id))
assert not bool(access.has_access(self.student, 'load', self.course, course_key=self.course.id))
# When masquerading is true, user should not be able to access staff content
with patch('lms.djangoapps.courseware.access.is_masquerading_as_student') as mock_masquerade:
mock_masquerade.return_value = True
assert not bool(access.has_access(self.global_staff, 'staff', self.course, course_key=self.course.id))
assert not bool(access.has_access(self.student, 'staff', self.course, course_key=self.course.id))
@patch('lms.djangoapps.courseware.access_utils.in_preview_mode', Mock(return_value=True))
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.
"""
# 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
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('lms.djangoapps.courseware.access.get_user_role') as mock_user_role:
mock_user_role.return_value = 'student'
assert not 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('lms.djangoapps.courseware.access.get_user_role') as mock_user_role:
mock_user_role.return_value = mocked_role
assert 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.
assert 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.
assert 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.
assert 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.
assert not bool(access.has_access(self.global_staff, 'load', chapter, course_key=self.course.id))
def test_has_access_to_course(self):
assert not access._has_access_to_course(None, 'staff', self.course.id)
assert not access._has_access_to_course(self.anonymous_user, 'staff', self.course.id)
assert not access._has_access_to_course(self.anonymous_user, 'instructor', self.course.id)
assert access._has_access_to_course(self.global_staff, 'staff', self.course.id)
assert access._has_access_to_course(self.global_staff, 'instructor', self.course.id)
# A user has staff access if they are in the staff group
assert access._has_access_to_course(self.course_staff, 'staff', self.course.id)
assert not access._has_access_to_course(self.course_staff, 'instructor', self.course.id)
# A user has staff and instructor access if they are in the instructor group
assert access._has_access_to_course(self.course_instructor, 'staff', self.course.id)
assert access._has_access_to_course(self.course_instructor, 'instructor', self.course.id)
# A user does not have staff or instructor access if they are
# not in either the staff or the the instructor group
assert not access._has_access_to_course(self.student, 'staff', self.course.id)
assert not access._has_access_to_course(self.student, 'instructor', self.course.id)
assert not access._has_access_to_course(self.student, 'not_staff_or_instructor', self.course.id)
def test__has_access_string(self):
user = Mock(is_staff=True)
assert not access._has_access_string(user, 'staff', 'not_global')
user._has_global_staff_access.return_value = True
assert access._has_access_string(user, 'staff', 'global')
self.assertRaises(ValueError, access._has_access_string, user, 'not_staff', 'global')
@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):
descriptor = Mock()
for (user, expected_response) in (
(self.student, expected_student),
(self.course_staff, expected_staff),
(self.course_instructor, expected_instructor)
):
assert bool(access._has_access_error_desc(user, action, descriptor, self.course.id)) == expected_response
with pytest.raises(ValueError):
access._has_access_error_desc(self.course_instructor, 'not_load_or_staff', descriptor, self.course.id)
def test__has_access_descriptor(self):
# TODO: override DISABLE_START_DATES and test the start date branch of the method
user = Mock()
descriptor = Mock(user_partitions=[])
descriptor._class_tags = {}
descriptor.merged_group_access = {}
# Always returns true because DISABLE_START_DATES is set in test.py
assert access._has_access_descriptor(user, 'load', descriptor)
assert access._has_access_descriptor(user, 'instructor', descriptor)
with pytest.raises(ValueError):
access._has_access_descriptor(user, 'not_load_or_staff', descriptor)
@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
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test__has_access_descriptor_staff_lock(self, visible_to_staff_only, start, expected_error_type=None):
"""
Tests that "visible_to_staff_only" overrides start date.
"""
expected_access = expected_error_type is None
mock_unit = Mock(location=self.course.location, user_partitions=[])
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
mock_unit.visible_to_staff_only = visible_to_staff_only
mock_unit.start = self.DATES[start]
mock_unit.merged_group_access = {}
self.verify_access(mock_unit, expected_access, expected_error_type)
def test__has_access_descriptor_beta_user(self):
mock_unit = Mock(user_partitions=[])
mock_unit._class_tags = {}
mock_unit.days_early_for_beta = 2
mock_unit.start = self.DATES[self.TOMORROW]
mock_unit.visible_to_staff_only = False
mock_unit.merged_group_access = {}
assert bool(access._has_access_descriptor(self.beta_user, 'load', mock_unit, course_key=self.course.id))
@ddt.data(None, YESTERDAY, TOMORROW)
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
@patch(
'lms.djangoapps.courseware.access_utils.get_current_request_hostname',
Mock(return_value='preview.localhost')
)
def test__has_access_descriptor_in_preview_mode(self, start):
"""
Tests that descriptor has access in preview mode.
"""
mock_unit = Mock(location=self.course.location, user_partitions=[])
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
mock_unit.visible_to_staff_only = False
mock_unit.start = self.DATES[start]
mock_unit.merged_group_access = {}
self.verify_access(mock_unit, True)
@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
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
@patch('lms.djangoapps.courseware.access_utils.get_current_request_hostname', Mock(return_value='localhost'))
def test__has_access_descriptor_when_not_in_preview_mode(self, start, expected_error_type):
"""
Tests that descriptor has no access when start date in future & without preview.
"""
expected_access = expected_error_type is None
mock_unit = Mock(location=self.course.location, user_partitions=[])
mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor
mock_unit.visible_to_staff_only = False
mock_unit.start = self.DATES[start]
mock_unit.merged_group_access = {}
self.verify_access(mock_unit, expected_access, expected_error_type)
def test__has_access_course_can_enroll(self):
yesterday = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1)
tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
# Non-staff can enroll if authenticated and specifically allowed for that course
# even outside the open enrollment period
user = UserFactory.create()
course = Mock(
enrollment_start=tomorrow, enrollment_end=tomorrow,
id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain=''
)
CourseEnrollmentAllowedFactory(email=user.email, course_id=course.id)
assert access._has_access_course(user, 'enroll', course)
# Staff can always enroll even outside the open enrollment period
user = StaffFactory.create(course_key=course.id)
assert access._has_access_course(user, 'enroll', course)
# Non-staff cannot enroll if it is between the start and end dates and invitation only
# and not specifically allowed
course = Mock(
enrollment_start=yesterday, enrollment_end=tomorrow,
id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain='',
invitation_only=True
)
user = UserFactory.create()
assert not access._has_access_course(user, 'enroll', course)
# 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,
id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain='',
invitation_only=False
)
assert access._has_access_course(user, 'enroll', course)
# Non-staff cannot enroll outside the open enrollment period if not specifically allowed
course = Mock(
enrollment_start=tomorrow, enrollment_end=tomorrow,
id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain='',
invitation_only=False
)
assert not access._has_access_course(user, 'enroll', course)
@override_settings(COURSES_INVITE_ONLY=False)
def test__course_default_invite_only_flag_false(self):
"""
Ensure that COURSES_INVITE_ONLY does not take precedence,
if it is not set over the course invitation_only settings.
"""
user = UserFactory.create()
# User cannot enroll in the course if it is just invitation only.
course = self._mock_course_with_invitation(invitation=True)
self.assertFalse(access._has_access_course(user, 'enroll', course))
# User can enroll in the course if it is not just invitation only.
course = self._mock_course_with_invitation(invitation=False)
self.assertTrue(access._has_access_course(user, 'enroll', course))
@override_settings(COURSES_INVITE_ONLY=True)
def test__course_default_invite_only_flag_true(self):
"""
Ensure that COURSES_INVITE_ONLY takes precedence over the course invitation_only settings.
"""
user = UserFactory.create()
# User cannot enroll in the course if it is just invitation only and COURSES_INVITE_ONLY is also set.
course = self._mock_course_with_invitation(invitation=True)
self.assertFalse(access._has_access_course(user, 'enroll', course))
# User cannot enroll in the course if COURSES_INVITE_ONLY is set despite of the course invitation_only value.
course = self._mock_course_with_invitation(invitation=False)
self.assertFalse(access._has_access_course(user, 'enroll', course))
def _mock_course_with_invitation(self, invitation):
yesterday = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1)
tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
return Mock(
enrollment_start=yesterday, enrollment_end=tomorrow,
id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain='',
invitation_only=invitation
)
def test__user_passed_as_none(self):
"""Ensure has_access handles a user being passed as null"""
access.has_access(None, 'staff', 'global', None)
def test__catalog_visibility(self):
"""
Tests the catalog visibility tri-states
"""
user = UserFactory.create()
course_id = CourseLocator('edX', 'test', '2012_Fall')
staff = StaffFactory.create(course_key=course_id)
course = Mock(
id=course_id,
catalog_visibility=CATALOG_VISIBILITY_CATALOG_AND_ABOUT
)
assert access._has_access_course(user, 'see_in_catalog', course)
assert access._has_access_course(user, 'see_about_page', course)
assert access._has_access_course(staff, 'see_in_catalog', course)
assert access._has_access_course(staff, 'see_about_page', course)
# Now set visibility to just about page
course = Mock(
id=CourseLocator('edX', 'test', '2012_Fall'),
catalog_visibility=CATALOG_VISIBILITY_ABOUT
)
assert not access._has_access_course(user, 'see_in_catalog', course)
assert access._has_access_course(user, 'see_about_page', course)
assert access._has_access_course(staff, 'see_in_catalog', course)
assert access._has_access_course(staff, 'see_about_page', course)
# Now set visibility to none, which means neither in catalog nor about pages
course = Mock(
id=CourseLocator('edX', 'test', '2012_Fall'),
catalog_visibility=CATALOG_VISIBILITY_NONE
)
assert not access._has_access_course(user, 'see_in_catalog', course)
assert not access._has_access_course(user, 'see_about_page', course)
assert access._has_access_course(staff, 'see_in_catalog', course)
assert access._has_access_course(staff, 'see_about_page', course)
@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 = [str(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)
# user should not be able to load course even if enrolled
CourseEnrollmentFactory(user=user, course_id=course.id)
response = access._has_access_course(user, 'load', course)
assert not response
assert isinstance(response, access_response.MilestoneAccessError)
# Staff can always access course
staff = StaffFactory.create(course_key=course.id)
assert access._has_access_course(staff, 'load', course)
# User should be able access after completing required course
fulfill_course_milestone(pre_requisite_course.id, user)
assert access._has_access_course(user, 'load', course)
@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.
"""
descriptor = CourseFactory()
descriptor.visible_to_staff_only = False
descriptor.mobile_available = mobile_available
assert bool(access._has_access_course(self.student, 'load_mobile', descriptor)) == student_expected
assert bool(access._has_access_course(self.staff, 'load_mobile', descriptor)) == staff_expected
@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 = [str(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=[str(course.id)])
response = self.client.get(url)
self.assertRedirects(
response,
reverse(
'dashboard'
)
)
assert response.status_code == 302
fulfill_course_milestone(pre_requisite_course.id, user)
response = self.client.get(url)
assert response.status_code == 200
class UserRoleTestCase(TestCase):
"""
Tests for user roles.
"""
def setUp(self):
super().setUp()
self.course_key = CourseLocator('edX', 'toy', '2012_Fall')
self.anonymous_user = AnonymousUserFactory()
self.student = UserFactory()
self.global_staff = UserFactory(is_staff=True)
self.course_staff = StaffFactory(course_key=self.course_key)
self.course_instructor = InstructorFactory(course_key=self.course_key)
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)
}
def test_user_role_staff(self):
"""Ensure that user role is student for staff masqueraded as student."""
assert 'staff' == access.get_user_role(self.course_staff, self.course_key)
# Masquerade staff
self._install_masquerade(self.course_staff)
assert 'student' == access.get_user_role(self.course_staff, self.course_key)
def test_user_role_instructor(self):
"""Ensure that user role is student for instructor masqueraded as student."""
assert 'instructor' == access.get_user_role(self.course_instructor, self.course_key)
# Masquerade instructor
self._install_masquerade(self.course_instructor)
assert 'student' == access.get_user_role(self.course_instructor, self.course_key)
def test_user_role_anonymous(self):
"""Ensure that user role is student for anonymous user."""
assert 'student' == access.get_user_role(self.anonymous_user, self.course_key)
@ddt.ddt
class CourseOverviewAccessTestCase(ModuleStoreTestCase):
"""
Tests confirming that has_access works equally on CourseBlocks and
CourseOverviews.
"""
def setUp(self):
super().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)
self.user_completed_pre_requisite = UserFactory.create()
fulfill_course_milestone(self.course_started.id, self.user_completed_pre_requisite)
self.user_staff = UserFactory.create(is_staff=True)
self.user_anonymous = AnonymousUserFactory.create()
COURSE_TEST_DATA = list(itertools.product(
['user_normal', 'user_staff', 'user_anonymous'],
['enroll', 'load', 'staff', 'instructor', 'see_exists', 'see_in_catalog', 'see_about_page'],
['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'],
['load'],
['course_default', 'course_with_pre_requisite', 'course_with_pre_requisites'],
))
@ddt.data(*(COURSE_TEST_DATA + LOAD_MOBILE_TEST_DATA + PREREQUISITES_TEST_DATA))
@ddt.unpack
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
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 CourseBlock to test with.
"""
user = getattr(self, user_attr_name)
course = getattr(self, course_attr_name)
course_overview = CourseOverview.get_from_id(course.id)
assert bool(access.has_access(user, action, course, course_key=course.id)) ==\
bool(access.has_access(user, action, course_overview, course_key=course.id))
def test_course_overview_unsupported_action(self):
"""
Check that calling has_access with an unsupported action raises a
ValueError.
"""
overview = CourseOverview.get_from_id(self.course_default.id)
with pytest.raises(ValueError):
access.has_access(self.user, '_non_existent_action', overview)
@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):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime.datetime(2018, 1, 1))
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)
if user_attr_name == 'user_staff' and action == 'see_exists':
# always checks staff role, and if the course has started, check the duration configuration
if course_attr_name == 'course_started':
num_queries = 2
else:
num_queries = 1
elif user_attr_name == 'user_normal' and action == 'see_exists':
if course_attr_name == 'course_started':
num_queries = 4
else:
# checks staff role and enrollment data
num_queries = 2
elif user_attr_name == 'user_anonymous' and action == 'see_exists':
if course_attr_name == 'course_started':
num_queries = 1
else:
num_queries = 0
else:
# if the course has started, check the duration configuration
if action == 'see_exists' and course_attr_name == 'course_started':
num_queries = 3
else:
num_queries = 0
course_overview = CourseOverview.get_from_id(course.id)
with self.assertNumQueries(num_queries, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
bool(access.has_access(user, action, course_overview, course_key=course.id))