diff --git a/src/pages-and-resources/proctoring/Settings.jsx b/src/pages-and-resources/proctoring/Settings.jsx index bb909dacb..26471bcfa 100644 --- a/src/pages-and-resources/proctoring/Settings.jsx +++ b/src/pages-and-resources/proctoring/Settings.jsx @@ -13,6 +13,7 @@ import { ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton, } from '@edx/paragon'; +import ExamsApiService from '../../data/services/ExamsApiService'; import StudioApiService from '../../data/services/StudioApiService'; import Loading from '../../generic/Loading'; import ConnectionErrorAlert from '../../generic/ConnectionErrorAlert'; @@ -36,7 +37,9 @@ function ProctoringSettings({ intl, onClose }) { const [loaded, setLoaded] = useState(false); const [loadingConnectionError, setLoadingConnectionError] = useState(false); const [loadingPermissionError, setLoadingPermissionError] = useState(false); + const [allowLtiProviders, setAllowLtiProviders] = useState(false); const [availableProctoringProviders, setAvailableProctoringProviders] = useState([]); + const [ltiProctoringProviders, setLtiProctoringProviders] = useState([]); const [courseStartDate, setCourseStartDate] = useState(''); const [saveSuccess, setSaveSuccess] = useState(false); const [saveError, setSaveError] = useState(false); @@ -85,6 +88,10 @@ function ProctoringSettings({ intl, onClose }) { } } + function isLtiProvider(provider) { + return ltiProctoringProviders.some(p => p.name === provider); + } + function setFocusToProctortrackEscalationEmailInput() { if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) { proctoringEscalationEmailInputRef.current.focus(); @@ -92,23 +99,35 @@ function ProctoringSettings({ intl, onClose }) { } function postSettingsBackToServer() { - const dataToPostBack = { + const providerIsLti = isLtiProvider(formValues.proctoringProvider); + const studioDataToPostBack = { proctored_exam_settings: { enable_proctored_exams: formValues.enableProctoredExams, - proctoring_provider: formValues.proctoringProvider, + // lti providers are managed outside edx-platform, lti_external indicates this + proctoring_provider: providerIsLti ? 'lti_external' : formValues.proctoringProvider, create_zendesk_tickets: formValues.createZendeskTickets, }, }; if (isEdxStaff) { - dataToPostBack.proctored_exam_settings.allow_proctoring_opt_out = formValues.allowOptingOut; + studioDataToPostBack.proctored_exam_settings.allow_proctoring_opt_out = formValues.allowOptingOut; } if (formValues.proctoringProvider === 'proctortrack') { - dataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.proctortrackEscalationEmail === '' ? null : formValues.proctortrackEscalationEmail; + studioDataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.proctortrackEscalationEmail === '' ? null : formValues.proctortrackEscalationEmail; } + // only save back to exam service if necessary setSubmissionInProgress(true); - StudioApiService.saveProctoredExamSettingsData(courseId, dataToPostBack).then(() => { + const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)]; + if (allowLtiProviders && ExamsApiService.isAvailable()) { + saveOperations.push( + ExamsApiService.saveCourseExamConfiguration( + courseId, { provider: providerIsLti ? formValues.proctoringProvider : null }, + ), + ); + } + Promise.all(saveOperations) + .then(() => { setSaveSuccess(true); setSaveError(false); setSubmissionInProgress(false); @@ -178,6 +197,11 @@ function ProctoringSettings({ intl, onClose }) { return markDisabled; } + function getProviderDisplayLabel(provider) { + // if a display label exists for this provider return it + return ltiProctoringProviders.find(p => p.name === provider)?.verbose_name || provider; + } + function getProctoringProviderOptions(providers) { return providers.map(provider => ( )); } @@ -344,7 +368,7 @@ function ProctoringSettings({ intl, onClose }) { )} {/* CREATE ZENDESK TICKETS */} - { isEdxStaff && formValues.enableProctoredExams && ( + { isEdxStaff && formValues.enableProctoredExams && !isLtiProvider(formValues.proctoringProvider) && (
@@ -446,23 +470,48 @@ function ProctoringSettings({ intl, onClose }) { useEffect( () => { - StudioApiService.getProctoredExamSettingsData(courseId) + Promise.all([ + StudioApiService.getProctoredExamSettingsData(courseId), + ExamsApiService.isAvailable() ? ExamsApiService.getCourseExamConfiguration(courseId) : Promise.resolve(), + ExamsApiService.isAvailable() ? ExamsApiService.getAvailableProviders() : Promise.resolve(), + ]) .then( - response => { - const proctoredExamSettings = response.data.proctored_exam_settings; + ([settingsResponse, examConfigResponse, ltiProvidersResponse]) => { + const proctoredExamSettings = settingsResponse.data.proctored_exam_settings; setLoaded(true); setLoading(false); setSubmissionInProgress(false); - setCourseStartDate(response.data.course_start_date); + setCourseStartDate(settingsResponse.data.course_start_date); const isProctortrack = proctoredExamSettings.proctoring_provider === 'proctortrack'; setShowProctortrackEscalationEmail(isProctortrack); - setAvailableProctoringProviders(response.data.available_proctoring_providers); + setAvailableProctoringProviders(settingsResponse.data.available_proctoring_providers); const proctoringEscalationEmail = proctoredExamSettings.proctoring_escalation_email; + // The list of providers returned by studio settings are the default behavior. If lti_external + // is available as an option display the list of LTI providers returned by the exam service. + // Setting 'lti_external' in studio indicates an LTI provider configured outside of edx-platform. + // This option is not directly selectable. + const proctoringProvidersStudio = settingsResponse.data.available_proctoring_providers; + const proctoringProvidersLti = ltiProvidersResponse?.data || []; + setAllowLtiProviders(proctoringProvidersStudio.includes('lti_external')); + setLtiProctoringProviders(proctoringProvidersLti); + // flatten provider objects and coalesce values to just the provider key + setAvailableProctoringProviders( + proctoringProvidersLti.reduce((result, provider) => [...result, provider.name], []).concat( + proctoringProvidersStudio.filter(value => value !== 'lti_external'), + ), + ); + + let selectedProvider; + if (proctoredExamSettings.proctoring_provider === 'lti_external') { + selectedProvider = examConfigResponse.data.provider; + } else { + selectedProvider = proctoredExamSettings.proctoring_provider; + } setFormValues({ ...formValues, + proctoringProvider: selectedProvider, enableProctoredExams: proctoredExamSettings.enable_proctored_exams, - proctoringProvider: proctoredExamSettings.proctoring_provider, allowOptingOut: proctoredExamSettings.allow_proctoring_opt_out, createZendeskTickets: proctoredExamSettings.create_zendesk_tickets, // The backend API may return null for the proctoringEscalationEmail value, which is the default. @@ -473,7 +522,7 @@ function ProctoringSettings({ intl, onClose }) { }, ).catch( error => { - if (error.response.status === 403) { + if (error.response?.status === 403) { setLoadingPermissionError(true); } else { setLoadingConnectionError(true); diff --git a/src/pages-and-resources/proctoring/Settings.test.jsx b/src/pages-and-resources/proctoring/Settings.test.jsx index 7ff4d8c29..ba7f72d14 100644 --- a/src/pages-and-resources/proctoring/Settings.test.jsx +++ b/src/pages-and-resources/proctoring/Settings.test.jsx @@ -4,12 +4,13 @@ import { } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; -import { initializeMockApp } from '@edx/frontend-platform'; +import { initializeMockApp, mergeConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; import { AppProvider } from '@edx/frontend-platform/react'; import StudioApiService from '../../data/services/StudioApiService'; +import ExamsApiService from '../../data/services/ExamsApiService'; import initializeStore from '../../store'; import PagesAndResourcesProvider from '../PagesAndResourcesProvider'; import ProctoredExamSettings from './Settings'; @@ -33,7 +34,19 @@ const intlWrapper = children => ( let axiosMock; describe('ProctoredExamSettings', () => { - beforeEach(() => { + function setupApp(isAdmin = true) { + mergeConfig({ + EXAMS_BASE_URL: 'http://exams.testing.co', + }, 'CourseAuthoringConfig'); + + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: isAdmin, + roles: [], + }, + }); store = initializeStore({ models: { courseApps: { @@ -41,39 +54,47 @@ describe('ProctoredExamSettings', () => { }, }, }); - }); + + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet( + `${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`, + ).reply(200, [ + { + name: 'test_lti', + verbose_name: 'LTI Provider', + }, + ]); + axiosMock.onGet( + `${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`, + ).reply(200, { + provider: null, + }); + + axiosMock.onGet( + StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), + ).reply(200, { + proctored_exam_settings: { + enable_proctored_exams: true, + allow_proctoring_opt_out: false, + proctoring_provider: 'mockproc', + proctoring_escalation_email: 'test@example.com', + create_zendesk_tickets: true, + }, + available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc', 'lti_external'], + course_start_date: '2070-01-01T00:00:00Z', + }); + } afterEach(() => { cleanup(); + axiosMock.reset(); + }); + beforeEach(async () => { + setupApp(); }); describe('Field dependencies', () => { beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - - axiosMock.onGet( - StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), - ).reply(200, { - proctored_exam_settings: { - enable_proctored_exams: true, - allow_proctoring_opt_out: false, - proctoring_provider: 'mockproc', - proctoring_escalation_email: 'test@example.com', - create_zendesk_tickets: true, - }, - available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'], - course_start_date: '2070-01-01T00:00:00Z', - }); - await act(async () => render(intlWrapper())); }); @@ -166,21 +187,23 @@ describe('ProctoredExamSettings', () => { expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull(); expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull(); }); + + it('Hides unsupported fields when lti provider is selected', async () => { + await waitFor(() => { + screen.getByDisplayValue('mockproc'); + }); + const selectElement = screen.getByDisplayValue('mockproc'); + await act(async () => { + fireEvent.change(selectElement, { target: { value: 'test_lti' } }); + }); + expect(screen.queryByTestId('escalationEmail')).toBeNull(); + expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull(); + expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull(); + }); }); describe('Validation with invalid escalation email', () => { beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: false, - roles: [], - }, - }); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onGet( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(200, { @@ -399,83 +422,114 @@ describe('ProctoredExamSettings', () => { course_start_date: '2013-01-01T00:00:00Z', }; - function setup(data, isAdmin) { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: isAdmin, - roles: [], - }, - }); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + function mockCourseData(data) { axiosMock.onGet(StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId)).reply(200, data); } it('Disables irrelevant proctoring provider fields when user is not an administrator and it is after start date', async () => { - setup(mockGetPastCourseData, false); + const isAdmin = false; + setupApp(isAdmin); + mockCourseData(mockGetPastCourseData); await act(async () => render(intlWrapper())); const providerOption = screen.getByTestId('proctortrack'); expect(providerOption.hasAttribute('disabled')).toEqual(true); }); it('Enables all proctoring provider options if user is not an administrator and it is before start date', async () => { - setup(mockGetFutureCourseData, false); + const isAdmin = false; + setupApp(isAdmin); + mockCourseData(mockGetFutureCourseData); await act(async () => render(intlWrapper())); const providerOption = screen.getByTestId('proctortrack'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); it('Enables all proctoring provider options if user administrator and it is after start date', async () => { - setup(mockGetPastCourseData, true); + const isAdmin = true; + setupApp(isAdmin); + mockCourseData(mockGetPastCourseData); await act(async () => render(intlWrapper())); const providerOption = screen.getByTestId('proctortrack'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); it('Enables all proctoring provider options if user administrator and it is before start date', async () => { - setup(mockGetFutureCourseData, true); + const isAdmin = true; + setupApp(isAdmin); + mockCourseData(mockGetFutureCourseData); await act(async () => render(intlWrapper())); const providerOption = screen.getByTestId('proctortrack'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); + + it('Does not include lti_external as a selectable option', async () => { + const courseData = mockGetFutureCourseData; + courseData.available_proctoring_providers = ['lti_external', 'proctortrack', 'mockproc']; + mockCourseData(courseData); + await act(async () => render(intlWrapper())); + await waitFor(() => { + screen.getByDisplayValue('mockproc'); + }); + expect(screen.queryByTestId('lti_external')).toBeNull(); + }); + + it('Includes lti proctoring provider options when lti_external is allowed by studio', async () => { + const courseData = mockGetFutureCourseData; + courseData.available_proctoring_providers = ['lti_external', 'proctortrack', 'mockproc']; + mockCourseData(courseData); + await act(async () => render(intlWrapper())); + await waitFor(() => { + screen.getByDisplayValue('mockproc'); + }); + const providerOption = screen.getByTestId('test_lti'); + // as as admin the provider should not be disabled + expect(providerOption.hasAttribute('disabled')).toEqual(false); + }); + + it('Does not request lti provider options if there is no exam service url configuration', async () => { + mergeConfig({ + EXAMS_BASE_URL: null, + }, 'CourseAuthoringConfig'); + + await act(async () => render(intlWrapper())); + await waitFor(() => { + screen.getByDisplayValue('mockproc'); + }); + // only outgoing request should be for studio settings + expect(axiosMock.history.get.length).toBe(1); + expect(axiosMock.history.get[0].url.includes('proctored_exam_settings')).toEqual(true); + }); + + it('Selected LTI proctoring provider is shown on page load', async () => { + const courseData = { ...mockGetFutureCourseData }; + courseData.available_proctoring_providers = ['lti_external', 'proctortrack', 'mockproc']; + courseData.proctored_exam_settings.proctoring_provider = 'lti_external'; + mockCourseData(courseData); + axiosMock.onGet( + `${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`, + ).reply(200, { + provider: 'test_lti', + }); + await act(async () => render(intlWrapper())); + await waitFor(() => { + screen.getByText('Proctoring provider'); + }); + + // make sure test_lti is the selected provider + expect(screen.getByDisplayValue('LTI Provider')).toBeInTheDocument(); + }); }); describe('Toggles field visibility based on user permissions', () => { - function setup(isAdmin) { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: isAdmin, - roles: [], - }, - }); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onGet(StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId)).reply(200, { - proctored_exam_settings: { - enable_proctored_exams: true, - allow_proctoring_opt_out: false, - proctoring_provider: 'mockproc', - proctoring_escalation_email: 'test@example.com', - create_zendesk_tickets: true, - }, - available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'], - course_start_date: '2070-01-01T00:00:00Z', - }); - } - it('Hides opting out and zendesk tickets for non edX staff', async () => { - setup(false); + setupApp(false); await act(async () => render(intlWrapper())); expect(screen.queryByTestId('allowOptingOutYes')).toBeNull(); expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull(); }); it('Shows opting out and zendesk tickets for edX staff', async () => { - setup(true); + setupApp(true); await act(async () => render(intlWrapper())); expect(screen.queryByTestId('allowOptingOutYes')).not.toBeNull(); expect(screen.queryByTestId('createZendeskTicketsYes')).not.toBeNull(); @@ -483,18 +537,6 @@ describe('ProctoredExamSettings', () => { }); describe('Connection states', () => { - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - }); - it('Shows the spinner before the connection is complete', async () => { await act(async () => { render(intlWrapper()); @@ -504,7 +546,7 @@ describe('ProctoredExamSettings', () => { }); }); - it('Show connection error message when we suffer server side error', async () => { + it('Show connection error message when we suffer studio server side error', async () => { axiosMock.onGet( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(500); @@ -516,6 +558,18 @@ describe('ProctoredExamSettings', () => { ); }); + it('Show connection error message when we suffer edx-exams server side error', async () => { + axiosMock.onGet( + `${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`, + ).reply(500); + + await act(async () => render(intlWrapper())); + const connectionError = screen.getByTestId('connectionErrorAlert'); + expect(connectionError.textContent).toEqual( + expect.stringContaining('We encountered a technical error when loading this page.'), + ); + }); + it('Show permission error message when user do not have enough permission', async () => { axiosMock.onGet( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), @@ -529,35 +583,17 @@ describe('ProctoredExamSettings', () => { }); }); - describe('Save settings', () => { - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient(), { onNoMatch: 'throwException' }); - axiosMock.onGet(StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId)).reply(200, { - proctored_exam_settings: { - enable_proctored_exams: true, - allow_proctoring_opt_out: false, - proctoring_provider: 'mockproc', - proctoring_escalation_email: 'test@example.com', - create_zendesk_tickets: true, - }, - available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'], - }); - }); - - it('Disable button while submitting', async () => { + describe.only('Save settings', () => { + beforeEach(async () => { axiosMock.onPost( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(200, 'success'); + axiosMock.onPatch( + `${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`, + ).reply(200, 'success'); + }); + it('Disable button while submitting', async () => { await act(async () => render(intlWrapper())); let submitButton = screen.getByTestId('submissionButton'); expect(screen.queryByTestId('saveInProgress')).toBeFalsy(); @@ -570,10 +606,6 @@ describe('ProctoredExamSettings', () => { }); it('Makes API call successfully with proctoring_escalation_email if proctortrack', async () => { - axiosMock.onPost( - StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), - ).reply(200, 'success'); - await act(async () => render(intlWrapper())); // Make a change to the provider to proctortrack and set the email const selectElement = screen.getByDisplayValue('mockproc'); @@ -609,10 +641,6 @@ describe('ProctoredExamSettings', () => { }); it('Makes API call successfully without proctoring_escalation_email if not proctortrack', async () => { - axiosMock.onPost( - StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), - ).reply(200, 'success'); - await act(async () => render(intlWrapper())); // make sure we have not selected proctortrack as the proctoring provider @@ -639,7 +667,112 @@ describe('ProctoredExamSettings', () => { expect(document.activeElement).toEqual(errorAlert); }); - it('Makes API call generated error', async () => { + it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => { + await act(async () => render(intlWrapper())); + // Make a change to the provider to proctortrack and set the email + const selectElement = screen.getByDisplayValue('mockproc'); + await act(async () => { + fireEvent.change(selectElement, { target: { value: 'test_lti' } }); + }); + const submitButton = screen.getByTestId('submissionButton'); + await act(async () => { + fireEvent.click(submitButton); + }); + + // update exam service config + expect(axiosMock.history.patch.length).toBe(1); + expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({ + provider: 'test_lti', + }); + + // update studio settings + expect(axiosMock.history.post.length).toBe(1); + expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({ + proctored_exam_settings: { + enable_proctored_exams: true, + allow_proctoring_opt_out: false, + proctoring_provider: 'lti_external', + create_zendesk_tickets: true, + }, + }); + + const errorAlert = screen.getByTestId('saveSuccess'); + expect(errorAlert.textContent).toEqual( + expect.stringContaining('Proctored exam settings saved successfully.'), + ); + expect(document.activeElement).toEqual(errorAlert); + }); + + it('Sets exam service provider to null if a non-lti provider is selected', async () => { + await act(async () => render(intlWrapper())); + const submitButton = screen.getByTestId('submissionButton'); + await act(async () => { + fireEvent.click(submitButton); + }); + // update exam service config + expect(axiosMock.history.patch.length).toBe(1); + expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({ + provider: null, + }); + expect(axiosMock.history.patch.length).toBe(1); + expect(axiosMock.history.post.length).toBe(1); + expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({ + proctored_exam_settings: { + enable_proctored_exams: true, + allow_proctoring_opt_out: false, + proctoring_provider: 'mockproc', + create_zendesk_tickets: true, + }, + }); + + const errorAlert = screen.getByTestId('saveSuccess'); + expect(errorAlert.textContent).toEqual( + expect.stringContaining('Proctored exam settings saved successfully.'), + ); + expect(document.activeElement).toEqual(errorAlert); + }); + + it('Does not update exam service if lti is not enabled in studio', async () => { + axiosMock.onGet( + StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), + ).reply(200, { + proctored_exam_settings: { + enable_proctored_exams: true, + allow_proctoring_opt_out: false, + proctoring_provider: 'mockproc', + proctoring_escalation_email: 'test@example.com', + create_zendesk_tickets: true, + }, + available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'], + course_start_date: '2070-01-01T00:00:00Z', + }); + + await act(async () => render(intlWrapper())); + const submitButton = screen.getByTestId('submissionButton'); + await act(async () => { + fireEvent.click(submitButton); + }); + // does not update exam service config + expect(axiosMock.history.patch.length).toBe(0); + // does update studio + expect(axiosMock.history.post.length).toBe(1); + expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({ + proctored_exam_settings: { + enable_proctored_exams: true, + allow_proctoring_opt_out: false, + proctoring_provider: 'mockproc', + create_zendesk_tickets: true, + }, + }); + + const errorAlert = screen.getByTestId('saveSuccess'); + expect(errorAlert.textContent).toEqual( + expect.stringContaining('Proctored exam settings saved successfully.'), + ); + expect(document.activeElement).toEqual(errorAlert); + }); + + it('Makes studio API call generated error', async () => { axiosMock.onPost( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(500); @@ -657,11 +790,29 @@ describe('ProctoredExamSettings', () => { expect(document.activeElement).toEqual(errorAlert); }); + it('Makes exams API call generated error', async () => { + axiosMock.onPatch( + `${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`, + ).reply(500, 'error'); + + await act(async () => render(intlWrapper())); + const submitButton = screen.getByTestId('submissionButton'); + await act(async () => { + fireEvent.click(submitButton); + }); + expect(axiosMock.history.post.length).toBe(1); + const errorAlert = screen.getByTestId('saveError'); + expect(errorAlert.textContent).toEqual( + expect.stringContaining('We encountered a technical error while trying to save proctored exam settings'), + ); + expect(document.activeElement).toEqual(errorAlert); + }); + it('Manages focus correctly after different save statuses', async () => { // first make a call that will cause a save error axiosMock.onPost( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), - ).replyOnce(500); + ).reply(500); await act(async () => render(intlWrapper())); const submitButton = screen.getByTestId('submissionButton'); @@ -678,7 +829,7 @@ describe('ProctoredExamSettings', () => { // now make a call that will allow for a successful save axiosMock.onPost( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), - ).replyOnce(200, 'success'); + ).reply(200, 'success'); await act(async () => { fireEvent.click(submitButton); }); @@ -693,28 +844,8 @@ describe('ProctoredExamSettings', () => { it('Include Zendesk ticket in post request if user is not an admin', async () => { // use non-admin user for test - initializeMockApp({ - authenticatedUser: { - userId: 4, - username: 'abc1234', - administrator: false, - roles: [], - }, - }); - axiosMock = new MockAdapter(getAuthenticatedHttpClient(), { onNoMatch: 'throwException' }); - axiosMock.onGet(StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId)).reply(200, { - proctored_exam_settings: { - enable_proctored_exams: true, - allow_proctoring_opt_out: false, - proctoring_provider: 'mockproc', - proctoring_escalation_email: 'test@example.com', - create_zendesk_tickets: true, - }, - available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'], - }); - axiosMock.onPost( - StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), - ).reply(200, 'success'); + const isAdmin = false; + setupApp(isAdmin); await act(async () => render(intlWrapper())); // Make a change to the proctoring provider