diff --git a/src/data/api.js b/src/data/api.js index 2db67097..a2f18c01 100644 --- a/src/data/api.js +++ b/src/data/api.js @@ -1,16 +1,26 @@ import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { logError } from '@edx/frontend-platform/logging'; import { ExamAction } from '../constants'; import { generateHumanizedTime } from '../helpers'; const BASE_API_URL = '/api/edx_proctoring/v1/proctored_exam/attempt'; async function fetchActiveAttempt() { + // fetch 'active' (timer is running) attempt if it exists const activeAttemptUrl = new URL(`${getConfig().EXAMS_BASE_URL}/api/v1/exams/attempt/latest`); const activeAttemptResponse = await getAuthenticatedHttpClient().get(activeAttemptUrl.href); return activeAttemptResponse.data; } +async function fetchLatestExamAttempt(sequenceId) { + // fetch lastest attempt for a specific exam + const attemptUrl = new URL(`${getConfig().EXAMS_BASE_URL}/api/v1/exams/attempt/latest`); + attemptUrl.searchParams.append('content_id', sequenceId); + const response = await getAuthenticatedHttpClient().get(attemptUrl.href); + return response.data; +} + export async function fetchExamAttemptsData(courseId, sequenceId) { let data; if (!getConfig().EXAMS_BASE_URL) { @@ -56,22 +66,24 @@ export async function fetchLatestAttempt(courseId) { return data; } -export async function pollExamAttempt(url) { +export async function pollExamAttempt(pollUrl, sequenceId) { let data; - if (!getConfig().EXAMS_BASE_URL) { + if (pollUrl) { const edxProctoringURL = new URL( - `${getConfig().LMS_BASE_URL}${url}`, + `${getConfig().LMS_BASE_URL}${pollUrl}`, ); const urlResponse = await getAuthenticatedHttpClient().get(edxProctoringURL.href); data = urlResponse.data; - } else { - data = await fetchActiveAttempt(); + } else if (getConfig().EXAMS_BASE_URL) { + data = await fetchLatestExamAttempt(sequenceId); // Update dictionaries returned by edx-exams to have correct status key for legacy compatibility if (data.attempt_status) { data.status = data.attempt_status; delete data.attempt_status; } + } else { + logError('pollExamAttempt called but no pollUrl is set'); } return data; } diff --git a/src/data/redux.test.jsx b/src/data/redux.test.jsx index 5dde4ebe..259bfbf6 100644 --- a/src/data/redux.test.jsx +++ b/src/data/redux.test.jsx @@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getConfig, mergeConfig } from '@edx/frontend-platform'; +import * as api from './api'; import * as thunks from './thunks'; import executeThunk from '../utils'; @@ -958,6 +959,35 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(state.examState.activeAttempt).toMatchSnapshot(); }); + + describe('pollAttempt api called directly', () => { + // in the download view we call this function directly withough invoking the wrapping thunk + it('should call pollUrl if one is provided', async () => { + const pollExamAttemptUrl = `${getConfig().LMS_BASE_URL}${attempt.exam_started_poll_url}`; + axiosMock.onGet(pollExamAttemptUrl).reply(200, { + time_remaining_seconds: 1739.9, + accessibility_time_string: 'you have 29 minutes remaining', + attempt_status: ExamStatus.STARTED, + }); + await api.pollExamAttempt(attempt.exam_started_poll_url); + expect(axiosMock.history.get[0].url).toEqual(pollExamAttemptUrl); + }); + it('should call the latest attempt for a sequence if a sequence id is provided instead of a pollUrl', async () => { + const sequenceId = 'block-v1:edX+Test+123'; + const expectedUrl = `${getConfig().EXAMS_BASE_URL}/api/v1/exams/attempt/latest?content_id=${encodeURIComponent(sequenceId)}`; + axiosMock.onGet(expectedUrl).reply(200, { + time_remaining_seconds: 1739.9, + status: ExamStatus.STARTED, + }); + await api.pollExamAttempt(null, sequenceId); + expect(axiosMock.history.get[0].url).toEqual(expectedUrl); + }); + test.only('pollUrl is required if edx-exams in not enabled, an error should be logged', async () => { + mergeConfig({ EXAMS_BASE_URL: null }); + api.pollExamAttempt(null, null); + expect(loggingService.logError).toHaveBeenCalled(); + }); + }); }); describe('Test pingAttempt', () => { diff --git a/src/instructions/proctored_exam/download-instructions/index.jsx b/src/instructions/proctored_exam/download-instructions/index.jsx index da0a4bf0..6b49b577 100644 --- a/src/instructions/proctored_exam/download-instructions/index.jsx +++ b/src/instructions/proctored_exam/download-instructions/index.jsx @@ -50,7 +50,7 @@ const DownloadSoftwareProctoredExamInstructions = ({ intl, skipProctoredExam }) ? `${getConfig().EXAMS_BASE_URL}/lti/start_proctoring/${attemptId}` : downloadUrl; const handleDownloadClick = () => { - pollExamAttempt(`${pollUrl}?sourceid=instructions`) + pollExamAttempt(pollUrl, sequenceId) .then((data) => { if (data.status === ExamStatus.READY_TO_START) { setSystemCheckStatus('success'); @@ -66,7 +66,7 @@ const DownloadSoftwareProctoredExamInstructions = ({ intl, skipProctoredExam }) }; const handleStartExamClick = () => { - pollExamAttempt(`${pollUrl}?sourceid=instructions`) + pollExamAttempt(pollUrl, sequenceId) .then((data) => ( data.status === ExamStatus.READY_TO_START ? getExamAttemptsData(courseId, sequenceId)