Esempio n. 1
0
    def test_proctored_exam_requirements(self):
        """
        Make sure that proctored exams are being registered as requirements
        """

        self.add_credit_course(self.course.id)
        create_exam(
            course_id=unicode(self.course.id),
            content_id='foo',
            exam_name='A Proctored Exam',
            time_limit_mins=10,
            is_proctored=True,
            is_active=True
        )

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)
        on_course_publish(self.course.id)

        # just inspect the proctored exam requirement
        requirements = [
            requirement
            for requirement in get_credit_requirements(self.course.id)
            if requirement['namespace'] == 'proctored_exam'
        ]

        self.assertEqual(len(requirements), 1)
        self.assertEqual(requirements[0]['namespace'], 'proctored_exam')
        self.assertEqual(requirements[0]['name'], 'proctored_exam_id:1')
        self.assertEqual(requirements[0]['display_name'], 'A Proctored Exam')
        self.assertEqual(requirements[0]['criteria'], {})
Esempio n. 2
0
    def test_attempt_with_unicode_characters(self):
        """
        test that the unicode characters are removed from exam names before registering with
        software secure.
        """
        def is_ascii(value):
            """
            returns True if string is ascii and False otherwise.
            """

            try:
                value.encode('ascii')
                return True
            except UnicodeEncodeError:
                return False

        def assert_get_payload_mock_unicode_characters(exam, context):
            """
            Add a mock so we can assert that the _get_payload call removes unicode characters.
            """

            # call into real implementation
            result = get_backend_provider(emphemeral=True)._get_payload(
                exam, context)
            self.assertFalse(isinstance(result['examName'], unicode))
            self.assertTrue(is_ascii(result['examName']))
            self.assertGreater(len(result['examName']), 0)
            return result

        with HTTMock(mock_response_content):

            exam_id = create_exam(
                course_id='foo/bar/baz',
                content_id='content with unicode characters',
                exam_name=u'Klüft skräms inför på fédéral électoral große',
                time_limit_mins=10,
                is_proctored=True)

            # patch the _get_payload method on the backend provider
            with patch.object(get_backend_provider(), '_get_payload',
                              assert_get_payload_mock_unicode_characters):
                attempt_id = create_exam_attempt(exam_id,
                                                 self.user.id,
                                                 taking_as_proctored=True)
                self.assertGreater(attempt_id, 0)

            # now try with an eastern language (Chinese)
            exam_id = create_exam(course_id='foo/bar/baz',
                                  content_id='content with chinese characters',
                                  exam_name=u'到处群魔乱舞',
                                  time_limit_mins=10,
                                  is_proctored=True)

            # patch the _get_payload method on the backend provider
            with patch.object(get_backend_provider(), '_get_payload',
                              assert_get_payload_mock_unicode_characters):
                attempt_id = create_exam_attempt(exam_id,
                                                 self.user.id,
                                                 taking_as_proctored=True)
                self.assertGreater(attempt_id, 0)
Esempio n. 3
0
    def test_proctored_exam_requirements(self):
        """
        Make sure that proctored exams are being registered as requirements
        """

        self.add_credit_course(self.course.id)

        create_exam(
            course_id=six.text_type(self.course.id),
            content_id=six.text_type(self.subsection.location),
            exam_name='A Proctored Exam',
            time_limit_mins=10,
            is_proctored=True,
            is_active=True
        )

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)

        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 2)
        self.assertEqual(requirements[1]['namespace'], 'proctored_exam')
        self.assertEqual(requirements[1]['name'], six.text_type(self.subsection.location))
        self.assertEqual(requirements[1]['display_name'], 'A Proctored Exam')
        self.assertEqual(requirements[1]['criteria'], {})
    def test_unicode_attempt(self):
        """
        Tests to make sure we can handle an attempt when a user's fullname has unicode characters in it
        """

        set_runtime_service("credit", MockCreditService(profile_fullname=u"अआईउऊऋऌ अआईउऊऋऌ"))

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)

        # try unicode exam name, also
        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content_unicode_name",
            exam_name=u"अआईउऊऋऌ अआईउऊऋऌ",
            time_limit_mins=10,
            is_proctored=True,
        )

        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)
Esempio n. 5
0
    def test_unicode_attempt(self):
        """
        Tests to make sure we can handle an attempt when a user's fullname has unicode characters in it
        """

        set_runtime_service('credit', MockCreditService(profile_fullname=u'अआईउऊऋऌ अआईउऊऋऌ'))

        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True,
            backend='software_secure',
        )

        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)

        # try unicode exam name, also
        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content_unicode_name',
            exam_name=u'अआईउऊऋऌ अआईउऊऋऌ',
            time_limit_mins=10,
            is_proctored=True,
            backend='software_secure',
        )

        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)
Esempio n. 6
0
    def test_unicode_attempt(self):
        """
        Tests to make sure we can handle an attempt when a user's fullname has unicode characters in it
        """

        set_runtime_service('credit', MockCreditService(profile_fullname=u'अआईउऊऋऌ अआईउऊऋऌ'))

        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True,
            backend='software_secure',
        )

        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)

        # try unicode exam name, also
        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content_unicode_name',
            exam_name=u'अआईउऊऋऌ अआईउऊऋऌ',
            time_limit_mins=10,
            is_proctored=True,
            backend='software_secure',
        )

        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)
Esempio n. 7
0
    def test_proctored_exam_requirements(self):
        """
        Make sure that proctored exams are being registered as requirements
        """

        self.add_credit_course(self.course.id)

        create_exam(course_id=six.text_type(self.course.id),
                    content_id=six.text_type(self.subsection.location),
                    exam_name='A Proctored Exam',
                    time_limit_mins=10,
                    is_proctored=True,
                    is_active=True)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)

        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 2)
        self.assertEqual(requirements[1]['namespace'], 'proctored_exam')
        self.assertEqual(requirements[1]['name'],
                         six.text_type(self.subsection.location))
        self.assertEqual(requirements[1]['display_name'], 'A Proctored Exam')
        self.assertEqual(requirements[1]['criteria'], {})
Esempio n. 8
0
    def test_proctored_exam_requirements(self):
        """
        Make sure that proctored exams are being registered as requirements
        """

        self.add_credit_course(self.course.id)

        create_exam(
            course_id=str(self.course.id),
            content_id=str(self.subsection.location),
            exam_name='A Proctored Exam',
            time_limit_mins=10,
            is_proctored=True,
            is_active=True
        )

        requirements = get_credit_requirements(self.course.id)
        assert len(requirements) == 0

        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        assert len(requirements) == 2
        assert requirements[1]['namespace'] == 'proctored_exam'
        assert requirements[1]['name'] == str(self.subsection.location)
        assert requirements[1]['display_name'] == 'A Proctored Exam'
        assert requirements[1]['criteria'] == {}
Esempio n. 9
0
    def test_proctored_exam_requirements(self):
        """
        Make sure that proctored exams are being registered as requirements
        """

        self.add_credit_course(self.course.id)
        create_exam(course_id=unicode(self.course.id),
                    content_id='foo',
                    exam_name='A Proctored Exam',
                    time_limit_mins=10,
                    is_proctored=True,
                    is_active=True)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)
        on_course_publish(self.course.id)

        # just inspect the proctored exam requirement
        requirements = [
            requirement
            for requirement in get_credit_requirements(self.course.id)
            if requirement['namespace'] == 'proctored_exam'
        ]

        self.assertEqual(len(requirements), 1)
        self.assertEqual(requirements[0]['namespace'], 'proctored_exam')
        self.assertEqual(requirements[0]['name'], 'proctored_exam_id:1')
        self.assertEqual(requirements[0]['display_name'], 'A Proctored Exam')
        self.assertEqual(requirements[0]['criteria'], {})
Esempio n. 10
0
    def test_proctored_exam_filtering(self):
        """
        Make sure that timed or inactive exams do not end up in the requirements table
        """

        self.add_credit_course(self.course.id)
        create_exam(
            course_id=unicode(self.course.id),
            content_id='foo',
            exam_name='A Proctored Exam',
            time_limit_mins=10,
            is_proctored=False,
            is_active=True
        )

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)

        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 1)

        # make sure we don't have a proctoring requirement
        self.assertFalse([
            requirement
            for requirement in requirements
            if requirement['namespace'] == 'proctored_exam'
        ])

        create_exam(
            course_id=unicode(self.course.id),
            content_id='foo2',
            exam_name='A Proctored Exam',
            time_limit_mins=10,
            is_proctored=True,
            is_active=False
        )

        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 1)

        # make sure we don't have a proctoring requirement
        self.assertFalse([
            requirement
            for requirement in requirements
            if requirement['namespace'] == 'proctored_exam'
        ])
    def test_disallow_review_resubmission(self):
        """
        Tests that an exception is raised if a review report is resubmitted for the same
        attempt
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt["external_id"])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt["attempt_code"], external_id=attempt["external_id"]
        )

        provider.on_review_callback(json.loads(test_payload))

        # now call again
        with self.assertRaises(ProctoredExamReviewAlreadyExists):
            provider.on_review_callback(json.loads(test_payload))
Esempio n. 12
0
    def test_disallow_review_resubmission(self):
        """
        Tests that an exception is raised if a review report is resubmitted for the same
        attempt
        """

        provider = get_backend_provider()

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = create_test_review_payload(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id'])

        provider.on_review_callback(json.loads(test_payload))

        # now call again
        with self.assertRaises(ProctoredExamReviewAlreadyExists):
            provider.on_review_callback(json.loads(test_payload))
    def test_allow_simulated_callbacks(self):
        """
        Verify that the configuration switch to
        not do confirmation of external_id/ssiRecordLocators
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt["external_id"])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt["attempt_code"], external_id="bogus"
        )

        # this should not raise an exception since we have
        # the ALLOW_CALLBACK_SIMULATION override
        provider.on_review_callback(json.loads(test_payload))

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertEqual(attempt["status"], ProctoredExamStudentAttemptStatus.verified)
Esempio n. 14
0
    def test_get_student_exam_attempt_features(self):
        query_features = [
            'email',
            'exam_name',
            'allowed_time_limit_mins',
            'is_sample_attempt',
            'started_at',
            'completed_at',
            'status',
            'Suspicious Count',
            'Suspicious Comments',
            'Rules Violation Count',
            'Rules Violation Comments',
        ]

        proctored_exam_id = create_exam(self.course_key, 'Test Content', 'Test Exam', 1)
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[0].id, '',
            'Test Code 1', True, False, 'ad13'
        )
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[1].id, '',
            'Test Code 2', True, False, 'ad13'
        )
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[2].id, '',
            'Test Code 3', True, False, 'asd'
        )

        proctored_exam_attempts = get_proctored_exam_results(self.course_key, query_features)
        self.assertEqual(len(proctored_exam_attempts), 3)
        for proctored_exam_attempt in proctored_exam_attempts:
            self.assertEqual(set(proctored_exam_attempt.keys()), set(query_features))
Esempio n. 15
0
    def test_missing_attempt_code(self):
        """
        Test that bad attept codes return errors
        """
        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True,
            backend='software_secure',
        )

        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)
            test_payload = create_test_review_payload(attempt_code='bag code',
                                                      external_id='bogus')
            response = self.client.post(
                reverse('edx_proctoring:anonymous.proctoring_review_callback'),
                data=test_payload,
                content_type='application/json')
            self.assertEqual(response.status_code, 400)
        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertEqual(attempt['status'],
                         ProctoredExamStudentAttemptStatus.created)
Esempio n. 16
0
    def test_allow_simulated_callbacks(self):
        """
        Verify that the configuration switch to
        not do confirmation of external_id/ssiRecordLocators
        """
        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True,
            backend='software_secure',
        )

        # this should not raise an exception since we have
        # the ALLOW_CALLBACK_SIMULATION override
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)
            attempt = get_exam_attempt_by_id(attempt_id)
            test_payload = create_test_review_payload(
                attempt_code=attempt['attempt_code'], external_id='bogus')
            response = self.client.post(
                reverse('edx_proctoring:anonymous.proctoring_review_callback'),
                data=test_payload,
                content_type='application/json')
            self.assertEqual(response.status_code, 200)
        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertEqual(attempt['status'],
                         ProctoredExamStudentAttemptStatus.verified)
Esempio n. 17
0
    def setUp(self):
        super(ReviewTests, self).setUp()
        self.dummy_request = RequestFactory().get('/')
        self.exam_creation_params = {
            'course_id': 'foo/bar/baz',
            'content_id': 'content',
            'exam_name': 'Sample Exam',
            'time_limit_mins': 10,
            'is_proctored': True,
            'backend': 'test'
        }
        self.exam_id = create_exam(**self.exam_creation_params)

        self.attempt_id = create_exam_attempt(
            self.exam_id,
            self.user.id,
            taking_as_proctored=True
        )

        self.attempt = get_exam_attempt_by_id(self.attempt_id)
        set_runtime_service('credit', MockCreditService())
        set_runtime_service('instructor', MockInstructorService())
        set_runtime_service('grades', MockGradesService())
        set_runtime_service('certificates', MockCertificateService())
        set_current_request(self.dummy_request)
Esempio n. 18
0
    def test_get_student_exam_attempt_features(self):
        query_features = [
            'user_email',
            'exam_name',
            'allowed_time_limit_mins',
            'is_sample_attempt',
            'started_at',
            'completed_at',
            'status',
        ]

        proctored_exam_id = create_exam(self.course_key, 'Test Content', 'Test Exam', 1)
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[0].id, '', 1,
            'Test Code 1', True, False, 'ad13'
        )
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[1].id, '', 2,
            'Test Code 2', True, False, 'ad13'
        )
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[2].id, '', 3,
            'Test Code 3', True, False, 'asd'
        )

        proctored_exam_attempts = get_proctored_exam_results(self.course_key, query_features)
        self.assertEqual(len(proctored_exam_attempts), 3)
        for proctored_exam_attempt in proctored_exam_attempts:
            self.assertEqual(set(proctored_exam_attempt.keys()), set(query_features))
Esempio n. 19
0
    def test_missing_attempt_code(self):
        """
        Test that bad attept codes return errors
        """
        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True,
            backend='software_secure',
        )

        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)
            test_payload = create_test_review_payload(
                attempt_code='bag code',
                external_id='bogus'
            )
            response = self.client.post(
                reverse('edx_proctoring:anonymous.proctoring_review_callback'),
                data=test_payload,
                content_type='application/json'
            )
            self.assertEqual(response.status_code, 400)
        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.created)
Esempio n. 20
0
    def test_allow_simulated_callbacks(self):
        """
        Verify that the configuration switch to
        not do confirmation of external_id/ssiRecordLocators
        """
        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True,
            backend='software_secure',
        )

        # this should not raise an exception since we have
        # the ALLOW_CALLBACK_SIMULATION override
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
            self.assertIsNotNone(attempt_id)
            attempt = get_exam_attempt_by_id(attempt_id)
            test_payload = create_test_review_payload(
                attempt_code=attempt['attempt_code'],
                external_id='bogus'
            )
            response = self.client.post(
                reverse('edx_proctoring:anonymous.proctoring_review_callback'),
                data=test_payload,
                content_type='application/json'
            )
            self.assertEqual(response.status_code, 200)
        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.verified)
Esempio n. 21
0
    def setUp(self):
        super(ReviewTests, self).setUp()
        self.dummy_request = RequestFactory().get('/')
        self.exam_creation_params = {
            'course_id': 'foo/bar/baz',
            'content_id': 'content',
            'exam_name': 'Sample Exam',
            'time_limit_mins': 10,
            'is_proctored': True,
            'backend': 'test'
        }
        self.exam_id = create_exam(**self.exam_creation_params)

        self.attempt_id = create_exam_attempt(
            self.exam_id,
            self.user.id,
            taking_as_proctored=True
        )

        self.attempt = get_exam_attempt_by_id(self.attempt_id)
        set_runtime_service('credit', MockCreditService())
        set_runtime_service('instructor', MockInstructorService())
        set_runtime_service('grades', MockGradesService())
        set_runtime_service('certificates', MockCertificateService())
        set_current_request(self.dummy_request)
    def test_review_mistmatched_tokens(self):
        """
        Asserts raising of an exception if we get a report for
        an attempt code which has a external_id which does not
        match the report
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt["external_id"])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt["attempt_code"], external_id="bogus"
        )

        with self.assertRaises(ProctoredExamSuspiciousLookup):
            provider.on_review_callback(json.loads(test_payload))
    def test_get_student_exam_attempt_features(self):
        query_features = [
            'email', 'exam_name', 'allowed_time_limit_mins',
            'is_sample_attempt', 'started_at', 'completed_at', 'status',
            'Suspicious Count', 'Suspicious Comments', 'Rules Violation Count',
            'Rules Violation Comments', 'track'
        ]

        proctored_exam_id = create_exam(self.course_key, 'Test Content',
                                        'Test Exam', 1)
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[0].id, 'Test Code 1', True, False,
            'ad13')
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[1].id, 'Test Code 2', True, False,
            'ad13')
        ProctoredExamStudentAttempt.create_exam_attempt(
            proctored_exam_id, self.users[2].id, 'Test Code 3', True, False,
            'asd')

        proctored_exam_attempts = get_proctored_exam_results(
            self.course_key, query_features)
        assert len(proctored_exam_attempts) == 3
        for proctored_exam_attempt in proctored_exam_attempts:
            assert set(proctored_exam_attempt.keys()) == set(query_features)
Esempio n. 24
0
    def test_review_mistmatched_tokens(self):
        """
        Asserts raising of an exception if we get a report for
        an attempt code which has a external_id which does not
        match the report
        """

        provider = get_backend_provider()

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = create_test_review_payload(
            attempt_code=attempt['attempt_code'], external_id='bogus')

        with self.assertRaises(ProctoredExamSuspiciousLookup):
            provider.on_review_callback(json.loads(test_payload))
Esempio n. 25
0
    def test_allow_simulated_callbacks(self):
        """
        Verify that the configuration switch to
        not do confirmation of external_id/ssiRecordLocators
        """

        provider = get_backend_provider()

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = create_test_review_payload(
            attempt_code=attempt['attempt_code'], external_id='bogus')

        # this should not raise an exception since we have
        # the ALLOW_CALLBACK_SIMULATION override
        provider.on_review_callback(json.loads(test_payload))

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertEqual(attempt['status'],
                         ProctoredExamStudentAttemptStatus.verified)
Esempio n. 26
0
    def test_credit_requirement_blocks_ordering(self):
        """
        Test ordering of the proctoring and ICRV blocks are in proper order.
        """

        self.add_credit_course(self.course.id)
        subsection = ItemFactory.create(parent=self.section,
                                        category='sequential',
                                        display_name='Dummy Subsection')
        create_exam(course_id=unicode(self.course.id),
                    content_id=unicode(subsection.location),
                    exam_name='A Proctored Exam',
                    time_limit_mins=10,
                    is_proctored=True,
                    is_active=True)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)
        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 2)
        self.assertEqual(requirements[1]['namespace'], 'proctored_exam')
        self.assertEqual(requirements[1]['name'], unicode(subsection.location))
        self.assertEqual(requirements[1]['display_name'], 'A Proctored Exam')
        self.assertEqual(requirements[1]['criteria'], {})

        # Create multiple ICRV blocks
        start = datetime.now(UTC)
        self.add_icrv_xblock(related_assessment_name="Midterm A",
                             start_date=start)

        start = start - timedelta(days=1)
        self.add_icrv_xblock(related_assessment_name="Midterm B",
                             start_date=start)

        # Primary sort is based on start date
        on_course_publish(self.course.id)
        requirements = get_credit_requirements(self.course.id)
        # grade requirement is added on publish of the requirements
        self.assertEqual(len(requirements), 4)
        # check requirements are added in the desired order
        # 1st Minimum grade then the blocks with start date than other blocks
        self.assertEqual(requirements[0]["display_name"], "Minimum Grade")
        self.assertEqual(requirements[1]["display_name"], "A Proctored Exam")
        self.assertEqual(requirements[2]["display_name"], "Midterm B")
        self.assertEqual(requirements[3]["display_name"], "Midterm A")
    def test_allow_review_resubmission(self):
        """
        Tests that an resubmission is allowed
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt["external_id"])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt["attempt_code"], external_id=attempt["external_id"]
        )

        provider.on_review_callback(json.loads(test_payload))

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

        # now call again, this will not throw exception
        test_payload = test_payload.replace("Clean", "Suspicious")
        provider.on_review_callback(json.loads(test_payload))

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

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, "Suspicious")
        self.assertEqual(
            review.video_url, "http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo"
        )
        self.assertIsNotNone(review.raw_data)

        # make sure history table is no longer empty
        records = ProctoredExamSoftwareSecureReviewHistory.objects.filter(attempt_code=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=attempt["attempt_code"])
        self.assertEqual(len(records), 2)
        self.assertEqual(records[0].review_status, "Clean")
        self.assertEqual(records[1].review_status, "Suspicious")
Esempio n. 28
0
    def test_attempt_with_no_review_policy(self):
        """
        Create an unstarted proctoring attempt with no review policy associated with it.
        """

        test_self = self        # So that we can access test methods in nested function.

        def assert_get_payload_mock_no_policy(self, exam, context):
            """
            Add a mock shim so we can assert that the _get_payload has been called with the right
            review policy
            """
            assert_get_payload_mock_no_policy.called = True

            test_self.assertNotIn('review_policy', context)

            # call into real implementation
            # pylint: disable=too-many-function-args
            result = software_secure_get_payload(self, exam, context)

            # assert that we use the default that is defined in system configuration
            test_self.assertEqual(result['reviewerNotes'], constants.DEFAULT_SOFTWARE_SECURE_REVIEW_POLICY)

            # the check that if a colon was passed in for the exam name, then the colon was changed to
            # a dash. This is because SoftwareSecure cannot handle a colon in the exam name
            for illegal_char in SOFTWARE_SECURE_INVALID_CHARS:
                if illegal_char in exam['exam_name']:
                    test_self.assertNotIn(illegal_char, result['examName'])
                    test_self.assertIn('_', result['examName'])

            return result

        for illegal_char in SOFTWARE_SECURE_INVALID_CHARS:
            exam_id = create_exam(
                course_id='foo/bar/baz',
                content_id='content with {}'.format(illegal_char),
                exam_name='Sample Exam with {} character'.format(illegal_char),
                time_limit_mins=10,
                is_proctored=True,
                backend='software_secure',
            )

            with HTTMock(mock_response_content):
                # patch the _get_payload method on the backend provider
                # so that we can assert that we are called with the review policy
                # undefined and that we use the system default
                with patch.object(SoftwareSecureBackendProvider, '_get_payload', assert_get_payload_mock_no_policy):
                    assert_get_payload_mock_no_policy.called = False
                    attempt_id = create_exam_attempt(
                        exam_id,
                        self.user.id,
                        taking_as_proctored=True
                    )
                    self.assertGreater(attempt_id, 0)

                    # make sure we recorded that there is no review policy
                    attempt = get_exam_attempt_by_id(attempt_id)
                    self.assertIsNone(attempt['review_policy_id'])
                    self.assertTrue(assert_get_payload_mock_no_policy.called)
    def test_review_callback(self, review_status, credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(
                exam_id,
                self.user.id,
                taking_as_proctored=True
            )

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id']
        )
        test_payload = test_payload.replace('Clean', review_status)

        provider.on_review_callback(json.loads(test_payload))

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

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, review_status)
        self.assertEqual(
            review.video_url,
            'http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo'
        )
        self.assertIsNotNone(review.raw_data)

        # 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
        )
Esempio n. 30
0
    def test_attempt_with_no_review_policy(self):
        """
        Create an unstarted proctoring attempt with no review policy associated with it.
        """

        test_self = self        # So that we can access test methods in nested function.

        def assert_get_payload_mock_no_policy(self, exam, context):
            """
            Add a mock shim so we can assert that the _get_payload has been called with the right
            review policy
            """
            assert_get_payload_mock_no_policy.called = True

            test_self.assertNotIn('review_policy', context)

            # call into real implementation
            # pylint: disable=too-many-function-args
            result = software_secure_get_payload(self, exam, context)

            # assert that we use the default that is defined in system configuration
            test_self.assertEqual(result['reviewerNotes'], constants.DEFAULT_SOFTWARE_SECURE_REVIEW_POLICY)

            # the check that if a colon was passed in for the exam name, then the colon was changed to
            # a dash. This is because SoftwareSecure cannot handle a colon in the exam name
            for illegal_char in SOFTWARE_SECURE_INVALID_CHARS:
                if illegal_char in exam['exam_name']:
                    test_self.assertNotIn(illegal_char, result['examName'])
                    test_self.assertIn('_', result['examName'])

            return result

        for illegal_char in SOFTWARE_SECURE_INVALID_CHARS:
            exam_id = create_exam(
                course_id='foo/bar/baz',
                content_id='content with {}'.format(illegal_char),
                exam_name='Sample Exam with {} character'.format(illegal_char),
                time_limit_mins=10,
                is_proctored=True,
                backend='software_secure',
            )

            with HTTMock(mock_response_content):
                # patch the _get_payload method on the backend provider
                # so that we can assert that we are called with the review policy
                # undefined and that we use the system default
                with patch.object(SoftwareSecureBackendProvider, '_get_payload', assert_get_payload_mock_no_policy):
                    assert_get_payload_mock_no_policy.called = False
                    attempt_id = create_exam_attempt(
                        exam_id,
                        self.user.id,
                        taking_as_proctored=True
                    )
                    self.assertGreater(attempt_id, 0)

                    # make sure we recorded that there is no review policy
                    attempt = get_exam_attempt_by_id(attempt_id)
                    self.assertIsNone(attempt['review_policy_id'])
                    self.assertTrue(assert_get_payload_mock_no_policy.called)
Esempio n. 31
0
 def _create_proctored_exam(self):
     """
     Calls the api's create_exam to create an exam object.
     """
     return create_exam(course_id=self.course_id,
                        content_id=self.content_id,
                        exam_name=self.exam_name,
                        time_limit_mins=self.default_time_limit)
Esempio n. 32
0
    def test_attempt_with_review_policy(self, review_policy_exception):
        """
        Create an unstarted proctoring attempt with a review policy associated with it.
        """

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        if review_policy_exception:
            add_allowance_for_user(
                exam_id, self.user.id,
                ProctoredExamStudentAllowance.REVIEW_POLICY_EXCEPTION,
                review_policy_exception)

        policy = ProctoredExamReviewPolicy.objects.create(
            set_by_user_id=self.user.id,
            proctored_exam_id=exam_id,
            review_policy='Foo Policy')

        def assert_get_payload_mock(exam, context):
            """
            Add a mock shim so we can assert that the _get_payload has been called with the right
            review policy
            """
            self.assertIn('review_policy', context)
            self.assertEqual(policy.review_policy, context['review_policy'])

            # call into real implementation
            result = get_backend_provider(emphemeral=True)._get_payload(
                exam, context)

            # assert that this is in the 'reviewerNotes' field that is passed to SoftwareSecure
            expected = context['review_policy']
            if review_policy_exception:
                expected = '{base}; {exception}'.format(
                    base=expected, exception=review_policy_exception)

            self.assertEqual(result['reviewerNotes'], expected)
            return result

        with HTTMock(mock_response_content):
            # patch the _get_payload method on the backend provider
            # so that we can assert that we are called with the review policy
            # as well as asserting that _get_payload includes that review policy
            # that was passed in
            with patch.object(get_backend_provider(), '_get_payload',
                              assert_get_payload_mock):
                attempt_id = create_exam_attempt(exam_id,
                                                 self.user.id,
                                                 taking_as_proctored=True)
                self.assertGreater(attempt_id, 0)

                # make sure we recorded the policy id at the time this was created
                attempt = get_exam_attempt_by_id(attempt_id)
                self.assertEqual(attempt['review_policy_id'], policy.id)
Esempio n. 33
0
    def test_credit_requirement_blocks_ordering(self):
        """
        Test ordering of the proctoring and ICRV blocks are in proper order.
        """

        self.add_credit_course(self.course.id)
        subsection = ItemFactory.create(parent=self.section, category='sequential', display_name='Dummy Subsection')
        create_exam(
            course_id=unicode(self.course.id),
            content_id=unicode(subsection.location),
            exam_name='A Proctored Exam',
            time_limit_mins=10,
            is_proctored=True,
            is_active=True
        )

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)
        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 2)
        self.assertEqual(requirements[1]['namespace'], 'proctored_exam')
        self.assertEqual(requirements[1]['name'], unicode(subsection.location))
        self.assertEqual(requirements[1]['display_name'], 'A Proctored Exam')
        self.assertEqual(requirements[1]['criteria'], {})

        # Create multiple ICRV blocks
        start = datetime.now(UTC)
        self.add_icrv_xblock(related_assessment_name="Midterm A", start_date=start)

        start = start - timedelta(days=1)
        self.add_icrv_xblock(related_assessment_name="Midterm B", start_date=start)

        # Primary sort is based on start date
        on_course_publish(self.course.id)
        requirements = get_credit_requirements(self.course.id)
        # grade requirement is added on publish of the requirements
        self.assertEqual(len(requirements), 4)
        # check requirements are added in the desired order
        # 1st Minimum grade then the blocks with start date than other blocks
        self.assertEqual(requirements[0]["display_name"], "Minimum Grade")
        self.assertEqual(requirements[1]["display_name"], "A Proctored Exam")
        self.assertEqual(requirements[2]["display_name"], "Midterm B")
        self.assertEqual(requirements[3]["display_name"], "Midterm A")
    def test_failure_submission(self, allow_rejects):
        """
        Tests that a submission of a failed test and make sure that we
        don't automatically update the status to failure
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(
                exam_id,
                self.user.id,
                taking_as_proctored=True
            )

        attempt = get_exam_attempt_by_id(attempt_id)

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id']
        )
        test_payload = test_payload.replace('Clean', 'Suspicious')

        # submit a Suspicious review payload
        provider.on_review_callback(json.loads(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(attempt_id)
        self.assertNotEqual(attempt['status'], ProctoredExamStudentAttemptStatus.rejected)

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

        # now simulate a update via Django Admin table which will actually
        # push through the failure into our attempt status (as well as trigger)
        # other workflow
        provider.on_review_saved(review, allow_rejects=allow_rejects)

        attempt = get_exam_attempt_by_id(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)
Esempio n. 35
0
    def test_review_callback(self, review_status, credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """

        provider = get_backend_provider()

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id'])
        test_payload = test_payload.replace('Clean', review_status)

        provider.on_review_callback(json.loads(test_payload))

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

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, review_status)
        self.assertEqual(
            review.video_url,
            'http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo'
        )
        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)
    def test_update_archived_attempt(self):
        """
        Test calling the on_review_saved interface point with an attempt_code that was archived
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(
                exam_id,
                self.user.id,
                taking_as_proctored=True
            )

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id']
        )

        # now process the report
        provider.on_review_callback(json.loads(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(attempt_id)
        self.assertEqual(attempt['status'], attempt['status'])

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

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

        # now simulate a update via Django Admin table which will actually
        # push through the failure into our attempt status but
        # as this is an archived attempt, we don't do anything
        provider.on_review_saved(review, allow_rejects=True)

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

        self.assertEqual(archived_attempt.status, attempt['status'])
Esempio n. 37
0
    def test_proctored_exam_filtering(self):
        """
        Make sure that timed or inactive exams do not end up in the requirements table
        """

        self.add_credit_course(self.course.id)
        create_exam(course_id=unicode(self.course.id),
                    content_id='foo',
                    exam_name='A Proctored Exam',
                    time_limit_mins=10,
                    is_proctored=False,
                    is_active=True)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)

        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 1)

        # make sure we don't have a proctoring requirement
        self.assertFalse([
            requirement for requirement in requirements
            if requirement['namespace'] == 'proctored_exam'
        ])

        create_exam(course_id=unicode(self.course.id),
                    content_id='foo2',
                    exam_name='A Proctored Exam',
                    time_limit_mins=10,
                    is_proctored=True,
                    is_active=False)

        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 1)

        # make sure we don't have a proctoring requirement
        self.assertFalse([
            requirement for requirement in requirements
            if requirement['namespace'] == 'proctored_exam'
        ])
Esempio n. 38
0
 def _create_practice_exam(self):
     """
     Calls the api's create_exam to create a practice exam object.
     """
     return create_exam(course_id=self.course_id,
                        content_id=self.content_id_practice,
                        exam_name=self.exam_name,
                        time_limit_mins=self.default_time_limit,
                        is_practice_exam=True,
                        is_proctored=True)
Esempio n. 39
0
 def _create_disabled_exam(self):
     """
     Calls the api's create_exam to create an exam object.
     """
     return create_exam(course_id=self.course_id,
                        is_proctored=False,
                        content_id=self.disabled_content_id,
                        exam_name=self.exam_name,
                        time_limit_mins=self.default_time_limit,
                        is_active=False)
Esempio n. 40
0
 def _create_timed_exam(self):
     """
     Calls the api's create_exam to create an exam object.
     """
     return create_exam(
         course_id=self.course_id,
         content_id=self.content_id_timed,
         exam_name=self.exam_name,
         time_limit_mins=self.default_time_limit,
         is_proctored=False
     )
    def test_review_dashboard(self):
        """
        The exam review dashboard will appear for backends that support the feature
        """
        self.setup_course(True, True)
        response = self.client.get(self.url)
        # the default backend does not support the review dashboard
        self.assertNotIn('Review Dashboard', response.content)

        backend = TestBackendProvider()
        config = apps.get_app_config('edx_proctoring')
        with patch.object(config, 'backends', {'test': backend}):
            create_exam(
                course_id=self.course.id,
                content_id='test_content',
                exam_name='Final Test Exam',
                time_limit_mins=10,
                backend='test',
            )
            response = self.client.get(self.url)
            self.assertIn('Review Dashboard', response.content)
    def test_review_on_archived_attempt(self):
        """
        Make sure we can process a review report for
        an attempt which has been archived
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id='foo/bar/baz',
            content_id='content',
            exam_name='Sample Exam',
            time_limit_mins=10,
            is_proctored=True
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(
                exam_id,
                self.user.id,
                taking_as_proctored=True
            )

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id']
        )

        # now delete the attempt, which puts it into the archive table
        remove_exam_attempt(attempt_id)

        # now process the report
        provider.on_review_callback(json.loads(test_payload))

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

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, 'Clean')
        self.assertEqual(
            review.video_url,
            'http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo'
        )
        self.assertIsNotNone(review.raw_data)

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

        self.assertEqual(len(comments), 6)
Esempio n. 43
0
    def test_failure_submission(self, allow_rejects):
        """
        Tests that a submission of a failed test and make sure that we
        don't automatically update the status to failure
        """

        provider = get_backend_provider()

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)

        test_payload = create_test_review_payload(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id'])
        test_payload = test_payload.replace('Clean', 'Suspicious')

        # submit a Suspicious review payload
        provider.on_review_callback(json.loads(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(attempt_id)
        self.assertNotEqual(attempt['status'],
                            ProctoredExamStudentAttemptStatus.rejected)

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

        # now simulate a update via Django Admin table which will actually
        # push through the failure into our attempt status (as well as trigger)
        # other workflow
        provider.on_review_saved(review, allow_rejects=allow_rejects)

        attempt = get_exam_attempt_by_id(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)
Esempio n. 44
0
    def test_review_dashboard(self):
        """
        The exam review dashboard will appear for backends that support the feature
        """
        self.setup_course(True, True)
        response = self.client.get(self.url)
        # the default backend does not support the review dashboard
        self.assertNotIn('Review Dashboard', response.content)

        backend = TestBackendProvider()
        config = apps.get_app_config('edx_proctoring')
        with patch.object(config, 'backends', {'test': backend}):
            create_exam(
                course_id=self.course.id,
                content_id='test_content',
                exam_name='Final Test Exam',
                time_limit_mins=10,
                backend='test',
            )
            response = self.client.get(self.url)
            self.assertIn('Review Dashboard', response.content)
    def test_attempt_with_review_policy(self, review_policy_exception):
        """
        Create an unstarted proctoring attempt with a review policy associated with it.
        """

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        if review_policy_exception:
            add_allowance_for_user(
                exam_id, self.user.id, ProctoredExamStudentAllowance.REVIEW_POLICY_EXCEPTION, review_policy_exception
            )

        policy = ProctoredExamReviewPolicy.objects.create(
            set_by_user_id=self.user.id, proctored_exam_id=exam_id, review_policy="Foo Policy"
        )

        def assert_get_payload_mock(exam, context):
            """
            Add a mock shim so we can assert that the _get_payload has been called with the right
            review policy
            """
            self.assertIn("review_policy", context)
            self.assertEqual(policy.review_policy, context["review_policy"])

            # call into real implementation
            result = get_backend_provider(emphemeral=True)._get_payload(exam, context)

            # assert that this is in the 'reviewerNotes' field that is passed to SoftwareSecure
            expected = context["review_policy"]
            if review_policy_exception:
                expected = "{base}; {exception}".format(base=expected, exception=review_policy_exception)

            self.assertEqual(result["reviewerNotes"], expected)
            return result

        with HTTMock(mock_response_content):
            # patch the _get_payload method on the backend provider
            # so that we can assert that we are called with the review policy
            # as well as asserting that _get_payload includes that review policy
            # that was passed in
            with patch.object(get_backend_provider(), "_get_payload", assert_get_payload_mock):
                attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
                self.assertGreater(attempt_id, 0)

                # make sure we recorded the policy id at the time this was created
                attempt = get_exam_attempt_by_id(attempt_id)
                self.assertEqual(attempt["review_policy_id"], policy.id)
Esempio n. 46
0
    def test_update_archived_attempt(self):
        """
        Test calling the on_review_saved interface point with an attempt_code that was archived
        """

        provider = get_backend_provider()

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = create_test_review_payload(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id'])

        # now process the report
        provider.on_review_callback(json.loads(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(attempt_id)
        self.assertEqual(attempt['status'], attempt['status'])

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

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

        # now simulate a update via Django Admin table which will actually
        # push through the failure into our attempt status but
        # as this is an archived attempt, we don't do anything
        provider.on_review_saved(review, allow_rejects=True)

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

        self.assertEqual(archived_attempt.status, attempt['status'])
Esempio n. 47
0
 def _create_exam_with_due_time(self, is_proctored=True, is_practice_exam=False, due_date=None):
     """
     Calls the api's create_exam to create an exam object.
     """
     return create_exam(
         course_id=self.course_id,
         content_id=self.content_id_for_exam_with_due_date,
         exam_name=self.exam_name,
         time_limit_mins=self.default_time_limit,
         is_proctored=is_proctored,
         is_practice_exam=is_practice_exam,
         due_date=due_date
     )
Esempio n. 48
0
 def _create_onboarding_exam(self):
     """
     Create an onboarding exam
     """
     return create_exam(
         course_id=self.course_id,
         content_id=self.content_id_onboarding,
         exam_name=self.exam_name,
         time_limit_mins=self.default_time_limit,
         is_practice_exam=True,
         is_proctored=True,
         backend='test',
     )
Esempio n. 49
0
 def _create_onboarding_exam(self):
     """
     Create an onboarding exam
     """
     return create_exam(
         course_id=self.course_id,
         content_id=self.content_id_onboarding,
         exam_name=self.exam_name,
         time_limit_mins=self.default_time_limit,
         is_practice_exam=True,
         is_proctored=True,
         backend='test',
     )
Esempio n. 50
0
 def _create_exam_with_due_time(self, is_proctored=True, is_practice_exam=False, due_date=None):
     """
     Calls the api's create_exam to create an exam object.
     """
     return create_exam(
         course_id=self.course_id,
         content_id=self.content_id_for_exam_with_due_date,
         exam_name=self.exam_name,
         time_limit_mins=self.default_time_limit,
         is_proctored=is_proctored,
         is_practice_exam=is_practice_exam,
         due_date=due_date
     )
Esempio n. 51
0
 def _create_practice_exam(self):
     """
     Calls the api's create_exam to create a practice exam object.
     """
     return create_exam(
         course_id=self.course_id,
         content_id=self.content_id_practice,
         exam_name=self.exam_name,
         time_limit_mins=self.default_time_limit,
         is_practice_exam=True,
         is_proctored=True,
         backend='null',
     )
    def test_attempt_with_no_review_policy(self):
        """
        Create an unstarted proctoring attempt with no review policy associated with it.
        """

        def assert_get_payload_mock_no_policy(exam, context):
            """
            Add a mock shim so we can assert that the _get_payload has been called with the right
            review policy
            """
            self.assertNotIn("review_policy", context)

            # call into real implementation
            result = get_backend_provider(emphemeral=True)._get_payload(
                exam, context
            )  # pylint: disable=protected-access

            # assert that we use the default that is defined in system configuration
            self.assertEqual(result["reviewerNotes"], constants.DEFAULT_SOFTWARE_SECURE_REVIEW_POLICY)

            # the check that if a colon was passed in for the exam name, then the colon was changed to
            # a dash. This is because SoftwareSecure cannot handle a colon in the exam name
            for illegal_char in SOFTWARE_SECURE_INVALID_CHARS:
                if illegal_char in exam["exam_name"]:
                    self.assertNotIn(illegal_char, result["examName"])
                    self.assertIn("_", result["examName"])

            return result

        for illegal_char in SOFTWARE_SECURE_INVALID_CHARS:
            exam_id = create_exam(
                course_id="foo/bar/baz",
                content_id="content with {}".format(illegal_char),
                exam_name="Sample Exam with {} character".format(illegal_char),
                time_limit_mins=10,
                is_proctored=True,
            )

            with HTTMock(mock_response_content):
                # patch the _get_payload method on the backend provider
                # so that we can assert that we are called with the review policy
                # undefined and that we use the system default
                with patch.object(
                    get_backend_provider(), "_get_payload", assert_get_payload_mock_no_policy
                ):  # pylint: disable=protected-access
                    attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
                    self.assertGreater(attempt_id, 0)

                    # make sure we recorded that there is no review policy
                    attempt = get_exam_attempt_by_id(attempt_id)
                    self.assertIsNone(attempt["review_policy_id"])
Esempio n. 53
0
    def test_review_on_archived_attempt(self):
        """
        Make sure we can process a review report for
        an attempt which has been archived
        """

        provider = get_backend_provider()

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id,
                                             self.user.id,
                                             taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt['external_id'])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt['attempt_code'],
            external_id=attempt['external_id'])

        # now delete the attempt, which puts it into the archive table
        remove_exam_attempt(attempt_id)

        # now process the report
        provider.on_review_callback(json.loads(test_payload))

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

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, 'Clean')
        self.assertEqual(
            review.video_url,
            'http://www.remoteproctor.com/AdminSite/Account/Reviewer/DirectLink-Generic.aspx?ID=foo'
        )
        self.assertIsNotNone(review.raw_data)

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

        self.assertEqual(len(comments), 6)
    def test_review_callback(self, review_status, credit_requirement_status):
        """
        Simulates callbacks from SoftwareSecure with various statuses
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt["external_id"])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt["attempt_code"], external_id=attempt["external_id"]
        )
        test_payload = test_payload.replace("Clean", review_status)

        provider.on_review_callback(json.loads(test_payload))

        # make sure that what we have in the Database matches what we expect
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(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)
Esempio n. 55
0
    def test_credit_requirement_blocks_ordering(self):
        """
        Test ordering of proctoring blocks.
        """

        self.add_credit_course(self.course.id)
        subsection = ItemFactory.create(parent=self.section,
                                        category='sequential',
                                        display_name='Dummy Subsection')
        create_exam(course_id=six.text_type(self.course.id),
                    content_id=six.text_type(subsection.location),
                    exam_name='A Proctored Exam',
                    time_limit_mins=10,
                    is_proctored=True,
                    is_active=True)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)
        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 2)
        self.assertEqual(requirements[1]['namespace'], 'proctored_exam')
        self.assertEqual(requirements[1]['name'],
                         six.text_type(subsection.location))
        self.assertEqual(requirements[1]['display_name'], 'A Proctored Exam')
        self.assertEqual(requirements[1]['criteria'], {})

        # Primary sort is based on start date
        on_course_publish(self.course.id)
        requirements = get_credit_requirements(self.course.id)
        # grade requirement is added on publish of the requirements
        self.assertEqual(len(requirements), 2)
        # check requirements are added in the desired order
        # 1st Minimum grade then the blocks with start date than other blocks
        self.assertEqual(requirements[0]["display_name"], "Minimum Grade")
        self.assertEqual(requirements[1]["display_name"], "A Proctored Exam")
Esempio n. 56
0
    def test_credit_requirement_blocks_ordering(self):
        """
        Test ordering of proctoring blocks.
        """

        self.add_credit_course(self.course.id)
        subsection = ItemFactory.create(parent=self.section, category='sequential', display_name='Dummy Subsection')
        create_exam(
            course_id=six.text_type(self.course.id),
            content_id=six.text_type(subsection.location),
            exam_name='A Proctored Exam',
            time_limit_mins=10,
            is_proctored=True,
            is_active=True
        )

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 0)
        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        self.assertEqual(len(requirements), 2)
        self.assertEqual(requirements[1]['namespace'], 'proctored_exam')
        self.assertEqual(requirements[1]['name'], six.text_type(subsection.location))
        self.assertEqual(requirements[1]['display_name'], 'A Proctored Exam')
        self.assertEqual(requirements[1]['criteria'], {})

        # Primary sort is based on start date
        on_course_publish(self.course.id)
        requirements = get_credit_requirements(self.course.id)
        # grade requirement is added on publish of the requirements
        self.assertEqual(len(requirements), 2)
        # check requirements are added in the desired order
        # 1st Minimum grade then the blocks with start date than other blocks
        self.assertEqual(requirements[0]["display_name"], "Minimum Grade")
        self.assertEqual(requirements[1]["display_name"], "A Proctored Exam")
Esempio n. 57
0
    def test_credit_requirement_blocks_ordering(self):
        """
        Test ordering of proctoring blocks.
        """

        self.add_credit_course(self.course.id)
        subsection = ItemFactory.create(parent=self.section, category='sequential', display_name='Dummy Subsection')
        create_exam(
            course_id=str(self.course.id),
            content_id=str(subsection.location),
            exam_name='A Proctored Exam',
            time_limit_mins=10,
            is_proctored=True,
            is_active=True
        )

        requirements = get_credit_requirements(self.course.id)
        assert len(requirements) == 0
        on_course_publish(self.course.id)

        requirements = get_credit_requirements(self.course.id)
        assert len(requirements) == 2
        assert requirements[1]['namespace'] == 'proctored_exam'
        assert requirements[1]['name'] == str(subsection.location)
        assert requirements[1]['display_name'] == 'A Proctored Exam'
        assert requirements[1]['criteria'] == {}

        # Primary sort is based on start date
        on_course_publish(self.course.id)
        requirements = get_credit_requirements(self.course.id)
        # grade requirement is added on publish of the requirements
        assert len(requirements) == 2
        # check requirements are added in the desired order
        # 1st Minimum grade then the blocks with start date than other blocks
        assert requirements[0]['display_name'] == 'Minimum Grade'
        assert requirements[1]['display_name'] == 'A Proctored Exam'
    def test_review_on_archived_attempt(self):
        """
        Make sure we can process a review report for
        an attempt which has been archived
        """

        provider = get_backend_provider()

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        # be sure to use the mocked out SoftwareSecure handlers
        with HTTMock(mock_response_content):
            attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)

        attempt = get_exam_attempt_by_id(attempt_id)
        self.assertIsNotNone(attempt["external_id"])

        test_payload = Template(TEST_REVIEW_PAYLOAD).substitute(
            attempt_code=attempt["attempt_code"], external_id=attempt["external_id"]
        )

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

        # now process the report
        provider.on_review_callback(json.loads(test_payload))

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

        self.assertIsNotNone(review)
        self.assertEqual(review.review_status, "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), 6)
Esempio n. 59
0
    def test_failing_register_attempt(self):
        """
        Makes sure we can register an attempt
        """

        exam_id = create_exam(course_id='foo/bar/baz',
                              content_id='content',
                              exam_name='Sample Exam',
                              time_limit_mins=10,
                              is_proctored=True)

        # now try a failing request
        with HTTMock(mock_response_error):
            with self.assertRaises(BackendProvideCannotRegisterAttempt):
                create_exam_attempt(exam_id,
                                    self.user.id,
                                    taking_as_proctored=True)
    def test_failing_register_attempt(self):
        """
        Makes sure we can register an attempt
        """

        exam_id = create_exam(
            course_id="foo/bar/baz",
            content_id="content",
            exam_name="Sample Exam",
            time_limit_mins=10,
            is_proctored=True,
        )

        # now try a failing request
        with HTTMock(mock_response_error):
            with self.assertRaises(BackendProvideCannotRegisterAttempt):
                create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)