diff --git a/eox_nelp/api_clients/authenticators.py b/eox_nelp/api_clients/authenticators.py index 00ecad77..58e042e3 100644 --- a/eox_nelp/api_clients/authenticators.py +++ b/eox_nelp/api_clients/authenticators.py @@ -57,7 +57,7 @@ def authenticate(self, api_client): if not headers: client = BackendApplicationClient(client_id=api_client.client_id) oauth = OAuth2Session(client_id=api_client.client_id, client=client) - authenticate_url = f"{api_client.base_url}/oauth/token" + authenticate_url = f"{api_client.base_url}/{api_client.authentication_path}" response = oauth.fetch_token( token_url=authenticate_url, client_secret=api_client.client_secret, diff --git a/eox_nelp/api_clients/futurex.py b/eox_nelp/api_clients/futurex.py index d169dc71..d43d9361 100644 --- a/eox_nelp/api_clients/futurex.py +++ b/eox_nelp/api_clients/futurex.py @@ -22,6 +22,7 @@ class FuturexApiClient(AbstractAPIRestClient): def __init__(self): self.client_id = getattr(settings, "FUTUREX_API_CLIENT_ID") self.client_secret = getattr(settings, "FUTUREX_API_CLIENT_SECRET") + self.authentication_path = "oauth/token/" super().__init__() diff --git a/eox_nelp/api_clients/pearson_engine.py b/eox_nelp/api_clients/pearson_engine.py new file mode 100644 index 00000000..af130ac6 --- /dev/null +++ b/eox_nelp/api_clients/pearson_engine.py @@ -0,0 +1,143 @@ +"""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. + """ + return { + "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_first_name": user.extrainfo.arabic_first_name, + "arabic_last_name": user.extrainfo.arabic_last_name, + } + + 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/exam-authorization/" + 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/mixins.py b/eox_nelp/api_clients/tests/mixins.py index 93216a41..a10adbd4 100644 --- a/eox_nelp/api_clients/tests/mixins.py +++ b/eox_nelp/api_clients/tests/mixins.py @@ -7,7 +7,6 @@ TestSOAPClientMixin: Basic tests that can be implemented by AbstractSOAPClient children. TestPKCS12AuthenticatorMixin: Basic tests that can be implemented by PKCS12Authenticator children. """ -from django.conf import settings from django.core.cache import cache from mock import Mock, patch from oauthlib.oauth2 import MissingTokenError @@ -279,16 +278,14 @@ def test_successful_authentication(self, oauth2_session_mock): "expires_in": 200, } oauth2_session_mock.return_value.fetch_token = fetch_token_mock - authentication_url = f"{settings.FUTUREX_API_URL}/oauth/token" - client_secret = settings.FUTUREX_API_CLIENT_SECRET api_client = self.api_class() self.assertTrue(hasattr(api_client, "session")) self.assertTrue("Authorization" in api_client.session.headers) fetch_token_mock.assert_called_with( - token_url=authentication_url, - client_secret=client_secret, + token_url=f"{api_client.base_url}/{api_client.authentication_path}", + client_secret=api_client.client_secret, include_client_id=True, ) diff --git a/eox_nelp/api_clients/tests/test_pearson_engine.py b/eox_nelp/api_clients/tests/test_pearson_engine.py new file mode 100644 index 00000000..c7495124 --- /dev/null +++ b/eox_nelp/api_clients/tests/test_pearson_engine.py @@ -0,0 +1,150 @@ +""" +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/exam-authorization/", { + "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_first_name = "اختبار" + arabic_last_name = "مستخدم" + + profile = Profile() + extrainfo = ExtraInfo() + + return MockUser() diff --git a/eox_nelp/settings/test.py b/eox_nelp/settings/test.py index 83b3686d..0df6be62 100644 --- a/eox_nelp/settings/test.py +++ b/eox_nelp/settings/test.py @@ -63,6 +63,12 @@ def plugin_settings(settings): # pylint: disable=function-redefined settings.PEARSON_RTI_WSDL_PASSWORD = "12345678p" settings.PEARSON_RTI_WSDL_CLIENT_ID = "12345678" + settings.PEARSON_ENGINE_API_URL = 'https://testing.com' + settings.PEARSON_ENGINE_API_CLIENT_SECRET = "12345678p" + settings.PEARSON_ENGINE_API_CLIENT_ID = "12345678" + + +PLATFORM_NAME = "Testing environment" SETTINGS = SettingsClass() plugin_settings(SETTINGS)