Exemple #1
0
 def test_create_exam_with_defaults(self):
     provider = BaseRestProctoringProvider(
         default_rules={'allow_grok': True})
     responses.add(responses.POST,
                   url=self.provider.create_exam_url,
                   json={'id': 'abcdefg'})
     self.backend_exam.pop('external_id')
     external_id = provider.on_exam_saved(self.backend_exam)
     request = json.loads(responses.calls[-1].request.body.decode('utf-8'))
     self.assertEqual(external_id, 'abcdefg')
     self.assertTrue(request['rules']['allow_grok'])
Exemple #2
0
 def test_create_exam_with_defaults(self):
     provider = BaseRestProctoringProvider(default_rules={'allow_grok': True})
     responses.add(
         responses.POST,
         url=self.provider.create_exam_url,
         json={'id': 'abcdefg'}
     )
     self.backend_exam.pop('external_id')
     external_id = provider.on_exam_saved(self.backend_exam)
     request = json.loads(responses.calls[1].request.body)
     self.assertEqual(external_id, 'abcdefg')
     self.assertTrue(request['rules']['allow_grok'])
Exemple #3
0
class RESTBackendTests(TestCase):
    """
    Tests for the REST backend
    """
    def setUp(self):  # pylint: disable=super-method-not-called
        "setup tests"
        BaseRestProctoringProvider.base_url = 'http://rr.fake'
        self.provider = BaseRestProctoringProvider('client_id',
                                                   'client_secret')
        responses.add(responses.POST,
                      url=self.provider.base_url + '/oauth2/access_token',
                      json={
                          'access_token': 'abcd',
                          'expires_in': 600
                      },
                      status=200)
        self.backend_exam = {
            'course_id': 'course123',
            'id': 1,
            'external_id': 'abcdefg',
            'name': 'my exam',
            'is_active': True,
            'is_proctored': True,
            'is_practice': False,
        }
        self.register_exam_context = {
            'attempt_code': '2',
            'obs_user_id': 'abcdefghij',
            'full_name': 'user name',
            'lms_host': 'http://lms.com'
        }

    @responses.activate
    def test_get_software_download_url(self):
        """
        Makes sure we get the expected download url
        """
        responses.add(
            responses.GET,
            url=self.provider.config_url,
            json={'download_url': 'http://example.com'},
            status=200,
            content_type='application/json',
        )
        self.assertEqual(self.provider.get_software_download_url(),
                         'http://example.com')

    @responses.activate
    def test_get_proctoring_config(self):
        responses.add(
            responses.GET,
            url=self.provider.config_url,
            json={
                'download_url': 'http://example.com',
                'config': {
                    'allow_test': 'Allow Testing'
                }
            },
            status=200,
            content_type='application/json',
        )
        config = self.provider.get_proctoring_config()
        self.assertEqual(config['config']['allow_test'], 'Allow Testing')

    @responses.activate
    def test_get_exam(self):
        responses.add(
            responses.GET,
            url=self.provider.exam_url.format(exam_id=self.backend_exam['id']),
            json=self.backend_exam)
        external_exam = self.provider.get_exam(self.backend_exam)
        self.assertEqual(external_exam, self.backend_exam)

    @responses.activate
    def test_get_attempt(self):
        attempt = {
            'id': 1,
            'external_id': 'abcd',
            'proctored_exam': self.backend_exam,
            'user': 1,
            'instructions': [],
        }
        responses.add(responses.GET,
                      url=self.provider.exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id'],
                          attempt_id=attempt['external_id']),
                      json=attempt)
        external_attempt = self.provider.get_attempt(attempt)
        self.assertEqual(external_attempt, attempt)
        self.assertEqual(
            responses.calls[-1].request.headers['Accept-Language'], 'en-us')

    @responses.activate
    def test_get_attempt_i18n(self):
        attempt = {
            'id': 1,
            'external_id': 'abcd',
            'proctored_exam': self.backend_exam,
            'user': 1,
            'instructions': []
        }
        responses.add(responses.GET,
                      url=self.provider.exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id'],
                          attempt_id=attempt['external_id']),
                      json=attempt)
        with translation.override('es'):
            external_attempt = self.provider.get_attempt(attempt)
        self.assertEqual(external_attempt, attempt)
        self.assertEqual(
            responses.calls[-1].request.headers['Accept-Language'], 'es;en-us')

    @responses.activate
    def test_on_exam_saved(self):
        responses.add(responses.POST,
                      url=self.provider.create_exam_url,
                      json={'id': 'abcdefg'})
        self.backend_exam.pop('external_id')
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, 'abcdefg')

    @responses.activate
    def test_create_exam_with_defaults(self):
        provider = BaseRestProctoringProvider(
            default_rules={'allow_grok': True})
        responses.add(responses.POST,
                      url=self.provider.create_exam_url,
                      json={'id': 'abcdefg'})
        self.backend_exam.pop('external_id')
        external_id = provider.on_exam_saved(self.backend_exam)
        request = json.loads(responses.calls[-1].request.body.decode('utf-8'))
        self.assertEqual(external_id, 'abcdefg')
        self.assertTrue(request['rules']['allow_grok'])

    @responses.activate
    def test_update_exam(self):
        responses.add(responses.POST,
                      url=self.provider.exam_url.format(
                          exam_id=self.backend_exam['external_id']),
                      json={'id': 'abcdefg'})
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, 'abcdefg')

    @responses.activate
    def test_failed_exam_save(self):
        responses.add(
            responses.POST,
            url=self.provider.exam_url.format(
                exam_id=self.backend_exam['external_id']),
            body='}{bad',
        )
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, None)

    @responses.activate
    def test_bad_exam_save(self):
        self.backend_exam['bad'] = object()
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, None)

    @responses.activate
    def test_register_exam_attempt(self):
        responses.add(responses.POST,
                      url=self.provider.create_exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id']),
                      json={'id': 2},
                      status=200)
        attempt_external_id = self.provider.register_exam_attempt(
            self.backend_exam, self.register_exam_context)
        request = json.loads(responses.calls[-1].request.body.decode('utf-8'))
        self.assertEqual(attempt_external_id, 2)
        self.assertEqual(request['status'], 'created')
        self.assertIn('lms_host', request)
        self.assertIn('full_name', request)

    @responses.activate
    def test_register_exam_attempt_failure(self):
        responses.add(responses.POST,
                      url=self.provider.create_exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id']),
                      json={'error': 'something'},
                      status=400)
        with self.assertRaises(BackendProviderCannotRegisterAttempt):
            self.provider.register_exam_attempt(self.backend_exam,
                                                self.register_exam_context)

    @ddt.data(*ProctoredExamStudentAttemptStatus.onboarding_errors)
    @responses.activate
    def test_attempt_failure_onboarding(self, failure_status):
        responses.add(responses.POST,
                      url=self.provider.create_exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id']),
                      json={'status': failure_status},
                      status=200)
        with self.assertRaises(
                BackendProviderOnboardingException) as exc_manager:
            self.provider.register_exam_attempt(self.backend_exam,
                                                self.register_exam_context)
        exception = exc_manager.exception
        assert exception.status == failure_status

    @responses.activate
    def test_attempt_no_id_returned(self):
        responses.add(responses.POST,
                      url=self.provider.create_exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id']),
                      json={'error': 'something'},
                      status=200)
        with self.assertRaises(BackendProviderSentNoAttemptID):
            self.provider.register_exam_attempt(self.backend_exam,
                                                self.register_exam_context)

    @ddt.data(
        ['start_exam_attempt', 'start'],
        ['stop_exam_attempt', 'stop'],
        ['mark_erroneous_exam_attempt', 'error'],
    )
    @ddt.unpack
    @responses.activate
    def test_update_exam_attempt_status(self, provider_method_name,
                                        corresponding_status):
        attempt_id = 2
        responses.add(responses.PATCH,
                      url=self.provider.exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id'],
                          attempt_id=attempt_id),
                      json={
                          'id': 2,
                          'status': corresponding_status
                      })
        status = getattr(self.provider, provider_method_name)(
            self.backend_exam['external_id'], attempt_id)
        self.assertEqual(status, corresponding_status)

    @responses.activate
    def test_malformed_json_in_response(self):
        attempt_id = 2
        responses.add(responses.PATCH,
                      url=self.provider.exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id'],
                          attempt_id=attempt_id),
                      body='"]')
        status = self.provider.mark_erroneous_exam_attempt(
            self.backend_exam['external_id'], attempt_id)
        # the important thing is that it didn't raise an exception for bad json
        self.assertEqual(status, None)

    @responses.activate
    def test_remove_attempt(self):
        attempt_id = 2
        responses.add(responses.DELETE,
                      url=self.provider.exam_attempt_url.format(
                          exam_id=self.backend_exam['external_id'],
                          attempt_id=attempt_id),
                      json={'status': "deleted"})
        status = self.provider.remove_exam_attempt(
            self.backend_exam['external_id'], attempt_id)
        self.assertTrue(status)

    def test_remove_attempt_no_attempt(self):
        status = self.provider.remove_exam_attempt(
            self.backend_exam['external_id'], None)
        self.assertFalse(status)

    def test_on_review_callback(self):
        """
        on_review_callback should just return the payload
        """
        attempt = {'id': 1, 'external_id': 'abcd', 'user': 1}
        payload = {
            'status': 'verified',
            'comments': [{
                'comment': 'something happened',
                'status': 'ok'
            }]
        }
        new_payload = self.provider.on_review_callback(attempt, payload)
        self.assertEqual(payload, new_payload)

    def test_get_javascript(self):
        self.assertEqual(self.provider.get_javascript(), '')

    @patch('edx_proctoring.backends.rest.get_files')
    def test_get_javascript_bundle(self, get_files_mock):
        get_files_mock.return_value = [{'name': 'rest', 'url': '/there/it/is'}]
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '/there/it/is')

        # test absolute URL
        get_files_mock.return_value = [{
            'name':
            'rest',
            'url':
            'http://www.example.com/there/it/is'
        }]
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, 'http://www.example.com/there/it/is')

    @patch('edx_proctoring.backends.rest.get_files')
    def test_get_javascript_empty_return(self, get_files_mock):
        get_files_mock.return_value = []
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')

    @patch('webpack_loader.loader.WebpackLoader.get_assets')
    def test_get_javascript_swallows_errors(self, mock_get_assets):
        mock_get_assets.return_value = {'status': 'done', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')
        mock_get_assets.return_value = {'status': 'error', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')
        mock_get_assets.return_value = {'status': 'turtle', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')

    @patch('edx_proctoring.backends.rest.get_files')
    def test_get_javascript_returns_absolute_url(self, get_files_mock):
        get_files_mock.return_value = [{'name': 'rest', 'url': '/there/it/is'}]
        javascript_url = self.provider.get_javascript()

        # no LMS_ROOT_URL setting, so return original URL
        self.assertEqual(javascript_url, '/there/it/is')

        with override_settings(LMS_ROOT_URL='http://www.example.com'):
            javascript_url = self.provider.get_javascript()
            self.assertEqual(javascript_url,
                             'http://www.example.com/there/it/is')

    def test_instructor_url(self):
        user = {
            'id': 1,
            'full_name': 'Instructor',
            'email': '*****@*****.**'
        }
        course_id = 'course+abc'
        base_url = self.provider.get_instructor_url(course_id, user)
        self.assertIn('?jwt=', base_url)
        # now try with an exam_id and an attempt_id.
        # the tokens will be different
        exam_id = 'abcd'
        attempt_id = 'defgh'
        exam_url = self.provider.get_instructor_url(course_id,
                                                    user,
                                                    exam_id=exam_id)
        self.assertNotEqual(exam_url, base_url)
        attempt_url = self.provider.get_instructor_url(course_id,
                                                       user,
                                                       exam_id='abcd',
                                                       attempt_id=attempt_id)
        self.assertNotEqual(attempt_url, base_url)
        self.assertNotEqual(attempt_url, exam_url)
        # let's ensure that the last token contains all of the expected data
        token = attempt_url.split('jwt=')[1]
        decoded = jwt.decode(token,
                             issuer=self.provider.client_id,
                             key=self.provider.client_secret,
                             algorithms=['HS256'])
        self.assertEqual(decoded['user'], user)
        self.assertEqual(decoded['course_id'], course_id)
        self.assertEqual(decoded['exam_id'], exam_id)
        self.assertEqual(decoded['attempt_id'], attempt_id)

        # test that correct URL is returned for a request with parameter "show_configuration_dashboard=true"
        config_url = self.provider.get_instructor_url(
            course_id,
            user,
            exam_id=exam_id,
            show_configuration_dashboard=True,
        )
        token = config_url.split('jwt=')[1]
        decoded = jwt.decode(token,
                             issuer=self.provider.client_id,
                             key=self.provider.client_secret,
                             algorithms=['HS256'])
        self.assertTrue(decoded['config'])
        self.assertEqual(decoded['exam_id'], exam_id)

    @responses.activate
    def test_retire_user(self):
        user_id = 'abcdef5'
        responses.add(responses.DELETE,
                      url=self.provider.user_info_url.format(user_id=user_id),
                      json=True)
        result = self.provider.retire_user(user_id)
        assert result is True

    @responses.activate
    def test_retire_unknown_user(self):
        user_id = 'abcdef5'
        responses.add(responses.DELETE,
                      url=self.provider.user_info_url.format(user_id=user_id),
                      json=False)
        result = self.provider.retire_user(user_id)
        assert result is False

    @responses.activate
    def test_retire_error(self):
        user_id = 'abcdef5'
        responses.add(responses.DELETE,
                      url=self.provider.user_info_url.format(user_id=user_id),
                      body='"')
        with self.assertRaises(BackendProviderCannotRetireUser):
            self.provider.retire_user(user_id)
Exemple #4
0
class RESTBackendTests(TestCase):
    """
    Tests for the REST backend
    """
    def setUp(self):
        "setup tests"
        BaseRestProctoringProvider.base_url = 'http://rr.fake'
        self.provider = BaseRestProctoringProvider('client_id', 'client_secret')
        responses.add(
            responses.POST,
            url=self.provider.base_url + '/oauth2/access_token',
            json={'access_token': 'abcd', 'expires_in': 600},
            status=200
        )
        self.backend_exam = {
            'course_id': 'course123',
            'id': 1,
            'external_id': 'abcdefg',
            'name': 'my exam',
            'is_active': True,
            'is_proctored': True,
            'is_practice': False,
        }

    @responses.activate
    def test_get_software_download_url(self):
        """
        Makes sure we get the expected download url
        """
        responses.add(
            responses.GET,
            url=self.provider.config_url,
            json={'download_url': 'http://example.com'},
            status=200,
            content_type='application/json',
        )
        self.assertEqual(self.provider.get_software_download_url(), 'http://example.com')

    @responses.activate
    def test_get_proctoring_config(self):
        responses.add(
            responses.GET,
            url=self.provider.config_url,
            json={'download_url': 'http://example.com', 'config': {'allow_test': 'Allow Testing'}},
            status=200,
            content_type='application/json',
        )
        config = self.provider.get_proctoring_config()
        self.assertEqual(config['config']['allow_test'], 'Allow Testing')

    @responses.activate
    def test_get_exam(self):
        responses.add(
            responses.GET,
            url=self.provider.exam_url.format(exam_id=self.backend_exam['id']),
            json=self.backend_exam
        )
        external_exam = self.provider.get_exam(self.backend_exam)
        self.assertEqual(external_exam, self.backend_exam)

    @responses.activate
    def test_get_attempt(self):
        attempt = {
            'id': 1,
            'external_id': 'abcd',
            'proctored_exam': self.backend_exam,
            'user': 1,
            'instructions': [],
        }
        responses.add(
            responses.GET,
            url=self.provider.exam_attempt_url.format(
                exam_id=self.backend_exam['external_id'], attempt_id=attempt['external_id']),
            json=attempt
        )
        external_attempt = self.provider.get_attempt(attempt)
        self.assertEqual(external_attempt, attempt)
        self.assertEqual(responses.calls[1].request.headers['Accept-Language'], 'en-us')

    @responses.activate
    def test_get_attempt_i18n(self):
        attempt = {
            'id': 1,
            'external_id': 'abcd',
            'proctored_exam': self.backend_exam,
            'user': 1,
            'instructions': []
        }
        responses.add(
            responses.GET,
            url=self.provider.exam_attempt_url.format(
                exam_id=self.backend_exam['external_id'], attempt_id=attempt['external_id']),
            json=attempt
        )
        with translation.override('es'):
            external_attempt = self.provider.get_attempt(attempt)
        self.assertEqual(external_attempt, attempt)
        self.assertEqual(responses.calls[1].request.headers['Accept-Language'], 'es;en-us')

    @responses.activate
    def test_on_exam_saved(self):
        responses.add(
            responses.POST,
            url=self.provider.create_exam_url,
            json={'id': 'abcdefg'}
        )
        self.backend_exam.pop('external_id')
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, 'abcdefg')

    @responses.activate
    def test_create_exam_with_defaults(self):
        provider = BaseRestProctoringProvider(default_rules={'allow_grok': True})
        responses.add(
            responses.POST,
            url=self.provider.create_exam_url,
            json={'id': 'abcdefg'}
        )
        self.backend_exam.pop('external_id')
        external_id = provider.on_exam_saved(self.backend_exam)
        request = json.loads(responses.calls[1].request.body)
        self.assertEqual(external_id, 'abcdefg')
        self.assertTrue(request['rules']['allow_grok'])

    @responses.activate
    def test_update_exam(self):
        responses.add(
            responses.POST,
            url=self.provider.exam_url.format(exam_id=self.backend_exam['external_id']),
            json={'id': 'abcdefg'}
        )
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, 'abcdefg')

    @responses.activate
    def test_failed_exam_save(self):
        responses.add(
            responses.POST,
            url=self.provider.exam_url.format(exam_id=self.backend_exam['external_id']),
            body='}{bad',
        )
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, None)

    @responses.activate
    def test_register_exam_attempt(self):
        context = {
            'attempt_code': '2',
            'obs_user_id': 'abcdefghij',
            'full_name': 'user name',
            'lms_host': 'http://lms.com'
        }
        responses.add(
            responses.POST,
            url=self.provider.create_exam_attempt_url.format(exam_id=self.backend_exam['external_id']),
            json={'id': 2}
        )
        attempt_external_id = self.provider.register_exam_attempt(self.backend_exam, context)
        request = json.loads(responses.calls[1].request.body)
        self.assertEqual(attempt_external_id, 2)
        self.assertEqual(request['status'], 'created')
        self.assertIn('lms_host', request)
        self.assertIn('full_name', request)

    @responses.activate
    def test_register_exam_attempt_failure(self):
        context = {
            'attempt_code': '2',
            'obs_user_id': 'abcdefghij',
            'full_name': 'user name',
            'lms_host': 'http://lms.com'
        }
        responses.add(
            responses.POST,
            url=self.provider.create_exam_attempt_url.format(exam_id=self.backend_exam['external_id']),
            json={'error': 'something'},
            status=400
        )
        with self.assertRaises(BackendProviderCannotRegisterAttempt):
            self.provider.register_exam_attempt(self.backend_exam, context)

    @responses.activate
    def test_start_exam_attempt(self):
        attempt_id = 2
        responses.add(
            responses.PATCH,
            url=self.provider.exam_attempt_url.format(exam_id=self.backend_exam['external_id'], attempt_id=attempt_id),
            json={'id': 2, 'status': 'start'}
        )
        status = self.provider.start_exam_attempt(self.backend_exam['external_id'], attempt_id)
        self.assertEqual(status, 'start')

    @responses.activate
    def test_stop_exam_attempt(self):
        attempt_id = 2
        responses.add(
            responses.PATCH,
            url=self.provider.exam_attempt_url.format(exam_id=self.backend_exam['external_id'], attempt_id=attempt_id),
            json={'id': 2, 'status': 'stop'}
        )
        status = self.provider.stop_exam_attempt(self.backend_exam['external_id'], attempt_id)
        self.assertEqual(status, 'stop')

    @responses.activate
    def test_on_review_callback(self):
        # on_review_callback should just return the payload
        attempt = {
            'id': 1,
            'external_id': 'abcd',
            'user': 1
        }
        payload = {
            'status': 'verified',
            'comments': [
                {'comment': 'something happend', 'status': 'ok'}
            ]
        }
        new_payload = self.provider.on_review_callback(attempt, payload)
        self.assertEqual(payload, new_payload)

    def test_get_javascript(self):
        self.assertEqual(self.provider.get_javascript(), '')

    @patch('edx_proctoring.backends.rest.get_files')
    def test_get_javascript_bundle(self, get_files_mock):
        get_files_mock.return_value = [{'name': 'rest', 'url': '/there/it/is'}]
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '/there/it/is')

    @patch('edx_proctoring.backends.rest.get_files')
    def test_get_javascript_empty_return(self, get_files_mock):
        get_files_mock.return_value = []
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')

    @patch('webpack_loader.loader.WebpackLoader.get_assets')
    def test_get_javascript_swallows_errors(self, mock_get_assets):
        mock_get_assets.return_value = {'status': 'done', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')
        mock_get_assets.return_value = {'status': 'error', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')
        mock_get_assets.return_value = {'status': 'turtle', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')

    def test_instructor_url(self):
        user = {
            'id': 1,
            'full_name': 'Instructor',
            'email': '*****@*****.**'
        }
        course_id = 'course+abc'
        base_url = self.provider.get_instructor_url(course_id, user)
        self.assertIn('?jwt=', base_url)
        # now try with an exam_id and an attempt_id.
        # the tokens will be different
        exam_id = 'abcd'
        attempt_id = 'defgh'
        exam_url = self.provider.get_instructor_url(course_id, user, exam_id=exam_id)
        self.assertNotEqual(exam_url, base_url)
        attempt_url = self.provider.get_instructor_url(course_id, user, exam_id='abcd', attempt_id=attempt_id)
        self.assertNotEqual(attempt_url, base_url)
        self.assertNotEqual(attempt_url, exam_url)
        # let's ensure that the last token contains all of the expected data
        token = attempt_url.split('jwt=')[1]
        decoded = jwt.decode(token,
                             issuer=self.provider.client_id,
                             key=self.provider.client_secret,
                             algorithms=['HS256'])
        self.assertEqual(decoded['user'], user)
        self.assertEqual(decoded['course_id'], course_id)
        self.assertEqual(decoded['exam_id'], exam_id)
        self.assertEqual(decoded['attempt_id'], attempt_id)
Exemple #5
0
class RESTBackendTests(TestCase):
    """
    Tests for the REST backend
    """
    def setUp(self):
        "setup tests"
        BaseRestProctoringProvider.base_url = 'http://rr.fake'
        self.provider = BaseRestProctoringProvider('client_id', 'client_secret')
        responses.add(
            responses.POST,
            url=self.provider.base_url + '/oauth2/access_token',
            json={'access_token': 'abcd', 'expires_in': 600},
            status=200
        )
        self.backend_exam = {
            'course_id': 'course123',
            'id': 1,
            'external_id': 'abcdefg',
            'name': 'my exam',
            'is_active': True,
            'is_proctored': True,
            'is_practice': False,
        }

    @responses.activate
    def test_get_software_download_url(self):
        """
        Makes sure we get the expected download url
        """
        responses.add(
            responses.GET,
            url=self.provider.config_url,
            json={'download_url': 'http://example.com'},
            status=200,
            content_type='application/json',
        )
        self.assertEqual(self.provider.get_software_download_url(), 'http://example.com')

    @responses.activate
    def test_get_proctoring_config(self):
        responses.add(
            responses.GET,
            url=self.provider.config_url,
            json={'download_url': 'http://example.com', 'config': {'allow_test': 'Allow Testing'}},
            status=200,
            content_type='application/json',
        )
        config = self.provider.get_proctoring_config()
        self.assertEqual(config['config']['allow_test'], 'Allow Testing')

    @responses.activate
    def test_get_exam(self):
        responses.add(
            responses.GET,
            url=self.provider.exam_url.format(exam_id=self.backend_exam['id']),
            json=self.backend_exam
        )
        external_exam = self.provider.get_exam(self.backend_exam)
        self.assertEqual(external_exam, self.backend_exam)

    @responses.activate
    def test_get_attempt(self):
        attempt = {
            'id': 1,
            'external_id': 'abcd',
            'proctored_exam': self.backend_exam,
            'user': 1,
            'instructions': [],
        }
        responses.add(
            responses.GET,
            url=self.provider.exam_attempt_url.format(
                exam_id=self.backend_exam['external_id'], attempt_id=attempt['external_id']),
            json=attempt
        )
        external_attempt = self.provider.get_attempt(attempt)
        self.assertEqual(external_attempt, attempt)
        self.assertEqual(responses.calls[1].request.headers['Accept-Language'], 'en-us')

    @responses.activate
    def test_get_attempt_i18n(self):
        attempt = {
            'id': 1,
            'external_id': 'abcd',
            'proctored_exam': self.backend_exam,
            'user': 1,
            'instructions': []
        }
        responses.add(
            responses.GET,
            url=self.provider.exam_attempt_url.format(
                exam_id=self.backend_exam['external_id'], attempt_id=attempt['external_id']),
            json=attempt
        )
        with translation.override('es'):
            external_attempt = self.provider.get_attempt(attempt)
        self.assertEqual(external_attempt, attempt)
        self.assertEqual(responses.calls[1].request.headers['Accept-Language'], 'es;en-us')

    @responses.activate
    def test_on_exam_saved(self):
        responses.add(
            responses.POST,
            url=self.provider.create_exam_url,
            json={'id': 'abcdefg'}
        )
        self.backend_exam.pop('external_id')
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, 'abcdefg')

    @responses.activate
    def test_create_exam_with_defaults(self):
        provider = BaseRestProctoringProvider(default_rules={'allow_grok': True})
        responses.add(
            responses.POST,
            url=self.provider.create_exam_url,
            json={'id': 'abcdefg'}
        )
        self.backend_exam.pop('external_id')
        external_id = provider.on_exam_saved(self.backend_exam)
        request = json.loads(responses.calls[1].request.body)
        self.assertEqual(external_id, 'abcdefg')
        self.assertTrue(request['rules']['allow_grok'])

    @responses.activate
    def test_update_exam(self):
        responses.add(
            responses.POST,
            url=self.provider.exam_url.format(exam_id=self.backend_exam['external_id']),
            json={'id': 'abcdefg'}
        )
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, 'abcdefg')

    @responses.activate
    def test_failed_exam_save(self):
        responses.add(
            responses.POST,
            url=self.provider.exam_url.format(exam_id=self.backend_exam['external_id']),
            body='}{bad',
        )
        external_id = self.provider.on_exam_saved(self.backend_exam)
        self.assertEqual(external_id, None)

    @responses.activate
    def test_register_exam_attempt(self):
        context = {
            'attempt_code': '2',
            'obs_user_id': 'abcdefghij',
            'full_name': 'user name',
            'lms_host': 'http://lms.com'
        }
        responses.add(
            responses.POST,
            url=self.provider.create_exam_attempt_url.format(exam_id=self.backend_exam['external_id']),
            json={'id': 2}
        )
        attempt_external_id = self.provider.register_exam_attempt(self.backend_exam, context)
        request = json.loads(responses.calls[1].request.body)
        self.assertEqual(attempt_external_id, 2)
        self.assertEqual(request['status'], 'created')
        self.assertIn('lms_host', request)
        self.assertIn('full_name', request)

    @responses.activate
    def test_register_exam_attempt_failure(self):
        context = {
            'attempt_code': '2',
            'obs_user_id': 'abcdefghij',
            'full_name': 'user name',
            'lms_host': 'http://lms.com'
        }
        responses.add(
            responses.POST,
            url=self.provider.create_exam_attempt_url.format(exam_id=self.backend_exam['external_id']),
            json={'error': 'something'},
            status=400
        )
        with self.assertRaises(BackendProviderCannotRegisterAttempt):
            self.provider.register_exam_attempt(self.backend_exam, context)

    @ddt.data(
        *ProctoredExamStudentAttemptStatus.onboarding_errors
    )
    @responses.activate
    def test_attempt_failure_onboarding(self, failure_status):
        context = {
            'attempt_code': '2',
            'obs_user_id': 'abcdefghij',
            'full_name': 'user name',
            'lms_host': 'http://lms.com'
        }
        responses.add(
            responses.POST,
            url=self.provider.create_exam_attempt_url.format(exam_id=self.backend_exam['external_id']),
            json={'status': failure_status},
            status=200
        )
        with self.assertRaises(BackendProviderOnboardingException) as exc_manager:
            self.provider.register_exam_attempt(self.backend_exam, context)
        exception = exc_manager.exception
        assert exception.status == failure_status

    @ddt.data(
        ['start_exam_attempt', 'start'],
        ['stop_exam_attempt', 'stop'],
        ['mark_erroneous_exam_attempt', 'error'],
    )
    @ddt.unpack
    @responses.activate
    def test_update_exam_attempt_status(self, provider_method_name, corresponding_status):
        attempt_id = 2
        responses.add(
            responses.PATCH,
            url=self.provider.exam_attempt_url.format(exam_id=self.backend_exam['external_id'], attempt_id=attempt_id),
            json={'id': 2, 'status': corresponding_status}
        )
        status = getattr(self.provider, provider_method_name)(self.backend_exam['external_id'], attempt_id)
        self.assertEqual(status, corresponding_status)

    @responses.activate
    def test_malformed_json_in_response(self):
        attempt_id = 2
        responses.add(
            responses.PATCH,
            url=self.provider.exam_attempt_url.format(exam_id=self.backend_exam['external_id'], attempt_id=attempt_id),
            body='"]'
        )
        status = self.provider.mark_erroneous_exam_attempt(self.backend_exam['external_id'], attempt_id)
        # the important thing is that it didn't raise an exception for bad json
        self.assertEqual(status, None)

    @responses.activate
    def test_remove_attempt(self):
        attempt_id = 2
        responses.add(
            responses.DELETE,
            url=self.provider.exam_attempt_url.format(exam_id=self.backend_exam['external_id'], attempt_id=attempt_id),
            json={'status': "deleted"}
        )
        status = self.provider.remove_exam_attempt(self.backend_exam['external_id'], attempt_id)
        self.assertTrue(status)

    def test_remove_attempt_no_attempt(self):
        status = self.provider.remove_exam_attempt(self.backend_exam['external_id'], None)
        self.assertFalse(status)

    def test_on_review_callback(self):
        """
        on_review_callback should just return the payload
        """
        attempt = {
            'id': 1,
            'external_id': 'abcd',
            'user': 1
        }
        payload = {
            'status': 'verified',
            'comments': [
                {'comment': 'something happened', 'status': 'ok'}
            ]
        }
        new_payload = self.provider.on_review_callback(attempt, payload)
        self.assertEqual(payload, new_payload)

    def test_get_javascript(self):
        self.assertEqual(self.provider.get_javascript(), '')

    @patch('edx_proctoring.backends.rest.get_files')
    def test_get_javascript_bundle(self, get_files_mock):
        get_files_mock.return_value = [{'name': 'rest', 'url': '/there/it/is'}]
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '/there/it/is')

        # test absolute URL
        get_files_mock.return_value = [{'name': 'rest', 'url': 'http://www.example.com/there/it/is'}]
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, 'http://www.example.com/there/it/is')

    @patch('edx_proctoring.backends.rest.get_files')
    def test_get_javascript_empty_return(self, get_files_mock):
        get_files_mock.return_value = []
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')

    @patch('webpack_loader.loader.WebpackLoader.get_assets')
    def test_get_javascript_swallows_errors(self, mock_get_assets):
        mock_get_assets.return_value = {'status': 'done', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')
        mock_get_assets.return_value = {'status': 'error', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')
        mock_get_assets.return_value = {'status': 'turtle', 'chunks': {}}
        javascript_url = self.provider.get_javascript()
        self.assertEqual(javascript_url, '')

    @patch('edx_proctoring.backends.rest.get_files')
    def test_get_javascript_returns_absolute_url(self, get_files_mock):
        get_files_mock.return_value = [{'name': 'rest', 'url': '/there/it/is'}]
        javascript_url = self.provider.get_javascript()

        # no LMS_ROOT_URL setting, so return original URL
        self.assertEqual(javascript_url, '/there/it/is')

        with override_settings(LMS_ROOT_URL='http://www.example.com'):
            javascript_url = self.provider.get_javascript()
            self.assertEqual(javascript_url, 'http://www.example.com/there/it/is')

    def test_instructor_url(self):
        user = {
            'id': 1,
            'full_name': 'Instructor',
            'email': '*****@*****.**'
        }
        course_id = 'course+abc'
        base_url = self.provider.get_instructor_url(course_id, user)
        self.assertIn('?jwt=', base_url)
        # now try with an exam_id and an attempt_id.
        # the tokens will be different
        exam_id = 'abcd'
        attempt_id = 'defgh'
        exam_url = self.provider.get_instructor_url(course_id, user, exam_id=exam_id)
        self.assertNotEqual(exam_url, base_url)
        attempt_url = self.provider.get_instructor_url(course_id, user, exam_id='abcd', attempt_id=attempt_id)
        self.assertNotEqual(attempt_url, base_url)
        self.assertNotEqual(attempt_url, exam_url)
        # let's ensure that the last token contains all of the expected data
        token = attempt_url.split('jwt=')[1]
        decoded = jwt.decode(token,
                             issuer=self.provider.client_id,
                             key=self.provider.client_secret,
                             algorithms=['HS256'])
        self.assertEqual(decoded['user'], user)
        self.assertEqual(decoded['course_id'], course_id)
        self.assertEqual(decoded['exam_id'], exam_id)
        self.assertEqual(decoded['attempt_id'], attempt_id)

        # test that correct URL is returned for a request with parameter "show_configuration_dashboard=true"
        config_url = self.provider.get_instructor_url(
            course_id,
            user,
            exam_id=exam_id,
            show_configuration_dashboard=True,
        )
        token = config_url.split('jwt=')[1]
        decoded = jwt.decode(token,
                             issuer=self.provider.client_id,
                             key=self.provider.client_secret,
                             algorithms=['HS256'])
        self.assertTrue(decoded['config'])
        self.assertEqual(decoded['exam_id'], exam_id)

    @responses.activate
    def test_retire_user(self):
        user_id = 'abcdef5'
        responses.add(
            responses.DELETE,
            url=self.provider.user_info_url.format(user_id=user_id),
            json=True
        )
        result = self.provider.retire_user(user_id)
        assert result is True

    @responses.activate
    def test_retire_unknown_user(self):
        user_id = 'abcdef5'
        responses.add(
            responses.DELETE,
            url=self.provider.user_info_url.format(user_id=user_id),
            json=False
        )
        result = self.provider.retire_user(user_id)
        assert result is False

    @responses.activate
    def test_retire_error(self):
        user_id = 'abcdef5'
        responses.add(
            responses.DELETE,
            url=self.provider.user_info_url.format(user_id=user_id),
            body='"'
        )
        with self.assertRaises(BackendProviderCannotRetireUser):
            self.provider.retire_user(user_id)