From adffc11856593de013a073fb790252bfb7c96d4a Mon Sep 17 00:00:00 2001 From: Cali Stenson Date: Tue, 30 Oct 2018 16:53:34 -0400 Subject: [PATCH] REVE-18: Test Content Type Gating For All Modes --- .../content_type_gating/tests/test_access.py | 329 +++++++++++++----- 1 file changed, 243 insertions(+), 86 deletions(-) diff --git a/openedx/features/content_type_gating/tests/test_access.py b/openedx/features/content_type_gating/tests/test_access.py index 3ab40370f6..80a51c9238 100644 --- a/openedx/features/content_type_gating/tests/test_access.py +++ b/openedx/features/content_type_gating/tests/test_access.py @@ -25,7 +25,11 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory )) class TestProblemTypeAccess(SharedModuleStoreTestCase): - PROBLEM_TYPES = ['problem', 'openassessment', 'drag-and-drop-v2', 'done', 'edx_sga', ] + PROBLEM_TYPES = ['problem', 'openassessment', 'drag-and-drop-v2', 'done', 'edx_sga'] + # 'html' is a component that just displays html, in these tests it is used to test that users who do not have access + # to graded problems still have access to non-problems + COMPONENT_TYPES = PROBLEM_TYPES + ['html'] + MODE_TYPES = ['credit', 'honor', 'audit', 'verified', 'professional', 'no-id-professional'] GRADED_SCORE_WEIGHT_TEST_CASES = [ # graded, has_score, weight, is_gated @@ -40,93 +44,182 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase): ] @classmethod - def setUpClass(self): - super(TestProblemTypeAccess, self).setUpClass() - self.factory = RequestFactory() - self.course = CourseFactory.create(run='testcourse1', display_name='Test Course Title') - CourseModeFactory.create(course_id=self.course.id, mode_slug='audit') - CourseModeFactory.create(course_id=self.course.id, mode_slug='verified') - with self.store.bulk_operations(self.course.id): - self.chapter = ItemFactory.create( - parent=self.course, - display_name='Overview' - ) - self.welcome = ItemFactory.create( - parent=self.chapter, - display_name='Welcome' - ) - ItemFactory.create( - parent=self.course, - category='chapter', - display_name='Week 1' - ) - self.chapter_subsection = ItemFactory.create( - parent=self.chapter, - category='sequential', - display_name='Lesson 1' - ) - self.vertical = ItemFactory.create( - parent=self.chapter_subsection, - category='vertical', - display_name='Lesson 1 Vertical - Unit 1' - ) - self.lti_block = ItemFactory.create( - parent=self.vertical, - category='lti_consumer', - display_name='lti_consumer', - has_score=True, - graded=True, - ) - self.lti_block_not_scored = ItemFactory.create( - parent=self.vertical, - category='lti_consumer', - display_name='lti_consumer_2', - has_score=False, - ) - self.problem_dict = {} - for prob_type in self.PROBLEM_TYPES: - block = ItemFactory.create( - parent=self.vertical, - category=prob_type, - display_name=prob_type, - graded=True, - ) - self.problem_dict[prob_type] = block + def setUpClass(cls): + super(TestProblemTypeAccess, cls).setUpClass() + cls.factory = RequestFactory() - ''' - Create components with the cartesian product of possible values of - graded/has_score/weight for the test_graded_score_weight_values test. - ''' - self.graded_score_weight_blocks = {} - for graded, has_score, weight, gated in self.GRADED_SCORE_WEIGHT_TEST_CASES: - case_name = ' Graded: ' + str(graded) + ' Has Score: ' + str(has_score) + ' Weight: ' + str(weight) - block = ItemFactory.create( - parent=self.vertical, - # has_score is determined by XBlock type. It is not a value set on an instance of an XBlock. - # Therefore, we create a problem component when has_score is True - # and an html component when has_score is False. - category='problem' if has_score else 'html', - display_name=case_name, - graded=graded, - weight=weight, - ) - self.graded_score_weight_blocks[(graded, has_score, weight)] = block + cls.courses = {} + + # default course is used for most tests, it includes an audit and verified track and all the problem types + # defined in 'PROBLEM_TYPES' and 'GRADED_SCORE_WEIGHT_TEST_CASES' + cls.courses['default'] = cls._create_course( + run='testcourse1', + display_name='Test Course Title', + modes=['audit', 'verified'], + component_types=cls.COMPONENT_TYPES + ) + # because default course is used for most tests self.course and self.problem_dict are set for ease of reference + cls.course = cls.courses['default']['course'] + cls.blocks_dict = cls.courses['default']['blocks'] + + # Create components with the cartesian product of possible values of + # graded/has_score/weight for the test_graded_score_weight_values test. + cls.graded_score_weight_blocks = {} + for graded, has_score, weight, gated in cls.GRADED_SCORE_WEIGHT_TEST_CASES: + case_name = ' Graded: ' + str(graded) + ' Has Score: ' + str(has_score) + ' Weight: ' + str(weight) + block = ItemFactory.create( + parent=cls.blocks_dict['vertical'], + # has_score is determined by XBlock type. It is not a value set on an instance of an XBlock. + # Therefore, we create a problem component when has_score is True + # and an html component when has_score is False. + category='problem' if has_score else 'html', + display_name=case_name, + graded=graded, + weight=weight, + ) + cls.graded_score_weight_blocks[(graded, has_score, weight)] = block + + # add LTI blocks to default course + cls.lti_block = ItemFactory.create( + parent=cls.blocks_dict['vertical'], + category='lti_consumer', + display_name='lti_consumer', + has_score=True, + graded=True, + ) + cls.lti_block_not_scored = ItemFactory.create( + parent=cls.blocks_dict['vertical'], + category='lti_consumer', + display_name='lti_consumer_2', + has_score=False, + ) + + # audit_only course only has an audit track available + cls.courses['audit_only'] = cls._create_course( + run='audit_only_course_run_1', + display_name='Audit Only Test Course Title', + modes=['audit'], + component_types=['problem', 'html'] + ) + + # all_track_types course has all track types defined in MODE_TYPES + cls.courses['all_track_types'] = cls._create_course( + run='all_track_types_run_1', + display_name='All Track/Mode Types Test Course Title', + modes=cls.MODE_TYPES, + component_types=['problem', 'html'] + ) def setUp(self): super(TestProblemTypeAccess, self).setUp() - self.audit_user = UserFactory.create() - self.enrollment = CourseEnrollmentFactory.create(user=self.audit_user, course_id=self.course.id, mode='audit') + + # enroll all users into the all track types course + self.users = {} + for mode_type in self.MODE_TYPES: + self.users[mode_type] = UserFactory.create() + CourseEnrollmentFactory.create( + user=self.users[mode_type], + course_id=self.courses['all_track_types']['course'].id, + mode=mode_type + ) + + # create audit_user for ease of reference + self.audit_user = self.users['audit'] + + # enroll audit and verified users into default course + for mode_type in ['audit', 'verified']: + CourseEnrollmentFactory.create( + user=self.users[mode_type], + course_id=self.course.id, + mode=mode_type + ) + + # enroll audit user into the audit_only course + CourseEnrollmentFactory.create( + user=self.audit_user, + course_id=self.courses['audit_only']['course'].id, + mode='audit' + ) + + @classmethod + def _create_course(cls, run, display_name, modes, component_types): + """ + Helper method to create a course + + Arguments: + run (str): name of course run + display_name (str): display name of course + modes (list of str): list of modes/tracks this course should have + component_types (list of str): list of problem types this course should have + + Returns: + (dict): { + 'course': (CourseDescriptorWithMixins): course definition + 'blocks': (dict) { + 'block_category_1': XBlock representing that block, + 'block_category_2': XBlock representing that block, + .... + } + + """ + course = CourseFactory.create(run=run, display_name=display_name) + + for mode in modes: + CourseModeFactory.create(course_id=course.id, mode_slug=mode) + + with cls.store.bulk_operations(course.id): + blocks_dict = {} + chapter = ItemFactory.create( + parent=course, + display_name='Overview' + ) + blocks_dict['chapter'] = ItemFactory.create( + parent=course, + category='chapter', + display_name='Week 1' + ) + blocks_dict['sequential'] = ItemFactory.create( + parent=chapter, + category='sequential', + display_name='Lesson 1' + ) + blocks_dict['vertical'] = ItemFactory.create( + parent=blocks_dict['sequential'], + category='vertical', + display_name='Lesson 1 Vertical - Unit 1' + ) + + for problem_type in component_types: + block = ItemFactory.create( + parent=blocks_dict['vertical'], + category=problem_type, + display_name=problem_type, + graded=True, + ) + blocks_dict[problem_type] = block + + return { + 'course': course, + 'blocks': blocks_dict, + } @patch("crum.get_current_request") - def assert_block_is_gated(self, block, is_gated, mock_get_current_request): - ''' + def _assert_block_is_gated(self, mock_get_current_request, block, is_gated, user_id, course_id): + """ + Asserts that a block in a specific course is gated for a specific user + This functions asserts whether the passed in block is gated by content type gating. This is determined by checking whether the has_access method called the IncorrectPartitionGroupError. This error gets swallowed up and is raised as a 404, which is why we are checking for a 404 being raised. However, the 404 could also be caused by other errors, which is why the actual assertion is checking whether the IncorrectPartitionGroupError was called. - ''' - fake_request = Mock() + + Arguments: + block: some soft of xblock descriptor, must implement .scope_ids.usage_id + is_gated (bool): if True, this user is expected to be gated from this block + user_id (int): id of user, if not set will be set to self.audit_user.id + course_id (CourseLocator): id of course, if not set will be set to self.course.id + """ fake_request = self.factory.get('') mock_get_current_request.return_value = fake_request @@ -134,13 +227,23 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase): wraps=IncorrectPartitionGroupError.__init__) as mock_access_error: if is_gated: with self.assertRaises(Http404): - block = load_single_xblock(fake_request, self.audit_user.id, unicode(self.course.id), - unicode(block.scope_ids.usage_id), course=None) + load_single_xblock( + request=fake_request, + user_id=user_id, + course_id=unicode(course_id), + usage_key_string=unicode(block.scope_ids.usage_id), + course=None + ) # check that has_access raised the IncorrectPartitionGroupError in order to gate the block self.assertTrue(mock_access_error.called) else: - block = load_single_xblock(fake_request, self.audit_user.id, unicode(self.course.id), - unicode(block.scope_ids.usage_id), course=None) + load_single_xblock( + request=fake_request, + user_id=user_id, + course_id=unicode(course_id), + usage_key_string=unicode(block.scope_ids.usage_id), + course=None + ) # check that has_access did not raise the IncorrectPartitionGroupError thereby not gating the block self.assertFalse(mock_access_error.called) @@ -150,16 +253,31 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase): outside sources. This tests that audit users cannot see LTI components with graded content but can see the LTI components which do not have graded content. """ - self.assert_block_is_gated(self.lti_block, True) - self.assert_block_is_gated(self.lti_block_not_scored, False) + self._assert_block_is_gated( + block=self.lti_block, + user_id=self.audit_user.id, + course_id=self.course.id, + is_gated=True + ) + self._assert_block_is_gated( + block=self.lti_block_not_scored, + user_id=self.audit_user.id, + course_id=self.course.id, + is_gated=False + ) @ddt.data( *PROBLEM_TYPES ) def test_audit_fails_access_graded_problems(self, prob_type): - block = self.problem_dict[prob_type] + block = self.blocks_dict[prob_type] is_gated = True - self.assert_block_is_gated(block, is_gated) + self._assert_block_is_gated( + block=block, + user_id=self.audit_user.id, + course_id=self.course.id, + is_gated=is_gated + ) @ddt.data( *GRADED_SCORE_WEIGHT_TEST_CASES @@ -168,4 +286,43 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase): def test_graded_score_weight_values(self, graded, has_score, weight, is_gated): # Verify that graded, has_score and weight must all be true for a component to be gated block = self.graded_score_weight_blocks[(graded, has_score, weight)] - self.assert_block_is_gated(block, is_gated) + self._assert_block_is_gated( + block=block, + user_id=self.audit_user.id, + course_id=self.course.id, + is_gated=is_gated + ) + + @ddt.data( + ('audit', 'problem', 'default', True), + ('verified', 'problem', 'default', False), + ('audit', 'html', 'default', False), + ('verified', 'html', 'default', False), + ('audit', 'problem', 'audit_only', False), + ('audit', 'html', 'audit_only', False), + ('credit', 'problem', 'all_track_types', False), + ('credit', 'html', 'all_track_types', False), + ('honor', 'problem', 'all_track_types', False), + ('honor', 'html', 'all_track_types', False), + ('audit', 'problem', 'all_track_types', True), + ('audit', 'html', 'all_track_types', False), + ('verified', 'problem', 'all_track_types', False), + ('verified', 'html', 'all_track_types', False), + ('professional', 'problem', 'all_track_types', False), + ('professional', 'html', 'all_track_types', False), + ('no-id-professional', 'problem', 'all_track_types', False), + ('no-id-professional', 'html', 'all_track_types', False), + ) + @ddt.unpack + def test_access_based_on_track(self, user_track, component_type, course, is_gated): + """ + If a user is enrolled as an audit user they should not have access to graded problems, unless there is no paid + track option. All paid type tracks should have access to all types of content. + All users should have access to non-problem component types, the 'html' components test that. + """ + self._assert_block_is_gated( + block=self.courses[course]['blocks'][component_type], + user_id=self.users[user_track].id, + course_id=self.courses[course]['course'].id, + is_gated=is_gated + )