diff --git a/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py b/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py index f59b68773291..c054afdaa6aa 100644 --- a/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py +++ b/lms/djangoapps/instructor/tests/views/test_special_exams_api_v2.py @@ -13,6 +13,7 @@ create_exam_attempt, get_allowances_for_course, ) +from edx_proctoring.exceptions import ProctoredBaseException from edx_proctoring.models import ProctoredExamStudentAttempt from rest_framework import status from rest_framework.test import APIClient @@ -217,6 +218,30 @@ def test_reset_no_attempts(self): response = self.client.post(self._url()) assert response.status_code == status.HTTP_404_NOT_FOUND + def test_reset_provider_unavailable_returns_descriptive_error(self): + """ + When removing the attempt raises a ProctoredBaseException (e.g. the proctoring + provider is unavailable), the view surfaces the exception's HTTP status and + message so the instructor dashboard shows a descriptive error rather than a 500. + """ + create_exam_attempt(self.exam_id, self.student.id) + message = ( + 'The proctoring provider is temporarily unavailable, so this attempt ' + 'could not be fully reset. Please try again in a few minutes.' + ) + # Simulate a proctoring-provider failure during removal (e.g. edx-proctoring's + # BackendProviderCannotRemoveAttempt), which resolves to a 502. Using a base + # ProctoredBaseException keeps the test independent of the edx-proctoring release. + provider_error = ProctoredBaseException(message) + provider_error.http_status = status.HTTP_502_BAD_GATEWAY + with patch( + 'lms.djangoapps.instructor.views.api_v2.remove_exam_attempt', + side_effect=provider_error, + ): + response = self.client.post(self._url()) + assert response.status_code == status.HTTP_502_BAD_GATEWAY + assert response.json()['detail'] == message + @override_settings(**PROCTORING_SETTINGS) @override_settings(ENABLE_SPECIAL_EXAMS=True) diff --git a/lms/djangoapps/instructor/views/api_v2.py b/lms/djangoapps/instructor/views/api_v2.py index 871aa90cf961..e63a66b0bb7c 100644 --- a/lms/djangoapps/instructor/views/api_v2.py +++ b/lms/djangoapps/instructor/views/api_v2.py @@ -4270,8 +4270,15 @@ def post(self, request, course_id, exam_id, username): status=status.HTTP_404_NOT_FOUND, ) - for attempt in user_attempts: - remove_exam_attempt(attempt['id'], requesting_user=request.user) + try: + for attempt in user_attempts: + remove_exam_attempt(attempt['id'], requesting_user=request.user) + except ProctoredBaseException as err: + # The proctoring provider (or another proctoring-layer error) prevented the + # attempt from being removed. Surface the typed exception's status and message + # (e.g. a 502 when the provider is unavailable) so the instructor dashboard can + # display a descriptive error instead of a generic 500. + return Response({'detail': str(err)}, status=err.http_status) return Response( {'success': True, 'message': f'Exam attempt reset for user {username}'},