@@ -1904,6 +1904,206 @@ def test_get_enrollment_status(self):
19041904 res_json = json .loads (response .content .decode ('utf-8' ))
19051905 assert res_json ['enrollment_status' ] == 'Enrollment status for nonotever@example.com: never enrolled'
19061906
1907+ @patch ("lms.djangoapps.instructor_task.api.submit_student_enrollment_batch" )
1908+ def test_enroll_async_processing_success (self , mock_submit_task ):
1909+ """Test async enrollment with async_processing=True"""
1910+ mock_task = Mock ()
1911+ mock_task .task_id = "test-task-id-123"
1912+ mock_task .task_state = "QUEUED"
1913+ mock_submit_task .return_value = mock_task
1914+ url = reverse ("students_update_enrollment" , kwargs = {"course_id" : str (self .course .id )})
1915+
1916+ response = self .client .post (
1917+ url ,
1918+ {
1919+ "identifiers" : self .notenrolled_student .email ,
1920+ "action" : "enroll" ,
1921+ "email_students" : False ,
1922+ "async_processing" : True ,
1923+ },
1924+ )
1925+
1926+ self .assertEqual (response .status_code , 200 )
1927+ res_json = json .loads (response .content .decode ("utf-8" ))
1928+
1929+ # Verify async response structure
1930+ self .assertEqual (res_json , {
1931+ "action" : "enroll" ,
1932+ "auto_enroll" : False ,
1933+ "async_processing" : True ,
1934+ "task_id" : "test-task-id-123" ,
1935+ "task_state" : "QUEUED" ,
1936+ "message" : "Async enroll task submitted for 1 students" ,
1937+ "total_students" : 1
1938+ })
1939+
1940+ # Verify the task was called with correct parameters
1941+ self .assertTrue (mock_submit_task .called )
1942+ call_args = mock_submit_task .call_args
1943+ self .assertEqual (call_args [1 ]["course_key" ], self .course .id )
1944+ self .assertEqual (call_args [1 ]["action" ], "enroll" )
1945+ self .assertEqual (call_args [1 ]["identifiers" ], [self .notenrolled_student .email ])
1946+
1947+ @patch ("lms.djangoapps.instructor_task.api.submit_student_enrollment_batch" )
1948+ def test_unenroll_async_processing_success (self , mock_submit_task ):
1949+ """Test async unenrollment with async_processing=True"""
1950+ mock_task = Mock ()
1951+ mock_task .task_id = "test-unenroll-task-456"
1952+ mock_task .task_state = "QUEUED"
1953+ mock_submit_task .return_value = mock_task
1954+ url = reverse ("students_update_enrollment" , kwargs = {"course_id" : str (self .course .id )})
1955+
1956+ response = self .client .post (
1957+ url ,
1958+ {
1959+ "identifiers" : self .enrolled_student .email ,
1960+ "action" : "unenroll" ,
1961+ "email_students" : True ,
1962+ "async_processing" : True ,
1963+ },
1964+ )
1965+
1966+ self .assertEqual (response .status_code , 200 )
1967+ res_json = json .loads (response .content .decode ("utf-8" ))
1968+
1969+ self .assertEqual (res_json , {
1970+ "action" : "unenroll" ,
1971+ "auto_enroll" : False ,
1972+ "async_processing" : True ,
1973+ "task_id" : "test-unenroll-task-456" ,
1974+ "task_state" : "QUEUED" ,
1975+ "message" : "Async unenroll task submitted for 1 students" ,
1976+ "total_students" : 1
1977+ })
1978+
1979+ @patch ("lms.djangoapps.instructor_task.api.submit_student_enrollment_batch" )
1980+ def test_async_enrollment_already_running_error (self , mock_submit_task ):
1981+ """Test handling of AlreadyRunningError in async mode"""
1982+ mock_submit_task .side_effect = AlreadyRunningError ("Task already running" )
1983+ url = reverse ("students_update_enrollment" , kwargs = {"course_id" : str (self .course .id )})
1984+
1985+ response = self .client .post (
1986+ url , {"identifiers" : self .notenrolled_student .email , "action" : "enroll" , "async_processing" : True }
1987+ )
1988+
1989+ self .assertEqual (response .status_code , 200 )
1990+ res_json = json .loads (response .content .decode ("utf-8" ))
1991+ self .assertTrue (res_json ["async_processing" ])
1992+ self .assertIn ("error" , res_json )
1993+ self .assertIn ("already running" , res_json ["error" ].lower ())
1994+ self .assertEqual (res_json ["action" ], "enroll" )
1995+
1996+ @patch ("lms.djangoapps.instructor_task.api.submit_student_enrollment_batch" )
1997+ def test_async_enrollment_multiple_identifiers (self , mock_submit_task ):
1998+ """Test async enrollment with multiple student identifiers"""
1999+ student2 = UserFactory (username = "Student2" , email = "student2@example.com" )
2000+ student3 = UserFactory (username = "Student3" , email = "student3@example.com" )
2001+ mock_task = Mock ()
2002+ mock_task .task_id = "test-bulk-task-789"
2003+ mock_task .task_state = "QUEUED"
2004+ mock_submit_task .return_value = mock_task
2005+ identifiers = f"{ self .notenrolled_student .email } ,{ student2 .email } ,{ student3 .email } "
2006+ url = reverse ("students_update_enrollment" , kwargs = {"course_id" : str (self .course .id )})
2007+
2008+ response = self .client .post (
2009+ url ,
2010+ {
2011+ "identifiers" : identifiers ,
2012+ "action" : "enroll" ,
2013+ "email_students" : False ,
2014+ "async_processing" : True ,
2015+ "auto_enroll" : True ,
2016+ },
2017+ )
2018+
2019+ self .assertEqual (response .status_code , 200 )
2020+ res_json = json .loads (response .content .decode ("utf-8" ))
2021+ self .assertEqual (res_json ["total_students" ], 3 )
2022+ self .assertTrue (res_json ["auto_enroll" ])
2023+
2024+ # Verify task was called with all identifiers
2025+ call_args = mock_submit_task .call_args
2026+ identifiers_list = call_args [1 ]["identifiers" ]
2027+ self .assertEqual (len (identifiers_list ), 3 )
2028+ self .assertIn (self .notenrolled_student .email , identifiers_list )
2029+ self .assertIn (student2 .email , identifiers_list )
2030+ self .assertIn (student3 .email , identifiers_list )
2031+
2032+ def test_async_processing_default_false (self ):
2033+ """Test that async_processing defaults to False for backward compatibility"""
2034+ url = reverse ("students_update_enrollment" , kwargs = {"course_id" : str (self .course .id )})
2035+
2036+ response = self .client .post (
2037+ url ,
2038+ {
2039+ "identifiers" : self .notenrolled_student .email ,
2040+ "action" : "enroll" ,
2041+ "email_students" : False ,
2042+ # async_processing not provided
2043+ },
2044+ )
2045+
2046+ self .assertEqual (response .status_code , 200 )
2047+ res_json = json .loads (response .content .decode ("utf-8" ))
2048+
2049+ # Should have sync response structure (with 'results')
2050+ self .assertIn ("results" , res_json )
2051+ self .assertNotIn ("async_processing" , res_json )
2052+ self .assertEqual (res_json ["action" ], "enroll" )
2053+
2054+ @patch ("lms.djangoapps.instructor_task.api.submit_student_enrollment_batch" )
2055+ def test_async_enrollment_with_reason (self , mock_submit_task ):
2056+ """Test async enrollment with reason field"""
2057+ mock_task = Mock ()
2058+ mock_task .task_id = "test-task-with-reason"
2059+ mock_task .task_state = "QUEUED"
2060+ mock_submit_task .return_value = mock_task
2061+ url = reverse ("students_update_enrollment" , kwargs = {"course_id" : str (self .course .id )})
2062+
2063+ response = self .client .post (
2064+ url ,
2065+ {
2066+ "identifiers" : self .notenrolled_student .email ,
2067+ "action" : "enroll" ,
2068+ "email_students" : False ,
2069+ "async_processing" : True ,
2070+ "reason" : "Testing async enrollment" ,
2071+ },
2072+ )
2073+
2074+ self .assertEqual (response .status_code , 200 )
2075+ res_json = json .loads (response .content .decode ("utf-8" ))
2076+ self .assertTrue (res_json ["async_processing" ])
2077+
2078+ # Verify reason was passed to task
2079+ call_args = mock_submit_task .call_args
2080+ self .assertEqual (call_args [1 ]["reason" ], "Testing async enrollment" )
2081+
2082+ def test_sync_enrollment_still_works (self ):
2083+ """Test that synchronous enrollment still works (async_processing=False)"""
2084+ url = reverse ("students_update_enrollment" , kwargs = {"course_id" : str (self .course .id )})
2085+
2086+ response = self .client .post (
2087+ url ,
2088+ {
2089+ "identifiers" : self .notenrolled_student .email ,
2090+ "action" : "enroll" ,
2091+ "email_students" : False ,
2092+ "async_processing" : False ,
2093+ },
2094+ )
2095+
2096+ self .assertEqual (response .status_code , 200 )
2097+ res_json = json .loads (response .content .decode ("utf-8" ))
2098+
2099+ # Should have sync response structure
2100+ self .assertIn ("results" , res_json )
2101+ self .assertEqual (len (res_json ["results" ]), 1 )
2102+ self .assertTrue (res_json ["results" ][0 ]["success" ])
2103+
2104+ # Verify actual enrollment happened
2105+ self .assertTrue (CourseEnrollment .is_enrolled (self .notenrolled_student , self .course .id ))
2106+
19072107
19082108@ddt .ddt
19092109class TestInstructorAPIBulkBetaEnrollment (SharedModuleStoreTestCase , LoginEnrollmentTestCase ):
0 commit comments