diff --git a/eox_nelp/api_clients/pearson_engine.py b/eox_nelp/api_clients/pearson_engine.py deleted file mode 100644 index d6043b85..00000000 --- a/eox_nelp/api_clients/pearson_engine.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Client module for Pearson Engine API integration. - -Classes: - PearsonEngineApiClient: Base class to interact with Pearson Engine services. -""" -from django.conf import settings - -from eox_nelp.api_clients import AbstractAPIRestClient -from eox_nelp.api_clients.authenticators import Oauth2Authenticator - - -class PearsonEngineApiClient(AbstractAPIRestClient): - """ - Client to interact with Pearson Engine API for importing candidate demographics - and exam authorization data. - - Attributes: - client_id (str): The client ID for Pearson Engine API. - client_secret (str): The client secret for Pearson Engine API. - authentication_path (str): The path for authentication. - """ - authentication_class = Oauth2Authenticator - - def __init__(self): - self.client_id = getattr(settings, "PEARSON_ENGINE_API_CLIENT_ID") - self.client_secret = getattr(settings, "PEARSON_ENGINE_API_CLIENT_SECRET") - self.authentication_path = "oauth2/token/" - - super().__init__() - - @property - def base_url(self): - """Return the base URL for Pearson Engine API.""" - return getattr(settings, "PEARSON_ENGINE_API_URL") - - def _get_user_data(self, user): - """ - Retrieve user data for the request payload. - - Args: - user: The user object containing user data. - - Returns: - dict: The user data formatted for the request. - """ - user_data = { - "username": user.username, - "first_name": user.first_name, - "last_name": user.last_name, - "email": user.email, - "country": user.profile.country.code, - "city": user.profile.city, - "phone": user.profile.phone_number, - "address": user.profile.mailing_address, - "arabic_full_name": user.extrainfo.arabic_name, - "arabic_first_name": user.extrainfo.arabic_first_name, - "arabic_last_name": user.extrainfo.arabic_last_name, - } - - if user.extrainfo.national_id: - user_data["national_id"] = user.extrainfo.national_id - - return user_data - - def _get_platform_data(self): - """ - Retrieve platform data for the request payload. - - Returns: - dict: The platform data formatted for the request. - """ - return { - "name": settings.PLATFORM_NAME, - "tenant": getattr(settings, "EDNX_TENANT_DOMAIN", None) - } - - def _get_exam_data(self, exam_id): - """ - Retrieve exam data for the request payload. - - Args: - exam_id: The external key for the exam. - - Returns: - dict: The exam data formatted for the request. - """ - return { - "external_key": exam_id, - } - - def import_candidate_demographics(self, user, **kwargs): - """ - Import candidate demographics into Pearson Engine. - - Args: - user: The user object containing user data. - - Returns: - dict: The response from Pearson Engine API. - """ - path = "rti/api/v1/candidate-demographics/" - data = { - "user_data": self._get_user_data(user), - "platform_data": self._get_platform_data(), - **kwargs - } - - return self.make_post(path, data) - - def import_exam_authorization(self, user, exam_id, **kwargs): - """ - Import exam authorization data into Pearson Engine. - - Args: - user: The user object containing user data. - exam_id: The external key for the exam. - - Returns: - dict: The response from Pearson Engine API. - """ - path = "rti/api/v1/exam-authorization/" - data = { - "user_data": {"username": user.username}, - "exam_data": self._get_exam_data(exam_id), - **kwargs - } - - return self.make_post(path, data) - - def real_time_import(self, user, exam_id, **kwargs): - """ - Perform a real-time import of exam authorization data. - - Args: - user: The user object containing user data. - exam_id: The external key for the exam. - - Returns: - dict: The response from Pearson Engine API. - """ - path = "rti/api/v1/real-time-import/" - data = { - "user_data": self._get_user_data(user), - "exam_data": self._get_exam_data(exam_id), - "platform_data": self._get_platform_data(), - **kwargs - } - - return self.make_post(path, data) diff --git a/eox_nelp/api_clients/tests/test_pearson_engine.py b/eox_nelp/api_clients/tests/test_pearson_engine.py deleted file mode 100644 index cb411cae..00000000 --- a/eox_nelp/api_clients/tests/test_pearson_engine.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -Test suite for PearsonEngineApiClient. - -This module contains unit tests for the PearsonEngineApiClient class, which -integrates with Pearson Engine services. The tests cover the main methods -import_candidate_demographics, import_exam_authorization, and real_time_import -to ensure they work as expected. - -Classes: - TestPearsonEngineApiClient: Unit test class for PearsonEngineApiClient. -""" -import unittest -from unittest.mock import patch - -import requests -from django_countries.fields import Country - -from eox_nelp.api_clients.pearson_engine import PearsonEngineApiClient -from eox_nelp.api_clients.tests.mixins import TestOauth2AuthenticatorMixin, TestRestApiClientMixin - - -class TestPearsonEngineApiClient(TestRestApiClientMixin, TestOauth2AuthenticatorMixin, unittest.TestCase): - """ - Test suite for PearsonEngineApiClient. - - This class tests the methods of PearsonEngineApiClient, including - import_candidate_demographics, import_exam_authorization, and real_time_import. - """ - - def setUp(self): - """ - Set up the test environment. - - This method initializes the API client class for testing. - """ - self.api_class = PearsonEngineApiClient - - @patch.object(PearsonEngineApiClient, "make_post") - @patch.object(PearsonEngineApiClient, "_authenticate") - def test_import_candidate_demographics(self, auth_mock, post_mock): - """ - Test import_candidate_demographics API call. - - Expected behavior: - - Response is the expected value. - - make_post is called with the correct path and payload. - """ - auth_mock.return_value = requests.Session() - expected_value = { - "status": {"success": True}, - } - post_mock.return_value = expected_value - - user = self._create_test_user() - api_client = self.api_class() - - response = api_client.import_candidate_demographics(user) - - self.assertDictEqual(response, expected_value) - # pylint: disable=protected-access - post_mock.assert_called_with("rti/api/v1/candidate-demographics/", { - "user_data": api_client._get_user_data(user), - "platform_data": api_client._get_platform_data(), - }) - - @patch.object(PearsonEngineApiClient, "make_post") - @patch.object(PearsonEngineApiClient, "_authenticate") - def test_import_exam_authorization(self, auth_mock, post_mock): - """ - Test import_exam_authorization API call. - - Expected behavior: - - Response is the expected value. - - make_post is called with the correct path and payload. - """ - auth_mock.return_value = requests.Session() - expected_value = { - "status": {"success": True, "message": "successful", "code": 1} - } - post_mock.return_value = expected_value - user = self._create_test_user() - exam_id = "exam-123" - api_client = self.api_class() - - response = api_client.import_exam_authorization(user, exam_id, transaction_type="Add") - - self.assertDictEqual(response, expected_value) - post_mock.assert_called_with("rti/api/v1/exam-authorization/", { - "user_data": {"username": user.username}, - "exam_data": api_client._get_exam_data(exam_id), # pylint: disable=protected-access - "transaction_type": "Add", - }) - - @patch.object(PearsonEngineApiClient, "make_post") - @patch.object(PearsonEngineApiClient, "_authenticate") - def test_real_time_import(self, auth_mock, post_mock): - """ - Test real_time_import API call. - - Expected behavior: - - Response is the expected value. - - make_post is called with the correct path and payload. - """ - auth_mock.return_value = requests.Session() - expected_value = { - "status": {"success": True, "message": "successful", "code": 1}, - } - post_mock.return_value = expected_value - user = self._create_test_user() - exam_id = "exam-123" - api_client = self.api_class() - - response = api_client.real_time_import(user, exam_id) - - self.assertDictEqual(response, expected_value) - # pylint: disable=protected-access - post_mock.assert_called_with("rti/api/v1/real-time-import/", { - "user_data": api_client._get_user_data(user), - "exam_data": api_client._get_exam_data(exam_id), - "platform_data": api_client._get_platform_data(), - }) - - def _create_test_user(self): - """ - Create a mock user for testing. - - Returns: - user: A mock user object with necessary attributes. - """ - # pylint: disable=missing-class-docstring - class MockUser: - username = "testuser" - first_name = "Test" - last_name = "User" - email = "testuser@example.com" - - class Profile: - country = Country("US") - city = "New York" - phone_number = "+1234567890" - mailing_address = "123 Test St" - - class ExtraInfo: - arabic_name = "مستخدم اختبار" - arabic_first_name = "اختبار" - arabic_last_name = "مستخدم" - national_id = "1234567890" - - profile = Profile() - extrainfo = ExtraInfo() - - return MockUser() diff --git a/eox_nelp/pearson_vue/tasks.py b/eox_nelp/pearson_vue/tasks.py index 0cc29db4..7c9c31ee 100644 --- a/eox_nelp/pearson_vue/tasks.py +++ b/eox_nelp/pearson_vue/tasks.py @@ -6,9 +6,9 @@ """ from celery import shared_task from django.contrib.auth import get_user_model +from nelc_api_clients.clients.pearson_engine import PearsonEngineApiClient from requests import exceptions -from eox_nelp.api_clients.pearson_engine import PearsonEngineApiClient from eox_nelp.pearson_vue.constants import ALLOWED_RTI_ACTIONS from eox_nelp.pearson_vue.pipeline import audit_method, rename_function from eox_nelp.pearson_vue.rti_backend import ( @@ -17,7 +17,7 @@ ExamAuthorizationDataImport, RealTimeImport, ) -from eox_nelp.pearson_vue.utils import update_user_engines +from eox_nelp.pearson_vue.utils import generate_action_parameters, update_user_engines User = get_user_model() @@ -133,11 +133,8 @@ def audit_pearson_engine_action(user_id, exam_id, action_key, **kwargs): user = User.objects.get(id=user_id) update_user_engines(user, action_name, exam_id) action = getattr(PearsonEngineApiClient(), action_key) - response = action( - user=user, - exam_id=exam_id, - **kwargs - ) + parameters = generate_action_parameters(user, exam_id) + response = action(**parameters, **kwargs) if response.get("error"): raise Exception(response.get("message", "Unknown error")) # pylint: disable=broad-exception-raised diff --git a/eox_nelp/pearson_vue/tests/test_tasks.py b/eox_nelp/pearson_vue/tests/test_tasks.py index 20315a1c..244ae384 100644 --- a/eox_nelp/pearson_vue/tests/test_tasks.py +++ b/eox_nelp/pearson_vue/tests/test_tasks.py @@ -117,6 +117,15 @@ def setUp(self): self.user, _ = User.objects.get_or_create(username="vader") self.exam_id = "exam123" self.kwargs = {"extra_info": "test"} + self.action_parameters = { + "user_data": "test", + "platform_data": "test", + "exam_data": "test", + } + self.generate_action_parameters_patcher = patch("eox_nelp.pearson_vue.tasks.generate_action_parameters") + self.generate_action_parameters_mock = self.generate_action_parameters_patcher.start() + self.generate_action_parameters_mock.return_value = self.action_parameters + self.addCleanup(self.generate_action_parameters_patcher.stop) @patch("eox_nelp.pearson_vue.tasks.update_user_engines") @patch("eox_nelp.pearson_vue.tasks.PearsonEngineApiClient") @@ -135,7 +144,7 @@ def test_real_time_import_rti(self, mock_api_client, update_user_engines_mock): real_time_import_task_v2(self.user.id, action_name=action_name, **self.kwargs) update_user_engines_mock.assert_called_once_with(self.user, action_name, None) - mock_action.assert_called_once_with(user=self.user, exam_id=None, **self.kwargs) + mock_action.assert_called_once_with(**self.action_parameters, **self.kwargs) @patch("eox_nelp.pearson_vue.tasks.update_user_engines") @patch("eox_nelp.pearson_vue.tasks.PearsonEngineApiClient") @@ -154,7 +163,7 @@ def test_real_time_import_cdd(self, mock_api_client, update_user_engines_mock): real_time_import_task_v2(self.user.id, action_name=action_name, **self.kwargs) update_user_engines_mock.assert_called_once_with(self.user, action_name, None) - mock_action.assert_called_once_with(user=self.user, exam_id=None, **self.kwargs) + mock_action.assert_called_once_with(**self.action_parameters, **self.kwargs) @patch("eox_nelp.pearson_vue.tasks.update_user_engines") @patch("eox_nelp.pearson_vue.tasks.PearsonEngineApiClient") @@ -173,7 +182,7 @@ def test_real_time_import_ead(self, mock_api_client, update_user_engines_mock): real_time_import_task_v2(self.user.id, exam_id=self.exam_id, action_name=action_name, **self.kwargs) update_user_engines_mock.assert_called_once_with(self.user, action_name, self.exam_id,) - mock_action.assert_called_once_with(user=self.user, exam_id=self.exam_id, **self.kwargs) + mock_action.assert_called_once_with(**self.action_parameters, **self.kwargs) @patch("eox_nelp.pearson_vue.tasks.update_user_engines") def test_real_time_import_invalid_action(self, update_user_engines_mock): @@ -186,6 +195,7 @@ def test_real_time_import_invalid_action(self, update_user_engines_mock): with self.assertRaises(KeyError): real_time_import_task_v2(self.user.id, action_name="invalid_action") update_user_engines_mock.assert_not_called() + self.generate_action_parameters_mock.assert_not_called() @patch("eox_nelp.pearson_vue.tasks.update_user_engines") @patch('eox_nelp.pearson_vue.tasks.PearsonEngineApiClient') @@ -201,6 +211,7 @@ def test_real_time_import_user_not_found(self, mock_api_client, update_user_engi real_time_import_task_v2(12345678, action_name="rti") mock_api_client.assert_not_called() update_user_engines_mock.assert_not_called() + self.generate_action_parameters_mock.assert_not_called() @patch("eox_nelp.pearson_vue.tasks.update_user_engines") @patch("eox_nelp.pearson_vue.tasks.PearsonEngineApiClient") @@ -221,10 +232,9 @@ def test_raise_exception_on_error_response(self, mock_api_client, update_user_en } action_name = "rti" mock_api_client.return_value = MagicMock(**{"real_time_import": mock_action}) - with self.assertRaises(Exception) as context: real_time_import_task_v2(self.user.id, action_name=action_name, **self.kwargs) update_user_engines_mock.assert_called_once_with(self.user, action_name, None) - mock_action.assert_called_once_with(user=self.user, exam_id=None, **self.kwargs) + mock_action.assert_called_once_with(**self.action_parameters, **self.kwargs) self.assertEqual(expected_message, str(context.exception)) diff --git a/eox_nelp/pearson_vue/tests/test_utils.py b/eox_nelp/pearson_vue/tests/test_utils.py index 23e89ba1..1278ad46 100644 --- a/eox_nelp/pearson_vue/tests/test_utils.py +++ b/eox_nelp/pearson_vue/tests/test_utils.py @@ -4,7 +4,7 @@ UpdatePayloadCddRequestTestCase: Tests cases for update_xml_with_dict using payload with cdd request tag cases. UpdatePayloadEadRequestTestCase: Test cased for update_xml_with_dict using payload with ead request tag cases. """ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import xmltodict from bs4 import BeautifulSoup @@ -15,7 +15,10 @@ from eox_nelp.pearson_vue.constants import PAYLOAD_CDD, PAYLOAD_EAD from eox_nelp.pearson_vue.models import PearsonEngine from eox_nelp.pearson_vue.utils import ( + generate_action_parameters, generate_client_authorization_id, + get_platform_data, + get_user_data, is_cp1252, update_user_engines, update_xml_with_dict, @@ -564,3 +567,140 @@ def test_does_not_increment_course_value_for_other_actions(self): self.assertEqual(user.pearsonengine.ead_triggers, 0) self.assertEqual(user.pearsonengine.rti_triggers, 0) self.assertDictEqual(user.pearsonengine.courses, {}) + + +class TestGetUserData(TestCase): + """Test case for get_user_data.""" + + def setUp(self): + """ + Set up the test environment. + """ + self.user = MagicMock() + self.user.username = "testuser" + self.user.first_name = "Test" + self.user.last_name = "User" + self.user.email = "test@example.com" + self.user.profile.country.code = "US" + self.user.profile.city = "Test City" + self.user.profile.phone_number = "123-456-7890" + self.user.profile.mailing_address = "123 Test St" + self.user.extrainfo.arabic_name = "اسم المستخدم" + self.user.extrainfo.arabic_first_name = "الاسم الاول" + self.user.extrainfo.arabic_last_name = "اسم العائلة" + self.user.extrainfo.national_id = "123456789" + + def test_get_user_data(self): + """ + Test get_user_data function with all user data available, including national_id. + + Expected behavior: + - The result is a dict with all user data. + """ + expected_result = { + "username": "testuser", + "first_name": "Test", + "last_name": "User", + "email": "test@example.com", + "country": "US", + "city": "Test City", + "phone": "123-456-7890", + "address": "123 Test St", + "arabic_full_name": "اسم المستخدم", + "arabic_first_name": "الاسم الاول", + "arabic_last_name": "اسم العائلة", + "national_id": "123456789", + } + + result = get_user_data(self.user) + + self.assertEqual(result, expected_result) + + +class TestGetPlatformData(TestCase): + """Test case for get_platform_data.""" + + def setUp(self): + """Set up the test environment.""" + self.settings_patcher = patch("eox_nelp.pearson_vue.utils.settings") + self.mock_settings = self.settings_patcher.start() + self.addCleanup(self.settings_patcher.stop) + + def test_get_platform_data_with_tenant(self): + """ + Test get_platform_data function with PLATFORM_NAME and EDNX_TENANT_DOMAIN defined. + + Expected behavior: + - The result is a dict with platform name and tenant domain. + """ + self.mock_settings.PLATFORM_NAME = "Test Platform" + self.mock_settings.EDNX_TENANT_DOMAIN = "test.example.com" + expected_result = { + "name": "Test Platform", + "tenant": "test.example.com", + } + + result = get_platform_data() + + self.assertEqual(result, expected_result) + + def test_get_platform_data_without_tenant(self): + """ + Test get_platform_data function with only PLATFORM_NAME defined. + + Expected behavior: + - The result is a dict with platform name and tenant is None. + """ + self.mock_settings.PLATFORM_NAME = "Test Platform" + self.mock_settings.EDNX_TENANT_DOMAIN = None + expected_result = { + "name": "Test Platform", + "tenant": None, + } + + result = get_platform_data() + + self.assertEqual(result, expected_result) + + +class TestGenerateActionParameters(TestCase): + """Test case for generate_action_parameters.""" + + def setUp(self): + """ + Set up the test environment. + """ + self.mock_get_user_data = self.patch("eox_nelp.pearson_vue.utils.get_user_data") + self.mock_get_platform_data = self.patch("eox_nelp.pearson_vue.utils.get_platform_data") + + self.mock_get_user_data.return_value = {"user_data": "mock"} + self.mock_get_platform_data.return_value = {"platform_data": "mock"} + + def patch(self, target, **kwargs): + """Patch a target and return the mock""" + patcher = patch(target, **kwargs) + mock = patcher.start() + self.addCleanup(patcher.stop) + return mock + + def test_generate_action_parameters(self): + """ + Test generate_action_parameters function with exam_id. + + Expected behavior: + - The result is a dict with user_data, platform_data, and exam_data. + - get_user_data, get_platform_data, and get_exam_data are called once. + - get_exam_data is called with exam_id. + """ + user = MagicMock() + exam_id = "exam123" + + result = generate_action_parameters(user, exam_id) + + self.assertEqual(result, { + "user_data": {"user_data": "mock"}, + "platform_data": {"platform_data": "mock"}, + "exam_data": {"external_key": exam_id}, + }) + self.mock_get_user_data.assert_called_once_with(user) + self.mock_get_platform_data.assert_called_once() diff --git a/eox_nelp/pearson_vue/utils.py b/eox_nelp/pearson_vue/utils.py index 2aa3de97..d2d3c51d 100644 --- a/eox_nelp/pearson_vue/utils.py +++ b/eox_nelp/pearson_vue/utils.py @@ -5,6 +5,7 @@ import re import xmltodict +from django.conf import settings from pydantic.v1.utils import deep_update from eox_nelp.edxapp_wrapper.student import AnonymousUserId, CourseEnrollment, anonymous_id_for_user @@ -67,3 +68,66 @@ def update_user_engines(user, action_type, course_id=None): pearson_engine.increment_trigger(action_type) if course_id: pearson_engine.increment_course_value(course_id) + + +def get_user_data(user): + """Retrieve user data for the request payload. + + Args: + user: The user object containing user data. + + Returns: + dict: The user data formatted for the request. + """ + user_data = { + "username": user.username, + "first_name": user.first_name, + "last_name": user.last_name, + "email": user.email, + "country": user.profile.country.code, + "city": user.profile.city, + "phone": user.profile.phone_number, + "address": user.profile.mailing_address, + "arabic_full_name": user.extrainfo.arabic_name, + "arabic_first_name": user.extrainfo.arabic_first_name, + "arabic_last_name": user.extrainfo.arabic_last_name, + } + + if user.extrainfo.national_id: + user_data["national_id"] = user.extrainfo.national_id + + return user_data + + +def get_platform_data(): + """Retrieve platform data for the request payload. + + Returns: + dict: The platform data formatted for the request. + """ + return { + "name": settings.PLATFORM_NAME, + "tenant": getattr(settings, "EDNX_TENANT_DOMAIN", None) + } + + +def generate_action_parameters(user, exam_id): + """ + Select the appropriate parameters for the action based on the action name. + + Args: + action_name (str): The name of the action to perform. + user (User): the user to be processed. + exam_id (str, optional): The ID of the exam for authorization. Default is None. + **kwargs: Additional keyword arguments + + Returns: + dict: The parameters for the action. + """ + action_parameters = { + "user_data": get_user_data(user), + "platform_data": get_platform_data(), + "exam_data": {"external_key": exam_id} + } + + return action_parameters diff --git a/eox_nelp/signals/tasks.py b/eox_nelp/signals/tasks.py index f5997ec5..2a4b4d43 100644 --- a/eox_nelp/signals/tasks.py +++ b/eox_nelp/signals/tasks.py @@ -61,7 +61,7 @@ def _post_futurex_progress(data): data (dict): dict to send to futurex enrollment-progress path. """ api_client = FuturexApiClient() - response = api_client.enrollment_progress(data) # pylint: disable=no-member + response = api_client.enrollment_progress(data) logger.info( "send_futurex_progress --- The data %s was sent to the futurex service host %s. The response was: %s",