@@ -297,4 +297,178 @@ describe('Data layer integration tests', () => {
297297 expect ( enabled ) . toBe ( false ) ;
298298 } ) ;
299299 } ) ;
300+
301+ describe ( 'Test fetchExamAttemptsData' , ( ) => {
302+ const sequenceIds = [
303+ 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345' ,
304+ 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@67890' ,
305+ 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@abcde' ,
306+ ] ;
307+
308+ beforeEach ( ( ) => {
309+ // Mock individual exam endpoints with different responses
310+ sequenceIds . forEach ( ( sequenceId , index ) => {
311+ // Handle both LMS and EXAMS service URL patterns
312+ const lmsExamUrl = new RegExp ( `.*edx_proctoring/v1/proctored_exam/attempt/course_id/${ encodeURIComponent ( courseId ) } .*content_id=${ encodeURIComponent ( sequenceId ) } .*` ) ;
313+ const examsServiceUrl = new RegExp ( `.*/api/v1/student/exam/attempt/course_id/${ encodeURIComponent ( courseId ) } /content_id/${ encodeURIComponent ( sequenceId ) } .*` ) ;
314+
315+ let attemptStatus = 'ready_to_start' ;
316+ if ( index === 0 ) {
317+ attemptStatus = 'created' ;
318+ } else if ( index === 1 ) {
319+ attemptStatus = 'submitted' ;
320+ }
321+
322+ const mockExamData = {
323+ exam : {
324+ id : index + 1 ,
325+ course_id : courseId ,
326+ content_id : sequenceId ,
327+ exam_name : `Test Exam ${ index + 1 } ` ,
328+ attempt_status : attemptStatus ,
329+ time_remaining_seconds : 3600 ,
330+ } ,
331+ } ;
332+
333+ // Mock both URL patterns
334+ axiosMock . onGet ( lmsExamUrl ) . reply ( 200 , mockExamData ) ;
335+ axiosMock . onGet ( examsServiceUrl ) . reply ( 200 , mockExamData ) ;
336+ } ) ;
337+ } ) ;
338+
339+ it ( 'should fetch exam data for all sequence IDs and dispatch setExamsData' , async ( ) => {
340+ await executeThunk ( thunks . fetchExamAttemptsData ( courseId , sequenceIds ) , store . dispatch ) ;
341+
342+ const state = store . getState ( ) ;
343+
344+ // Verify the examsData was set in the store
345+ expect ( state . courseHome . examsData ) . toHaveLength ( 3 ) ;
346+ expect ( state . courseHome . examsData ) . toEqual ( [
347+ {
348+ id : 1 ,
349+ courseId,
350+ contentId : sequenceIds [ 0 ] ,
351+ examName : 'Test Exam 1' ,
352+ attemptStatus : 'created' ,
353+ timeRemainingSeconds : 3600 ,
354+ } ,
355+ {
356+ id : 2 ,
357+ courseId,
358+ contentId : sequenceIds [ 1 ] ,
359+ examName : 'Test Exam 2' ,
360+ attemptStatus : 'submitted' ,
361+ timeRemainingSeconds : 3600 ,
362+ } ,
363+ {
364+ id : 3 ,
365+ courseId,
366+ contentId : sequenceIds [ 2 ] ,
367+ examName : 'Test Exam 3' ,
368+ attemptStatus : 'ready_to_start' ,
369+ timeRemainingSeconds : 3600 ,
370+ } ,
371+ ] ) ;
372+
373+ // Verify all API calls were made
374+ expect ( axiosMock . history . get ) . toHaveLength ( 3 ) ;
375+ } ) ;
376+
377+ it ( 'should handle 404 responses and include empty objects in results' , async ( ) => {
378+ // Override one endpoint to return 404 for both URL patterns
379+ const examUrl404LMS = new RegExp ( `.*edx_proctoring/v1/proctored_exam/attempt/course_id/${ encodeURIComponent ( courseId ) } .*content_id=${ encodeURIComponent ( sequenceIds [ 1 ] ) } .*` ) ;
380+ const examUrl404Exams = new RegExp ( `.*/api/v1/student/exam/attempt/course_id/${ encodeURIComponent ( courseId ) } /content_id/${ encodeURIComponent ( sequenceIds [ 1 ] ) } .*` ) ;
381+ axiosMock . onGet ( examUrl404LMS ) . reply ( 404 ) ;
382+ axiosMock . onGet ( examUrl404Exams ) . reply ( 404 ) ;
383+
384+ await executeThunk ( thunks . fetchExamAttemptsData ( courseId , sequenceIds ) , store . dispatch ) ;
385+
386+ const state = store . getState ( ) ;
387+
388+ // Verify the examsData includes empty object for 404 response
389+ expect ( state . courseHome . examsData ) . toHaveLength ( 3 ) ;
390+ expect ( state . courseHome . examsData [ 1 ] ) . toEqual ( { } ) ;
391+ } ) ;
392+
393+ it ( 'should handle API errors and log them while continuing with other requests' , async ( ) => {
394+ // Override one endpoint to return 500 error for both URL patterns
395+ const examUrl500LMS = new RegExp ( `.*edx_proctoring/v1/proctored_exam/attempt/course_id/${ encodeURIComponent ( courseId ) } .*content_id=${ encodeURIComponent ( sequenceIds [ 0 ] ) } .*` ) ;
396+ const examUrl500Exams = new RegExp ( `.*/api/v1/student/exam/attempt/course_id/${ encodeURIComponent ( courseId ) } /content_id/${ encodeURIComponent ( sequenceIds [ 0 ] ) } .*` ) ;
397+ axiosMock . onGet ( examUrl500LMS ) . reply ( 500 , { error : 'Server Error' } ) ;
398+ axiosMock . onGet ( examUrl500Exams ) . reply ( 500 , { error : 'Server Error' } ) ;
399+
400+ await executeThunk ( thunks . fetchExamAttemptsData ( courseId , sequenceIds ) , store . dispatch ) ;
401+
402+ const state = store . getState ( ) ;
403+
404+ // Verify error was logged for the failed request
405+ expect ( loggingService . logError ) . toHaveBeenCalled ( ) ;
406+
407+ // Verify the examsData still includes results for successful requests
408+ expect ( state . courseHome . examsData ) . toHaveLength ( 3 ) ;
409+ // First item should be the error result (just empty object for API errors)
410+ expect ( state . courseHome . examsData [ 0 ] ) . toEqual ( { } ) ;
411+ } ) ;
412+
413+ it ( 'should handle empty sequence IDs array' , async ( ) => {
414+ await executeThunk ( thunks . fetchExamAttemptsData ( courseId , [ ] ) , store . dispatch ) ;
415+
416+ const state = store . getState ( ) ;
417+
418+ expect ( state . courseHome . examsData ) . toEqual ( [ ] ) ;
419+ expect ( axiosMock . history . get ) . toHaveLength ( 0 ) ;
420+ } ) ;
421+
422+ it ( 'should handle mixed success and error responses' , async ( ) => {
423+ // Setup mixed responses
424+ const examUrl1LMS = new RegExp ( `.*edx_proctoring/v1/proctored_exam/attempt/course_id/${ encodeURIComponent ( courseId ) } .*content_id=${ encodeURIComponent ( sequenceIds [ 0 ] ) } .*` ) ;
425+ const examUrl1Exams = new RegExp ( `.*/api/v1/student/exam/attempt/course_id/${ encodeURIComponent ( courseId ) } /content_id/${ encodeURIComponent ( sequenceIds [ 0 ] ) } .*` ) ;
426+ const examUrl2LMS = new RegExp ( `.*edx_proctoring/v1/proctored_exam/attempt/course_id/${ encodeURIComponent ( courseId ) } .*content_id=${ encodeURIComponent ( sequenceIds [ 1 ] ) } .*` ) ;
427+ const examUrl2Exams = new RegExp ( `.*/api/v1/student/exam/attempt/course_id/${ encodeURIComponent ( courseId ) } /content_id/${ encodeURIComponent ( sequenceIds [ 1 ] ) } .*` ) ;
428+ const examUrl3LMS = new RegExp ( `.*edx_proctoring/v1/proctored_exam/attempt/course_id/${ encodeURIComponent ( courseId ) } .*content_id=${ encodeURIComponent ( sequenceIds [ 2 ] ) } .*` ) ;
429+ const examUrl3Exams = new RegExp ( `.*/api/v1/student/exam/attempt/course_id/${ encodeURIComponent ( courseId ) } /content_id/${ encodeURIComponent ( sequenceIds [ 2 ] ) } .*` ) ;
430+
431+ axiosMock . onGet ( examUrl1LMS ) . reply ( 200 , {
432+ exam : {
433+ id : 1 ,
434+ exam_name : 'Success Exam' ,
435+ course_id : courseId ,
436+ content_id : sequenceIds [ 0 ] ,
437+ attempt_status : 'created' ,
438+ time_remaining_seconds : 3600 ,
439+ } ,
440+ } ) ;
441+ axiosMock . onGet ( examUrl1Exams ) . reply ( 200 , {
442+ exam : {
443+ id : 1 ,
444+ exam_name : 'Success Exam' ,
445+ course_id : courseId ,
446+ content_id : sequenceIds [ 0 ] ,
447+ attempt_status : 'created' ,
448+ time_remaining_seconds : 3600 ,
449+ } ,
450+ } ) ;
451+ axiosMock . onGet ( examUrl2LMS ) . reply ( 404 ) ;
452+ axiosMock . onGet ( examUrl2Exams ) . reply ( 404 ) ;
453+ axiosMock . onGet ( examUrl3LMS ) . reply ( 500 , { error : 'Server Error' } ) ;
454+ axiosMock . onGet ( examUrl3Exams ) . reply ( 500 , { error : 'Server Error' } ) ;
455+
456+ await executeThunk ( thunks . fetchExamAttemptsData ( courseId , sequenceIds ) , store . dispatch ) ;
457+
458+ const state = store . getState ( ) ;
459+
460+ expect ( state . courseHome . examsData ) . toHaveLength ( 3 ) ;
461+ expect ( state . courseHome . examsData [ 0 ] ) . toMatchObject ( {
462+ id : 1 ,
463+ examName : 'Success Exam' ,
464+ courseId,
465+ contentId : sequenceIds [ 0 ] ,
466+ } ) ;
467+ expect ( state . courseHome . examsData [ 1 ] ) . toEqual ( { } ) ;
468+ expect ( state . courseHome . examsData [ 2 ] ) . toEqual ( { } ) ;
469+
470+ // Verify error was logged for the 500 error (may be called more than once due to multiple URL patterns)
471+ expect ( loggingService . logError ) . toHaveBeenCalled ( ) ;
472+ } ) ;
473+ } ) ;
300474} ) ;
0 commit comments