Skip to content

Commit

Permalink
feat: validate cp1252 pydantic (#189)
Browse files Browse the repository at this point in the history
* feat: add util for is_cp1252 encoding

* feat: add Cp1252Str type for main fields

* feat: test with not arabic chars desired fields
  • Loading branch information
johanseto authored Jul 8, 2024
1 parent 6cee695 commit 40d40db
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 23 deletions.
69 changes: 47 additions & 22 deletions eox_nelp/pearson_vue/data_classes.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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")
Expand All @@ -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)
22 changes: 22 additions & 0 deletions eox_nelp/pearson_vue/tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
35 changes: 34 additions & 1 deletion eox_nelp/pearson_vue/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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))
9 changes: 9 additions & 0 deletions eox_nelp/pearson_vue/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
This includes xml helpers:
- update_xml_with_dict
"""
import re

import xmltodict
from pydantic.v1.utils import deep_update

Expand Down Expand Up @@ -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

0 comments on commit 40d40db

Please sign in to comment.