class InstructorServiceTests(SharedModuleStoreTestCase): """ Tests for the InstructorService """ shard = 1 @classmethod def setUpClass(cls): super(InstructorServiceTests, cls).setUpClass() cls.course = CourseFactory.create() cls.problem_location = msk_from_problem_urlname( cls.course.id, 'robot-some-problem-urlname' ) cls.other_problem_location = msk_from_problem_urlname( cls.course.id, 'robot-some-other_problem-urlname' ) cls.problem_urlname = unicode(cls.problem_location) cls.other_problem_urlname = unicode(cls.other_problem_location) def setUp(self): super(InstructorServiceTests, self).setUp() self.student = UserFactory() CourseEnrollment.enroll(self.student, self.course.id) self.service = InstructorService() self.module_to_reset = StudentModule.objects.create( student=self.student, course_id=self.course.id, module_state_key=self.problem_location, state=json.dumps({'attempts': 2}), ) @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') def test_reset_student_attempts_delete(self, _mock_signal): """ Test delete student state. """ # make sure the attempt is there self.assertEqual( StudentModule.objects.filter( student=self.module_to_reset.student, course_id=self.course.id, module_state_key=self.module_to_reset.module_state_key, ).count(), 1 ) self.service.delete_student_attempt( self.student.username, unicode(self.course.id), self.problem_urlname, requesting_user=self.student, ) # make sure the module has been deleted self.assertEqual( StudentModule.objects.filter( student=self.module_to_reset.student, course_id=self.course.id, module_state_key=self.module_to_reset.module_state_key, ).count(), 0 ) def test_reset_bad_content_id(self): """ Negative test of trying to reset attempts with bad content_id """ result = self.service.delete_student_attempt( self.student.username, unicode(self.course.id), 'foo/bar/baz', requesting_user=self.student, ) self.assertIsNone(result) def test_reset_bad_user(self): """ Negative test of trying to reset attempts with bad user identifier """ result = self.service.delete_student_attempt( 'bad_student', unicode(self.course.id), 'foo/bar/baz', requesting_user=self.student, ) self.assertIsNone(result) def test_reset_non_existing_attempt(self): """ Negative test of trying to reset attempts with bad user identifier """ result = self.service.delete_student_attempt( self.student.username, unicode(self.course.id), self.other_problem_urlname, requesting_user=self.student, ) self.assertIsNone(result) def test_is_user_staff(self): """ Test to assert that the user is staff or not """ result = self.service.is_course_staff( self.student, unicode(self.course.id) ) self.assertFalse(result) # allow staff access to the student allow_access(self.course, self.student, 'staff') result = self.service.is_course_staff( self.student, unicode(self.course.id) ) self.assertTrue(result) def test_report_suspicious_attempt(self): """ Test to verify that the create_zendesk_ticket() is called """ requester_name = "edx-proctoring" email = "*****@*****.**" subject = "Proctored Exam Review: {review_status}".format(review_status="Suspicious") body = "A proctored exam attempt for {exam_name} in {course_name} by username: {student_username} was " \ "reviewed as {review_status} by the proctored exam review provider." body = body.format( exam_name="test_exam", course_name=self.course.display_name, student_username="******", review_status="Suspicious" ) tags = ["proctoring"] with mock.patch("lms.djangoapps.instructor.services.create_zendesk_ticket") as mock_create_zendesk_ticket: self.service.send_support_notification( course_id=unicode(self.course.id), exam_name="test_exam", student_username="******", review_status="Suspicious" ) mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, body, tags)
class InstructorServiceTests(SharedModuleStoreTestCase): """ Tests for the InstructorService """ @classmethod def setUpClass(cls): super().setUpClass() cls.email = '*****@*****.**' cls.course = CourseFactory.create(proctoring_escalation_email=cls.email) cls.problem_location = msk_from_problem_urlname( cls.course.id, 'robot-some-problem-urlname' ) cls.other_problem_location = msk_from_problem_urlname( cls.course.id, 'robot-some-other_problem-urlname' ) cls.problem_urlname = str(cls.problem_location) cls.other_problem_urlname = str(cls.other_problem_location) cls.complete_error_prefix = ('Error occurred while attempting to complete student attempt for ' 'user {user} for content_id {content_id}. ') def setUp(self): super().setUp() self.student = UserFactory() CourseEnrollment.enroll(self.student, self.course.id) self.service = InstructorService() self.module_to_reset = StudentModule.objects.create( student=self.student, course_id=self.course.id, module_state_key=self.problem_location, state=json.dumps({'attempts': 2}), ) @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') def test_reset_student_attempts_delete(self, _mock_signal): """ Test delete student state. """ # make sure the attempt is there assert StudentModule.objects.filter(student=self.module_to_reset.student, course_id=self.course.id, module_state_key=self.module_to_reset.module_state_key).count() == 1 self.service.delete_student_attempt( self.student.username, str(self.course.id), self.problem_urlname, requesting_user=self.student, ) # make sure the module has been deleted assert StudentModule.objects.filter(student=self.module_to_reset.student, course_id=self.course.id, module_state_key=self.module_to_reset.module_state_key).count() == 0 def test_reset_bad_content_id(self): """ Negative test of trying to reset attempts with bad content_id """ result = self.service.delete_student_attempt( # lint-amnesty, pylint: disable=assignment-from-none self.student.username, str(self.course.id), 'foo/bar/baz', requesting_user=self.student, ) assert result is None def test_reset_bad_user(self): """ Negative test of trying to reset attempts with bad user identifier """ result = self.service.delete_student_attempt( # lint-amnesty, pylint: disable=assignment-from-none 'bad_student', str(self.course.id), 'foo/bar/baz', requesting_user=self.student, ) assert result is None def test_reset_non_existing_attempt(self): """ Negative test of trying to reset attempts with bad user identifier """ result = self.service.delete_student_attempt( # lint-amnesty, pylint: disable=assignment-from-none self.student.username, str(self.course.id), self.other_problem_urlname, requesting_user=self.student, ) assert result is None @mock.patch('completion.handlers.BlockCompletion.objects.submit_completion') def test_complete_student_attempt_success(self, mock_submit): """ Assert complete_student_attempt correctly publishes completion for all completable children of the given content_id """ # Section, subsection, and unit are all aggregators and not completable so should # not be submitted. section = ItemFactory.create(parent=self.course, category='chapter') subsection = ItemFactory.create(parent=section, category='sequential') unit = ItemFactory.create(parent=subsection, category='vertical') # should both be submitted video = ItemFactory.create(parent=unit, category='video') problem = ItemFactory.create(parent=unit, category='problem') # Not a completable block ItemFactory.create(parent=unit, category='discussion') with override_waffle_switch(ENABLE_COMPLETION_TRACKING_SWITCH, True): self.service.complete_student_attempt(self.student.username, str(subsection.location)) # Only Completable leaf blocks should have completion published assert mock_submit.call_count == 2 mock_submit.assert_any_call(user=self.student, block_key=video.location, completion=1.0) mock_submit.assert_any_call(user=self.student, block_key=problem.location, completion=1.0) @mock.patch('completion.handlers.BlockCompletion.objects.submit_completion') def test_complete_student_attempt_split_test(self, mock_submit): """ Asserts complete_student_attempt correctly publishes completion when a split test is involved This test case exists because we ran into a bug about the user_service not existing when a split_test existed inside of a subsection. Associated with this change was adding in the user state into the module before attempting completion and this ensures that is working properly. """ partition = UserPartition( 0, 'first_partition', 'First Partition', [ Group(0, 'alpha'), Group(1, 'beta') ] ) course = CourseFactory.create(user_partitions=[partition]) section = ItemFactory.create(parent=course, category='chapter') subsection = ItemFactory.create(parent=section, category='sequential') c0_url = course.id.make_usage_key('vertical', 'split_test_cond0') c1_url = course.id.make_usage_key('vertical', 'split_test_cond1') split_test = ItemFactory.create( parent=subsection, category='split_test', user_partition_id=0, group_id_to_child={'0': c0_url, '1': c1_url}, ) cond0vert = ItemFactory.create(parent=split_test, category='vertical', location=c0_url) ItemFactory.create(parent=cond0vert, category='video') ItemFactory.create(parent=cond0vert, category='problem') cond1vert = ItemFactory.create(parent=split_test, category='vertical', location=c1_url) ItemFactory.create(parent=cond1vert, category='video') ItemFactory.create(parent=cond1vert, category='html') with override_waffle_switch(ENABLE_COMPLETION_TRACKING_SWITCH, True): self.service.complete_student_attempt(self.student.username, str(subsection.location)) # Only the group the user was assigned to should have completion published. # Either cond0vert's children or cond1vert's children assert mock_submit.call_count == 2 @mock.patch('lms.djangoapps.instructor.tasks.log.error') def test_complete_student_attempt_bad_user(self, mock_logger): """ Assert complete_student_attempt with a bad user raises error and returns None """ username = '******' self.service.complete_student_attempt(username, self.problem_urlname) mock_logger.assert_called_once_with( self.complete_error_prefix.format(user=username, content_id=self.problem_urlname) + 'User does not exist!' ) @mock.patch('lms.djangoapps.instructor.tasks.log.error') def test_complete_student_attempt_bad_content_id(self, mock_logger): """ Assert complete_student_attempt with a bad content_id raises error and returns None """ username = self.student.username self.service.complete_student_attempt(username, 'foo/bar/baz') mock_logger.assert_called_once_with( self.complete_error_prefix.format(user=username, content_id='foo/bar/baz') + 'Invalid content_id!' ) @mock.patch('lms.djangoapps.instructor.tasks.log.error') def test_complete_student_attempt_nonexisting_item(self, mock_logger): """ Assert complete_student_attempt with nonexisting item in the modulestore raises error and returns None """ username = self.student.username block = self.problem_urlname self.service.complete_student_attempt(username, block) mock_logger.assert_called_once_with( self.complete_error_prefix.format(user=username, content_id=block) + 'Block not found in the modulestore!' ) @mock.patch('lms.djangoapps.instructor.tasks.log.error') def test_complete_student_attempt_failed_module(self, mock_logger): """ Assert complete_student_attempt with failed get_module raises error and returns None """ username = self.student.username with mock.patch('lms.djangoapps.instructor.tasks.get_module_for_descriptor', return_value=None): self.service.complete_student_attempt(username, str(self.course.location)) mock_logger.assert_called_once_with( self.complete_error_prefix.format(user=username, content_id=self.course.location) + 'Module unable to be created from descriptor!' ) def test_is_user_staff(self): """ Test to assert that the user is staff or not """ result = self.service.is_course_staff( self.student, str(self.course.id) ) assert not result # allow staff access to the student allow_access(self.course, self.student, 'staff') result = self.service.is_course_staff( self.student, str(self.course.id) ) assert result def test_report_suspicious_attempt(self): """ Test to verify that the create_zendesk_ticket() is called """ requester_name = "edx-proctoring" email = "*****@*****.**" subject = "Proctored Exam Review: {review_status}".format(review_status="Suspicious") body = "A proctored exam attempt for {exam_name} in {course_name} by username: {student_username} was " \ "reviewed as {review_status} by the proctored exam review provider.\n" \ "Review link: {url}" args = { 'exam_name': 'test_exam', 'student_username': '******', 'url': 'not available', 'course_name': self.course.display_name, 'review_status': 'Suspicious', } expected_body = body.format(**args) tags = ["proctoring"] with mock.patch("lms.djangoapps.instructor.services.create_zendesk_ticket") as mock_create_zendesk_ticket: self.service.send_support_notification( course_id=str(self.course.id), exam_name=args['exam_name'], student_username=args["student_username"], review_status="Suspicious", review_url=None, ) mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, expected_body, tags) # Now check sending a notification with a review link args['url'] = 'http://review/url' with mock.patch("lms.djangoapps.instructor.services.create_zendesk_ticket") as mock_create_zendesk_ticket: self.service.send_support_notification( course_id=str(self.course.id), exam_name=args['exam_name'], student_username=args["student_username"], review_status="Suspicious", review_url=args['url'], ) expected_body = body.format(**args) mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, expected_body, tags) def test_get_proctoring_escalation_email_from_course_key(self): """ Test that it returns the correct proctoring escalation email from a course key object """ email = self.service.get_proctoring_escalation_email(self.course.id) assert email == self.email def test_get_proctoring_escalation_email_from_course_id(self): """ Test that it returns the correct proctoring escalation email from a course id string """ email = self.service.get_proctoring_escalation_email(str(self.course.id)) assert email == self.email def test_get_proctoring_escalation_email_no_course(self): """ Test that it raises an exception if the course is not found """ with pytest.raises(ObjectDoesNotExist): self.service.get_proctoring_escalation_email('a/b/c') def test_get_proctoring_escalation_email_invalid_key(self): """ Test that it raises an exception if the course_key is invalid """ with pytest.raises(InvalidKeyError): self.service.get_proctoring_escalation_email('invalid key')
class InstructorServiceTests(SharedModuleStoreTestCase): """ Tests for the InstructorService """ @classmethod def setUpClass(cls): super(InstructorServiceTests, cls).setUpClass() cls.course = CourseFactory.create() cls.problem_location = msk_from_problem_urlname( cls.course.id, 'robot-some-problem-urlname' ) cls.other_problem_location = msk_from_problem_urlname( cls.course.id, 'robot-some-other_problem-urlname' ) cls.problem_urlname = unicode(cls.problem_location) cls.other_problem_urlname = unicode(cls.other_problem_location) def setUp(self): super(InstructorServiceTests, self).setUp() self.student = UserFactory() CourseEnrollment.enroll(self.student, self.course.id) self.service = InstructorService() self.module_to_reset = StudentModule.objects.create( student=self.student, course_id=self.course.id, module_state_key=self.problem_location, state=json.dumps({'attempts': 2}), ) @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') def test_reset_student_attempts_delete(self, _mock_signal): """ Test delete student state. """ # make sure the attempt is there self.assertEqual( StudentModule.objects.filter( student=self.module_to_reset.student, course_id=self.course.id, module_state_key=self.module_to_reset.module_state_key, ).count(), 1 ) self.service.delete_student_attempt( self.student.username, unicode(self.course.id), self.problem_urlname, requesting_user=self.student, ) # make sure the module has been deleted self.assertEqual( StudentModule.objects.filter( student=self.module_to_reset.student, course_id=self.course.id, module_state_key=self.module_to_reset.module_state_key, ).count(), 0 ) def test_reset_bad_content_id(self): """ Negative test of trying to reset attempts with bad content_id """ result = self.service.delete_student_attempt( self.student.username, unicode(self.course.id), 'foo/bar/baz', requesting_user=self.student, ) self.assertIsNone(result) def test_reset_bad_user(self): """ Negative test of trying to reset attempts with bad user identifier """ result = self.service.delete_student_attempt( 'bad_student', unicode(self.course.id), 'foo/bar/baz', requesting_user=self.student, ) self.assertIsNone(result) def test_reset_non_existing_attempt(self): """ Negative test of trying to reset attempts with bad user identifier """ result = self.service.delete_student_attempt( self.student.username, unicode(self.course.id), self.other_problem_urlname, requesting_user=self.student, ) self.assertIsNone(result) def test_is_user_staff(self): """ Test to assert that the user is staff or not """ result = self.service.is_course_staff( self.student, unicode(self.course.id) ) self.assertFalse(result) # allow staff access to the student allow_access(self.course, self.student, 'staff') result = self.service.is_course_staff( self.student, unicode(self.course.id) ) self.assertTrue(result) def test_report_suspicious_attempt(self): """ Test to verify that the create_zendesk_ticket() is called """ requester_name = "edx-proctoring" email = "*****@*****.**" subject = "Proctored Exam Review: {review_status}".format(review_status="Suspicious") body = "A proctored exam attempt for {exam_name} in {course_name} by username: {student_username} was " \ "reviewed as {review_status} by the proctored exam review provider." body = body.format( exam_name="test_exam", course_name=self.course.display_name, student_username="******", review_status="Suspicious" ) tags = ["proctoring"] with mock.patch("lms.djangoapps.instructor.services.create_zendesk_ticket") as mock_create_zendesk_ticket: self.service.send_support_notification( course_id=unicode(self.course.id), exam_name="test_exam", student_username="******", review_status="Suspicious" ) mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, body, tags)
class InstructorServiceTests(SharedModuleStoreTestCase): """ Tests for the InstructorService """ @classmethod def setUpClass(cls): super().setUpClass() cls.email = '*****@*****.**' cls.course = CourseFactory.create(proctoring_escalation_email=cls.email) cls.problem_location = msk_from_problem_urlname( cls.course.id, 'robot-some-problem-urlname' ) cls.other_problem_location = msk_from_problem_urlname( cls.course.id, 'robot-some-other_problem-urlname' ) cls.problem_urlname = str(cls.problem_location) cls.other_problem_urlname = str(cls.other_problem_location) def setUp(self): super().setUp() self.student = UserFactory() CourseEnrollment.enroll(self.student, self.course.id) self.service = InstructorService() self.module_to_reset = StudentModule.objects.create( student=self.student, course_id=self.course.id, module_state_key=self.problem_location, state=json.dumps({'attempts': 2}), ) @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') def test_reset_student_attempts_delete(self, _mock_signal): """ Test delete student state. """ # make sure the attempt is there assert StudentModule.objects.filter(student=self.module_to_reset.student, course_id=self.course.id, module_state_key=self.module_to_reset.module_state_key).count() == 1 self.service.delete_student_attempt( self.student.username, str(self.course.id), self.problem_urlname, requesting_user=self.student, ) # make sure the module has been deleted assert StudentModule.objects.filter(student=self.module_to_reset.student, course_id=self.course.id, module_state_key=self.module_to_reset.module_state_key).count() == 0 def test_reset_bad_content_id(self): """ Negative test of trying to reset attempts with bad content_id """ result = self.service.delete_student_attempt( # lint-amnesty, pylint: disable=assignment-from-none self.student.username, str(self.course.id), 'foo/bar/baz', requesting_user=self.student, ) assert result is None def test_reset_bad_user(self): """ Negative test of trying to reset attempts with bad user identifier """ result = self.service.delete_student_attempt( # lint-amnesty, pylint: disable=assignment-from-none 'bad_student', str(self.course.id), 'foo/bar/baz', requesting_user=self.student, ) assert result is None def test_reset_non_existing_attempt(self): """ Negative test of trying to reset attempts with bad user identifier """ result = self.service.delete_student_attempt( # lint-amnesty, pylint: disable=assignment-from-none self.student.username, str(self.course.id), self.other_problem_urlname, requesting_user=self.student, ) assert result is None def test_is_user_staff(self): """ Test to assert that the user is staff or not """ result = self.service.is_course_staff( self.student, str(self.course.id) ) assert not result # allow staff access to the student allow_access(self.course, self.student, 'staff') result = self.service.is_course_staff( self.student, str(self.course.id) ) assert result def test_report_suspicious_attempt(self): """ Test to verify that the create_zendesk_ticket() is called """ requester_name = "edx-proctoring" email = "*****@*****.**" subject = "Proctored Exam Review: {review_status}".format(review_status="Suspicious") body = "A proctored exam attempt for {exam_name} in {course_name} by username: {student_username} was " \ "reviewed as {review_status} by the proctored exam review provider.\n" \ "Review link: {url}" args = { 'exam_name': 'test_exam', 'student_username': '******', 'url': 'not available', 'course_name': self.course.display_name, 'review_status': 'Suspicious', } expected_body = body.format(**args) tags = ["proctoring"] with mock.patch("lms.djangoapps.instructor.services.create_zendesk_ticket") as mock_create_zendesk_ticket: self.service.send_support_notification( course_id=str(self.course.id), exam_name=args['exam_name'], student_username=args["student_username"], review_status="Suspicious", review_url=None, ) mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, expected_body, tags) # Now check sending a notification with a review link args['url'] = 'http://review/url' with mock.patch("lms.djangoapps.instructor.services.create_zendesk_ticket") as mock_create_zendesk_ticket: self.service.send_support_notification( course_id=str(self.course.id), exam_name=args['exam_name'], student_username=args["student_username"], review_status="Suspicious", review_url=args['url'], ) expected_body = body.format(**args) mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, expected_body, tags) def test_get_proctoring_escalation_email(self): """ Test that it returns the correct proctoring escalation email """ email = self.service.get_proctoring_escalation_email(str(self.course.id)) assert email == self.email def test_get_proctoring_escalation_email_no_course(self): """ Test that it raises an exception if the course is not found """ with pytest.raises(ObjectDoesNotExist): self.service.get_proctoring_escalation_email('a/b/c') def test_get_proctoring_escalation_email_invalid_key(self): """ Test that it raises an exception if the course_key is invalid """ with pytest.raises(InvalidKeyError): self.service.get_proctoring_escalation_email('invalid key')