diff --git a/enterprise_access/apps/api_client/lms_client.py b/enterprise_access/apps/api_client/lms_client.py index 368069f9..b73a7b7b 100755 --- a/enterprise_access/apps/api_client/lms_client.py +++ b/enterprise_access/apps/api_client/lms_client.py @@ -404,9 +404,14 @@ class LmsUserApiClient(BaseUserApiClient): API client for user-specific calls to the LMS service. """ enterprise_api_base_url = f"{settings.LMS_URL}/enterprise/api/v1/" + enterprise_learner_portal_api_base_url = f"{settings.LMS_URL}/enterprise_learner_portal/api/v1/" + default_enterprise_enrollment_intentions_learner_status_endpoint = ( f'{enterprise_api_base_url}default-enterprise-enrollment-intentions/learner-status/' ) + enterprise_course_enrollments_endpoint = ( + f'{enterprise_learner_portal_api_base_url}enterprise_course_enrollments/' + ) def get_default_enterprise_enrollment_intentions_learner_status(self, enterprise_customer_uuid): """ @@ -435,3 +440,35 @@ def get_default_enterprise_enrollment_intentions_learner_status(self, enterprise f"Response content: {response.content if response else None}" ) raise + + def get_enterprise_course_enrollments(self, enterprise_customer_uuid, **params): + """ + Fetches course enrollments for a given enterprise customer. + + Arguments: + enterprise_customer_uuid (str): UUID of the enterprise customer + params (dict): Additional query parameters to include in the request + + Returns: + dict: Dictionary representation of the JSON response from the API + """ + query_params = { + 'enterprise_id': enterprise_customer_uuid, + **params, + } + response = None + try: + response = self.get( + self.enterprise_course_enrollments_endpoint, + params=query_params, + timeout=settings.LMS_CLIENT_TIMEOUT + ) + response.raise_for_status() + return response.json() + except requests.exceptions.HTTPError as exc: + logger.exception( + f"Failed to fetch enterprise course enrollments for enterprise customer " + f"{enterprise_customer_uuid} and learner {self.request_user.lms_user_id}: {exc} " + f"Response content: {response.content if response else None}" + ) + raise diff --git a/enterprise_access/apps/api_client/tests/test_lms_client.py b/enterprise_access/apps/api_client/tests/test_lms_client.py index 6ecee2c8..c12eaf15 100644 --- a/enterprise_access/apps/api_client/tests/test_lms_client.py +++ b/enterprise_access/apps/api_client/tests/test_lms_client.py @@ -317,6 +317,28 @@ def setUp(self): self.mock_course_run_key = 'course-v1:edX+DemoX+Demo_Course' self.mock_enterprise_catalog_uuid = self.faker.uuid4() + self.mock_enterprise_course_enrollment = { + "certificate_download_url": None, + "emails_enabled": True, + "course_run_id": "course-v1:BabsonX+MIS01x+1T2019", + "course_run_status": "in_progress", + "created": "2023-09-29T14:24:45.409031+00:00", + "start_date": "2019-03-19T10:00:00Z", + "end_date": "2025-12-31T04:30:00Z", + "display_name": "AI for Leaders", + "course_run_url": "https://learning.edx.org/course/course-v1:BabsonX+MIS01x+1T2019/home", + "due_dates": [], + "pacing": "self", + "org_name": "BabsonX", + "is_revoked": False, + "is_enrollment_active": True, + "mode": "verified", + "resume_course_run_url": None, + "course_key": "BabsonX+MIS01x", + "course_type": "verified-audit", + "product_source": "edx", + "enroll_by": "2025-12-21T23:59:59.099999Z" + } self.mock_default_enterprise_enrollment_intentions_learner_status = { "uuid": self.faker.uuid4(), "content_key": self.mock_course_key, @@ -342,6 +364,120 @@ def setUp(self): "is_existing_enrollment_audit": None } + @mock.patch('requests.Session.send') + @mock.patch('crum.get_current_request') + def test_get_enterprise_course_enrollments( + self, + mock_crum_get_current_request, + mock_send, + ): + """ + Verify client hits the right URL for enterprise course enrollments. + """ + expected_url = LmsUserApiClient.enterprise_course_enrollments_endpoint + request = self.factory.get(expected_url) + request.headers = { + "Authorization": 'test-auth', + self.request_id_key: 'test-request-id' + } + request.user = self.user + context = { + "request": request + } + + mock_crum_get_current_request.return_value = request + + expected_result = [self.mock_enterprise_course_enrollment] + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = expected_result + + mock_send.return_value = mock_response + + client = LmsUserApiClient(context['request']) + additional_params = {'is_active': True} + result = client.get_enterprise_course_enrollments( + enterprise_customer_uuid=self.mock_enterprise_customer_uuid, + **additional_params + ) + + mock_send.assert_called_once() + + prepared_request = mock_send.call_args[0][0] + prepared_request_kwargs = mock_send.call_args[1] + + # Assert base request URL/method is correct + parsed_url = urlparse(prepared_request.url) + self.assertEqual(f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}", expected_url) + self.assertEqual(prepared_request.method, 'GET') + + # Assert query parameters are correctly set + parsed_params = parse_qs(parsed_url.query) + expected_params = { + 'enterprise_id': [self.mock_enterprise_customer_uuid], + 'is_active': ['True'] + } + self.assertEqual(parsed_params, expected_params) + + # Assert headers are correctly set + self.assertEqual(prepared_request.headers['Authorization'], 'test-auth') + self.assertEqual(prepared_request.headers[self.request_id_key], 'test-request-id') + + # Assert timeout is set + self.assertIn('timeout', prepared_request_kwargs) + self.assertEqual(prepared_request_kwargs['timeout'], settings.LMS_CLIENT_TIMEOUT) + + # Assert the response is as expected + self.assertEqual(result, expected_result) + + @mock.patch('requests.Session.send') + @mock.patch('crum.get_current_request') + @mock.patch('enterprise_access.apps.api_client.lms_client.logger', return_value=mock.MagicMock()) + def test_get_enterprise_course_enrollments_http_error( + self, + mock_logger, + mock_crum_get_current_request, + mock_send, + ): + """ + Verify client raises HTTPError on non-200 response. + """ + expected_url = LmsUserApiClient.enterprise_course_enrollments_endpoint + request = self.factory.get(expected_url) + request.headers = { + "Authorization": 'test-auth', + self.request_id_key: 'test-request-id' + } + request.user = self.user + context = { + "request": request + } + + mock_crum_get_current_request.return_value = request + + mock_response = mock.Mock() + mock_response.status_code = 400 + mock_response.json.return_value = {'detail': 'Bad Request'} + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("HTTPError") + + mock_send.return_value = mock_response + + client = LmsUserApiClient(context['request']) + + with self.assertRaises(requests.exceptions.HTTPError): + client.get_enterprise_course_enrollments( + enterprise_customer_uuid=self.mock_enterprise_customer_uuid + ) + + mock_send.assert_called_once() + + # Verify that logger.exception was called with the expected message + mock_logger.exception.assert_called_once_with( + f"Failed to fetch enterprise course enrollments for enterprise customer " + f"{self.mock_enterprise_customer_uuid} and learner {self.user.lms_user_id}: HTTPError " + f"Response content: {mock_response.content}" + ) + @mock.patch('requests.Session.send') @mock.patch('crum.get_current_request') def test_get_default_enterprise_enrollment_intentions_learner_status( @@ -419,7 +555,7 @@ def test_get_default_enterprise_enrollment_intentions_learner_status( # Assert timeout is set self.assertIn('timeout', prepared_request_kwargs) - self.assertEqual(prepared_request_kwargs['timeout'], settings.LICENSE_MANAGER_CLIENT_TIMEOUT) + self.assertEqual(prepared_request_kwargs['timeout'], settings.LMS_CLIENT_TIMEOUT) # Assert the response is as expected self.assertEqual(result, expected_result)