Beispiel #1
0
    def test_status_reviewed_by_field(self):
        """
        Test that `reviewed_by` field of Review model is correctly assigned to None or to a User object.
        """
        # no reviewed_by field
        test_payload = self.get_review_payload(ReviewStatus.suspicious)
        # submit a Suspicious review payload
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        review = ProctoredExamSoftwareSecureReview.objects.get(
            attempt_code=self.attempt['attempt_code'])
        self.assertIsNone(review.reviewed_by)

        # reviewed_by field with no corresponding User object
        reviewed_by_email = '*****@*****.**'
        test_payload['reviewed_by'] = reviewed_by_email

        # submit a Suspicious review payload
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)
        review = ProctoredExamSoftwareSecureReview.objects.get(
            attempt_code=self.attempt['attempt_code'])
        self.assertIsNone(review.reviewed_by)

        # reviewed_by field with corresponding User object
        user = User.objects.create(email=reviewed_by_email,
                                   username='******')
        # submit a Suspicious review payload
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)
        review = ProctoredExamSoftwareSecureReview.objects.get(
            attempt_code=self.attempt['attempt_code'])
        self.assertEqual(review.reviewed_by, user)
Beispiel #2
0
    def test_psi_review_callback(self, psi_review_status, review_status,
                                 credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """
        test_payload = json.loads(
            create_test_review_payload(
                attempt_code=self.attempt['attempt_code'],
                external_id=self.attempt['external_id'],
                review_status=psi_review_status))

        self.attempt['proctored_exam']['backend'] = 'software_secure'
        if review_status is None:
            with self.assertRaises(ProctoredExamBadReviewStatus):
                ProctoredExamReviewCallback().make_review(
                    self.attempt, test_payload)
        else:
            ProctoredExamReviewCallback().make_review(self.attempt,
                                                      test_payload)
            # make sure that what we have in the Database matches what we expect
            review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(
                self.attempt['attempt_code'])

            self.assertIsNotNone(review)
            self.assertEqual(review.review_status, review_status)
            self.assertFalse(review.video_url)

            self.assertIsNotNone(review.raw_data)
            self.assertIsNone(review.reviewed_by)

            # now check the comments that were stored
            comments = ProctoredExamSoftwareSecureComment.objects.filter(
                review_id=review.id)

            self.assertEqual(len(comments), 6)

            # check that we got credit requirement set appropriately

            credit_service = get_runtime_service('credit')
            credit_status = credit_service.get_credit_state(
                self.user.id, 'foo/bar/baz')

            self.assertEqual(
                credit_status['credit_requirement_status'][0]['status'],
                credit_requirement_status)

            instructor_service = get_runtime_service('instructor')
            notifications = instructor_service.notifications
            if psi_review_status == SoftwareSecureReviewStatus.suspicious:
                # check to see whether the zendesk ticket was created
                self.assertEqual(len(notifications), 1)
                exam = self.attempt['proctored_exam']
                review_url = 'http://testserver/edx_proctoring/v1/instructor/foo/bar/baz/1?attempt=testexternalid'
                self.assertEqual(notifications,
                                 [(exam['course_id'], exam['exam_name'],
                                   self.attempt['user']['username'],
                                   review.review_status, review_url)])
            else:
                self.assertEqual(len(notifications), 0)
    def test_disallow_review_resubmission(self):
        """
        Tests that an exception is raised if a review report is resubmitted for the same
        attempt
        """
        test_payload = self.get_review_payload(ReviewStatus.passed)

        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        # now call again
        with self.assertRaises(ProctoredExamReviewAlreadyExists):
            ProctoredExamReviewCallback().make_review(self.attempt, test_payload)
Beispiel #4
0
    def test_failure_submission_rejected(self):
        """
        Tests that a submission of a failed test and make sure that we
        don't automatically update the status to failure
        """
        test_payload = self.get_review_payload(ReviewStatus.suspicious)
        allow_rejects = not constants.REQUIRE_FAILURE_SECOND_REVIEWS
        # submit a Suspicious review payload
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        # now look at the attempt and make sure it did not
        # transition to failure on the callback,
        # as we'll need a manual confirmation via Django Admin pages
        attempt = get_exam_attempt_by_id(self.attempt_id)
        self.assertNotEqual(attempt['status'],
                            ProctoredExamStudentAttemptStatus.rejected)

        review = ProctoredExamSoftwareSecureReview.objects.get(
            attempt_code=self.attempt['attempt_code'])

        attempt = get_exam_attempt_by_id(self.attempt_id)

        # if we don't allow rejects to be stored in attempt status
        # then we should expect a 'second_review_required' status
        expected_status = (
            ProctoredExamStudentAttemptStatus.rejected if allow_rejects else
            ProctoredExamStudentAttemptStatus.second_review_required)
        self.assertEqual(attempt['status'], expected_status)
        self.assertEqual(review.review_status,
                         SoftwareSecureReviewStatus.suspicious)
Beispiel #5
0
    def test_review_on_archived_attempt(self):
        """
        Make sure we can process a review report for
        an attempt which has been archived
        """
        test_payload = self.get_review_payload(ReviewStatus.passed)

        # now delete the attempt, which puts it into the archive table
        remove_exam_attempt(self.attempt_id, requesting_user=self.user)

        # now process the report
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        # make sure that what we have in the Database matches what we expect
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(
            self.attempt['attempt_code'])

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status,
                         SoftwareSecureReviewStatus.clean)
        self.assertFalse(review.video_url)

        self.assertIsNotNone(review.raw_data)

        # now check the comments that were stored
        comments = ProctoredExamSoftwareSecureComment.objects.filter(
            review_id=review.id)

        self.assertEqual(len(comments), 3)
    def test_bad_review_status(self):
        """
        Tests that an exception is raised if the review has an invalid status
        """
        test_payload = self.get_review_payload('bogus')

        with self.assertRaises(ProctoredExamBadReviewStatus):
            ProctoredExamReviewCallback().make_review(self.attempt, test_payload)
Beispiel #7
0
    def test_allow_review_resubmission(self):
        """
        Tests that an resubmission is allowed
        """
        test_payload = self.get_review_payload(ReviewStatus.passed)

        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        # make sure history table is empty
        records = ProctoredExamSoftwareSecureReviewHistory.objects.filter(
            attempt_code=self.attempt['attempt_code'])
        self.assertEqual(len(records), 0)

        # now call again, this will not throw exception
        test_payload['status'] = ReviewStatus.suspicious
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        # make sure that what we have in the Database matches what we expect
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(
            self.attempt['attempt_code'])

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status,
                         SoftwareSecureReviewStatus.suspicious)
        self.assertFalse(review.video_url)

        self.assertIsNotNone(review.raw_data)

        # make sure history table is no longer empty
        records = ProctoredExamSoftwareSecureReviewHistory.objects.filter(
            attempt_code=self.attempt['attempt_code'])
        self.assertEqual(len(records), 1)
        self.assertEqual(records[0].review_status, 'Clean')

        # now try to delete the record and make sure it was archived

        review.delete()

        records = ProctoredExamSoftwareSecureReviewHistory.objects.filter(
            attempt_code=self.attempt['attempt_code'])
        self.assertEqual(len(records), 2)
        self.assertEqual(records[0].review_status,
                         SoftwareSecureReviewStatus.clean)
        self.assertEqual(records[1].review_status,
                         SoftwareSecureReviewStatus.suspicious)
    def test_reviewed_by_is_course_or_global_staff(self, logger_mock):
        """
        Test that a "reviewed_by" field of a review that corresponds to a user
        that is not a course staff or global staff causes a warning to be logged.
        Test that no warning is logged if a user is course staff or global staff.
        """
        test_payload = self.get_review_payload()
        reviewed_by_email = '*****@*****.**'
        test_payload['reviewed_by'] = reviewed_by_email

        # reviewed_by field with corresponding User object
        user = User.objects.create(email=reviewed_by_email,
                                   username='******')

        log_format_string = (
            'user=%(user)s does not have the required permissions '
            'to submit a review for attempt_code=%(attempt_code)s.')

        log_format_dictionary = {
            'user': user,
            'attempt_code': self.attempt['attempt_code'],
        }

        with patch('edx_proctoring.views.is_user_course_or_global_staff',
                   return_value=False):
            ProctoredExamReviewCallback().make_review(self.attempt,
                                                      test_payload)

            # using assert_any_call instead of assert_called_with due to logging in analytics emit_event function
            logger_mock.assert_any_call(log_format_string,
                                        log_format_dictionary)

        logger_mock.reset_mock()

        with patch('edx_proctoring.views.is_user_course_or_global_staff',
                   return_value=True):
            ProctoredExamReviewCallback().make_review(self.attempt,
                                                      test_payload)

            # the mock API doesn't have a "assert_not_called_with" method
            # pylint: disable=wrong-assert-type
            self.assertFalse(
                call(log_format_string, log_format_dictionary) in
                logger_mock.call_args_list)
    def test_failure_not_reviewed(self):
        """
        Tests that a review which comes back as "not reviewed"
        transitions to an error state
        """
        test_payload = self.get_review_payload(ReviewStatus.not_reviewed)
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        attempt = get_exam_attempt_by_id(self.attempt_id)
        self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.error)
    def test_clean_status(self):
        """
        Test that defining `passing_statuses` on the backend works
        """
        test_backend = get_backend_provider(name='test')
        with patch.object(test_backend, 'passing_statuses', [SoftwareSecureReviewStatus.clean], create=True):
            test_payload = self.get_review_payload(status=ReviewStatus.violation)
            ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

            attempt = get_exam_attempt_by_id(self.attempt_id)
            self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.second_review_required)
Beispiel #11
0
    def test_update_archived_attempt(self):
        """
        Test calling the interface point with an attempt_code that was archived
        """
        test_payload = self.get_review_payload()

        # now process the report
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        # now look at the attempt and make sure it did not
        # transition to failure on the callback,
        # as we'll need a manual confirmation via Django Admin pages
        attempt = get_exam_attempt_by_id(self.attempt_id)
        self.assertEqual(attempt['status'], 'verified')

        # now delete the attempt, which puts it into the archive table
        remove_exam_attempt(self.attempt_id, requesting_user=self.user)

        review = ProctoredExamSoftwareSecureReview.objects.get(
            attempt_code=self.attempt['attempt_code'])

        # look at the attempt again, since it moved into Archived state
        # then it should still remain unchanged
        archived_attempt = ProctoredExamStudentAttemptHistory.objects.filter(
            attempt_code=self.attempt['attempt_code']).latest('created')

        self.assertEqual(archived_attempt.status, attempt['status'])
        self.assertEqual(review.review_status,
                         SoftwareSecureReviewStatus.clean)

        # now we'll make another review for the archived attempt. It should NOT update the status
        test_payload = self.get_review_payload(ReviewStatus.suspicious)
        self.attempt['is_archived'] = True
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)
        attempt, is_archived = locate_attempt_by_attempt_code(
            self.attempt['attempt_code'])
        self.assertTrue(is_archived)
        self.assertEqual(attempt.status, 'verified')
    def test_onboarding_attempts_no_second_review_necessary(self):
        """
        Test that onboarding exams do not require a manual pass of review before they land in rejected
        """
        exam_creation_params = self.exam_creation_params.copy()
        exam_creation_params.update({
            'is_practice_exam': True,
            'content_id': 'onboarding_content',
        })
        onboarding_exam_id = create_exam(**exam_creation_params)
        onboarding_attempt_id = create_exam_attempt(
            onboarding_exam_id,
            self.user.id,
            taking_as_proctored=True,
        )
        onboarding_attempt = get_exam_attempt_by_id(onboarding_attempt_id)
        test_payload = self.get_review_payload(ReviewStatus.suspicious)
        ProctoredExamReviewCallback().make_review(onboarding_attempt, test_payload)

        onboarding_attempt = get_exam_attempt_by_id(onboarding_attempt_id)
        assert onboarding_attempt['status'] != ProctoredExamStudentAttemptStatus.second_review_required
    def test_review_update_attempt_active_field(self):
        """
        Make sure we update the is_active_attempt field when an attempt is archived
        """
        test_payload = self.get_review_payload(ReviewStatus.passed)
        ProctoredExamReviewCallback().make_review(self.attempt, test_payload)

        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(
            self.attempt['attempt_code'])
        self.assertTrue(review.is_attempt_active)

        # now delete the attempt, which puts it into the archive table
        with mock.patch('edx_proctoring.api.update_attempt_status'
                        ) as mock_update_status:
            remove_exam_attempt(self.attempt_id, requesting_user=self.user)

        # check that the field has been updated
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(
            self.attempt['attempt_code'])
        self.assertFalse(review.is_attempt_active)

        # check that update_attempt_status has not been called, as the attempt has been archived
        mock_update_status.assert_not_called()