From 40d40db27d648f09e5aec68dcd8e6e0f96845df9 Mon Sep 17 00:00:00 2001 From: Johan Seto Kaiba <51926076+johanseto@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:50:33 -0500 Subject: [PATCH] feat: validate cp1252 pydantic (#189) * feat: add util for is_cp1252 encoding * feat: add Cp1252Str type for main fields * feat: test with not arabic chars desired fields --- eox_nelp/pearson_vue/data_classes.py | 69 ++++++++++++++------- eox_nelp/pearson_vue/tests/test_pipeline.py | 22 +++++++ eox_nelp/pearson_vue/tests/test_utils.py | 35 ++++++++++- eox_nelp/pearson_vue/utils.py | 9 +++ 4 files changed, 112 insertions(+), 23 deletions(-) diff --git a/eox_nelp/pearson_vue/data_classes.py b/eox_nelp/pearson_vue/data_classes.py index 275c14ca..b2ae4118 100644 --- a/eox_nelp/pearson_vue/data_classes.py +++ b/eox_nelp/pearson_vue/data_classes.py @@ -1,19 +1,44 @@ """ Module to add data_classes related Pearson Vue Integration """ -from pydantic import BaseModel, Field +from pydantic import AfterValidator, BaseModel, Field +from typing_extensions import Annotated + +from eox_nelp.pearson_vue.utils import is_cp1252 + + +def validate_is_cp1252(text): + """Validate if a field is cp1252, raise exception if not. + Anyway return the same text. + + Args: + text (str): input to be validated + + Raises: + ValueError: if some char is not CP1252 + + Returns: + text (str) + """ + if not is_cp1252(text): + raise ValueError(f"{text} -> some char is not CP1252") + + return text + + +Cp1252Str = Annotated[str, AfterValidator(validate_is_cp1252)] class Phone(BaseModel): """Phone data class model""" - phone_number: str = Field(alias="phoneNumber", min_length=1, max_length=20) - phone_country_code: str = Field(alias="phoneCountryCode", min_length=1, max_length=3) + phone_number: Cp1252Str = Field(alias="phoneNumber", min_length=1, max_length=20) + phone_country_code: Cp1252Str = Field(alias="phoneCountryCode", min_length=1, max_length=3) class Mobile(BaseModel): """Mobile data class model""" - mobile_number: str = Field(alias="mobileNumber", min_length=1, max_length=20) - mobile_country_code: str = Field(alias="mobileCountryCode", min_length=1, max_length=3) + mobile_number: Cp1252Str = Field(alias="mobileNumber", min_length=1, max_length=20) + mobile_country_code: Cp1252Str = Field(alias="mobileCountryCode", min_length=1, max_length=3) class NativeAddress(BaseModel): @@ -28,9 +53,9 @@ class NativeAddress(BaseModel): class Address(BaseModel): """Address data class model""" - address1: str = Field(alias="address1", min_length=1, max_length=40) - city: str = Field(alias="city", min_length=1, max_length=32) - country: str = Field(alias="country", min_length=1, max_length=3) + address1: Cp1252Str = Field(alias="address1", min_length=1, max_length=40) + city: Cp1252Str = Field(alias="city", min_length=1, max_length=32) + country: Cp1252Str = Field(alias="country", min_length=1, max_length=3) phone: Phone = Field(alias="phone") mobile: Mobile = Field(alias="mobile") native_address: NativeAddress = Field(alias="nativeAddress") @@ -46,33 +71,33 @@ class AlternateAddress(Address): class CandidateName(BaseModel): """CandidateName data class model""" - first_name: str = Field(alias="firstName", min_length=1, max_length=30) - last_name: str = Field(alias="lastName", min_length=1, max_length=50) + first_name: Cp1252Str = Field(alias="firstName", min_length=1, max_length=30) + last_name: Cp1252Str = Field(alias="lastName", min_length=1, max_length=50) class WebAccountInfo(BaseModel): """WebAccountInfo data class model""" - email: str = Field(alias="email", min_length=1, max_length=255) + email: Cp1252Str = Field(alias="email", min_length=1, max_length=255) class CddRequest(BaseModel): """CddRequest data class model""" - client_candidate_id: str = Field(alias="@clientCandidateID", min_length=1, max_length=50) - client_id: str = Field(alias="@clientID", min_length=1) + client_candidate_id: Cp1252Str = Field(alias="@clientCandidateID", min_length=1, max_length=50) + client_id: Cp1252Str = Field(alias="@clientID", min_length=1) candidate_name: CandidateName = Field(alias="candidateName") - last_update: str = Field(alias="lastUpdate", min_length=1) + last_update: Cp1252Str = Field(alias="lastUpdate", min_length=1) primary_address: PrimaryAddress = Field(alias="primaryAddress") web_account_info: WebAccountInfo = Field(alias="webAccountInfo") class EadRequest(BaseModel): """EadRequest data class model""" - client_id: str = Field(alias="@clientID", min_length=1) - authorization_transaction_type: str = Field(alias="@authorizationTransactionType", min_length=1) - client_authorization_id: str = Field(alias="@clientAuthorizationID", min_length=1, max_length=25) - client_candidate_id: str = Field(alias="clientCandidateID", min_length=1, max_length=50) + client_id: Cp1252Str = Field(alias="@clientID", min_length=1) + authorization_transaction_type: Cp1252Str = Field(alias="@authorizationTransactionType", min_length=1) + client_authorization_id: Cp1252Str = Field(alias="@clientAuthorizationID", min_length=1, max_length=25) + client_candidate_id: Cp1252Str = Field(alias="clientCandidateID", min_length=1, max_length=50) exam_authorization_count: int = Field(alias="examAuthorizationCount") - exam_series_code: str = Field(alias="examSeriesCode", min_length=1, max_length=20) - elegibility_appt_date_first: str = Field(alias="eligibilityApptDateFirst", min_length=1) - elegibility_appt_date_last: str = Field(alias="eligibilityApptDateLast", min_length=1) - last_update: str = Field(alias="lastUpdate", min_length=1) + exam_series_code: Cp1252Str = Field(alias="examSeriesCode", min_length=1, max_length=20) + elegibility_appt_date_first: Cp1252Str = Field(alias="eligibilityApptDateFirst", min_length=1) + elegibility_appt_date_last: Cp1252Str = Field(alias="eligibilityApptDateLast", min_length=1) + last_update: Cp1252Str = Field(alias="lastUpdate", min_length=1) diff --git a/eox_nelp/pearson_vue/tests/test_pipeline.py b/eox_nelp/pearson_vue/tests/test_pipeline.py index e96b9e28..96c20d7e 100644 --- a/eox_nelp/pearson_vue/tests/test_pipeline.py +++ b/eox_nelp/pearson_vue/tests/test_pipeline.py @@ -845,6 +845,19 @@ def setUp(self): {"primaryAddress": {"phone": {"phoneCountryCode": ""}}}, {"primaryAddress": {"phone": {"phoneNumber": ""}}}, {"webAccountInfo": {"email": ""}}, + {"@clientCandidateID": "فلان"}, + {"@clientID": "فلان"}, + {"candidateName": {"firstName": "فلان"}}, + {"candidateName": {"lastName": "فلان"}}, + {"lastUpdate": "فلان"}, + {"primaryAddress": {"address1": "فلان"}}, + {"primaryAddress": {"city": "فلان"}}, + {"primaryAddress": {"country": "فلان"}}, + {"primaryAddress": {"mobile": {"mobileCountryCode": "فلان"}}}, + {"primaryAddress": {"mobile": {"mobileNumber": "فلان"}}}, + {"primaryAddress": {"phone": {"phoneCountryCode": "فلان"}}}, + {"primaryAddress": {"phone": {"phoneNumber": "فلان"}}}, + {"webAccountInfo": {"email": "فلان"}}, ) def test_wrong_cdd_request(self, wrong_update): """Test validator with a wrong cdd_request updating with empty string @@ -898,6 +911,15 @@ def setUp(self): {"examAuthorizationCount": ""}, {"examSeriesCode": ""}, {"lastUpdate": ""}, + {"@authorizationTransactionType": "فلان"}, + {"@clientAuthorizationID": "فلان"}, + {"@clientID": "فلان"}, + {"clientCandidateID": "فلان"}, + {"eligibilityApptDateFirst": "فلان"}, + {"eligibilityApptDateLast": "فلان"}, + {"examAuthorizationCount": "فلان"}, + {"examSeriesCode": "فلان"}, + {"lastUpdate": "فلان"}, ) def test_wrong_ead_request(self, wrong_update): """Test validator with a wrong ead_request updating with empty string diff --git a/eox_nelp/pearson_vue/tests/test_utils.py b/eox_nelp/pearson_vue/tests/test_utils.py index 6f818c9c..d36c0d8c 100644 --- a/eox_nelp/pearson_vue/tests/test_utils.py +++ b/eox_nelp/pearson_vue/tests/test_utils.py @@ -13,7 +13,7 @@ from eox_nelp.edxapp_wrapper.student import AnonymousUserId, CourseEnrollment from eox_nelp.pearson_vue.constants import PAYLOAD_CDD, PAYLOAD_EAD -from eox_nelp.pearson_vue.utils import generate_client_authorization_id, update_xml_with_dict +from eox_nelp.pearson_vue.utils import generate_client_authorization_id, is_cp1252, update_xml_with_dict User = get_user_model() @@ -418,3 +418,36 @@ def test_generate_client_authorization_id(self): result = generate_client_authorization_id(self.user.id, self.course_id) self.assertEqual(expected_result, result) + + +class TestIsCp1252(TestCase): + """Class to test is_cp1252 function""" + def test_english_true(self): + """Tests if a string with English characters is CP1252 encoded.""" + text = "This is a test string" + + self.assertTrue(is_cp1252(text)) + + def test_arabic_false(self): + """Tests if a string with Arabic characters is not CP1252 encoded.""" + text = "هذا هو اختبار باللغة العربية" # Arabic text + + self.assertFalse(is_cp1252(text)) + + def test_mixed_chars(self): + """Tests if a string with mixed characters (English and Arabic) is not CP1252 encoded.""" + text = "This is a test with عربية characters" + + self.assertFalse(is_cp1252(text)) + + def test_empty_string(self): + """Tests if an empty string is considered CP1252 encoded.""" + text = "" + + self.assertTrue(is_cp1252(text)) + + def test_special_chars_false(self): + """Tests if a string with special characters is not CP1252 encoded.""" + text = "This string has ©®€ symbols" + + self.assertFalse(is_cp1252(text)) diff --git a/eox_nelp/pearson_vue/utils.py b/eox_nelp/pearson_vue/utils.py index 07ed833a..17d655d6 100644 --- a/eox_nelp/pearson_vue/utils.py +++ b/eox_nelp/pearson_vue/utils.py @@ -2,6 +2,8 @@ This includes xml helpers: - update_xml_with_dict """ +import re + import xmltodict from pydantic.v1.utils import deep_update @@ -43,3 +45,10 @@ def generate_client_authorization_id(user_id: int, course_id: str) -> str: anonymous_user_id_instance = AnonymousUserId.objects.get(anonymous_user_id=anonymous_user_id) return f"{course_enrollment.id}-{anonymous_user_id_instance.id}" + + +def is_cp1252(text): + """Checks if the given text matchs the format CP1252""" + cp1252_regex = r'^[\x00-\x7F\x80-\x9F\xA0-\xFF]*$' + + return re.match(cp1252_regex, text) is not None