From 0d3dac5e7cbec555243ee4d6c82104d25473d8f7 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Sun, 28 Jul 2024 17:18:12 -0500 Subject: [PATCH 1/4] feat: add Pearson engine client --- eox_nelp/api_clients/authenticators.py | 2 +- eox_nelp/api_clients/futurex.py | 1 + eox_nelp/api_clients/pearson_engine.py | 141 +++++++++++++++++ eox_nelp/api_clients/tests/mixins.py | 7 +- .../api_clients/tests/test_pearson_engine.py | 149 ++++++++++++++++++ eox_nelp/settings/test.py | 6 + 6 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 eox_nelp/api_clients/pearson_engine.py create mode 100644 eox_nelp/api_clients/tests/test_pearson_engine.py 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..75e7dbe8 --- /dev/null +++ b/eox_nelp/api_clients/pearson_engine.py @@ -0,0 +1,141 @@ +"""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): + """ + 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/" + + candidate = { + "user_data": self._get_user_data(user), + "platform_data": self._get_platform_data(), + } + + return self.make_post(path, candidate) + + def import_exam_authorization(self, user, exam_id): + """ + 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/" + exam_data = { + "user_data": {"username": user.username}, + "exam_data": self._get_exam_data(exam_id) + } + + return self.make_post(path, exam_data) + + def real_time_import(self, user, exam_id): + """ + 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(), + } + + 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..0a0fb497 --- /dev/null +++ b/eox_nelp/api_clients/tests/test_pearson_engine.py @@ -0,0 +1,149 @@ +""" +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) + + 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 + }) + + @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) From d9114ead21428f906372bc723bb67c2cb4860a1e Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Mon, 29 Jul 2024 12:37:04 -0500 Subject: [PATCH 2/4] feat: add transaction_type to exam payload --- eox_nelp/api_clients/pearson_engine.py | 5 +++-- eox_nelp/api_clients/tests/test_pearson_engine.py | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/eox_nelp/api_clients/pearson_engine.py b/eox_nelp/api_clients/pearson_engine.py index 75e7dbe8..0e28bb87 100644 --- a/eox_nelp/api_clients/pearson_engine.py +++ b/eox_nelp/api_clients/pearson_engine.py @@ -101,7 +101,7 @@ def import_candidate_demographics(self, user): return self.make_post(path, candidate) - def import_exam_authorization(self, user, exam_id): + def import_exam_authorization(self, user, exam_id, transaction_type="Add"): """ Import exam authorization data into Pearson Engine. @@ -115,7 +115,8 @@ def import_exam_authorization(self, user, exam_id): path = "rti/api/v1/exam-authorization/" exam_data = { "user_data": {"username": user.username}, - "exam_data": self._get_exam_data(exam_id) + "exam_data": self._get_exam_data(exam_id), + "transaction_type": transaction_type, } return self.make_post(path, exam_data) diff --git a/eox_nelp/api_clients/tests/test_pearson_engine.py b/eox_nelp/api_clients/tests/test_pearson_engine.py index 0a0fb497..3db7c043 100644 --- a/eox_nelp/api_clients/tests/test_pearson_engine.py +++ b/eox_nelp/api_clients/tests/test_pearson_engine.py @@ -60,7 +60,7 @@ def test_import_candidate_demographics(self, auth_mock, post_mock): # 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() + "platform_data": api_client._get_platform_data(), }) @patch.object(PearsonEngineApiClient, "make_post") @@ -87,7 +87,8 @@ def test_import_exam_authorization(self, auth_mock, post_mock): 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 + "exam_data": api_client._get_exam_data(exam_id), # pylint: disable=protected-access + "transaction_type": "Add", }) @patch.object(PearsonEngineApiClient, "make_post") @@ -102,7 +103,7 @@ def test_real_time_import(self, auth_mock, post_mock): """ auth_mock.return_value = requests.Session() expected_value = { - "status": {"success": True, "message": "successful", "code": 1} + "status": {"success": True, "message": "successful", "code": 1}, } post_mock.return_value = expected_value user = self._create_test_user() @@ -116,7 +117,7 @@ def test_real_time_import(self, auth_mock, post_mock): 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() + "platform_data": api_client._get_platform_data(), }) def _create_test_user(self): From d5c6534b1d1003b33b68bfda5856b56e6735c56d Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Mon, 29 Jul 2024 13:01:09 -0500 Subject: [PATCH 3/4] chore: add kwargs parameter to client methods --- eox_nelp/api_clients/pearson_engine.py | 10 ++++++---- eox_nelp/api_clients/tests/test_pearson_engine.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/eox_nelp/api_clients/pearson_engine.py b/eox_nelp/api_clients/pearson_engine.py index 0e28bb87..016cb430 100644 --- a/eox_nelp/api_clients/pearson_engine.py +++ b/eox_nelp/api_clients/pearson_engine.py @@ -82,7 +82,7 @@ def _get_exam_data(self, exam_id): "external_key": exam_id, } - def import_candidate_demographics(self, user): + def import_candidate_demographics(self, user, **kwargs): """ Import candidate demographics into Pearson Engine. @@ -97,11 +97,12 @@ def import_candidate_demographics(self, user): candidate = { "user_data": self._get_user_data(user), "platform_data": self._get_platform_data(), + **kwargs } return self.make_post(path, candidate) - def import_exam_authorization(self, user, exam_id, transaction_type="Add"): + def import_exam_authorization(self, user, exam_id, **kwargs): """ Import exam authorization data into Pearson Engine. @@ -116,12 +117,12 @@ def import_exam_authorization(self, user, exam_id, transaction_type="Add"): exam_data = { "user_data": {"username": user.username}, "exam_data": self._get_exam_data(exam_id), - "transaction_type": transaction_type, + **kwargs } return self.make_post(path, exam_data) - def real_time_import(self, user, exam_id): + def real_time_import(self, user, exam_id, **kwargs): """ Perform a real-time import of exam authorization data. @@ -137,6 +138,7 @@ def real_time_import(self, user, exam_id): "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 index 3db7c043..c7495124 100644 --- a/eox_nelp/api_clients/tests/test_pearson_engine.py +++ b/eox_nelp/api_clients/tests/test_pearson_engine.py @@ -82,7 +82,7 @@ def test_import_exam_authorization(self, auth_mock, post_mock): exam_id = "exam-123" api_client = self.api_class() - response = api_client.import_exam_authorization(user, exam_id) + 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/", { From ce30e0fe4ea2db2c1855d9a746e6b98e360fcd27 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Mon, 29 Jul 2024 17:35:00 -0500 Subject: [PATCH 4/4] chore: pr recommendation --- eox_nelp/api_clients/pearson_engine.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/eox_nelp/api_clients/pearson_engine.py b/eox_nelp/api_clients/pearson_engine.py index 016cb430..af130ac6 100644 --- a/eox_nelp/api_clients/pearson_engine.py +++ b/eox_nelp/api_clients/pearson_engine.py @@ -93,14 +93,13 @@ def import_candidate_demographics(self, user, **kwargs): dict: The response from Pearson Engine API. """ path = "rti/api/v1/candidate-demographics/" - - candidate = { + data = { "user_data": self._get_user_data(user), "platform_data": self._get_platform_data(), **kwargs } - return self.make_post(path, candidate) + return self.make_post(path, data) def import_exam_authorization(self, user, exam_id, **kwargs): """ @@ -114,13 +113,13 @@ def import_exam_authorization(self, user, exam_id, **kwargs): dict: The response from Pearson Engine API. """ path = "rti/api/v1/exam-authorization/" - exam_data = { + data = { "user_data": {"username": user.username}, "exam_data": self._get_exam_data(exam_id), **kwargs } - return self.make_post(path, exam_data) + return self.make_post(path, data) def real_time_import(self, user, exam_id, **kwargs): """