From c3440ed83b4156375e75514d535190bdf284a163 Mon Sep 17 00:00:00 2001 From: echarrod Date: Mon, 8 Jul 2019 12:53:45 +0100 Subject: [PATCH 01/89] SDK-1055: Update Django version --- examples/yoti_example_django/requirements.in | 2 +- examples/yoti_example_django/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/yoti_example_django/requirements.in b/examples/yoti_example_django/requirements.in index 83f389f0..341513f4 100644 --- a/examples/yoti_example_django/requirements.in +++ b/examples/yoti_example_django/requirements.in @@ -1,5 +1,5 @@ cryptography>=2.3 -Django==2.2.2 +Django==2.2.3 django-sslserver>=0.2.0 python-dotenv>=0.7.1 requests>=2.20.0 diff --git a/examples/yoti_example_django/requirements.txt b/examples/yoti_example_django/requirements.txt index 1117e496..2c8728e8 100644 --- a/examples/yoti_example_django/requirements.txt +++ b/examples/yoti_example_django/requirements.txt @@ -11,7 +11,7 @@ cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests cryptography==2.5 django-sslserver==0.20 -django==2.2.2 +django==2.2.3 future==0.16.0 # via yoti idna==2.7 # via requests protobuf==3.6.0 # via yoti From d550be3825ad8c670f69e9d98a9d4554652d305c Mon Sep 17 00:00:00 2001 From: echarrod Date: Mon, 8 Jul 2019 15:17:31 +0100 Subject: [PATCH 02/89] Add LF normalization in .gitattributes file --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto From ee1d83c0634d124b3122e7ae152dff0b7c9c2cec Mon Sep 17 00:00:00 2001 From: echarrod Date: Mon, 8 Jul 2019 15:30:21 +0100 Subject: [PATCH 03/89] Run code formatter --- examples/yoti_example_django/requirements.txt | 2 +- examples/yoti_example_flask/requirements.txt | 2 +- requirements.txt | 2 +- yoti_python_sdk/profile.py | 3 ++- yoti_python_sdk/tests/test_profile.py | 4 +++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/yoti_example_django/requirements.txt b/examples/yoti_example_django/requirements.txt index 2c8728e8..9fe5134f 100644 --- a/examples/yoti_example_django/requirements.txt +++ b/examples/yoti_example_django/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements.txt requirements.in +# pip-compile --output-file=requirements.txt requirements.in # asn1==2.2.0 # via yoti asn1crypto==0.24.0 # via cryptography diff --git a/examples/yoti_example_flask/requirements.txt b/examples/yoti_example_flask/requirements.txt index e1b3f8b9..ed381f7b 100644 --- a/examples/yoti_example_flask/requirements.txt +++ b/examples/yoti_example_flask/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements.txt requirements.in +# pip-compile --output-file=requirements.txt requirements.in # asn1==2.2.0 # via yoti asn1crypto==0.24.0 # via cryptography diff --git a/requirements.txt b/requirements.txt index fa266531..04c5577c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements.txt requirements.in +# pip-compile --output-file=requirements.txt requirements.in # asn1==2.2.0 asn1crypto==0.24.0 # via cryptography diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index 8cb0e96a..1f1d8be6 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -37,7 +37,8 @@ def __init__(self, profile_attributes): except Exception as exc: if logging.getLogger().propagate: logging.warning( - 'Error parsing profile attribute:{0}, exception: {1} - {2}'.format(field.name, type(exc).__name__, exc)) + 'Error parsing profile attribute:{0}, exception: {1} - {2}'.format(field.name, + type(exc).__name__, exc)) self.ensure_postal_address() diff --git a/yoti_python_sdk/tests/test_profile.py b/yoti_python_sdk/tests/test_profile.py index ae5aeece..0ff855ae 100644 --- a/yoti_python_sdk/tests/test_profile.py +++ b/yoti_python_sdk/tests/test_profile.py @@ -136,7 +136,9 @@ def test_error_parsing_attribute_has_none_value(): assert profile.get_attribute(int_attribute_name) is None -@pytest.mark.parametrize("content_type", [Protobuf.CT_DATE, Protobuf.CT_INT, Protobuf.CT_JPEG, Protobuf.CT_PNG, Protobuf.CT_JSON, Protobuf.CT_UNDEFINED]) +@pytest.mark.parametrize("content_type", + [Protobuf.CT_DATE, Protobuf.CT_INT, Protobuf.CT_JPEG, Protobuf.CT_PNG, Protobuf.CT_JSON, + Protobuf.CT_UNDEFINED]) def test_parse_empty_values_returns_none(content_type): attribute_name = "attribute_name" From 06682e30289b9ea4c495fdd3880633ed769fbabf Mon Sep 17 00:00:00 2001 From: echarrod Date: Tue, 9 Jul 2019 11:07:23 +0100 Subject: [PATCH 04/89] SDK-993: Add docstrings to attribute properties --- yoti_python_sdk/profile.py | 59 +++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index 1f1d8be6..337ec2ff 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -44,53 +44,104 @@ def __init__(self, profile_attributes): @property def date_of_birth(self): + """date_of_birth represents the user's date of birth as a string. + Will be changed to return a datetime in v3.0.0. + Will be None if not provided by Yoti. + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_DATE_OF_BIRTH) @property def family_name(self): + """family_name represents the user's family name. This will be None if not provided by Yoti. + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_FAMILY_NAME) + @property + def given_names(self): + """given_names represents the user's given names. This will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_GIVEN_NAMES) + @property def full_name(self): + """full_name represents the user's full name. + If family_name and given_names are present, the value will be equal to the string 'given_names + " " + family_name'. + Will be None if not provided by Yoti. + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_FULL_NAME) @property def gender(self): + """gender corresponds to the gender in the registered document. + The value will be one of the strings "MALE", "FEMALE", "TRANSGENDER" or "OTHER". + Will be None if not provided by Yoti. + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_GENDER) - @property - def given_names(self): - return self.get_attribute(config.ATTRIBUTE_GIVEN_NAMES) - @property def nationality(self): + """nationality corresponds to the nationality in the passport. + The value is an ISO-3166-1 alpha-3 code with ICAO9303 (passport) extensions. + Will be None if not provided by Yoti. + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_NATIONALITY) @property def email_address(self): + """email_address represents the user's email address. This will be None if not provided by Yoti. + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_EMAIL_ADDRESS) @property def phone_number(self): + """phone_number represents the user's mobile phone number. This will be None if not provided by Yoti. + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_PHONE_NUMBER) @property def postal_address(self): + """postal_address represents the user's address. This will be None if not provided by Yoti. + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_POSTAL_ADDRESS) @property def selfie(self): + """selfie is a photograph of the user. Will be None if not provided by Yoti. + :return: Attribute(image) + """ return self.get_attribute(config.ATTRIBUTE_SELFIE) @property def structured_postal_address(self): + """structured_postal_address represents the user's address represented as an OrderedDict. + This will be None if not provided by Yoti. + :return: Attribute(OrderedDict) + """ return self.get_attribute(config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS) @property def document_images(self): + """document_images returns a tuple of document images cropped from the image in the capture page. + There can be multiple images as per the number of regions in the capture in this attribute. + Will be None if not provided by Yoti. + :return: Attribute(tuple(image)) + """ return self.get_attribute(config.ATTRIBUTE_DOCUMENT_IMAGES) def get_attribute(self, attribute_name): + """retrieves an attribute based on its name + :param attribute_name: + :return: Attribute + """ if attribute_name in self.attributes: return self.attributes.get(attribute_name) else: From 261077528cf27725657ac4d12799c11582a96f37 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Tue, 16 Jul 2019 12:29:29 +0100 Subject: [PATCH 05/89] SDK-495: Check-in new static assets into flask example project --- .../static/assets/app-store-badge.png | Bin 0 -> 4077 bytes .../static/assets/app-store-badge@2x.png | Bin 0 -> 8819 bytes .../static/assets/company-logo.jpg | Bin 0 -> 4682 bytes .../static/assets/google-play-badge.png | Bin 0 -> 4957 bytes .../static/assets/google-play-badge@2x.png | Bin 0 -> 11267 bytes .../static/assets/icons/address.svg | 3 + .../static/assets/icons/calendar.svg | 5 + .../static/assets/icons/chevron-down-grey.svg | 7 + .../static/assets/icons/document.svg | 3 + .../static/assets/icons/email.svg | 14 + .../static/assets/icons/gender.svg | 5 + .../static/assets/icons/nationality.svg | 3 + .../static/assets/icons/phone.svg | 3 + .../static/assets/icons/profile.svg | 3 + .../static/assets/icons/verified.svg | 6 + .../yoti_example_flask/static/assets/logo.png | Bin 0 -> 2988 bytes .../static/assets/logo@2x.png | Bin 0 -> 5609 bytes examples/yoti_example_flask/static/index.css | 174 +++++++ .../yoti_example_flask/static/profile.css | 426 ++++++++++++++++++ 19 files changed, 652 insertions(+) create mode 100755 examples/yoti_example_flask/static/assets/app-store-badge.png create mode 100755 examples/yoti_example_flask/static/assets/app-store-badge@2x.png create mode 100644 examples/yoti_example_flask/static/assets/company-logo.jpg create mode 100755 examples/yoti_example_flask/static/assets/google-play-badge.png create mode 100755 examples/yoti_example_flask/static/assets/google-play-badge@2x.png create mode 100755 examples/yoti_example_flask/static/assets/icons/address.svg create mode 100755 examples/yoti_example_flask/static/assets/icons/calendar.svg create mode 100644 examples/yoti_example_flask/static/assets/icons/chevron-down-grey.svg create mode 100755 examples/yoti_example_flask/static/assets/icons/document.svg create mode 100755 examples/yoti_example_flask/static/assets/icons/email.svg create mode 100755 examples/yoti_example_flask/static/assets/icons/gender.svg create mode 100755 examples/yoti_example_flask/static/assets/icons/nationality.svg create mode 100755 examples/yoti_example_flask/static/assets/icons/phone.svg create mode 100755 examples/yoti_example_flask/static/assets/icons/profile.svg create mode 100755 examples/yoti_example_flask/static/assets/icons/verified.svg create mode 100755 examples/yoti_example_flask/static/assets/logo.png create mode 100755 examples/yoti_example_flask/static/assets/logo@2x.png create mode 100644 examples/yoti_example_flask/static/index.css create mode 100644 examples/yoti_example_flask/static/profile.css diff --git a/examples/yoti_example_flask/static/assets/app-store-badge.png b/examples/yoti_example_flask/static/assets/app-store-badge.png new file mode 100755 index 0000000000000000000000000000000000000000..3ec996cc6288d68279c1d735c9d627c64d8a48c6 GIT binary patch literal 4077 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 literal 0 HcmV?d00001 diff --git a/examples/yoti_example_flask/static/assets/app-store-badge@2x.png b/examples/yoti_example_flask/static/assets/app-store-badge@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..84b34068fc22aa74f388f6db1a583ac6592f5420 GIT binary patch literal 8819 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 MlZs4*lPCscyWU2PMP1t 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 + + diff --git a/examples/yoti_example_flask/static/assets/icons/calendar.svg b/examples/yoti_example_flask/static/assets/icons/calendar.svg new file mode 100755 index 00000000..4f6b9bb7 --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/calendar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/yoti_example_flask/static/assets/icons/chevron-down-grey.svg b/examples/yoti_example_flask/static/assets/icons/chevron-down-grey.svg new file mode 100644 index 00000000..6753becb --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/chevron-down-grey.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/yoti_example_flask/static/assets/icons/document.svg b/examples/yoti_example_flask/static/assets/icons/document.svg new file mode 100755 index 00000000..4c41271e --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/document.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/yoti_example_flask/static/assets/icons/email.svg b/examples/yoti_example_flask/static/assets/icons/email.svg new file mode 100755 index 00000000..c4582d6e --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/email.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/examples/yoti_example_flask/static/assets/icons/gender.svg b/examples/yoti_example_flask/static/assets/icons/gender.svg new file mode 100755 index 00000000..af5c5772 --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/gender.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/yoti_example_flask/static/assets/icons/nationality.svg b/examples/yoti_example_flask/static/assets/icons/nationality.svg new file mode 100755 index 00000000..e57d7522 --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/nationality.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/yoti_example_flask/static/assets/icons/phone.svg b/examples/yoti_example_flask/static/assets/icons/phone.svg new file mode 100755 index 00000000..b19cce04 --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/phone.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/yoti_example_flask/static/assets/icons/profile.svg b/examples/yoti_example_flask/static/assets/icons/profile.svg new file mode 100755 index 00000000..5c514fc1 --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/profile.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/yoti_example_flask/static/assets/icons/verified.svg b/examples/yoti_example_flask/static/assets/icons/verified.svg new file mode 100755 index 00000000..7ca4dbb3 --- /dev/null +++ b/examples/yoti_example_flask/static/assets/icons/verified.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/yoti_example_flask/static/assets/logo.png b/examples/yoti_example_flask/static/assets/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..c60227fabf339e9540e5daac4d2d25e121137752 GIT binary patch literal 2988 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 Date: Tue, 16 Jul 2019 12:40:42 +0100 Subject: [PATCH 06/89] SDK-495: Replacing index template with new style for index and changing Application ID to Scenario ID --- examples/yoti_example_flask/.env.example | 2 +- examples/yoti_example_flask/Dockerfile | 4 +- examples/yoti_example_flask/app.py | 4 +- .../yoti_example_flask/docker-compose.yml | 2 +- examples/yoti_example_flask/settings.py | 2 +- .../yoti_example_flask/templates/index.html | 86 ++++++++++++++++--- 6 files changed, 79 insertions(+), 21 deletions(-) diff --git a/examples/yoti_example_flask/.env.example b/examples/yoti_example_flask/.env.example index c853270b..967ac5f0 100644 --- a/examples/yoti_example_flask/.env.example +++ b/examples/yoti_example_flask/.env.example @@ -1,3 +1,3 @@ -YOTI_APPLICATION_ID=yourApplicationId +YOTI_APPLICATION_ID=yourScenarioId YOTI_CLIENT_SDK_ID=yourClientSdkId YOTI_KEY_FILE_PATH=yourKeyFilePath \ No newline at end of file diff --git a/examples/yoti_example_flask/Dockerfile b/examples/yoti_example_flask/Dockerfile index 8c706e5b..e0b04c7a 100644 --- a/examples/yoti_example_flask/Dockerfile +++ b/examples/yoti_example_flask/Dockerfile @@ -1,8 +1,8 @@ FROM python:3.6.3 -ARG YOTI_APPLICATION_ID +ARG YOTI_SCENARIO_ID ARG YOTI_CLIENT_SDK_ID ARG YOTI_KEY_FILE_PATH -RUN if [ "$YOTI_APPLICATION_ID" = "yourApplicationId" ] ; then echo YOTI_APPLICATION_ID not set; exit 1; else echo YOTI_APPLICATION_ID is $YOTI_APPLICATION_ID ; fi +RUN if [ "$YOTI_SCENARIO_ID" = "yourScenarioId" ] ; then echo YOTI_SCENARIO_ID not set; exit 1; else echo YOTI_SCENARIO_ID is $YOTI_SCENARIO_ID ; fi RUN if [ "$YOTI_CLIENT_SDK_ID" = "yourClientSdkId" ] ; then echo YOTI_CLIENT_SDK_ID not set; exit 1; else echo YOTI_CLIENT_SDK_ID is $YOTI_CLIENT_SDK_ID ; fi RUN if [ "$YOTI_KEY_FILE_PATH" = "yourKeyFilePath" ] ; then echo YOTI_KEY_FILE_PATH not set; exit 1; else echo YOTI_KEY_FILE_PATH is $YOTI_KEY_FILE_PATH ; fi ADD . /code diff --git a/examples/yoti_example_flask/app.py b/examples/yoti_example_flask/app.py index 9395a013..72a957e6 100644 --- a/examples/yoti_example_flask/app.py +++ b/examples/yoti_example_flask/app.py @@ -11,7 +11,7 @@ load_dotenv(dotenv_path) from settings import ( - YOTI_APPLICATION_ID, + YOTI_SCENARIO_ID, YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH, ) @@ -28,7 +28,7 @@ def save_image(selfie_data): @app.route('/') def index(): - return render_template('index.html', app_id=YOTI_APPLICATION_ID) + return render_template('index.html', scenario_id=YOTI_SCENARIO_ID) @app.route('/yoti/auth') diff --git a/examples/yoti_example_flask/docker-compose.yml b/examples/yoti_example_flask/docker-compose.yml index 31d5a202..f4baf912 100644 --- a/examples/yoti_example_flask/docker-compose.yml +++ b/examples/yoti_example_flask/docker-compose.yml @@ -4,7 +4,7 @@ services: build: context: ./ args: - YOTI_APPLICATION_ID: "${YOTI_APPLICATION_ID}" + YOTI_SCENARIO_ID: "${YOTI_SCENARIO_ID}" YOTI_CLIENT_SDK_ID: "${YOTI_CLIENT_SDK_ID}" YOTI_KEY_FILE_PATH: "${YOTI_KEY_FILE_PATH}" ports: diff --git a/examples/yoti_example_flask/settings.py b/examples/yoti_example_flask/settings.py index cf939e5d..4c88d947 100644 --- a/examples/yoti_example_flask/settings.py +++ b/examples/yoti_example_flask/settings.py @@ -1,5 +1,5 @@ from os import environ -YOTI_APPLICATION_ID = environ.get('YOTI_APPLICATION_ID') +YOTI_SCENARIO_ID = environ.get('YOTI_SCENARIO_ID') YOTI_CLIENT_SDK_ID = environ.get('YOTI_CLIENT_SDK_ID') YOTI_KEY_FILE_PATH = environ.get('YOTI_KEY_FILE_PATH') diff --git a/examples/yoti_example_flask/templates/index.html b/examples/yoti_example_flask/templates/index.html index fd02bba1..832adc15 100644 --- a/examples/yoti_example_flask/templates/index.html +++ b/examples/yoti_example_flask/templates/index.html @@ -1,25 +1,83 @@ - + + - - - - Index + + + Yoti client example + + - - + + +
+
+
+ Yoti +
+ +

We now accept Yoti

+ +
+
+
+ + + +
From 5099bb14fb6cd170e18a665d4ab19e08f6b68dcf Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Thu, 25 Jul 2019 12:33:12 +0100 Subject: [PATCH 59/89] SDK-695: Add document_details to dynamic_share in django example Added document_details to the dynamic scenario for the dynamic_share example in the Django example project --- .../yoti_example/templates/attribute_snippet.html | 12 ++++++++++++ .../yoti_example/templates/profile.html | 1 + examples/yoti_example_django/yoti_example/views.py | 8 +++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/examples/yoti_example_django/yoti_example/templates/attribute_snippet.html b/examples/yoti_example_django/yoti_example/templates/attribute_snippet.html index 04fa0e9b..0b5c894a 100644 --- a/examples/yoti_example_django/yoti_example/templates/attribute_snippet.html +++ b/examples/yoti_example_django/yoti_example/templates/attribute_snippet.html @@ -13,6 +13,18 @@ {% for image in prop.value %} {% endfor %} + {% elif prop.name == "document_details" %} + + + + + {% if prop.value.expiration_date %} + + {% endif %} + {% if prop.value.issuing_authority %} + + {% endif %} +
Type{{ prop.value.document_type }}
Issuing Country{{ prop.value.issuing_country }}
Document Number{{ prop.value.document_number }}
Expiration Date{{ prop.value.expiration_date }}
Issuing Authority{{ prop.value.issuing_authority }}
{% elif prop.name == "structured_postal_address" %} {% for key, value in prop.value.items %} diff --git a/examples/yoti_example_django/yoti_example/templates/profile.html b/examples/yoti_example_django/yoti_example/templates/profile.html index f9b18d31..5c6841c8 100644 --- a/examples/yoti_example_django/yoti_example/templates/profile.html +++ b/examples/yoti_example_django/yoti_example/templates/profile.html @@ -55,6 +55,7 @@ {% if structured_postal_address %} {% include "attribute_snippet.html" with name="Structured Address" icon="yoti-icon-address" prop=structured_postal_address prevalue="" %} {% endif %} {% if gender %} {% include "attribute_snippet.html" with name="Gender" icon="yoti-icon-gender" prop=gender prevalue="" %} {% endif %} {% if document_images %} {% include "attribute_snippet.html" with name="Document Images" icon="yoti-icon-profile" prop=document_images prevalue="" %} {% endif %} + {% if document_details %} {% include "attribute_snippet.html" with name="Document Details" icon="yoti-icon-profile" prop=document_details prevalue="" %} {% endif %} diff --git a/examples/yoti_example_django/yoti_example/views.py b/examples/yoti_example_django/yoti_example/views.py index 61d20a38..c77dffcb 100644 --- a/examples/yoti_example_django/yoti_example/views.py +++ b/examples/yoti_example_django/yoti_example/views.py @@ -30,7 +30,13 @@ class DynamicShareView(TemplateView): def get(self, request, *args, **kwargs): client = Client(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH) - policy = DynamicPolicyBuilder().with_full_name().with_age_over(18).build() + policy = ( + DynamicPolicyBuilder() + .with_full_name() + .with_age_over(18) + .with_document_details() + .build() + ) scenario = ( DynamicScenarioBuilder() .with_policy(policy) From 99173fb515bd50f02d32dec2d8d9bc92afa101e6 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Thu, 25 Jul 2019 14:52:29 +0100 Subject: [PATCH 60/89] SDK-695: Fix failing unit test in python2.7 Unit tests for document_details in profile were failing on python27 due to differences in how strings are encoded causing issues with setting up the test fixture correctly. This patch resolves the issue and has been tested on python2.7 and python 3.6 --- yoti_python_sdk/tests/test_profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yoti_python_sdk/tests/test_profile.py b/yoti_python_sdk/tests/test_profile.py index ea7eebb1..1d9104fe 100644 --- a/yoti_python_sdk/tests/test_profile.py +++ b/yoti_python_sdk/tests/test_profile.py @@ -560,7 +560,7 @@ def test_get_attribute_returns_none(): def test_get_document_details_usa(): attribute_list = create_single_attribute_list( name=config.ATTRIBUTE_DOCUMENT_DETAILS, - value=bytes(USA_DOCUMENT_DETAILS, "utf-8"), + value=USA_DOCUMENT_DETAILS.encode(), anchors=None, content_type=Protobuf.CT_STRING, ) @@ -576,7 +576,7 @@ def test_get_document_details_usa(): def test_get_document_details_india(): attribute_list = create_single_attribute_list( name=config.ATTRIBUTE_DOCUMENT_DETAILS, - value=bytes(INDIA_DOCUMENT_DETAILS, "utf-8"), + value=INDIA_DOCUMENT_DETAILS.encode(), anchors=None, content_type=Protobuf.CT_STRING, ) From 089d473bebf59dcc2a12d7754f18d590a6fc7a91 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Fri, 9 Aug 2019 16:54:30 +0100 Subject: [PATCH 61/89] SDK-695: Remove DocumentDetails from Example projects Replace with Email Address attribute --- examples/yoti_example_django/yoti_example/views.py | 2 +- examples/yoti_example_flask/app.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/yoti_example_django/yoti_example/views.py b/examples/yoti_example_django/yoti_example/views.py index c77dffcb..2370643b 100644 --- a/examples/yoti_example_django/yoti_example/views.py +++ b/examples/yoti_example_django/yoti_example/views.py @@ -34,7 +34,7 @@ def get(self, request, *args, **kwargs): DynamicPolicyBuilder() .with_full_name() .with_age_over(18) - .with_document_details() + .with_email() .build() ) scenario = ( diff --git a/examples/yoti_example_flask/app.py b/examples/yoti_example_flask/app.py index f3f4a20c..f6d01d26 100644 --- a/examples/yoti_example_flask/app.py +++ b/examples/yoti_example_flask/app.py @@ -34,11 +34,7 @@ def index(): def dynamic_share(): client = Client(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH) policy = ( - DynamicPolicyBuilder() - .with_full_name() - .with_age_over(18) - .with_document_details() - .build() + DynamicPolicyBuilder().with_full_name().with_age_over(18).with_email().build() ) scenario = ( DynamicScenarioBuilder() From c82e9494db07688ccec473d40cf0d0a293736156 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Mon, 12 Aug 2019 10:43:47 +0100 Subject: [PATCH 62/89] SDK-695: Use pytest.raises to verify Exceptions in unit test Use list indexing instead of pop to unpack document details data --- yoti_python_sdk/document_details.py | 14 +++++++------- yoti_python_sdk/tests/test_document_details.py | 13 +++++-------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/yoti_python_sdk/document_details.py b/yoti_python_sdk/document_details.py index e02e2e09..aa637b64 100644 --- a/yoti_python_sdk/document_details.py +++ b/yoti_python_sdk/document_details.py @@ -40,12 +40,12 @@ def __validate_data(self, data): def __parse_data(self, data): data = data.split() - self.__document_type = data.pop(0) - self.__issuing_country = data.pop(0) - self.__document_number = data.pop(0) - if len(data) > 0: - date = data.pop(0) + self.__document_type = data[0] + self.__issuing_country = data[1] + self.__document_number = data[2] + if len(data) > 3: + date = data[3] if date != "-": self.__expiration_date = date_parser.from_iso_format(date) - if len(data) > 0: - self.__issuing_authority = data.pop(0) + if len(data) > 4: + self.__issuing_authority = data[4] diff --git a/yoti_python_sdk/tests/test_document_details.py b/yoti_python_sdk/tests/test_document_details.py index c8daff46..19480fb6 100644 --- a/yoti_python_sdk/tests/test_document_details.py +++ b/yoti_python_sdk/tests/test_document_details.py @@ -3,26 +3,23 @@ from yoti_python_sdk.document_details import DocumentDetails import datetime +import pytest def test_exception_for_empty_data(): DATA = "" - try: + with pytest.raises(ValueError) as exc: DocumentDetails(DATA) - except ValueError: - return - assert False # An exception should have been thrown + assert str(exc.value) == "Invalid value for DocumentDetails" def test_exception_for_short_data(): DATA = "PASS_CARD GBR" - try: + with pytest.raises(ValueError) as exc: DocumentDetails(DATA) - except ValueError: - return - assert False # An exception should have been thrown + assert str(exc.value) == "Invalid value for DocumentDetails" def test_parse_3_words(): From 32bcca876dc8a778de3b5e8eb059ca8b76b40f6b Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Mon, 12 Aug 2019 14:56:06 +0100 Subject: [PATCH 63/89] SDK-695: Add missed opportunity to use pytest.raises --- yoti_python_sdk/tests/test_document_details.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/yoti_python_sdk/tests/test_document_details.py b/yoti_python_sdk/tests/test_document_details.py index 19480fb6..20701dd5 100644 --- a/yoti_python_sdk/tests/test_document_details.py +++ b/yoti_python_sdk/tests/test_document_details.py @@ -85,8 +85,6 @@ def test_expiration_date_is_dash(): def test_invalid_date(): DATA = "PASSPORT GBR 1234abc X016-05-01" - try: + with pytest.raises(ValueError) as exc: DocumentDetails(DATA) - except ValueError: - return - return False # An exception should have been thrown + assert str(exc.value) == "Invalid value for DocumentDetails" From a26d46781520d6d8fd063da338742429ef05d0a8 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Fri, 19 Jul 2019 10:42:40 +0100 Subject: [PATCH 64/89] SDK-977: Adding Precommit configuration to project which invokes black and flake8 Precommit hooks can be installed by running `pre-commit install` This also required an update for the version of virtualenv requested by requirements.txt. --- .pre-commit-config.yaml | 9 +++++++++ requirements.in | 3 ++- requirements.txt | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..c5769c9d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +- repo: https://github.com/ambv/black + rev: stable + hooks: + - id: black + language_version: python3.6 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v1.2.3 + hooks: + - id: flake8 diff --git a/requirements.in b/requirements.in index 1df1a61b..6ec178ff 100644 --- a/requirements.in +++ b/requirements.in @@ -11,6 +11,7 @@ pytest==3.3.2 pytz==2018.9 requests>=2.20.0 urllib3>=1.24.2 -virtualenv==13.1.2 +virtualenv==15.2 wheel==0.24.0 deprecated==1.2.6 +pre-commit==1.17.0 diff --git a/requirements.txt b/requirements.txt index b497f3bc..ad0141fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,31 +6,41 @@ # asn1==2.2.0 asn1crypto==0.24.0 # via cryptography +aspy.yaml==1.3.0 # via pre-commit attrs==18.1.0 # via pytest certifi==2018.11.29 # via requests cffi==1.11.5 # via cryptography +cfgv==2.0.0 # via pre-commit chardet==3.0.4 # via requests click==6.6 cryptography==2.4.1 deprecated==1.2.6 future==0.15.2 +identify==1.4.5 # via pre-commit idna==2.7 # via cryptography, requests +importlib-metadata==0.18 # via pre-commit +importlib-resources==1.0.2 # via pre-commit itsdangerous==0.24 mock==2.0.0 +nodeenv==1.3.3 # via pre-commit pbr==1.10.0 pluggy==0.6.0 # via pytest +pre-commit==1.17.0 protobuf==3.7.0 py==1.5.3 # via pytest pycparser==2.18 # via cffi pyopenssl==18.0.0 pytest==3.3.2 pytz==2018.9 +pyyaml==5.1.1 # via aspy.yaml, pre-commit requests==2.21.0 -six==1.10.0 # via cryptography, mock, protobuf, pyopenssl, pytest +six==1.10.0 # via cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest +toml==0.10.0 # via pre-commit urllib3==1.24.2 -virtualenv==13.1.2 +virtualenv==15.2.0 wheel==0.24.0 wrapt==1.11.2 # via deprecated +zipp==0.5.2 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools==41.0.1 # via protobuf, pytest From ce1f1f1c9cf22dc34937b5beb28fb60d70bb095e Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Mon, 22 Jul 2019 16:47:45 +0100 Subject: [PATCH 65/89] SDK-977: Ignore line length when linting The 79 character limit was getting too onerous with long import lines and other structures that can't be broken up. So I've set the linter to ignore it. --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c5769c9d..502d4916 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,3 +7,5 @@ rev: v1.2.3 hooks: - id: flake8 + args: + - --ignore=E501 From 31c82bc594ee42c439766df34c17fa3a4a297aab Mon Sep 17 00:00:00 2001 From: Emma Smith <52929061+emmas-yoti@users.noreply.github.com> Date: Wed, 24 Jul 2019 15:35:40 +0100 Subject: [PATCH 66/89] SDK-977: Ignore line break warnings PEP8 W5 Long variable names in some of our test cases gets autoformatted by black to insert a newline to split many of the assertions across multiple lines i.e. assert (a \n== b\n). This triggers Warning 503 in flake8 Additionally, long boolean expressions/if conditions can't easily be broken up while preserving the same semantic meaning. So suppressing this warning going forwards. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 502d4916..435a7aa9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,4 +8,4 @@ hooks: - id: flake8 args: - - --ignore=E501 + - --ignore=E501,W5 From fb13a63e62403f7f21b49549cece7763ced7ad8b Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Mon, 29 Jul 2019 18:21:00 +0100 Subject: [PATCH 67/89] SDK-977: Switch autoformatter from Black to YAPF Configuring YAPF to split multi-line argument lists into 1-element-per-line for preference. --- .pre-commit-config.yaml | 9 +++++---- .style.yapf | 6 ++++++ 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .style.yapf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 435a7aa9..302c1a9d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,11 @@ -- repo: https://github.com/ambv/black - rev: stable +repos: +- repo: https://github.com/pre-commit/mirrors-yapf + rev: master hooks: - - id: black + - id: yapf language_version: python3.6 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.2.3 + rev: v2.2.3 hooks: - id: flake8 args: diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 00000000..3761ee54 --- /dev/null +++ b/.style.yapf @@ -0,0 +1,6 @@ +[style] +based_on_style=google +DEDENT_CLOSING_BRACKETS=true +SPLIT_ALL_COMMA_SEPARATED_VALUES=true +SPLIT_BEFORE_EXPRESSION_AFTER_OPENING_PAREN=true +SPLIT_BEFORE_FIRST_ARGUMENT=true From a4ab2a15089e2d6d7e5329d320a566cca1318b14 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Tue, 30 Jul 2019 13:24:24 +0100 Subject: [PATCH 68/89] Revert "SDK-977: Switch autoformatter from Black to YAPF" This reverts commit 1ce8605ab7867dd84b38d8df145583e7233c6a20. --- .pre-commit-config.yaml | 9 ++++----- .style.yapf | 6 ------ 2 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 .style.yapf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 302c1a9d..435a7aa9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,10 @@ -repos: -- repo: https://github.com/pre-commit/mirrors-yapf - rev: master +- repo: https://github.com/ambv/black + rev: stable hooks: - - id: yapf + - id: black language_version: python3.6 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.2.3 + rev: v1.2.3 hooks: - id: flake8 args: diff --git a/.style.yapf b/.style.yapf deleted file mode 100644 index 3761ee54..00000000 --- a/.style.yapf +++ /dev/null @@ -1,6 +0,0 @@ -[style] -based_on_style=google -DEDENT_CLOSING_BRACKETS=true -SPLIT_ALL_COMMA_SEPARATED_VALUES=true -SPLIT_BEFORE_EXPRESSION_AFTER_OPENING_PAREN=true -SPLIT_BEFORE_FIRST_ARGUMENT=true From ad76c7e2ea7915467913792553c515d74ca86176 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Tue, 30 Jul 2019 13:29:52 +0100 Subject: [PATCH 69/89] SDK-977: Remove pinned python version for black --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 435a7aa9..654b9b23 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,6 @@ rev: stable hooks: - id: black - language_version: python3.6 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.2.3 hooks: From db6b426f128685c9f3b759a065a7e514e18b9b2d Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Mon, 12 Aug 2019 17:22:15 +0100 Subject: [PATCH 70/89] SDK-977: Reformat Python SDK code base --- examples/aml/app.py | 12 +-- examples/yoti_example_django/app_settings.py | 6 +- .../yoti_example/settings.py | 88 +++++++++---------- setup.py | 73 ++++++++------- yoti_python_sdk/__init__.py | 25 +++--- yoti_python_sdk/aml.py | 26 +++--- yoti_python_sdk/attribute.py | 8 +- yoti_python_sdk/attribute_parser.py | 21 +++-- yoti_python_sdk/crypto.py | 31 +++---- yoti_python_sdk/image.py | 10 ++- yoti_python_sdk/multivalue.py | 9 +- .../tests/attribute_fixture_parser.py | 11 +-- yoti_python_sdk/tests/file_helper.py | 4 +- yoti_python_sdk/tests/test_aml.py | 14 +-- .../tests/test_attribute_parser.py | 15 ++-- yoti_python_sdk/tests/test_crypto.py | 26 +++--- yoti_python_sdk/tests/test_image.py | 23 +++-- 17 files changed, 214 insertions(+), 188 deletions(-) diff --git a/examples/aml/app.py b/examples/aml/app.py index e301a38a..688acb99 100644 --- a/examples/aml/app.py +++ b/examples/aml/app.py @@ -7,11 +7,11 @@ from yoti_python_sdk import Client from yoti_python_sdk import aml -dotenv_path = join(dirname(__file__), '.env') +dotenv_path = join(dirname(__file__), ".env") load_dotenv(dotenv_path) -YOTI_CLIENT_SDK_ID = environ.get('YOTI_CLIENT_SDK_ID') -YOTI_KEY_FILE_PATH = environ.get('YOTI_KEY_FILE_PATH') +YOTI_CLIENT_SDK_ID = environ.get("YOTI_CLIENT_SDK_ID") +YOTI_KEY_FILE_PATH = environ.get("YOTI_KEY_FILE_PATH") # The following exits cleanly on Ctrl-C, @@ -25,11 +25,7 @@ def cli_exception(exception_type, value, tb): family_name = "Heath" aml_address = aml.AmlAddress(country="GBR") -aml_profile = aml.AmlProfile( - given_names, - family_name, - aml_address -) +aml_profile = aml.AmlProfile(given_names, family_name, aml_address) if sys.stdin.isatty(): sys.excepthook = cli_exception diff --git a/examples/yoti_example_django/app_settings.py b/examples/yoti_example_django/app_settings.py index 4c88d947..f9b14cb6 100644 --- a/examples/yoti_example_django/app_settings.py +++ b/examples/yoti_example_django/app_settings.py @@ -1,5 +1,5 @@ from os import environ -YOTI_SCENARIO_ID = environ.get('YOTI_SCENARIO_ID') -YOTI_CLIENT_SDK_ID = environ.get('YOTI_CLIENT_SDK_ID') -YOTI_KEY_FILE_PATH = environ.get('YOTI_KEY_FILE_PATH') +YOTI_SCENARIO_ID = environ.get("YOTI_SCENARIO_ID") +YOTI_CLIENT_SDK_ID = environ.get("YOTI_CLIENT_SDK_ID") +YOTI_KEY_FILE_PATH = environ.get("YOTI_KEY_FILE_PATH") diff --git a/examples/yoti_example_django/yoti_example/settings.py b/examples/yoti_example_django/yoti_example/settings.py index a06faebd..73bd1b2a 100644 --- a/examples/yoti_example_django/yoti_example/settings.py +++ b/examples/yoti_example_django/yoti_example/settings.py @@ -21,7 +21,7 @@ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'p3dh1cx&ey8oesy-nj_md-ouo75xi548m22j^zrv((d1@k%%3!' +SECRET_KEY = "p3dh1cx&ey8oesy-nj_md-ouo75xi548m22j^zrv((d1@k%%3!" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,52 +31,52 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'sslserver' + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "sslserver", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'yoti_example.urls' +ROOT_URLCONF = "yoti_example.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [join(BASE_DIR, 'yoti_example/templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [join(BASE_DIR, "yoti_example/templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] }, - }, + } ] -WSGI_APPLICATION = 'yoti_example.wsgi.application' +WSGI_APPLICATION = "yoti_example.wsgi.application" # Database # https://docs.djangoproject.com/en/1.10/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": join(BASE_DIR, "db.sqlite3"), } } @@ -85,25 +85,19 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -114,8 +108,8 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ -STATIC_URL = '/static/' -STATICFILES_FOLDER_NAME = 'static' -PROJECT_DIR = 'yoti_example/' +STATIC_URL = "/static/" +STATICFILES_FOLDER_NAME = "static" +PROJECT_DIR = "yoti_example/" STATIC_ROOT = os.path.join(BASE_DIR, STATICFILES_FOLDER_NAME) -STATICFILES_DIRS = os.path.join(PROJECT_DIR, STATICFILES_FOLDER_NAME), +STATICFILES_DIRS = (os.path.join(PROJECT_DIR, STATICFILES_FOLDER_NAME),) diff --git a/setup.py b/setup.py index a26ec964..da33b41a 100644 --- a/setup.py +++ b/setup.py @@ -3,43 +3,56 @@ from yoti_python_sdk import __version__ -long_description = 'This package contains the tools you need to quickly ' \ - 'integrate your Python back-end with Yoti, so that your ' \ - 'users can share their identity details with your ' \ - 'application in a secure and trusted way.' +long_description = ( + "This package contains the tools you need to quickly " + "integrate your Python back-end with Yoti, so that your " + "users can share their identity details with your " + "application in a secure and trusted way." +) setup( - name='yoti', + name="yoti", version=__version__, packages=find_packages(), - license='MIT', - description='The Yoti Python SDK, providing API support for Login, Verify (2FA) and Age Verification.', + license="MIT", + description="The Yoti Python SDK, providing API support for Login, Verify (2FA) and Age Verification.", long_description=long_description, - url='https://github.com/getyoti/yoti-python-sdk', - author='Yoti', - author_email='websdk@yoti.com', - install_requires=['cryptography>=2.2.1', 'protobuf>=3.1.0', - 'requests>=2.11.1', 'future>=0.11.0', 'asn1==2.2.0', 'pyopenssl>=18.0.0'], + url="https://github.com/getyoti/yoti-python-sdk", + author="Yoti", + author_email="websdk@yoti.com", + install_requires=[ + "cryptography>=2.2.1", + "protobuf>=3.1.0", + "requests>=2.11.1", + "future>=0.11.0", + "asn1==2.2.0", + "pyopenssl>=18.0.0", + ], extras_require={ - 'examples': ['Django>1.11.16', 'Flask>=0.10', 'python-dotenv>=0.7.1', 'django-sslserver>=0.2', - 'Werkzeug==0.11.15'], + "examples": [ + "Django>1.11.16", + "Flask>=0.10", + "python-dotenv>=0.7.1", + "django-sslserver>=0.2", + "Werkzeug==0.11.15", + ] }, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Intended Audience :: Developers', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Software Development :: Libraries :: Python Modules', + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: Software Development :: Libraries :: Python Modules", ], - keywords='yoti sdk 2FA multifactor authentication verification identity login register verify 2Factor', + keywords="yoti sdk 2FA multifactor authentication verification identity login register verify 2Factor", ) diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py index 6d3956a9..13189ff3 100644 --- a/yoti_python_sdk/__init__.py +++ b/yoti_python_sdk/__init__.py @@ -6,27 +6,26 @@ from yoti_python_sdk.client import Client DEFAULTS = { - 'YOTI_API_URL': 'https://api.yoti.com', - 'YOTI_API_PORT': 443, - 'YOTI_API_VERSION': 'v1', + "YOTI_API_URL": "https://api.yoti.com", + "YOTI_API_PORT": 443, + "YOTI_API_VERSION": "v1", } main_ns = {} directory_name = os.path.dirname(__file__) -version_path = os.path.join(directory_name, 'version.py') +version_path = os.path.join(directory_name, "version.py") ver_path = convert_path(version_path) with open(ver_path) as ver_file: exec(ver_file.read(), main_ns) -__version__ = main_ns['__version__'] -YOTI_API_URL = environ.get('YOTI_API_URL', DEFAULTS['YOTI_API_URL']) -YOTI_API_PORT = environ.get('YOTI_API_PORT', DEFAULTS['YOTI_API_PORT']) -YOTI_API_VERSION = environ.get('YOTI_API_VERSION', DEFAULTS['YOTI_API_VERSION']) -YOTI_API_ENDPOINT = '{0}:{1}/api/{2}'.format(YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION) +__version__ = main_ns["__version__"] +YOTI_API_URL = environ.get("YOTI_API_URL", DEFAULTS["YOTI_API_URL"]) +YOTI_API_PORT = environ.get("YOTI_API_PORT", DEFAULTS["YOTI_API_PORT"]) +YOTI_API_VERSION = environ.get("YOTI_API_VERSION", DEFAULTS["YOTI_API_VERSION"]) +YOTI_API_ENDPOINT = "{0}:{1}/api/{2}".format( + YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION +) -__all__ = [ - 'Client', - __version__ -] +__all__ = ["Client", __version__] diff --git a/yoti_python_sdk/aml.py b/yoti_python_sdk/aml.py index 93a52f88..c1d620d0 100644 --- a/yoti_python_sdk/aml.py +++ b/yoti_python_sdk/aml.py @@ -7,14 +7,16 @@ def __init__(self, response_text): raise ValueError("AML Response is not valid") try: - self.on_pep_list = json.loads(response_text).get('on_pep_list') - self.on_fraud_list = json.loads(response_text).get('on_fraud_list') - self.on_watch_list = json.loads(response_text).get('on_watch_list') + self.on_pep_list = json.loads(response_text).get("on_pep_list") + self.on_fraud_list = json.loads(response_text).get("on_fraud_list") + self.on_watch_list = json.loads(response_text).get("on_watch_list") except (AttributeError, IOError, TypeError, OSError) as exc: - error = 'Could not parse AML result from response: "{0}"'.format(response_text) - exception = '{0}: {1}'.format(type(exc).__name__, exc) - raise RuntimeError('{0}: {1}'.format(error, exception)) + error = 'Could not parse AML result from response: "{0}"'.format( + response_text + ) + exception = "{0}: {1}".format(type(exc).__name__, exc) + raise RuntimeError("{0}: {1}".format(error, exception)) self.__check_for_none_values(self.on_pep_list) self.__check_for_none_values(self.on_fraud_list) @@ -23,12 +25,16 @@ def __init__(self, response_text): @staticmethod def __check_for_none_values(arg): if arg is None: - raise TypeError(str.format("{0} argument was unable to be retrieved from the response", arg)) + raise TypeError( + str.format( + "{0} argument was unable to be retrieved from the response", arg + ) + ) def __iter__(self): - yield 'on_pep_list', self.on_pep_list - yield 'on_fraud_list', self.on_fraud_list - yield 'on_watch_list', self.on_watch_list + yield "on_pep_list", self.on_pep_list + yield "on_fraud_list", self.on_fraud_list + yield "on_watch_list", self.on_watch_list class AmlAddress: diff --git a/yoti_python_sdk/attribute.py b/yoti_python_sdk/attribute.py index 9d2e514e..ec3b06da 100644 --- a/yoti_python_sdk/attribute.py +++ b/yoti_python_sdk/attribute.py @@ -23,8 +23,12 @@ def anchors(self): @property def sources(self): - return list(filter(lambda a: a.anchor_type == config.ANCHOR_SOURCE, self.__anchors)) + return list( + filter(lambda a: a.anchor_type == config.ANCHOR_SOURCE, self.__anchors) + ) @property def verifiers(self): - return list(filter(lambda a: a.anchor_type == config.ANCHOR_VERIFIER, self.__anchors)) + return list( + filter(lambda a: a.anchor_type == config.ANCHOR_VERIFIER, self.__anchors) + ) diff --git a/yoti_python_sdk/attribute_parser.py b/yoti_python_sdk/attribute_parser.py index 50d3ecbc..95ce8554 100644 --- a/yoti_python_sdk/attribute_parser.py +++ b/yoti_python_sdk/attribute_parser.py @@ -11,27 +11,34 @@ def value_based_on_content_type(value, content_type=None): from yoti_python_sdk.image import Image + if content_type == Protobuf.CT_STRING: - return value.decode('utf-8') - elif value == b'': - raise ValueError("Content type: '{0}' should not have an empty value".format(content_type)) + return value.decode("utf-8") + elif value == b"": + raise ValueError( + "Content type: '{0}' should not have an empty value".format(content_type) + ) elif content_type == Protobuf.CT_DATE: - return value.decode('utf-8') + return value.decode("utf-8") elif content_type in Image.allowed_types(): return Image(value, content_type) elif content_type == Protobuf.CT_JSON: return convert_to_dict(value) elif content_type == Protobuf.CT_INT: - string_value = value.decode('utf-8') + string_value = value.decode("utf-8") int_value = int(string_value) return int_value elif content_type == Protobuf.CT_MULTI_VALUE: return tuple(multivalue.parse(value)) if logging.getLogger().propagate: - logging.warning("Unknown type '{0}', attempting to parse it as a String".format(content_type)) + logging.warning( + "Unknown type '{0}', attempting to parse it as a String".format( + content_type + ) + ) - return value.decode('utf-8') + return value.decode("utf-8") def convert_to_dict(byte_value): diff --git a/yoti_python_sdk/crypto.py b/yoti_python_sdk/crypto.py index 63d9fa64..abaebfe9 100644 --- a/yoti_python_sdk/crypto.py +++ b/yoti_python_sdk/crypto.py @@ -3,61 +3,52 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.primitives.ciphers import ( - Cipher, algorithms, modes -) +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes class Crypto: def __init__(self, pem_container): self.private_key = serialization.load_pem_private_key( - data=pem_container, - password=None, - backend=default_backend() + data=pem_container, password=None, backend=default_backend() ) def get_public_key(self): public_key = self.private_key.public_key() der = public_key.public_bytes( encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo + format=serialization.PublicFormat.SubjectPublicKeyInfo, ) - return base64.b64encode(der).decode('utf-8') + return base64.b64encode(der).decode("utf-8") def decrypt_token(self, encrypted_token): try: if not isinstance(encrypted_token, bytes): # On Python 2 the token is str and to b64decode it we need bytes or unicode - encrypted_token = encrypted_token.encode('utf-8') + encrypted_token = encrypted_token.encode("utf-8") data = base64.urlsafe_b64decode(encrypted_token) decrypted = self.private_key.decrypt( - ciphertext=data, - padding=padding.PKCS1v15() + ciphertext=data, padding=padding.PKCS1v15() ) return decrypted except Exception as exc: raise ValueError( - 'Could not decrypt token: {0}, {1}'.format( - encrypted_token, exc - ) + "Could not decrypt token: {0}, {1}".format(encrypted_token, exc) ) def sign(self, message): signature = self.private_key.sign( - data=message.encode('utf-8'), + data=message.encode("utf-8"), padding=padding.PKCS1v15(), - algorithm=hashes.SHA256() + algorithm=hashes.SHA256(), ) - return base64.b64encode(signature).decode('utf-8') + return base64.b64encode(signature).decode("utf-8") @staticmethod def decipher(key, iv, cipher_text): decryptor = Cipher( - algorithm=algorithms.AES(key), - mode=modes.CBC(iv), - backend=default_backend() + algorithm=algorithms.AES(key), mode=modes.CBC(iv), backend=default_backend() ).decryptor() plaintext = decryptor.update(cipher_text) + decryptor.finalize() diff --git a/yoti_python_sdk/image.py b/yoti_python_sdk/image.py index ca146ac6..63d7cb60 100644 --- a/yoti_python_sdk/image.py +++ b/yoti_python_sdk/image.py @@ -14,7 +14,11 @@ def __init__(self, image_bytes, image_content_type): elif image_content_type == Protobuf.CT_PNG: self.__content_type = CONTENT_TYPE_PNG else: - raise TypeError("Content type '{0}' is not a supported image type".format(image_content_type)) + raise TypeError( + "Content type '{0}' is not a supported image type".format( + image_content_type + ) + ) self.__data = image_bytes @@ -34,5 +38,5 @@ def mime_type(self): return "image/{0}".format(self.__content_type) def base64_content(self): - data = base64.b64encode(self.__data).decode('utf-8') - return 'data:{0};base64,{1}'.format(self.mime_type(), data) + data = base64.b64encode(self.__data).decode("utf-8") + return "data:{0};base64,{1}".format(self.mime_type(), data) diff --git a/yoti_python_sdk/multivalue.py b/yoti_python_sdk/multivalue.py index 1414c348..772b4ebe 100644 --- a/yoti_python_sdk/multivalue.py +++ b/yoti_python_sdk/multivalue.py @@ -3,7 +3,9 @@ def parse(multi_value_bytes): - from yoti_python_sdk import attribute_parser # needed here (and not above) for Python 2.7 & 3.4 dependency handling + from yoti_python_sdk import ( + attribute_parser, + ) # needed here (and not above) for Python 2.7 & 3.4 dependency handling proto = protobuf.Protobuf() multi_value_list = [] @@ -12,8 +14,9 @@ def parse(multi_value_bytes): for multi_value_item in parsed_multi_value.values: multi_value_list.append( attribute_parser.value_based_on_content_type( - multi_value_item.data, - multi_value_item.content_type)) + multi_value_item.data, multi_value_item.content_type + ) + ) return multi_value_list diff --git a/yoti_python_sdk/tests/attribute_fixture_parser.py b/yoti_python_sdk/tests/attribute_fixture_parser.py index aca50070..fbccb78f 100644 --- a/yoti_python_sdk/tests/attribute_fixture_parser.py +++ b/yoti_python_sdk/tests/attribute_fixture_parser.py @@ -5,8 +5,8 @@ from yoti_python_sdk.protobuf import protobuf from yoti_python_sdk.tests import file_helper -FIXTURES_DIR = join(dirname(abspath(__file__)), 'fixtures') -ATTRIBUTE_DOCUMENT_IMAGES = join(FIXTURES_DIR, 'attribute_document_images.txt') +FIXTURES_DIR = join(dirname(abspath(__file__)), "fixtures") +ATTRIBUTE_DOCUMENT_IMAGES = join(FIXTURES_DIR, "attribute_document_images.txt") def get_attribute_from_base64_text(file_path): @@ -17,8 +17,9 @@ def get_attribute_from_base64_text(file_path): def parse_multi_value(): multi_value_proto_attribute = get_attribute_from_base64_text( - ATTRIBUTE_DOCUMENT_IMAGES) + ATTRIBUTE_DOCUMENT_IMAGES + ) return attribute_parser.value_based_on_content_type( - multi_value_proto_attribute.value, - multi_value_proto_attribute.content_type) + multi_value_proto_attribute.value, multi_value_proto_attribute.content_type + ) diff --git a/yoti_python_sdk/tests/file_helper.py b/yoti_python_sdk/tests/file_helper.py index eebaed80..642c0d82 100644 --- a/yoti_python_sdk/tests/file_helper.py +++ b/yoti_python_sdk/tests/file_helper.py @@ -2,7 +2,7 @@ import io from os.path import abspath, dirname, join -FIXTURES_DIR = join(dirname(abspath(__file__)), 'fixtures') +FIXTURES_DIR = join(dirname(abspath(__file__)), "fixtures") def get_file_bytes(file_path): @@ -13,5 +13,5 @@ def get_file_bytes(file_path): def read_file(file_path): - with io.open(file_path, mode='r', encoding='utf-8') as file: + with io.open(file_path, mode="r", encoding="utf-8") as file: return file.read() diff --git a/yoti_python_sdk/tests/test_aml.py b/yoti_python_sdk/tests/test_aml.py index e2b3f2bd..77cf1947 100644 --- a/yoti_python_sdk/tests/test_aml.py +++ b/yoti_python_sdk/tests/test_aml.py @@ -5,7 +5,9 @@ from yoti_python_sdk import aml VALID_RESPONSE = '{"on_fraud_list":false,"on_pep_list":false,"on_watch_list":true}' -INVALID_FORMAT_RESPONSE = json.loads('{"on_fraud_list":false,"on_pep_list":false,"on_watch_list":true}') +INVALID_FORMAT_RESPONSE = json.loads( + '{"on_fraud_list":false,"on_pep_list":false,"on_watch_list":true}' +) MISSING_FRAUD_LIST_RESPONSE = '{"on_pep_list":false,"on_watch_list":true}' VALID_AML_ADDRESS = aml.AmlAddress(country="FRA", postcode="ABC123") @@ -17,7 +19,7 @@ def test_getting_aml_result_with_valid_response(): def test_getting_aml_result_with_invalid_format_response(): with pytest.raises(RuntimeError) as exc: aml.AmlResult(INVALID_FORMAT_RESPONSE) - expected_error = 'Could not parse AML result from response' + expected_error = "Could not parse AML result from response" assert expected_error in str(exc) @@ -28,7 +30,7 @@ def test_getting_aml_result_with_missing_value(): def test_getting_aml_result_with_empty_string_response(): with pytest.raises(ValueError): - aml.AmlResult('') + aml.AmlResult("") def test_getting_aml_result_with_none_value(): @@ -42,7 +44,5 @@ def test_setting_aml_address_with_valid_values(): def test_setting_aml_profile_with_valid_values(): aml.AmlProfile( - given_names="Joe", - family_name="Bloggs", - address=VALID_AML_ADDRESS, - ssn="123456") + given_names="Joe", family_name="Bloggs", address=VALID_AML_ADDRESS, ssn="123456" + ) diff --git a/yoti_python_sdk/tests/test_attribute_parser.py b/yoti_python_sdk/tests/test_attribute_parser.py index 33c362eb..071ac175 100644 --- a/yoti_python_sdk/tests/test_attribute_parser.py +++ b/yoti_python_sdk/tests/test_attribute_parser.py @@ -11,16 +11,19 @@ INT_VALUE = int(STRING_VALUE) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def proto(): return protobuf.Protobuf() @pytest.mark.parametrize( "content_type, expected_value", - [(proto().CT_STRING, STRING_VALUE), - (proto().CT_DATE, STRING_VALUE), - (proto().CT_INT, INT_VALUE)]) + [ + (proto().CT_STRING, STRING_VALUE), + (proto().CT_DATE, STRING_VALUE), + (proto().CT_INT, INT_VALUE), + ], +) def test_attribute_parser_values_based_on_content_type(content_type, expected_value): result = attribute_parser.value_based_on_content_type(BYTE_VALUE, content_type) assert result == expected_value @@ -31,7 +34,9 @@ def test_attribute_parser_values_based_on_other_content_types(proto): logger = logging.getLogger() logger.propagate = False - result = attribute_parser.value_based_on_content_type(BYTE_VALUE, proto.CT_UNDEFINED) + result = attribute_parser.value_based_on_content_type( + BYTE_VALUE, proto.CT_UNDEFINED + ) assert result == STRING_VALUE result = attribute_parser.value_based_on_content_type(BYTE_VALUE) diff --git a/yoti_python_sdk/tests/test_crypto.py b/yoti_python_sdk/tests/test_crypto.py index 85ef523b..2a2f4c22 100644 --- a/yoti_python_sdk/tests/test_crypto.py +++ b/yoti_python_sdk/tests/test_crypto.py @@ -6,29 +6,27 @@ from yoti_python_sdk.crypto import Crypto -@pytest.mark.parametrize('invalid_token', [ - '', - None, - 'some_invalid_token', - True, - 123, -]) +@pytest.mark.parametrize("invalid_token", ["", None, "some_invalid_token", True, 123]) def test_decrypting_an_invalid_toke__should_not_be_allowed(invalid_token, crypto): with pytest.raises(ValueError): crypto.decrypt_token(invalid_token) def test_given_proper_encrypted_token__decrypting_should_yield_decrypted_token( - encrypted_request_token, decrypted_request_token, crypto): + encrypted_request_token, decrypted_request_token, crypto +): expected_token = decrypted_request_token - decrypted_token = crypto.decrypt_token(encrypted_request_token).decode('utf-8') + decrypted_token = crypto.decrypt_token(encrypted_request_token).decode("utf-8") assert decrypted_token == expected_token -@pytest.mark.parametrize('with_padding,stripped', [ - (b'\xfa\x01', b'\xfa'), - (b'\xfa\x06\x06\x06\x06\x06\x06', b'\xfa'), - (b'\xfa\x08\x08\x08\x08\x08\x08\x08\x08', b'\xfa'), -]) +@pytest.mark.parametrize( + "with_padding,stripped", + [ + (b"\xfa\x01", b"\xfa"), + (b"\xfa\x06\x06\x06\x06\x06\x06", b"\xfa"), + (b"\xfa\x08\x08\x08\x08\x08\x08\x08\x08", b"\xfa"), + ], +) def test_strip_pkcs5_padding(with_padding, stripped): assert Crypto.strip_pkcs5_padding(with_padding) == stripped diff --git a/yoti_python_sdk/tests/test_image.py b/yoti_python_sdk/tests/test_image.py index c6da7a70..2ef9da41 100644 --- a/yoti_python_sdk/tests/test_image.py +++ b/yoti_python_sdk/tests/test_image.py @@ -7,21 +7,26 @@ def test_image_with_unsupported_type(): with pytest.raises(TypeError): - Image(b'', Protobuf.CT_UNDEFINED) + Image(b"", Protobuf.CT_UNDEFINED) -@pytest.mark.parametrize("content_type, expected_mime_type", - [(Protobuf.CT_JPEG, 'image/jpeg'), - (Protobuf.CT_PNG, 'image/png')]) +@pytest.mark.parametrize( + "content_type, expected_mime_type", + [(Protobuf.CT_JPEG, "image/jpeg"), (Protobuf.CT_PNG, "image/png")], +) def test_image_mime_type(content_type, expected_mime_type): - image = Image(b'', content_type) + image = Image(b"", content_type) assert image.mime_type() == expected_mime_type -@pytest.mark.parametrize("content_type, expected_base64_content", - [(Protobuf.CT_JPEG, 'data:image/jpeg;base64,dGVzdCBzdHJpbmc='), - (Protobuf.CT_PNG, 'data:image/png;base64,dGVzdCBzdHJpbmc=')]) +@pytest.mark.parametrize( + "content_type, expected_base64_content", + [ + (Protobuf.CT_JPEG, "data:image/jpeg;base64,dGVzdCBzdHJpbmc="), + (Protobuf.CT_PNG, "data:image/png;base64,dGVzdCBzdHJpbmc="), + ], +) def test_image_base64_content(content_type, expected_base64_content): - image = Image(b'test string', content_type) + image = Image(b"test string", content_type) assert image.base64_content() == expected_base64_content From 84e8e54cabe6a713a0a5c613946f1fae0297dee4 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Mon, 12 Aug 2019 17:40:21 +0100 Subject: [PATCH 71/89] SDK-1120: Fix all line endings to UNIX LF line endings --- examples/yoti_example_django/requirements.txt | 52 +- examples/yoti_example_flask/requirements.txt | 56 +- yoti_python_sdk/attribute.py | 68 +- yoti_python_sdk/config.py | 56 +- yoti_python_sdk/profile.py | 350 ++--- .../tests/fixtures/anchor_driving_license.txt | 2 +- .../tests/fixtures/anchor_passport.txt | 2 +- .../tests/fixtures/anchor_yoti_admin.txt | 2 +- yoti_python_sdk/tests/protobuf_attribute.py | 30 +- yoti_python_sdk/tests/test_anchor.py | 242 ++-- yoti_python_sdk/tests/test_attribute.py | 84 +- yoti_python_sdk/tests/test_client.py | 762 +++++------ yoti_python_sdk/tests/test_profile.py | 1178 ++++++++--------- 13 files changed, 1442 insertions(+), 1442 deletions(-) diff --git a/examples/yoti_example_django/requirements.txt b/examples/yoti_example_django/requirements.txt index 9fe5134f..a5e3d690 100644 --- a/examples/yoti_example_django/requirements.txt +++ b/examples/yoti_example_django/requirements.txt @@ -1,26 +1,26 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile --output-file=requirements.txt requirements.in -# -asn1==2.2.0 # via yoti -asn1crypto==0.24.0 # via cryptography -certifi==2018.4.16 # via requests -cffi==1.11.5 # via cryptography -chardet==3.0.4 # via requests -cryptography==2.5 -django-sslserver==0.20 -django==2.2.3 -future==0.16.0 # via yoti -idna==2.7 # via requests -protobuf==3.6.0 # via yoti -pycparser==2.18 # via cffi -pyopenssl==18.0.0 # via yoti -python-dotenv==0.8.2 -pytz==2018.4 # via django -requests==2.21.0 -six==1.11.0 # via cryptography, protobuf, pyopenssl -sqlparse==0.3.0 # via django -urllib3==1.24.2 -yoti==2.7.0 +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements.txt requirements.in +# +asn1==2.2.0 # via yoti +asn1crypto==0.24.0 # via cryptography +certifi==2018.4.16 # via requests +cffi==1.11.5 # via cryptography +chardet==3.0.4 # via requests +cryptography==2.5 +django-sslserver==0.20 +django==2.2.3 +future==0.16.0 # via yoti +idna==2.7 # via requests +protobuf==3.6.0 # via yoti +pycparser==2.18 # via cffi +pyopenssl==18.0.0 # via yoti +python-dotenv==0.8.2 +pytz==2018.4 # via django +requests==2.21.0 +six==1.11.0 # via cryptography, protobuf, pyopenssl +sqlparse==0.3.0 # via django +urllib3==1.24.2 +yoti==2.7.0 diff --git a/examples/yoti_example_flask/requirements.txt b/examples/yoti_example_flask/requirements.txt index ed381f7b..03a53d46 100644 --- a/examples/yoti_example_flask/requirements.txt +++ b/examples/yoti_example_flask/requirements.txt @@ -1,28 +1,28 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile --output-file=requirements.txt requirements.in -# -asn1==2.2.0 # via yoti -asn1crypto==0.24.0 # via cryptography -certifi==2018.4.16 # via requests -cffi==1.11.5 # via cryptography -chardet==3.0.4 # via requests -click==6.7 # via flask -cryptography==2.5 -flask==1.0.2 -future==0.16.0 # via yoti -idna==2.7 # via requests -itsdangerous==0.24 # via flask -jinja2==2.10.1 -markupsafe==1.0 # via jinja2 -protobuf==3.6.0 # via yoti -pycparser==2.18 # via cffi -pyopenssl==19.0.0 -python-dotenv==0.8.2 -requests==2.21.0 -six==1.11.0 # via cryptography, protobuf, pyopenssl -urllib3==1.24.2 -werkzeug==0.14.1 # via flask -yoti==2.7.0 +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements.txt requirements.in +# +asn1==2.2.0 # via yoti +asn1crypto==0.24.0 # via cryptography +certifi==2018.4.16 # via requests +cffi==1.11.5 # via cryptography +chardet==3.0.4 # via requests +click==6.7 # via flask +cryptography==2.5 +flask==1.0.2 +future==0.16.0 # via yoti +idna==2.7 # via requests +itsdangerous==0.24 # via flask +jinja2==2.10.1 +markupsafe==1.0 # via jinja2 +protobuf==3.6.0 # via yoti +pycparser==2.18 # via cffi +pyopenssl==19.0.0 +python-dotenv==0.8.2 +requests==2.21.0 +six==1.11.0 # via cryptography, protobuf, pyopenssl +urllib3==1.24.2 +werkzeug==0.14.1 # via flask +yoti==2.7.0 diff --git a/yoti_python_sdk/attribute.py b/yoti_python_sdk/attribute.py index ec3b06da..c933154a 100644 --- a/yoti_python_sdk/attribute.py +++ b/yoti_python_sdk/attribute.py @@ -1,34 +1,34 @@ -from yoti_python_sdk import config - - -class Attribute: - def __init__(self, name="", value="", anchors=None): - if anchors is None: - anchors = {} - self.__name = name - self.__value = value - self.__anchors = anchors - - @property - def name(self): - return self.__name - - @property - def value(self): - return self.__value - - @property - def anchors(self): - return self.__anchors - - @property - def sources(self): - return list( - filter(lambda a: a.anchor_type == config.ANCHOR_SOURCE, self.__anchors) - ) - - @property - def verifiers(self): - return list( - filter(lambda a: a.anchor_type == config.ANCHOR_VERIFIER, self.__anchors) - ) +from yoti_python_sdk import config + + +class Attribute: + def __init__(self, name="", value="", anchors=None): + if anchors is None: + anchors = {} + self.__name = name + self.__value = value + self.__anchors = anchors + + @property + def name(self): + return self.__name + + @property + def value(self): + return self.__value + + @property + def anchors(self): + return self.__anchors + + @property + def sources(self): + return list( + filter(lambda a: a.anchor_type == config.ANCHOR_SOURCE, self.__anchors) + ) + + @property + def verifiers(self): + return list( + filter(lambda a: a.anchor_type == config.ANCHOR_VERIFIER, self.__anchors) + ) diff --git a/yoti_python_sdk/config.py b/yoti_python_sdk/config.py index f52dee2b..758db08a 100644 --- a/yoti_python_sdk/config.py +++ b/yoti_python_sdk/config.py @@ -1,28 +1,28 @@ -# -*- coding: utf-8 -*- - -SDK_IDENTIFIER = "Python" -ATTRIBUTE_AGE_OVER = "age_over:" -ATTRIBUTE_AGE_UNDER = "age_under:" -ATTRIBUTE_DATE_OF_BIRTH = "date_of_birth" -ATTRIBUTE_FAMILY_NAME = "family_name" -ATTRIBUTE_FULL_NAME = "full_name" -ATTRIBUTE_GENDER = "gender" -ATTRIBUTE_GIVEN_NAMES = "given_names" -ATTRIBUTE_NATIONALITY = "nationality" -ATTRIBUTE_EMAIL_ADDRESS = "email_address" -ATTRIBUTE_PHONE_NUMBER = "phone_number" -ATTRIBUTE_POSTAL_ADDRESS = "postal_address" -ATTRIBUTE_SELFIE = "selfie" -ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS = "structured_postal_address" -ATTRIBUTE_DOCUMENT_IMAGES = "document_images" -ATTRIBUTE_DOCUMENT_DETAILS = "document_details" -ANCHOR_SOURCE = "SOURCE" -ANCHOR_VERIFIER = "VERIFIER" -KEY_AGE_VERIFIED = "is_age_verified" -KEY_BASE64_SELFIE = "base64_selfie_uri" -KEY_FORMATTED_ADDRESS = "formatted_address" -X_YOTI_AUTH_KEY = "X-Yoti-Auth-Key" -X_YOTI_AUTH_DIGEST = "X-Yoti-Auth-Digest" -X_YOTI_SDK = "X-Yoti-SDK" -X_YOTI_SDK_VERSION = X_YOTI_SDK + "-Version" -JSON_CONTENT_TYPE = "application/json" +# -*- coding: utf-8 -*- + +SDK_IDENTIFIER = "Python" +ATTRIBUTE_AGE_OVER = "age_over:" +ATTRIBUTE_AGE_UNDER = "age_under:" +ATTRIBUTE_DATE_OF_BIRTH = "date_of_birth" +ATTRIBUTE_FAMILY_NAME = "family_name" +ATTRIBUTE_FULL_NAME = "full_name" +ATTRIBUTE_GENDER = "gender" +ATTRIBUTE_GIVEN_NAMES = "given_names" +ATTRIBUTE_NATIONALITY = "nationality" +ATTRIBUTE_EMAIL_ADDRESS = "email_address" +ATTRIBUTE_PHONE_NUMBER = "phone_number" +ATTRIBUTE_POSTAL_ADDRESS = "postal_address" +ATTRIBUTE_SELFIE = "selfie" +ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS = "structured_postal_address" +ATTRIBUTE_DOCUMENT_IMAGES = "document_images" +ATTRIBUTE_DOCUMENT_DETAILS = "document_details" +ANCHOR_SOURCE = "SOURCE" +ANCHOR_VERIFIER = "VERIFIER" +KEY_AGE_VERIFIED = "is_age_verified" +KEY_BASE64_SELFIE = "base64_selfie_uri" +KEY_FORMATTED_ADDRESS = "formatted_address" +X_YOTI_AUTH_KEY = "X-Yoti-Auth-Key" +X_YOTI_AUTH_DIGEST = "X-Yoti-Auth-Digest" +X_YOTI_SDK = "X-Yoti-SDK" +X_YOTI_SDK_VERSION = X_YOTI_SDK + "-Version" +JSON_CONTENT_TYPE = "application/json" diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index 1bd75552..19317e03 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -1,175 +1,175 @@ -# -*- coding: utf-8 -*- -import logging - -from yoti_python_sdk import attribute_parser, config, multivalue -from yoti_python_sdk.anchor import Anchor -from yoti_python_sdk.attribute import Attribute -from yoti_python_sdk.image import Image -from yoti_python_sdk import document_details - - -class Profile: - def __init__(self, profile_attributes): - self.attributes = {} - - if profile_attributes: - for field in profile_attributes: - try: - value = attribute_parser.value_based_on_content_type( - field.value, field.content_type - ) - - # this will be removed in v3.0.0, so selfie also returns an Image object - if field.content_type in Image.allowed_types(): - if field.name == config.ATTRIBUTE_SELFIE: - value = field.value - - if field.name == config.ATTRIBUTE_DOCUMENT_IMAGES: - value = multivalue.filter_values(value, Image) - if field.name == config.ATTRIBUTE_DOCUMENT_DETAILS: - value = document_details.DocumentDetails(value) - - anchors = Anchor().parse_anchors(field.anchors) - - self.attributes[field.name] = Attribute(field.name, value, anchors) - - except ValueError as ve: - if logging.getLogger().propagate: - logging.warning(ve) - except Exception as exc: - if logging.getLogger().propagate: - logging.warning( - "Error parsing profile attribute:{0}, exception: {1} - {2}".format( - field.name, type(exc).__name__, exc - ) - ) - - self.ensure_postal_address() - - @property - def date_of_birth(self): - """date_of_birth represents the user's date of birth as a string. - Will be changed to return a datetime in v3.0.0. - Will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_DATE_OF_BIRTH) - - @property - def family_name(self): - """family_name represents the user's family name. This will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_FAMILY_NAME) - - @property - def given_names(self): - """given_names represents the user's given names. This will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_GIVEN_NAMES) - - @property - def full_name(self): - """full_name represents the user's full name. - If family_name and given_names are present, the value will be equal to the string 'given_names + " " + family_name'. - Will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_FULL_NAME) - - @property - def gender(self): - """gender corresponds to the gender in the registered document. - The value will be one of the strings "MALE", "FEMALE", "TRANSGENDER" or "OTHER". - Will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_GENDER) - - @property - def nationality(self): - """nationality corresponds to the nationality in the passport. - The value is an ISO-3166-1 alpha-3 code with ICAO9303 (passport) extensions. - Will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_NATIONALITY) - - @property - def email_address(self): - """email_address represents the user's email address. This will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_EMAIL_ADDRESS) - - @property - def phone_number(self): - """phone_number represents the user's mobile phone number. This will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_PHONE_NUMBER) - - @property - def postal_address(self): - """postal_address represents the user's address. This will be None if not provided by Yoti. - :return: Attribute(str) - """ - return self.get_attribute(config.ATTRIBUTE_POSTAL_ADDRESS) - - @property - def selfie(self): - """selfie is a photograph of the user. Will be None if not provided by Yoti. - :return: Attribute(image) - """ - return self.get_attribute(config.ATTRIBUTE_SELFIE) - - @property - def structured_postal_address(self): - """structured_postal_address represents the user's address represented as an OrderedDict. - This will be None if not provided by Yoti. - :return: Attribute(OrderedDict) - """ - return self.get_attribute(config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS) - - @property - def document_images(self): - """document_images returns a tuple of document images cropped from the image in the capture page. - There can be multiple images as per the number of regions in the capture in this attribute. - Will be None if not provided by Yoti. - :return: Attribute(tuple(image)) - """ - return self.get_attribute(config.ATTRIBUTE_DOCUMENT_IMAGES) - - @property - def document_details(self): - return self.get_attribute(config.ATTRIBUTE_DOCUMENT_DETAILS) - - def get_attribute(self, attribute_name): - """retrieves an attribute based on its name - :param attribute_name: - :return: Attribute - """ - if attribute_name in self.attributes: - return self.attributes.get(attribute_name) - else: - return None - - def ensure_postal_address(self): - if ( - config.ATTRIBUTE_POSTAL_ADDRESS not in self.attributes - and config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS in self.attributes - ): - structured_postal_address = self.attributes[ - config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS - ] - - if config.KEY_FORMATTED_ADDRESS in structured_postal_address.value: - formatted_address = structured_postal_address.value[ - config.KEY_FORMATTED_ADDRESS - ] - self.attributes[config.ATTRIBUTE_POSTAL_ADDRESS] = Attribute( - config.ATTRIBUTE_POSTAL_ADDRESS, - formatted_address, - structured_postal_address.anchors, - ) +# -*- coding: utf-8 -*- +import logging + +from yoti_python_sdk import attribute_parser, config, multivalue +from yoti_python_sdk.anchor import Anchor +from yoti_python_sdk.attribute import Attribute +from yoti_python_sdk.image import Image +from yoti_python_sdk import document_details + + +class Profile: + def __init__(self, profile_attributes): + self.attributes = {} + + if profile_attributes: + for field in profile_attributes: + try: + value = attribute_parser.value_based_on_content_type( + field.value, field.content_type + ) + + # this will be removed in v3.0.0, so selfie also returns an Image object + if field.content_type in Image.allowed_types(): + if field.name == config.ATTRIBUTE_SELFIE: + value = field.value + + if field.name == config.ATTRIBUTE_DOCUMENT_IMAGES: + value = multivalue.filter_values(value, Image) + if field.name == config.ATTRIBUTE_DOCUMENT_DETAILS: + value = document_details.DocumentDetails(value) + + anchors = Anchor().parse_anchors(field.anchors) + + self.attributes[field.name] = Attribute(field.name, value, anchors) + + except ValueError as ve: + if logging.getLogger().propagate: + logging.warning(ve) + except Exception as exc: + if logging.getLogger().propagate: + logging.warning( + "Error parsing profile attribute:{0}, exception: {1} - {2}".format( + field.name, type(exc).__name__, exc + ) + ) + + self.ensure_postal_address() + + @property + def date_of_birth(self): + """date_of_birth represents the user's date of birth as a string. + Will be changed to return a datetime in v3.0.0. + Will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_DATE_OF_BIRTH) + + @property + def family_name(self): + """family_name represents the user's family name. This will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_FAMILY_NAME) + + @property + def given_names(self): + """given_names represents the user's given names. This will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_GIVEN_NAMES) + + @property + def full_name(self): + """full_name represents the user's full name. + If family_name and given_names are present, the value will be equal to the string 'given_names + " " + family_name'. + Will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_FULL_NAME) + + @property + def gender(self): + """gender corresponds to the gender in the registered document. + The value will be one of the strings "MALE", "FEMALE", "TRANSGENDER" or "OTHER". + Will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_GENDER) + + @property + def nationality(self): + """nationality corresponds to the nationality in the passport. + The value is an ISO-3166-1 alpha-3 code with ICAO9303 (passport) extensions. + Will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_NATIONALITY) + + @property + def email_address(self): + """email_address represents the user's email address. This will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_EMAIL_ADDRESS) + + @property + def phone_number(self): + """phone_number represents the user's mobile phone number. This will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_PHONE_NUMBER) + + @property + def postal_address(self): + """postal_address represents the user's address. This will be None if not provided by Yoti. + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_POSTAL_ADDRESS) + + @property + def selfie(self): + """selfie is a photograph of the user. Will be None if not provided by Yoti. + :return: Attribute(image) + """ + return self.get_attribute(config.ATTRIBUTE_SELFIE) + + @property + def structured_postal_address(self): + """structured_postal_address represents the user's address represented as an OrderedDict. + This will be None if not provided by Yoti. + :return: Attribute(OrderedDict) + """ + return self.get_attribute(config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS) + + @property + def document_images(self): + """document_images returns a tuple of document images cropped from the image in the capture page. + There can be multiple images as per the number of regions in the capture in this attribute. + Will be None if not provided by Yoti. + :return: Attribute(tuple(image)) + """ + return self.get_attribute(config.ATTRIBUTE_DOCUMENT_IMAGES) + + @property + def document_details(self): + return self.get_attribute(config.ATTRIBUTE_DOCUMENT_DETAILS) + + def get_attribute(self, attribute_name): + """retrieves an attribute based on its name + :param attribute_name: + :return: Attribute + """ + if attribute_name in self.attributes: + return self.attributes.get(attribute_name) + else: + return None + + def ensure_postal_address(self): + if ( + config.ATTRIBUTE_POSTAL_ADDRESS not in self.attributes + and config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS in self.attributes + ): + structured_postal_address = self.attributes[ + config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS + ] + + if config.KEY_FORMATTED_ADDRESS in structured_postal_address.value: + formatted_address = structured_postal_address.value[ + config.KEY_FORMATTED_ADDRESS + ] + self.attributes[config.ATTRIBUTE_POSTAL_ADDRESS] = Attribute( + config.ATTRIBUTE_POSTAL_ADDRESS, + formatted_address, + structured_postal_address.anchors, + ) diff --git a/yoti_python_sdk/tests/fixtures/anchor_driving_license.txt b/yoti_python_sdk/tests/fixtures/anchor_driving_license.txt index d8cda973..d6474a68 100644 --- a/yoti_python_sdk/tests/fixtures/anchor_driving_license.txt +++ b/yoti_python_sdk/tests/fixtures/anchor_driving_license.txt @@ -1 +1 @@ -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 diff --git a/yoti_python_sdk/tests/fixtures/anchor_passport.txt b/yoti_python_sdk/tests/fixtures/anchor_passport.txt index d2ad157c..fee8e008 100644 --- a/yoti_python_sdk/tests/fixtures/anchor_passport.txt +++ b/yoti_python_sdk/tests/fixtures/anchor_passport.txt @@ -1 +1 @@ -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 diff --git a/yoti_python_sdk/tests/fixtures/anchor_yoti_admin.txt b/yoti_python_sdk/tests/fixtures/anchor_yoti_admin.txt index dca6e9ad..0dd7fce5 100644 --- a/yoti_python_sdk/tests/fixtures/anchor_yoti_admin.txt +++ b/yoti_python_sdk/tests/fixtures/anchor_yoti_admin.txt @@ -1 +1 @@ -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 diff --git a/yoti_python_sdk/tests/protobuf_attribute.py b/yoti_python_sdk/tests/protobuf_attribute.py index 1fa5f758..59fd8d5a 100644 --- a/yoti_python_sdk/tests/protobuf_attribute.py +++ b/yoti_python_sdk/tests/protobuf_attribute.py @@ -1,15 +1,15 @@ -# -*- coding: utf-8 -*- -from yoti_python_sdk.protobuf.protobuf import Protobuf - - -class ProtobufAttribute(object): - name = "" - value = "" - anchors = "" - content_type = Protobuf.CT_UNDEFINED - - def __init__(self, name, value, anchors, content_type): - self.name = name - self.value = value - self.anchors = anchors - self.content_type = content_type +# -*- coding: utf-8 -*- +from yoti_python_sdk.protobuf.protobuf import Protobuf + + +class ProtobufAttribute(object): + name = "" + value = "" + anchors = "" + content_type = Protobuf.CT_UNDEFINED + + def __init__(self, name, value, anchors, content_type): + self.name = name + self.value = value + self.anchors = anchors + self.content_type = content_type diff --git a/yoti_python_sdk/tests/test_anchor.py b/yoti_python_sdk/tests/test_anchor.py index 3428c37a..bfd5d465 100644 --- a/yoti_python_sdk/tests/test_anchor.py +++ b/yoti_python_sdk/tests/test_anchor.py @@ -1,121 +1,121 @@ -# -*- coding: utf-8 -*- -import logging -import time -from datetime import datetime - -from yoti_python_sdk.protobuf.attribute_public_api import Attribute_pb2 - -import yoti_python_sdk -from yoti_python_sdk import config -from yoti_python_sdk.anchor import Anchor -from yoti_python_sdk.tests import anchor_fixture_parser - - -def get_utc_offset(): - utc_offset = int(time.timezone / 60 / 60) - - if time.daylight: - utc_offset = utc_offset - time.daylight - - return utc_offset - - -def test_parse_anchors_driving_license(): - parsed_anchor = anchor_fixture_parser.get_parsed_driving_license_anchor() - - assert parsed_anchor.anchor_type == config.ANCHOR_SOURCE - assert parsed_anchor.sub_type == "" - assert parsed_anchor.value == "DRIVING_LICENCE" - assert parsed_anchor.origin_server_certs.serial_number == int( - "46131813624213904216516051554755262812" - ) - assert parsed_anchor.signed_timestamp == datetime( - 2018, 4, 11, 12 - get_utc_offset(), 13, 3, 923537 - ) - - -def test_parse_anchors_passport(): - parsed_anchor = anchor_fixture_parser.get_parsed_passport_anchor() - - assert parsed_anchor.anchor_type == config.ANCHOR_SOURCE - assert parsed_anchor.sub_type == "OCR" - assert parsed_anchor.value == "PASSPORT" - assert parsed_anchor.origin_server_certs.serial_number == int( - "277870515583559162487099305254898397834" - ) - assert parsed_anchor.signed_timestamp == datetime( - 2018, 4, 12, 13 - get_utc_offset(), 14, 32, 835537 - ) - - -def test_parse_yoti_admin(): - parsed_anchor = anchor_fixture_parser.get_parsed_yoti_admin_anchor() - - assert parsed_anchor.anchor_type == config.ANCHOR_VERIFIER - assert parsed_anchor.sub_type == "" - assert parsed_anchor.value == "YOTI_ADMIN" - assert parsed_anchor.origin_server_certs.serial_number == int( - "256616937783084706710155170893983549581" - ) - assert parsed_anchor.signed_timestamp == datetime( - 2018, 4, 11, 12 - get_utc_offset(), 13, 4, 95238 - ) - - -def test_anchor_returns_correct_default_values(): - default_anchor = yoti_python_sdk.anchor.Anchor() - - assert default_anchor.anchor_type == "Unknown" - assert default_anchor.signed_timestamp is None - assert default_anchor.sub_type == "" - assert default_anchor.value == "" - assert default_anchor.origin_server_certs is None - - -def test_error_parsing_anchor_certificate_carries_on_parsing(): - driving_license_anchor = anchor_fixture_parser.get_anchor_from_base64_text( - anchor_fixture_parser.ANCHOR_DRIVING_LICENSE - )[0] - anchors = list() - anchors.append(Attribute_pb2.Anchor()) - anchors.append(driving_license_anchor) - - # 1st anchor will log a warning when being parsed - logger = logging.getLogger() - logger.propagate = False - parsed_anchors = Anchor.parse_anchors(anchors) - logger.propagate = True - - assert len(parsed_anchors) == 1 - - parsed_anchor = parsed_anchors[0] - assert parsed_anchor.anchor_type == config.ANCHOR_SOURCE - assert parsed_anchor.sub_type == "" - assert parsed_anchor.value == "DRIVING_LICENCE" - assert parsed_anchor.origin_server_certs.serial_number == int( - "46131813624213904216516051554755262812" - ) - assert parsed_anchor.signed_timestamp == datetime( - 2018, 4, 11, 12 - get_utc_offset(), 13, 3, 923537 - ) - - -def test_processing_unknown_anchor_data(): - unknown_anchor_data = anchor_fixture_parser.get_anchor_from_base64_text( - anchor_fixture_parser.ANCHOR_UNKNOWN_ANCHOR - ) - anchors = Anchor.parse_anchors(unknown_anchor_data) - - assert len(anchors) == 1 - assert ("", "Unknown", "TEST UNKNOWN SUB TYPE") in [ - (anchor.value, anchor.anchor_type, anchor.sub_type) for anchor in anchors - ] - - expected_timestamp = datetime(2019, 3, 5, 10, 45, 11, 840037) - actual_timestamp = anchors[0].signed_timestamp - - assert expected_timestamp == actual_timestamp - - assert "document-registration-server" in [ - a.value for a in anchors[0].origin_server_certs.issuer - ] +# -*- coding: utf-8 -*- +import logging +import time +from datetime import datetime + +from yoti_python_sdk.protobuf.attribute_public_api import Attribute_pb2 + +import yoti_python_sdk +from yoti_python_sdk import config +from yoti_python_sdk.anchor import Anchor +from yoti_python_sdk.tests import anchor_fixture_parser + + +def get_utc_offset(): + utc_offset = int(time.timezone / 60 / 60) + + if time.daylight: + utc_offset = utc_offset - time.daylight + + return utc_offset + + +def test_parse_anchors_driving_license(): + parsed_anchor = anchor_fixture_parser.get_parsed_driving_license_anchor() + + assert parsed_anchor.anchor_type == config.ANCHOR_SOURCE + assert parsed_anchor.sub_type == "" + assert parsed_anchor.value == "DRIVING_LICENCE" + assert parsed_anchor.origin_server_certs.serial_number == int( + "46131813624213904216516051554755262812" + ) + assert parsed_anchor.signed_timestamp == datetime( + 2018, 4, 11, 12 - get_utc_offset(), 13, 3, 923537 + ) + + +def test_parse_anchors_passport(): + parsed_anchor = anchor_fixture_parser.get_parsed_passport_anchor() + + assert parsed_anchor.anchor_type == config.ANCHOR_SOURCE + assert parsed_anchor.sub_type == "OCR" + assert parsed_anchor.value == "PASSPORT" + assert parsed_anchor.origin_server_certs.serial_number == int( + "277870515583559162487099305254898397834" + ) + assert parsed_anchor.signed_timestamp == datetime( + 2018, 4, 12, 13 - get_utc_offset(), 14, 32, 835537 + ) + + +def test_parse_yoti_admin(): + parsed_anchor = anchor_fixture_parser.get_parsed_yoti_admin_anchor() + + assert parsed_anchor.anchor_type == config.ANCHOR_VERIFIER + assert parsed_anchor.sub_type == "" + assert parsed_anchor.value == "YOTI_ADMIN" + assert parsed_anchor.origin_server_certs.serial_number == int( + "256616937783084706710155170893983549581" + ) + assert parsed_anchor.signed_timestamp == datetime( + 2018, 4, 11, 12 - get_utc_offset(), 13, 4, 95238 + ) + + +def test_anchor_returns_correct_default_values(): + default_anchor = yoti_python_sdk.anchor.Anchor() + + assert default_anchor.anchor_type == "Unknown" + assert default_anchor.signed_timestamp is None + assert default_anchor.sub_type == "" + assert default_anchor.value == "" + assert default_anchor.origin_server_certs is None + + +def test_error_parsing_anchor_certificate_carries_on_parsing(): + driving_license_anchor = anchor_fixture_parser.get_anchor_from_base64_text( + anchor_fixture_parser.ANCHOR_DRIVING_LICENSE + )[0] + anchors = list() + anchors.append(Attribute_pb2.Anchor()) + anchors.append(driving_license_anchor) + + # 1st anchor will log a warning when being parsed + logger = logging.getLogger() + logger.propagate = False + parsed_anchors = Anchor.parse_anchors(anchors) + logger.propagate = True + + assert len(parsed_anchors) == 1 + + parsed_anchor = parsed_anchors[0] + assert parsed_anchor.anchor_type == config.ANCHOR_SOURCE + assert parsed_anchor.sub_type == "" + assert parsed_anchor.value == "DRIVING_LICENCE" + assert parsed_anchor.origin_server_certs.serial_number == int( + "46131813624213904216516051554755262812" + ) + assert parsed_anchor.signed_timestamp == datetime( + 2018, 4, 11, 12 - get_utc_offset(), 13, 3, 923537 + ) + + +def test_processing_unknown_anchor_data(): + unknown_anchor_data = anchor_fixture_parser.get_anchor_from_base64_text( + anchor_fixture_parser.ANCHOR_UNKNOWN_ANCHOR + ) + anchors = Anchor.parse_anchors(unknown_anchor_data) + + assert len(anchors) == 1 + assert ("", "Unknown", "TEST UNKNOWN SUB TYPE") in [ + (anchor.value, anchor.anchor_type, anchor.sub_type) for anchor in anchors + ] + + expected_timestamp = datetime(2019, 3, 5, 10, 45, 11, 840037) + actual_timestamp = anchors[0].signed_timestamp + + assert expected_timestamp == actual_timestamp + + assert "document-registration-server" in [ + a.value for a in anchors[0].origin_server_certs.issuer + ] diff --git a/yoti_python_sdk/tests/test_attribute.py b/yoti_python_sdk/tests/test_attribute.py index 200bf1ea..5672c4bf 100644 --- a/yoti_python_sdk/tests/test_attribute.py +++ b/yoti_python_sdk/tests/test_attribute.py @@ -1,42 +1,42 @@ -import yoti_python_sdk.attribute - -from yoti_python_sdk import config -from yoti_python_sdk.tests import anchor_fixture_parser - -NAME = "name" -VALUE = "value" - - -def test_attribute_get_values(): - parsed_anchors = [] - - attribute = yoti_python_sdk.attribute.Attribute(NAME, VALUE, parsed_anchors) - - assert attribute.name == NAME - assert attribute.value == VALUE - assert attribute.anchors == parsed_anchors - - -def test_attribute_get_sources(): - anchors = create_source_and_verifier_anchors() - attribute = yoti_python_sdk.attribute.Attribute(NAME, VALUE, anchors) - sources = attribute.sources - - assert len(sources) == 1 - assert sources[0].anchor_type == config.ANCHOR_SOURCE - - -def test_attribute_get_verifiers(): - anchors = create_source_and_verifier_anchors() - attribute = yoti_python_sdk.attribute.Attribute(NAME, VALUE, anchors) - verifiers = attribute.verifiers - - assert len(verifiers) == 1 - assert verifiers[0].anchor_type == config.ANCHOR_VERIFIER - - -def create_source_and_verifier_anchors(): - passport_anchor = anchor_fixture_parser.get_parsed_passport_anchor() # source - yoti_admin_anchor = anchor_fixture_parser.get_parsed_yoti_admin_anchor() # verifier - - return [passport_anchor, yoti_admin_anchor] +import yoti_python_sdk.attribute + +from yoti_python_sdk import config +from yoti_python_sdk.tests import anchor_fixture_parser + +NAME = "name" +VALUE = "value" + + +def test_attribute_get_values(): + parsed_anchors = [] + + attribute = yoti_python_sdk.attribute.Attribute(NAME, VALUE, parsed_anchors) + + assert attribute.name == NAME + assert attribute.value == VALUE + assert attribute.anchors == parsed_anchors + + +def test_attribute_get_sources(): + anchors = create_source_and_verifier_anchors() + attribute = yoti_python_sdk.attribute.Attribute(NAME, VALUE, anchors) + sources = attribute.sources + + assert len(sources) == 1 + assert sources[0].anchor_type == config.ANCHOR_SOURCE + + +def test_attribute_get_verifiers(): + anchors = create_source_and_verifier_anchors() + attribute = yoti_python_sdk.attribute.Attribute(NAME, VALUE, anchors) + verifiers = attribute.verifiers + + assert len(verifiers) == 1 + assert verifiers[0].anchor_type == config.ANCHOR_VERIFIER + + +def create_source_and_verifier_anchors(): + passport_anchor = anchor_fixture_parser.get_parsed_passport_anchor() # source + yoti_admin_anchor = anchor_fixture_parser.get_parsed_yoti_admin_anchor() # verifier + + return [passport_anchor, yoti_admin_anchor] diff --git a/yoti_python_sdk/tests/test_client.py b/yoti_python_sdk/tests/test_client.py index c1e500ae..18769183 100644 --- a/yoti_python_sdk/tests/test_client.py +++ b/yoti_python_sdk/tests/test_client.py @@ -1,381 +1,381 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import json -from datetime import datetime -from os import environ - -import pytest -from cryptography.fernet import base64 -from past.builtins import basestring - -from yoti_python_sdk import config - -try: - from unittest import mock -except ImportError: - import mock - -import yoti_python_sdk -from yoti_python_sdk import YOTI_API_ENDPOINT -from yoti_python_sdk import Client -from yoti_python_sdk import aml -from yoti_python_sdk.config import ( - JSON_CONTENT_TYPE, - X_YOTI_AUTH_KEY, - X_YOTI_AUTH_DIGEST, - X_YOTI_SDK, - SDK_IDENTIFIER, - X_YOTI_SDK_VERSION, -) -from yoti_python_sdk.client import NO_KEY_FILE_SPECIFIED_ERROR -from yoti_python_sdk.activity_details import ActivityDetails -from yoti_python_sdk.tests.conftest import YOTI_CLIENT_SDK_ID, PEM_FILE_PATH -from yoti_python_sdk.tests.mocks import ( - mocked_requests_get, - mocked_requests_get_null_profile, - mocked_requests_get_empty_profile, - mocked_requests_get_missing_profile, - mocked_requests_post_aml_profile, - mocked_requests_post_aml_profile_not_found, - mocked_timestamp, - mocked_uuid4, -) - -INVALID_KEY_FILE_PATH = "/invalid/path/to/file.txt" -INVALID_KEY_FILES = ( - INVALID_KEY_FILE_PATH, - "wrong_pa&*#@th", - -19, - 1, - False, - True, - {}, - [], -) - - -@pytest.fixture(scope="module") -def expected_get_headers(x_yoti_auth_key, x_yoti_auth_digest_get): - sdk_version = yoti_python_sdk.__version__ - - return { - "Content-Type": JSON_CONTENT_TYPE, - "Accept": JSON_CONTENT_TYPE, - X_YOTI_AUTH_KEY: x_yoti_auth_key, - X_YOTI_AUTH_DIGEST: x_yoti_auth_digest_get, - X_YOTI_SDK: SDK_IDENTIFIER, - X_YOTI_SDK_VERSION: "Python-" + sdk_version, - } - - -@pytest.fixture(scope="module") -def expected_post_headers(x_yoti_auth_key, x_yoti_auth_digest_post): - sdk_version = yoti_python_sdk.__version__ - - return { - X_YOTI_AUTH_KEY: x_yoti_auth_key, - X_YOTI_AUTH_DIGEST: x_yoti_auth_digest_post, - X_YOTI_SDK: SDK_IDENTIFIER, - X_YOTI_SDK_VERSION: "Python-" + sdk_version, - "Content-Type": JSON_CONTENT_TYPE, - "Accept": JSON_CONTENT_TYPE, - } - - -@pytest.fixture(scope="module") -def expected_activity_details_url(decrypted_request_token): - nonce = mocked_uuid4() - timestamp = int(mocked_timestamp() * 1000) - return "{0}/profile/{1}?nonce={2}×tamp={3}&appId={4}".format( - YOTI_API_ENDPOINT, decrypted_request_token, nonce, timestamp, YOTI_CLIENT_SDK_ID - ) - - -@pytest.fixture(scope="module") -def expected_aml_url(): - nonce = mocked_uuid4() - timestamp = int(mocked_timestamp() * 1000) - return "{0}/aml-check?appId={1}×tamp={2}&nonce={3}".format( - YOTI_API_ENDPOINT, YOTI_CLIENT_SDK_ID, timestamp, nonce - ) - - -def test_creating_client_instance_with_valid_key_file_env(): - environ["YOTI_KEY_FILE_PATH"] = PEM_FILE_PATH - Client(YOTI_CLIENT_SDK_ID) - - -def test_creating_client_instance_without_private_key_file(): - if environ.get("YOTI_KEY_FILE_PATH"): - del environ["YOTI_KEY_FILE_PATH"] - with pytest.raises(RuntimeError) as exc: - Client(YOTI_CLIENT_SDK_ID) - assert str(exc.value) == NO_KEY_FILE_SPECIFIED_ERROR - - -@pytest.mark.parametrize("key_file", INVALID_KEY_FILES) -def test_creating_client_instance_with_invalid_key_file_arg(key_file): - with pytest.raises(RuntimeError) as exc: - Client(YOTI_CLIENT_SDK_ID, key_file) - expected_error = "Could not read private key file" - assert expected_error in str(exc) - assert str(key_file) in str(exc) - - -@pytest.mark.parametrize("key_file", INVALID_KEY_FILES) -def test_creating_client_instance_with_invalid_key_file_env(key_file): - environ["YOTI_KEY_FILE_PATH"] = str(key_file) - with pytest.raises(RuntimeError) as exc: - Client(YOTI_CLIENT_SDK_ID) - expected_error = "Could not read private key file" - expected_error_source = "specified by the YOTI_KEY_FILE_PATH env variable" - assert expected_error in str(exc) - assert expected_error_source in str(exc) - assert str(key_file) in str(exc) - - -def test_creating_client_instance_with_invalid_key_file_env_but_valid_key_file_arg(): - environ["YOTI_KEY_FILE_PATH"] = INVALID_KEY_FILE_PATH - Client(YOTI_CLIENT_SDK_ID, PEM_FILE_PATH) - - -def test_creating_client_instance_with_valid_key_file_env_but_invalid_key_file_arg(): - environ["YOTI_KEY_FILE_PATH"] = PEM_FILE_PATH - with pytest.raises(RuntimeError) as exc: - Client(YOTI_CLIENT_SDK_ID, INVALID_KEY_FILE_PATH) - expected_error = "Could not read private key file" - assert expected_error in str(exc) - assert str(INVALID_KEY_FILE_PATH) in str(exc) - - -@mock.patch("requests.get", side_effect=mocked_requests_get) -@mock.patch("time.time", side_effect=mocked_timestamp) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -def test_requesting_activity_details_with_correct_data( - mock_uuid4, - mock_time, - mock_get, - client, - expected_activity_details_url, - expected_get_headers, - encrypted_request_token, -): - activity_details = client.get_activity_details(encrypted_request_token) - - mock_get.assert_called_once_with( - url=expected_activity_details_url, headers=expected_get_headers - ) - assert isinstance(activity_details, ActivityDetails) - - assert ( - activity_details.user_id - == "Hig2yAT79cWvseSuXcIuCLa5lNkAPy70rxetUaeHlTJGmiwc/g1MWdYWYrexWvPU" - ) - assert ( - activity_details.receipt_id - == "9HNJDX5bEIN5TqBm0OGzVIc1LaAmbzfx6eIrwNdwpHvKeQmgPujyogC+r7hJCVPl" - ) - assert activity_details.timestamp == datetime(2016, 7, 19, 8, 55, 38) - - selfie_user_profile = activity_details.user_profile.get(config.ATTRIBUTE_SELFIE) - assert isinstance(selfie_user_profile, basestring) - - selfie_profile = activity_details.profile.selfie.value - assert isinstance(selfie_profile, basestring) - assert ( - activity_details.profile.get_attribute(config.ATTRIBUTE_SELFIE) - == activity_details.profile.selfie - ) - - base64_selfie_uri = getattr(activity_details, config.KEY_BASE64_SELFIE) - assert isinstance(base64_selfie_uri, basestring) - assert base64_selfie_uri.startswith("data:image/jpeg;base64") - - phone_number = activity_details.profile.phone_number - assert phone_number is not None - assert len(phone_number.anchors) == 1 - assert "Unknown" in [anchor.anchor_type for anchor in phone_number.anchors] - assert "" in [anchor.value for anchor in phone_number.anchors] - - -@mock.patch("requests.get", side_effect=mocked_requests_get_null_profile) -@mock.patch("time.time", side_effect=mocked_timestamp) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -def test_requesting_activity_details_with_null_profile( - mock_uuid4, - mock_time, - mock_get, - client, - expected_activity_details_url, - expected_get_headers, - encrypted_request_token, -): - activity_details = client.get_activity_details(encrypted_request_token) - - mock_get.assert_called_once_with( - url=expected_activity_details_url, headers=expected_get_headers - ) - assert ( - activity_details.user_id - == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" - ) - assert ( - activity_details.receipt_id - == "Eq3+P8qjAlxr4d2mXKCUvzKdJTchI53ghwYPZXyA/cF5T+m/HCP1bK5LOmudZASN" - ) - assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) - assert isinstance(activity_details, ActivityDetails) - - -@mock.patch("requests.get", side_effect=mocked_requests_get_empty_profile) -@mock.patch("time.time", side_effect=mocked_timestamp) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -def test_requesting_activity_details_with_empty_profile( - mock_uuid4, - mock_time, - mock_get, - client, - expected_activity_details_url, - expected_get_headers, - encrypted_request_token, -): - activity_details = client.get_activity_details(encrypted_request_token) - - mock_get.assert_called_once_with( - url=expected_activity_details_url, headers=expected_get_headers - ) - assert ( - activity_details.user_id - == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" - ) - assert ( - activity_details.receipt_id - == "Eq3+P8qjAlxr4d2mXKCUvzKdJTchI53ghwYPZXyA/cF5T+m/HCP1bK5LOmudZASN" - ) - assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) - assert isinstance(activity_details, ActivityDetails) - - -@mock.patch("requests.get", side_effect=mocked_requests_get_missing_profile) -@mock.patch("time.time", side_effect=mocked_timestamp) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -def test_requesting_activity_details_with_missing_profile( - mock_uuid4, - mock_time, - mock_get, - client, - expected_activity_details_url, - expected_get_headers, - encrypted_request_token, -): - activity_details = client.get_activity_details(encrypted_request_token) - - mock_get.assert_called_once_with( - url=expected_activity_details_url, headers=expected_get_headers - ) - assert ( - activity_details.user_id - == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" - ) - assert ( - activity_details.receipt_id - == "Eq3+P8qjAlxr4d2mXKCUvzKdJTchI53ghwYPZXyA/cF5T+m/HCP1bK5LOmudZASN" - ) - assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) - assert isinstance(activity_details, ActivityDetails) - - -@mock.patch("requests.get", side_effect=mocked_requests_get) -@mock.patch("time.time", side_effect=mocked_timestamp) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -def test_creating_request_with_unsupported_http_method( - mock_uuid4, mock_time, mock_get, client, expected_get_headers -): - with pytest.raises(ValueError): - client._Client__create_request( - http_method="UNSUPPORTED_METHOD", path=YOTI_API_ENDPOINT, content=None - ) - - -@mock.patch("requests.get", side_effect=mocked_requests_get) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -@mock.patch("time.time", side_effect=mocked_timestamp) -def test_creating_request_with_supported_http_method( - mock_uuid4, mock_time, mock_get, client, expected_get_headers -): - client._Client__create_request( - http_method="GET", path=YOTI_API_ENDPOINT, content=None - ) - - -@mock.patch("requests.get", side_effect=mocked_requests_get) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -@mock.patch("time.time", side_effect=mocked_timestamp) -def test_creating_request_content_is_added( - mock_uuid4, mock_time, mock_get, client, expected_get_headers -): - content = '{"Content"}' - content_bytes = content.encode() - request = client._Client__create_request( - http_method="GET", path=YOTI_API_ENDPOINT, content=content_bytes - ) - - b64encoded = base64.b64encode(content_bytes) - b64ascii = b64encoded.decode("ascii") - - assert request.endswith("&" + b64ascii) - - -@mock.patch("requests.post", side_effect=mocked_requests_post_aml_profile) -@mock.patch("time.time", side_effect=mocked_timestamp) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -def test_perform_aml_check_details_with_correct_data( - mock_uuid4, mock_time, mock_post, client, expected_aml_url, expected_post_headers -): - given_names = "Given Name" - family_name = "Family Name" - - aml_address = aml.AmlAddress(country="GBR") - aml_profile = aml.AmlProfile(given_names, family_name, aml_address) - - aml_result = client.perform_aml_check(aml_profile) - - aml_profile_json = json.dumps(aml_profile.__dict__, sort_keys=True) - aml_profile_bytes = aml_profile_json.encode() - - mock_post.assert_called_once_with( - url=expected_aml_url, headers=expected_post_headers, data=aml_profile_bytes - ) - - assert isinstance(aml_result, aml.AmlResult) - assert isinstance(aml_result.on_watch_list, bool) - assert isinstance(aml_result.on_fraud_list, bool) - assert isinstance(aml_result.on_pep_list, bool) - - -def test_perform_aml_check_with_null_profile(client): - aml_profile = None - - with pytest.raises(TypeError) as exc: - client.perform_aml_check(aml_profile) - expected_error = "aml_profile not set" - assert expected_error in str(exc) - - -@mock.patch("requests.post", side_effect=mocked_requests_post_aml_profile_not_found) -@mock.patch("time.time", side_effect=mocked_timestamp) -@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) -def test_perform_aml_check_with_unsuccessful_call( - mock_uuid4, mock_time, mock_post, client -): - given_names = "Given Name" - family_name = "Family Name" - - aml_address = aml.AmlAddress(country="GBR") - aml_profile = aml.AmlProfile(given_names, family_name, aml_address) - - with pytest.raises(RuntimeError) as exc: - client.perform_aml_check(aml_profile) - expected_error = "Unsuccessful Yoti API call:" - assert expected_error in str(exc) +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import json +from datetime import datetime +from os import environ + +import pytest +from cryptography.fernet import base64 +from past.builtins import basestring + +from yoti_python_sdk import config + +try: + from unittest import mock +except ImportError: + import mock + +import yoti_python_sdk +from yoti_python_sdk import YOTI_API_ENDPOINT +from yoti_python_sdk import Client +from yoti_python_sdk import aml +from yoti_python_sdk.config import ( + JSON_CONTENT_TYPE, + X_YOTI_AUTH_KEY, + X_YOTI_AUTH_DIGEST, + X_YOTI_SDK, + SDK_IDENTIFIER, + X_YOTI_SDK_VERSION, +) +from yoti_python_sdk.client import NO_KEY_FILE_SPECIFIED_ERROR +from yoti_python_sdk.activity_details import ActivityDetails +from yoti_python_sdk.tests.conftest import YOTI_CLIENT_SDK_ID, PEM_FILE_PATH +from yoti_python_sdk.tests.mocks import ( + mocked_requests_get, + mocked_requests_get_null_profile, + mocked_requests_get_empty_profile, + mocked_requests_get_missing_profile, + mocked_requests_post_aml_profile, + mocked_requests_post_aml_profile_not_found, + mocked_timestamp, + mocked_uuid4, +) + +INVALID_KEY_FILE_PATH = "/invalid/path/to/file.txt" +INVALID_KEY_FILES = ( + INVALID_KEY_FILE_PATH, + "wrong_pa&*#@th", + -19, + 1, + False, + True, + {}, + [], +) + + +@pytest.fixture(scope="module") +def expected_get_headers(x_yoti_auth_key, x_yoti_auth_digest_get): + sdk_version = yoti_python_sdk.__version__ + + return { + "Content-Type": JSON_CONTENT_TYPE, + "Accept": JSON_CONTENT_TYPE, + X_YOTI_AUTH_KEY: x_yoti_auth_key, + X_YOTI_AUTH_DIGEST: x_yoti_auth_digest_get, + X_YOTI_SDK: SDK_IDENTIFIER, + X_YOTI_SDK_VERSION: "Python-" + sdk_version, + } + + +@pytest.fixture(scope="module") +def expected_post_headers(x_yoti_auth_key, x_yoti_auth_digest_post): + sdk_version = yoti_python_sdk.__version__ + + return { + X_YOTI_AUTH_KEY: x_yoti_auth_key, + X_YOTI_AUTH_DIGEST: x_yoti_auth_digest_post, + X_YOTI_SDK: SDK_IDENTIFIER, + X_YOTI_SDK_VERSION: "Python-" + sdk_version, + "Content-Type": JSON_CONTENT_TYPE, + "Accept": JSON_CONTENT_TYPE, + } + + +@pytest.fixture(scope="module") +def expected_activity_details_url(decrypted_request_token): + nonce = mocked_uuid4() + timestamp = int(mocked_timestamp() * 1000) + return "{0}/profile/{1}?nonce={2}×tamp={3}&appId={4}".format( + YOTI_API_ENDPOINT, decrypted_request_token, nonce, timestamp, YOTI_CLIENT_SDK_ID + ) + + +@pytest.fixture(scope="module") +def expected_aml_url(): + nonce = mocked_uuid4() + timestamp = int(mocked_timestamp() * 1000) + return "{0}/aml-check?appId={1}×tamp={2}&nonce={3}".format( + YOTI_API_ENDPOINT, YOTI_CLIENT_SDK_ID, timestamp, nonce + ) + + +def test_creating_client_instance_with_valid_key_file_env(): + environ["YOTI_KEY_FILE_PATH"] = PEM_FILE_PATH + Client(YOTI_CLIENT_SDK_ID) + + +def test_creating_client_instance_without_private_key_file(): + if environ.get("YOTI_KEY_FILE_PATH"): + del environ["YOTI_KEY_FILE_PATH"] + with pytest.raises(RuntimeError) as exc: + Client(YOTI_CLIENT_SDK_ID) + assert str(exc.value) == NO_KEY_FILE_SPECIFIED_ERROR + + +@pytest.mark.parametrize("key_file", INVALID_KEY_FILES) +def test_creating_client_instance_with_invalid_key_file_arg(key_file): + with pytest.raises(RuntimeError) as exc: + Client(YOTI_CLIENT_SDK_ID, key_file) + expected_error = "Could not read private key file" + assert expected_error in str(exc) + assert str(key_file) in str(exc) + + +@pytest.mark.parametrize("key_file", INVALID_KEY_FILES) +def test_creating_client_instance_with_invalid_key_file_env(key_file): + environ["YOTI_KEY_FILE_PATH"] = str(key_file) + with pytest.raises(RuntimeError) as exc: + Client(YOTI_CLIENT_SDK_ID) + expected_error = "Could not read private key file" + expected_error_source = "specified by the YOTI_KEY_FILE_PATH env variable" + assert expected_error in str(exc) + assert expected_error_source in str(exc) + assert str(key_file) in str(exc) + + +def test_creating_client_instance_with_invalid_key_file_env_but_valid_key_file_arg(): + environ["YOTI_KEY_FILE_PATH"] = INVALID_KEY_FILE_PATH + Client(YOTI_CLIENT_SDK_ID, PEM_FILE_PATH) + + +def test_creating_client_instance_with_valid_key_file_env_but_invalid_key_file_arg(): + environ["YOTI_KEY_FILE_PATH"] = PEM_FILE_PATH + with pytest.raises(RuntimeError) as exc: + Client(YOTI_CLIENT_SDK_ID, INVALID_KEY_FILE_PATH) + expected_error = "Could not read private key file" + assert expected_error in str(exc) + assert str(INVALID_KEY_FILE_PATH) in str(exc) + + +@mock.patch("requests.get", side_effect=mocked_requests_get) +@mock.patch("time.time", side_effect=mocked_timestamp) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +def test_requesting_activity_details_with_correct_data( + mock_uuid4, + mock_time, + mock_get, + client, + expected_activity_details_url, + expected_get_headers, + encrypted_request_token, +): + activity_details = client.get_activity_details(encrypted_request_token) + + mock_get.assert_called_once_with( + url=expected_activity_details_url, headers=expected_get_headers + ) + assert isinstance(activity_details, ActivityDetails) + + assert ( + activity_details.user_id + == "Hig2yAT79cWvseSuXcIuCLa5lNkAPy70rxetUaeHlTJGmiwc/g1MWdYWYrexWvPU" + ) + assert ( + activity_details.receipt_id + == "9HNJDX5bEIN5TqBm0OGzVIc1LaAmbzfx6eIrwNdwpHvKeQmgPujyogC+r7hJCVPl" + ) + assert activity_details.timestamp == datetime(2016, 7, 19, 8, 55, 38) + + selfie_user_profile = activity_details.user_profile.get(config.ATTRIBUTE_SELFIE) + assert isinstance(selfie_user_profile, basestring) + + selfie_profile = activity_details.profile.selfie.value + assert isinstance(selfie_profile, basestring) + assert ( + activity_details.profile.get_attribute(config.ATTRIBUTE_SELFIE) + == activity_details.profile.selfie + ) + + base64_selfie_uri = getattr(activity_details, config.KEY_BASE64_SELFIE) + assert isinstance(base64_selfie_uri, basestring) + assert base64_selfie_uri.startswith("data:image/jpeg;base64") + + phone_number = activity_details.profile.phone_number + assert phone_number is not None + assert len(phone_number.anchors) == 1 + assert "Unknown" in [anchor.anchor_type for anchor in phone_number.anchors] + assert "" in [anchor.value for anchor in phone_number.anchors] + + +@mock.patch("requests.get", side_effect=mocked_requests_get_null_profile) +@mock.patch("time.time", side_effect=mocked_timestamp) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +def test_requesting_activity_details_with_null_profile( + mock_uuid4, + mock_time, + mock_get, + client, + expected_activity_details_url, + expected_get_headers, + encrypted_request_token, +): + activity_details = client.get_activity_details(encrypted_request_token) + + mock_get.assert_called_once_with( + url=expected_activity_details_url, headers=expected_get_headers + ) + assert ( + activity_details.user_id + == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" + ) + assert ( + activity_details.receipt_id + == "Eq3+P8qjAlxr4d2mXKCUvzKdJTchI53ghwYPZXyA/cF5T+m/HCP1bK5LOmudZASN" + ) + assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) + assert isinstance(activity_details, ActivityDetails) + + +@mock.patch("requests.get", side_effect=mocked_requests_get_empty_profile) +@mock.patch("time.time", side_effect=mocked_timestamp) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +def test_requesting_activity_details_with_empty_profile( + mock_uuid4, + mock_time, + mock_get, + client, + expected_activity_details_url, + expected_get_headers, + encrypted_request_token, +): + activity_details = client.get_activity_details(encrypted_request_token) + + mock_get.assert_called_once_with( + url=expected_activity_details_url, headers=expected_get_headers + ) + assert ( + activity_details.user_id + == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" + ) + assert ( + activity_details.receipt_id + == "Eq3+P8qjAlxr4d2mXKCUvzKdJTchI53ghwYPZXyA/cF5T+m/HCP1bK5LOmudZASN" + ) + assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) + assert isinstance(activity_details, ActivityDetails) + + +@mock.patch("requests.get", side_effect=mocked_requests_get_missing_profile) +@mock.patch("time.time", side_effect=mocked_timestamp) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +def test_requesting_activity_details_with_missing_profile( + mock_uuid4, + mock_time, + mock_get, + client, + expected_activity_details_url, + expected_get_headers, + encrypted_request_token, +): + activity_details = client.get_activity_details(encrypted_request_token) + + mock_get.assert_called_once_with( + url=expected_activity_details_url, headers=expected_get_headers + ) + assert ( + activity_details.user_id + == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" + ) + assert ( + activity_details.receipt_id + == "Eq3+P8qjAlxr4d2mXKCUvzKdJTchI53ghwYPZXyA/cF5T+m/HCP1bK5LOmudZASN" + ) + assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) + assert isinstance(activity_details, ActivityDetails) + + +@mock.patch("requests.get", side_effect=mocked_requests_get) +@mock.patch("time.time", side_effect=mocked_timestamp) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +def test_creating_request_with_unsupported_http_method( + mock_uuid4, mock_time, mock_get, client, expected_get_headers +): + with pytest.raises(ValueError): + client._Client__create_request( + http_method="UNSUPPORTED_METHOD", path=YOTI_API_ENDPOINT, content=None + ) + + +@mock.patch("requests.get", side_effect=mocked_requests_get) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +@mock.patch("time.time", side_effect=mocked_timestamp) +def test_creating_request_with_supported_http_method( + mock_uuid4, mock_time, mock_get, client, expected_get_headers +): + client._Client__create_request( + http_method="GET", path=YOTI_API_ENDPOINT, content=None + ) + + +@mock.patch("requests.get", side_effect=mocked_requests_get) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +@mock.patch("time.time", side_effect=mocked_timestamp) +def test_creating_request_content_is_added( + mock_uuid4, mock_time, mock_get, client, expected_get_headers +): + content = '{"Content"}' + content_bytes = content.encode() + request = client._Client__create_request( + http_method="GET", path=YOTI_API_ENDPOINT, content=content_bytes + ) + + b64encoded = base64.b64encode(content_bytes) + b64ascii = b64encoded.decode("ascii") + + assert request.endswith("&" + b64ascii) + + +@mock.patch("requests.post", side_effect=mocked_requests_post_aml_profile) +@mock.patch("time.time", side_effect=mocked_timestamp) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +def test_perform_aml_check_details_with_correct_data( + mock_uuid4, mock_time, mock_post, client, expected_aml_url, expected_post_headers +): + given_names = "Given Name" + family_name = "Family Name" + + aml_address = aml.AmlAddress(country="GBR") + aml_profile = aml.AmlProfile(given_names, family_name, aml_address) + + aml_result = client.perform_aml_check(aml_profile) + + aml_profile_json = json.dumps(aml_profile.__dict__, sort_keys=True) + aml_profile_bytes = aml_profile_json.encode() + + mock_post.assert_called_once_with( + url=expected_aml_url, headers=expected_post_headers, data=aml_profile_bytes + ) + + assert isinstance(aml_result, aml.AmlResult) + assert isinstance(aml_result.on_watch_list, bool) + assert isinstance(aml_result.on_fraud_list, bool) + assert isinstance(aml_result.on_pep_list, bool) + + +def test_perform_aml_check_with_null_profile(client): + aml_profile = None + + with pytest.raises(TypeError) as exc: + client.perform_aml_check(aml_profile) + expected_error = "aml_profile not set" + assert expected_error in str(exc) + + +@mock.patch("requests.post", side_effect=mocked_requests_post_aml_profile_not_found) +@mock.patch("time.time", side_effect=mocked_timestamp) +@mock.patch("uuid.uuid4", side_effect=mocked_uuid4) +def test_perform_aml_check_with_unsuccessful_call( + mock_uuid4, mock_time, mock_post, client +): + given_names = "Given Name" + family_name = "Family Name" + + aml_address = aml.AmlAddress(country="GBR") + aml_profile = aml.AmlProfile(given_names, family_name, aml_address) + + with pytest.raises(RuntimeError) as exc: + client.perform_aml_check(aml_profile) + expected_error = "Unsuccessful Yoti API call:" + assert expected_error in str(exc) diff --git a/yoti_python_sdk/tests/test_profile.py b/yoti_python_sdk/tests/test_profile.py index 1d9104fe..18573af1 100644 --- a/yoti_python_sdk/tests/test_profile.py +++ b/yoti_python_sdk/tests/test_profile.py @@ -1,589 +1,589 @@ -# -*- coding: utf-8 -*- -import collections -import json -import logging - -import pytest - -from yoti_python_sdk import config -from yoti_python_sdk.attribute import Attribute -from yoti_python_sdk.profile import Profile -from yoti_python_sdk.protobuf.protobuf import Protobuf -from yoti_python_sdk.tests import attribute_fixture_parser, image_helper -from yoti_python_sdk.tests.protobuf_attribute import ProtobufAttribute - -ADDRESS_FORMAT_KEY = "address_format" -ADDRESS_FORMAT_VALUE = 1 -INDIA_FORMAT_VALUE = 2 -USA_FORMAT_VALUE = 3 - -BUILDING_NUMBER_KEY = "building_number" -BUILDING_NUMBER_VALUE = "15a" - -CARE_OF_KEY = "care_of" -CARE_OF_VALUE = "S/O: Name" - -STATE_KEY = "state" -INDIA_STATE_VALUE = "Punjab" -USA_STATE_VALUE = "AL" - -BUILDING_KEY = "building" -BUILDING_VALUE = "House No.1111-A" - -STREET_KEY = "street" -STREET_VALUE = "42nd Street" - -DISTRICT_KEY = "district" -DISTRICT_VALUE = "DISTRICT 10" - -SUBDISTRICT_KEY = "subdistrict" -SUBDISTRICT_VALUE = "Sub-DISTRICT 10" - -POST_OFFICE_KEY = "post_office" -INDIA_POST_OFFICE_VALUE = "Rajguru Nagar" - -ADDRESS_LINE_1_KEY = "address_line_1" -ADDRESS_LINE_1_VALUE = "15a North Street" - -TOWN_CITY_KEY = "town_city" -TOWN_CITY_VALUE = "TOWN/CITY NAME" - -POSTAL_CODE_KEY = "postal_code" -POSTAL_CODE_VALUE = "SM5 2HW" -INDIA_POSTAL_CODE_VALUE = "141012" -USA_POSTAL_CODE_VALUE = "36201" - -COUNTRY_ISO_KEY = "country_iso" -COUNTRY_ISO_VALUE = "GBR" -INDIA_COUNTRY_ISO_VALUE = "IND" -USA_COUNTRY_ISO_VALUE = "USA" - -COUNTRY_KEY = "country" -COUNTRY_VALUE = "UK" -INDIA_COUNTRY_VALUE = "India" -USA_COUNTRY_VALUE = "USA" - -FORMATTED_ADDRESS_VALUE = "15a North Street\nCARSHALTON\nSM5 2HW\nUK" -INDIA_FORMATTED_ADDRESS_VALUE = "S/O: Name\nHouse No.1111-A\n42nd Street\nTOWN/CITY NAME\nSub-DISTRICT 10\nDISTRICT 10\nPunjab\n141012\nRajgura Nagar\nIndia" -USA_FORMATTED_ADDRESS_VALUE = "15a North Street\nTOWN/CITY NAME\nAL\n36201\nUSA" - -USA_DOCUMENT_DETAILS = "DRIVING_LICENCE USA 12345678 2016-05-01" -INDIA_DOCUMENT_DETAILS = "DRIVING_LICENCE IND MH-05-2006-1234567 2016-05-01" -DRIVING_LICENCE = "DRIVING_LICENCE" -USA_DRIVING_LICENCE_NUMBER = "12345678" -IND_DRIVING_LICENCE_NUMBER = "MH-05-2006-1234567" -EXPIRY_DATE = "2016-05-01" - - -def create_single_attribute_list(name, value, anchors, content_type): - attribute = ProtobufAttribute(name, value, anchors, content_type) - - attribute_list = list() - attribute_list.append(attribute) - return attribute_list - - -def create_attribute_list_with_selfie_field(): - return create_single_attribute_list( - name=config.ATTRIBUTE_SELFIE, - value="base64(ง •̀_•́)ง", - anchors=None, - content_type=Protobuf.CT_JPEG, - ) - - -def create_attribute_list_with_email_field(): - return create_single_attribute_list( - name=config.ATTRIBUTE_EMAIL_ADDRESS, - value="y@ti.com".encode(), - anchors=None, - content_type=Protobuf.CT_STRING, - ) - - -def create_attribute_list_with_structured_postal_address_field(json_address_value): - return create_single_attribute_list( - name=config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS, - value=json_address_value, - anchors=None, - content_type=Protobuf.CT_JSON, - ) - - -@pytest.mark.parametrize( - "string, expected_int", [("0", 0), ("1", 1), ("123", 123), ("-10", -10), ("-1", -1)] -) -def test_try_parse_int_value(string, expected_int): - attribute_name = "int_attribute" - attribute_list = create_single_attribute_list( - name=attribute_name, - value=str.encode(string), - anchors=None, - content_type=Protobuf.CT_INT, - ) - - profile = Profile(attribute_list) - int_attribute = profile.get_attribute(attribute_name) - assert int_attribute.value == expected_int - - -def test_error_parsing_attribute_has_none_value(): - int_attribute_name = "int_attribute" - - attribute_list = create_single_attribute_list( - name=int_attribute_name, - value=str.encode("invalid_int"), - anchors=None, - content_type=Protobuf.CT_INT, - ) - - # disable logging for the below call: warning shown as int is invalid - logger = logging.getLogger() - logger.propagate = False - - profile = Profile(attribute_list) - - logger.propagate = True - - assert profile.get_attribute(int_attribute_name) is None - - -@pytest.mark.parametrize( - "content_type", - [ - Protobuf.CT_DATE, - Protobuf.CT_INT, - Protobuf.CT_JPEG, - Protobuf.CT_PNG, - Protobuf.CT_JSON, - Protobuf.CT_UNDEFINED, - ], -) -def test_parse_empty_values_returns_none(content_type): - attribute_name = "attribute_name" - - attribute_list = create_single_attribute_list( - name=attribute_name, value=b"", anchors=None, content_type=content_type - ) - - # disable logging for the below call: warning logged as value is empty - logger = logging.getLogger() - logger.propagate = False - - profile = Profile(attribute_list) - - logger.propagate = True - - assert profile.get_attribute(attribute_name) is None - - -@pytest.mark.parametrize("value", [b"", "".encode()]) -def test_parse_empty_string_value_returns_attribute(value): - attribute_name = "attribute_name" - - attribute_list = create_single_attribute_list( - name=attribute_name, value=value, anchors=None, content_type=Protobuf.CT_STRING - ) - - profile = Profile(attribute_list) - - assert profile.get_attribute(attribute_name).value == "" - - -def test_error_parsing_attribute_does_not_affect_other_attribute(): - string_attribute_name = "string_attribute" - int_attribute_name = "int_attribute" - string_value = "string" - - attribute_list = list() - - attribute_list.append( - ProtobufAttribute( - name=string_attribute_name, - value=str.encode(string_value), - anchors=None, - content_type=Protobuf.CT_STRING, - ) - ) - - attribute_list.append( - ProtobufAttribute( - name=int_attribute_name, - value=str.encode("invalid_int"), - anchors=None, - content_type=Protobuf.CT_INT, - ) - ) - - # disable logging for the below call: warning shown as int is invalid - logger = logging.getLogger() - logger.propagate = False - - profile = Profile(attribute_list) - - logger.propagate = True - - assert len(profile.attributes) == 1 - - retrieved_string_attribute = profile.get_attribute(string_attribute_name) - assert retrieved_string_attribute.name == string_attribute_name - assert retrieved_string_attribute.value == string_value - - -def test_try_parse_structured_postal_address_uk(): - structured_postal_address = { - ADDRESS_FORMAT_KEY: ADDRESS_FORMAT_VALUE, - BUILDING_NUMBER_KEY: BUILDING_NUMBER_VALUE, - ADDRESS_LINE_1_KEY: ADDRESS_LINE_1_VALUE, - TOWN_CITY_KEY: TOWN_CITY_VALUE, - POSTAL_CODE_KEY: POSTAL_CODE_VALUE, - COUNTRY_ISO_KEY: COUNTRY_ISO_VALUE, - COUNTRY_KEY: COUNTRY_VALUE, - config.KEY_FORMATTED_ADDRESS: FORMATTED_ADDRESS_VALUE, - } - - structured_postal_address_json = json.dumps(structured_postal_address).encode() - - profile = Profile( - create_attribute_list_with_structured_postal_address_field( - structured_postal_address_json - ) - ) - - actual_structured_postal_address = profile.structured_postal_address.value - actual_address_format = actual_structured_postal_address[ADDRESS_FORMAT_KEY] - actual_building_number = actual_structured_postal_address[BUILDING_NUMBER_KEY] - actual_address_line_1 = actual_structured_postal_address[ADDRESS_LINE_1_KEY] - actual_town_city = actual_structured_postal_address[TOWN_CITY_KEY] - actual_postal_code = actual_structured_postal_address[POSTAL_CODE_KEY] - actual_country_iso = actual_structured_postal_address[COUNTRY_ISO_KEY] - actual_country = actual_structured_postal_address[COUNTRY_KEY] - actual_formatted_address = actual_structured_postal_address[ - config.KEY_FORMATTED_ADDRESS - ] - - assert type(actual_structured_postal_address) is collections.OrderedDict - assert actual_address_format == ADDRESS_FORMAT_VALUE - assert actual_building_number == BUILDING_NUMBER_VALUE - assert actual_address_line_1 == ADDRESS_LINE_1_VALUE - assert actual_town_city == TOWN_CITY_VALUE - assert actual_postal_code == POSTAL_CODE_VALUE - assert actual_country_iso == COUNTRY_ISO_VALUE - assert actual_country == COUNTRY_VALUE - assert actual_formatted_address == FORMATTED_ADDRESS_VALUE - - -def test_other_json_type_is_parsed(): - json_attribute_name = "other_json" - key_a = "keyA" - key_b = "keyB" - value_a = "valueA" - value_b = "valueB" - json_value = {key_a: value_a, key_b: value_b} - - encoded_json = json.dumps(json_value).encode() - - attribute_list = create_single_attribute_list( - name=json_attribute_name, - value=encoded_json, - anchors=None, - content_type=Protobuf.CT_JSON, - ) - - profile = Profile(attribute_list) - - retrieved_attribute = profile.get_attribute(json_attribute_name) - - assert retrieved_attribute.name == json_attribute_name - assert type(retrieved_attribute.value) is collections.OrderedDict - assert retrieved_attribute.value[key_a] == value_a - assert retrieved_attribute.value[key_b] == value_b - - -def test_try_parse_structured_postal_address_india(): - structured_postal_address = { - ADDRESS_FORMAT_KEY: INDIA_FORMAT_VALUE, - CARE_OF_KEY: CARE_OF_VALUE, - BUILDING_KEY: BUILDING_VALUE, - STREET_KEY: STREET_VALUE, - TOWN_CITY_KEY: TOWN_CITY_VALUE, - SUBDISTRICT_KEY: SUBDISTRICT_VALUE, - DISTRICT_KEY: DISTRICT_VALUE, - STATE_KEY: INDIA_STATE_VALUE, - POSTAL_CODE_KEY: INDIA_POSTAL_CODE_VALUE, - POST_OFFICE_KEY: INDIA_POST_OFFICE_VALUE, - COUNTRY_ISO_KEY: INDIA_COUNTRY_ISO_VALUE, - COUNTRY_KEY: INDIA_COUNTRY_VALUE, - config.KEY_FORMATTED_ADDRESS: INDIA_FORMATTED_ADDRESS_VALUE, - } - - structured_postal_address_bytes = json.dumps(structured_postal_address).encode() - - profile = Profile( - create_attribute_list_with_structured_postal_address_field( - structured_postal_address_bytes - ) - ) - - actual_structured_postal_address_profile = profile.structured_postal_address.value - - assert type(actual_structured_postal_address_profile) is collections.OrderedDict - assert ( - actual_structured_postal_address_profile[ADDRESS_FORMAT_KEY] - == INDIA_FORMAT_VALUE - ) - assert actual_structured_postal_address_profile[CARE_OF_KEY] == CARE_OF_VALUE - assert actual_structured_postal_address_profile[BUILDING_KEY] == BUILDING_VALUE - assert actual_structured_postal_address_profile[STREET_KEY] == STREET_VALUE - assert actual_structured_postal_address_profile[TOWN_CITY_KEY] == TOWN_CITY_VALUE - assert ( - actual_structured_postal_address_profile[SUBDISTRICT_KEY] == SUBDISTRICT_VALUE - ) - assert actual_structured_postal_address_profile[DISTRICT_KEY] == DISTRICT_VALUE - assert actual_structured_postal_address_profile[STATE_KEY] == INDIA_STATE_VALUE - assert ( - actual_structured_postal_address_profile[POSTAL_CODE_KEY] - == INDIA_POSTAL_CODE_VALUE - ) - assert ( - actual_structured_postal_address_profile[POST_OFFICE_KEY] - == INDIA_POST_OFFICE_VALUE - ) - assert ( - actual_structured_postal_address_profile[COUNTRY_ISO_KEY] - == INDIA_COUNTRY_ISO_VALUE - ) - assert actual_structured_postal_address_profile[COUNTRY_KEY] == INDIA_COUNTRY_VALUE - assert ( - actual_structured_postal_address_profile[config.KEY_FORMATTED_ADDRESS] - == INDIA_FORMATTED_ADDRESS_VALUE - ) - - -def test_try_parse_structured_postal_address_usa(): - structured_postal_address = { - ADDRESS_FORMAT_KEY: USA_FORMAT_VALUE, - ADDRESS_LINE_1_KEY: ADDRESS_LINE_1_VALUE, - TOWN_CITY_KEY: TOWN_CITY_VALUE, - STATE_KEY: USA_STATE_VALUE, - POSTAL_CODE_KEY: USA_POSTAL_CODE_VALUE, - COUNTRY_ISO_KEY: USA_COUNTRY_ISO_VALUE, - COUNTRY_KEY: USA_COUNTRY_VALUE, - config.KEY_FORMATTED_ADDRESS: USA_FORMATTED_ADDRESS_VALUE, - } - - structured_postal_address_bytes = json.dumps(structured_postal_address).encode() - - profile = Profile( - create_attribute_list_with_structured_postal_address_field( - structured_postal_address_bytes - ) - ) - - actual_structured_postal_address_profile = profile.structured_postal_address.value - - assert type(actual_structured_postal_address_profile) is collections.OrderedDict - assert ( - actual_structured_postal_address_profile[ADDRESS_FORMAT_KEY] == USA_FORMAT_VALUE - ) - assert ( - actual_structured_postal_address_profile[ADDRESS_LINE_1_KEY] - == ADDRESS_LINE_1_VALUE - ) - assert actual_structured_postal_address_profile[TOWN_CITY_KEY] == TOWN_CITY_VALUE - assert actual_structured_postal_address_profile[STATE_KEY] == USA_STATE_VALUE - assert ( - actual_structured_postal_address_profile[POSTAL_CODE_KEY] - == USA_POSTAL_CODE_VALUE - ) - assert ( - actual_structured_postal_address_profile[COUNTRY_ISO_KEY] - == USA_COUNTRY_ISO_VALUE - ) - assert actual_structured_postal_address_profile[COUNTRY_KEY] == USA_COUNTRY_VALUE - assert ( - actual_structured_postal_address_profile[config.KEY_FORMATTED_ADDRESS] - == USA_FORMATTED_ADDRESS_VALUE - ) - - -def test_try_parse_structured_postal_address_nested_json(): - formatted_address_json = { - "item1": [[1, "a1"], [2, "a2"]], - "item2": [[3, "b3"], [4, "b4"]], - } - - structured_postal_address = { - ADDRESS_FORMAT_KEY: ADDRESS_FORMAT_VALUE, - BUILDING_NUMBER_KEY: BUILDING_NUMBER_VALUE, - ADDRESS_LINE_1_KEY: ADDRESS_LINE_1_VALUE, - TOWN_CITY_KEY: TOWN_CITY_VALUE, - POSTAL_CODE_KEY: POSTAL_CODE_VALUE, - COUNTRY_ISO_KEY: COUNTRY_ISO_VALUE, - COUNTRY_KEY: COUNTRY_VALUE, - config.KEY_FORMATTED_ADDRESS: formatted_address_json, - } - - structured_postal_address_bytes = json.dumps(structured_postal_address).encode() - - profile = Profile( - create_attribute_list_with_structured_postal_address_field( - structured_postal_address_bytes - ) - ) - - actual_structured_postal_address_profile = profile.structured_postal_address.value - - assert type(actual_structured_postal_address_profile) is collections.OrderedDict - assert ( - actual_structured_postal_address_profile[ADDRESS_FORMAT_KEY] - == ADDRESS_FORMAT_VALUE - ) - assert ( - actual_structured_postal_address_profile[BUILDING_NUMBER_KEY] - == BUILDING_NUMBER_VALUE - ) - assert ( - actual_structured_postal_address_profile[ADDRESS_LINE_1_KEY] - == ADDRESS_LINE_1_VALUE - ) - assert actual_structured_postal_address_profile[TOWN_CITY_KEY] == TOWN_CITY_VALUE - assert ( - actual_structured_postal_address_profile[POSTAL_CODE_KEY] == POSTAL_CODE_VALUE - ) - assert ( - actual_structured_postal_address_profile[COUNTRY_ISO_KEY] == COUNTRY_ISO_VALUE - ) - assert actual_structured_postal_address_profile[COUNTRY_KEY] == COUNTRY_VALUE - - assert ( - actual_structured_postal_address_profile[config.KEY_FORMATTED_ADDRESS] - == formatted_address_json - ) - - -def test_set_address_to_be_formatted_address(): - structured_postal_address = {config.KEY_FORMATTED_ADDRESS: FORMATTED_ADDRESS_VALUE} - structured_postal_address_bytes = json.dumps(structured_postal_address).encode() - - profile = Profile( - create_attribute_list_with_structured_postal_address_field( - structured_postal_address_bytes - ) - ) - - assert profile.postal_address.value == FORMATTED_ADDRESS_VALUE - - -def test_document_images(): - document_images_attribute = attribute_fixture_parser.get_attribute_from_base64_text( - attribute_fixture_parser.ATTRIBUTE_DOCUMENT_IMAGES - ) - - attribute_list = list() - attribute_list.append(document_images_attribute) - - profile = Profile(attribute_list) - - document_images_attribute = profile.document_images - assert len(document_images_attribute.value) == 2 - image_helper.assert_is_expected_image( - document_images_attribute.value[0], "jpeg", "vWgD//2Q==" - ) - image_helper.assert_is_expected_image( - document_images_attribute.value[1], "jpeg", "38TVEH/9k=" - ) - - -def test_nested_multi_value(): - attribute_name = "nested_multi_value" - inner_multi_value = attribute_fixture_parser.parse_multi_value() - - outer_tuple = (inner_multi_value,) - - profile = Profile(profile_attributes=None) - profile.attributes[attribute_name] = Attribute( - name=attribute_name, value=outer_tuple, anchors=None - ) - - retrieved_multi_value = profile.get_attribute(attribute_name) - - assert isinstance(retrieved_multi_value.value, tuple) - - for item in retrieved_multi_value.value: - assert isinstance(item, tuple) - - image_helper.assert_is_expected_image( - retrieved_multi_value.value[0][0], "jpeg", "vWgD//2Q==" - ) - image_helper.assert_is_expected_image( - retrieved_multi_value.value[0][1], "jpeg", "38TVEH/9k=" - ) - - -def test_get_attribute_document_images(): - attribute_list = create_single_attribute_list( - name=config.ATTRIBUTE_DOCUMENT_IMAGES, - value=[], - anchors=None, - content_type=Protobuf.CT_MULTI_VALUE, - ) - - profile = Profile(attribute_list) - - assert ( - profile.get_attribute(config.ATTRIBUTE_DOCUMENT_IMAGES) - == profile.document_images - ) - - -def test_get_attribute_selfie(): - profile = Profile(create_attribute_list_with_selfie_field()) - - assert profile.get_attribute(config.ATTRIBUTE_SELFIE) == profile.selfie - - -def test_get_attribute_email_address(): - profile = Profile(create_attribute_list_with_email_field()) - - assert ( - profile.get_attribute(config.ATTRIBUTE_EMAIL_ADDRESS) == profile.email_address - ) - - -def test_get_attribute_returns_none(): - profile = Profile(None) - - assert profile.get_attribute(config.ATTRIBUTE_SELFIE) is None - - -def test_get_document_details_usa(): - attribute_list = create_single_attribute_list( - name=config.ATTRIBUTE_DOCUMENT_DETAILS, - value=USA_DOCUMENT_DETAILS.encode(), - anchors=None, - content_type=Protobuf.CT_STRING, - ) - profile = Profile(attribute_list) - document = profile.document_details.value - - assert document.document_type == DRIVING_LICENCE - assert document.issuing_country == USA_COUNTRY_ISO_VALUE - assert document.document_number == USA_DRIVING_LICENCE_NUMBER - assert document.expiration_date.isoformat() == EXPIRY_DATE - - -def test_get_document_details_india(): - attribute_list = create_single_attribute_list( - name=config.ATTRIBUTE_DOCUMENT_DETAILS, - value=INDIA_DOCUMENT_DETAILS.encode(), - anchors=None, - content_type=Protobuf.CT_STRING, - ) - profile = Profile(attribute_list) - document = profile.document_details.value - - assert document.document_type == DRIVING_LICENCE - assert document.issuing_country == INDIA_COUNTRY_ISO_VALUE - assert document.document_number == IND_DRIVING_LICENCE_NUMBER - assert document.expiration_date.isoformat() == EXPIRY_DATE +# -*- coding: utf-8 -*- +import collections +import json +import logging + +import pytest + +from yoti_python_sdk import config +from yoti_python_sdk.attribute import Attribute +from yoti_python_sdk.profile import Profile +from yoti_python_sdk.protobuf.protobuf import Protobuf +from yoti_python_sdk.tests import attribute_fixture_parser, image_helper +from yoti_python_sdk.tests.protobuf_attribute import ProtobufAttribute + +ADDRESS_FORMAT_KEY = "address_format" +ADDRESS_FORMAT_VALUE = 1 +INDIA_FORMAT_VALUE = 2 +USA_FORMAT_VALUE = 3 + +BUILDING_NUMBER_KEY = "building_number" +BUILDING_NUMBER_VALUE = "15a" + +CARE_OF_KEY = "care_of" +CARE_OF_VALUE = "S/O: Name" + +STATE_KEY = "state" +INDIA_STATE_VALUE = "Punjab" +USA_STATE_VALUE = "AL" + +BUILDING_KEY = "building" +BUILDING_VALUE = "House No.1111-A" + +STREET_KEY = "street" +STREET_VALUE = "42nd Street" + +DISTRICT_KEY = "district" +DISTRICT_VALUE = "DISTRICT 10" + +SUBDISTRICT_KEY = "subdistrict" +SUBDISTRICT_VALUE = "Sub-DISTRICT 10" + +POST_OFFICE_KEY = "post_office" +INDIA_POST_OFFICE_VALUE = "Rajguru Nagar" + +ADDRESS_LINE_1_KEY = "address_line_1" +ADDRESS_LINE_1_VALUE = "15a North Street" + +TOWN_CITY_KEY = "town_city" +TOWN_CITY_VALUE = "TOWN/CITY NAME" + +POSTAL_CODE_KEY = "postal_code" +POSTAL_CODE_VALUE = "SM5 2HW" +INDIA_POSTAL_CODE_VALUE = "141012" +USA_POSTAL_CODE_VALUE = "36201" + +COUNTRY_ISO_KEY = "country_iso" +COUNTRY_ISO_VALUE = "GBR" +INDIA_COUNTRY_ISO_VALUE = "IND" +USA_COUNTRY_ISO_VALUE = "USA" + +COUNTRY_KEY = "country" +COUNTRY_VALUE = "UK" +INDIA_COUNTRY_VALUE = "India" +USA_COUNTRY_VALUE = "USA" + +FORMATTED_ADDRESS_VALUE = "15a North Street\nCARSHALTON\nSM5 2HW\nUK" +INDIA_FORMATTED_ADDRESS_VALUE = "S/O: Name\nHouse No.1111-A\n42nd Street\nTOWN/CITY NAME\nSub-DISTRICT 10\nDISTRICT 10\nPunjab\n141012\nRajgura Nagar\nIndia" +USA_FORMATTED_ADDRESS_VALUE = "15a North Street\nTOWN/CITY NAME\nAL\n36201\nUSA" + +USA_DOCUMENT_DETAILS = "DRIVING_LICENCE USA 12345678 2016-05-01" +INDIA_DOCUMENT_DETAILS = "DRIVING_LICENCE IND MH-05-2006-1234567 2016-05-01" +DRIVING_LICENCE = "DRIVING_LICENCE" +USA_DRIVING_LICENCE_NUMBER = "12345678" +IND_DRIVING_LICENCE_NUMBER = "MH-05-2006-1234567" +EXPIRY_DATE = "2016-05-01" + + +def create_single_attribute_list(name, value, anchors, content_type): + attribute = ProtobufAttribute(name, value, anchors, content_type) + + attribute_list = list() + attribute_list.append(attribute) + return attribute_list + + +def create_attribute_list_with_selfie_field(): + return create_single_attribute_list( + name=config.ATTRIBUTE_SELFIE, + value="base64(ง •̀_•́)ง", + anchors=None, + content_type=Protobuf.CT_JPEG, + ) + + +def create_attribute_list_with_email_field(): + return create_single_attribute_list( + name=config.ATTRIBUTE_EMAIL_ADDRESS, + value="y@ti.com".encode(), + anchors=None, + content_type=Protobuf.CT_STRING, + ) + + +def create_attribute_list_with_structured_postal_address_field(json_address_value): + return create_single_attribute_list( + name=config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS, + value=json_address_value, + anchors=None, + content_type=Protobuf.CT_JSON, + ) + + +@pytest.mark.parametrize( + "string, expected_int", [("0", 0), ("1", 1), ("123", 123), ("-10", -10), ("-1", -1)] +) +def test_try_parse_int_value(string, expected_int): + attribute_name = "int_attribute" + attribute_list = create_single_attribute_list( + name=attribute_name, + value=str.encode(string), + anchors=None, + content_type=Protobuf.CT_INT, + ) + + profile = Profile(attribute_list) + int_attribute = profile.get_attribute(attribute_name) + assert int_attribute.value == expected_int + + +def test_error_parsing_attribute_has_none_value(): + int_attribute_name = "int_attribute" + + attribute_list = create_single_attribute_list( + name=int_attribute_name, + value=str.encode("invalid_int"), + anchors=None, + content_type=Protobuf.CT_INT, + ) + + # disable logging for the below call: warning shown as int is invalid + logger = logging.getLogger() + logger.propagate = False + + profile = Profile(attribute_list) + + logger.propagate = True + + assert profile.get_attribute(int_attribute_name) is None + + +@pytest.mark.parametrize( + "content_type", + [ + Protobuf.CT_DATE, + Protobuf.CT_INT, + Protobuf.CT_JPEG, + Protobuf.CT_PNG, + Protobuf.CT_JSON, + Protobuf.CT_UNDEFINED, + ], +) +def test_parse_empty_values_returns_none(content_type): + attribute_name = "attribute_name" + + attribute_list = create_single_attribute_list( + name=attribute_name, value=b"", anchors=None, content_type=content_type + ) + + # disable logging for the below call: warning logged as value is empty + logger = logging.getLogger() + logger.propagate = False + + profile = Profile(attribute_list) + + logger.propagate = True + + assert profile.get_attribute(attribute_name) is None + + +@pytest.mark.parametrize("value", [b"", "".encode()]) +def test_parse_empty_string_value_returns_attribute(value): + attribute_name = "attribute_name" + + attribute_list = create_single_attribute_list( + name=attribute_name, value=value, anchors=None, content_type=Protobuf.CT_STRING + ) + + profile = Profile(attribute_list) + + assert profile.get_attribute(attribute_name).value == "" + + +def test_error_parsing_attribute_does_not_affect_other_attribute(): + string_attribute_name = "string_attribute" + int_attribute_name = "int_attribute" + string_value = "string" + + attribute_list = list() + + attribute_list.append( + ProtobufAttribute( + name=string_attribute_name, + value=str.encode(string_value), + anchors=None, + content_type=Protobuf.CT_STRING, + ) + ) + + attribute_list.append( + ProtobufAttribute( + name=int_attribute_name, + value=str.encode("invalid_int"), + anchors=None, + content_type=Protobuf.CT_INT, + ) + ) + + # disable logging for the below call: warning shown as int is invalid + logger = logging.getLogger() + logger.propagate = False + + profile = Profile(attribute_list) + + logger.propagate = True + + assert len(profile.attributes) == 1 + + retrieved_string_attribute = profile.get_attribute(string_attribute_name) + assert retrieved_string_attribute.name == string_attribute_name + assert retrieved_string_attribute.value == string_value + + +def test_try_parse_structured_postal_address_uk(): + structured_postal_address = { + ADDRESS_FORMAT_KEY: ADDRESS_FORMAT_VALUE, + BUILDING_NUMBER_KEY: BUILDING_NUMBER_VALUE, + ADDRESS_LINE_1_KEY: ADDRESS_LINE_1_VALUE, + TOWN_CITY_KEY: TOWN_CITY_VALUE, + POSTAL_CODE_KEY: POSTAL_CODE_VALUE, + COUNTRY_ISO_KEY: COUNTRY_ISO_VALUE, + COUNTRY_KEY: COUNTRY_VALUE, + config.KEY_FORMATTED_ADDRESS: FORMATTED_ADDRESS_VALUE, + } + + structured_postal_address_json = json.dumps(structured_postal_address).encode() + + profile = Profile( + create_attribute_list_with_structured_postal_address_field( + structured_postal_address_json + ) + ) + + actual_structured_postal_address = profile.structured_postal_address.value + actual_address_format = actual_structured_postal_address[ADDRESS_FORMAT_KEY] + actual_building_number = actual_structured_postal_address[BUILDING_NUMBER_KEY] + actual_address_line_1 = actual_structured_postal_address[ADDRESS_LINE_1_KEY] + actual_town_city = actual_structured_postal_address[TOWN_CITY_KEY] + actual_postal_code = actual_structured_postal_address[POSTAL_CODE_KEY] + actual_country_iso = actual_structured_postal_address[COUNTRY_ISO_KEY] + actual_country = actual_structured_postal_address[COUNTRY_KEY] + actual_formatted_address = actual_structured_postal_address[ + config.KEY_FORMATTED_ADDRESS + ] + + assert type(actual_structured_postal_address) is collections.OrderedDict + assert actual_address_format == ADDRESS_FORMAT_VALUE + assert actual_building_number == BUILDING_NUMBER_VALUE + assert actual_address_line_1 == ADDRESS_LINE_1_VALUE + assert actual_town_city == TOWN_CITY_VALUE + assert actual_postal_code == POSTAL_CODE_VALUE + assert actual_country_iso == COUNTRY_ISO_VALUE + assert actual_country == COUNTRY_VALUE + assert actual_formatted_address == FORMATTED_ADDRESS_VALUE + + +def test_other_json_type_is_parsed(): + json_attribute_name = "other_json" + key_a = "keyA" + key_b = "keyB" + value_a = "valueA" + value_b = "valueB" + json_value = {key_a: value_a, key_b: value_b} + + encoded_json = json.dumps(json_value).encode() + + attribute_list = create_single_attribute_list( + name=json_attribute_name, + value=encoded_json, + anchors=None, + content_type=Protobuf.CT_JSON, + ) + + profile = Profile(attribute_list) + + retrieved_attribute = profile.get_attribute(json_attribute_name) + + assert retrieved_attribute.name == json_attribute_name + assert type(retrieved_attribute.value) is collections.OrderedDict + assert retrieved_attribute.value[key_a] == value_a + assert retrieved_attribute.value[key_b] == value_b + + +def test_try_parse_structured_postal_address_india(): + structured_postal_address = { + ADDRESS_FORMAT_KEY: INDIA_FORMAT_VALUE, + CARE_OF_KEY: CARE_OF_VALUE, + BUILDING_KEY: BUILDING_VALUE, + STREET_KEY: STREET_VALUE, + TOWN_CITY_KEY: TOWN_CITY_VALUE, + SUBDISTRICT_KEY: SUBDISTRICT_VALUE, + DISTRICT_KEY: DISTRICT_VALUE, + STATE_KEY: INDIA_STATE_VALUE, + POSTAL_CODE_KEY: INDIA_POSTAL_CODE_VALUE, + POST_OFFICE_KEY: INDIA_POST_OFFICE_VALUE, + COUNTRY_ISO_KEY: INDIA_COUNTRY_ISO_VALUE, + COUNTRY_KEY: INDIA_COUNTRY_VALUE, + config.KEY_FORMATTED_ADDRESS: INDIA_FORMATTED_ADDRESS_VALUE, + } + + structured_postal_address_bytes = json.dumps(structured_postal_address).encode() + + profile = Profile( + create_attribute_list_with_structured_postal_address_field( + structured_postal_address_bytes + ) + ) + + actual_structured_postal_address_profile = profile.structured_postal_address.value + + assert type(actual_structured_postal_address_profile) is collections.OrderedDict + assert ( + actual_structured_postal_address_profile[ADDRESS_FORMAT_KEY] + == INDIA_FORMAT_VALUE + ) + assert actual_structured_postal_address_profile[CARE_OF_KEY] == CARE_OF_VALUE + assert actual_structured_postal_address_profile[BUILDING_KEY] == BUILDING_VALUE + assert actual_structured_postal_address_profile[STREET_KEY] == STREET_VALUE + assert actual_structured_postal_address_profile[TOWN_CITY_KEY] == TOWN_CITY_VALUE + assert ( + actual_structured_postal_address_profile[SUBDISTRICT_KEY] == SUBDISTRICT_VALUE + ) + assert actual_structured_postal_address_profile[DISTRICT_KEY] == DISTRICT_VALUE + assert actual_structured_postal_address_profile[STATE_KEY] == INDIA_STATE_VALUE + assert ( + actual_structured_postal_address_profile[POSTAL_CODE_KEY] + == INDIA_POSTAL_CODE_VALUE + ) + assert ( + actual_structured_postal_address_profile[POST_OFFICE_KEY] + == INDIA_POST_OFFICE_VALUE + ) + assert ( + actual_structured_postal_address_profile[COUNTRY_ISO_KEY] + == INDIA_COUNTRY_ISO_VALUE + ) + assert actual_structured_postal_address_profile[COUNTRY_KEY] == INDIA_COUNTRY_VALUE + assert ( + actual_structured_postal_address_profile[config.KEY_FORMATTED_ADDRESS] + == INDIA_FORMATTED_ADDRESS_VALUE + ) + + +def test_try_parse_structured_postal_address_usa(): + structured_postal_address = { + ADDRESS_FORMAT_KEY: USA_FORMAT_VALUE, + ADDRESS_LINE_1_KEY: ADDRESS_LINE_1_VALUE, + TOWN_CITY_KEY: TOWN_CITY_VALUE, + STATE_KEY: USA_STATE_VALUE, + POSTAL_CODE_KEY: USA_POSTAL_CODE_VALUE, + COUNTRY_ISO_KEY: USA_COUNTRY_ISO_VALUE, + COUNTRY_KEY: USA_COUNTRY_VALUE, + config.KEY_FORMATTED_ADDRESS: USA_FORMATTED_ADDRESS_VALUE, + } + + structured_postal_address_bytes = json.dumps(structured_postal_address).encode() + + profile = Profile( + create_attribute_list_with_structured_postal_address_field( + structured_postal_address_bytes + ) + ) + + actual_structured_postal_address_profile = profile.structured_postal_address.value + + assert type(actual_structured_postal_address_profile) is collections.OrderedDict + assert ( + actual_structured_postal_address_profile[ADDRESS_FORMAT_KEY] == USA_FORMAT_VALUE + ) + assert ( + actual_structured_postal_address_profile[ADDRESS_LINE_1_KEY] + == ADDRESS_LINE_1_VALUE + ) + assert actual_structured_postal_address_profile[TOWN_CITY_KEY] == TOWN_CITY_VALUE + assert actual_structured_postal_address_profile[STATE_KEY] == USA_STATE_VALUE + assert ( + actual_structured_postal_address_profile[POSTAL_CODE_KEY] + == USA_POSTAL_CODE_VALUE + ) + assert ( + actual_structured_postal_address_profile[COUNTRY_ISO_KEY] + == USA_COUNTRY_ISO_VALUE + ) + assert actual_structured_postal_address_profile[COUNTRY_KEY] == USA_COUNTRY_VALUE + assert ( + actual_structured_postal_address_profile[config.KEY_FORMATTED_ADDRESS] + == USA_FORMATTED_ADDRESS_VALUE + ) + + +def test_try_parse_structured_postal_address_nested_json(): + formatted_address_json = { + "item1": [[1, "a1"], [2, "a2"]], + "item2": [[3, "b3"], [4, "b4"]], + } + + structured_postal_address = { + ADDRESS_FORMAT_KEY: ADDRESS_FORMAT_VALUE, + BUILDING_NUMBER_KEY: BUILDING_NUMBER_VALUE, + ADDRESS_LINE_1_KEY: ADDRESS_LINE_1_VALUE, + TOWN_CITY_KEY: TOWN_CITY_VALUE, + POSTAL_CODE_KEY: POSTAL_CODE_VALUE, + COUNTRY_ISO_KEY: COUNTRY_ISO_VALUE, + COUNTRY_KEY: COUNTRY_VALUE, + config.KEY_FORMATTED_ADDRESS: formatted_address_json, + } + + structured_postal_address_bytes = json.dumps(structured_postal_address).encode() + + profile = Profile( + create_attribute_list_with_structured_postal_address_field( + structured_postal_address_bytes + ) + ) + + actual_structured_postal_address_profile = profile.structured_postal_address.value + + assert type(actual_structured_postal_address_profile) is collections.OrderedDict + assert ( + actual_structured_postal_address_profile[ADDRESS_FORMAT_KEY] + == ADDRESS_FORMAT_VALUE + ) + assert ( + actual_structured_postal_address_profile[BUILDING_NUMBER_KEY] + == BUILDING_NUMBER_VALUE + ) + assert ( + actual_structured_postal_address_profile[ADDRESS_LINE_1_KEY] + == ADDRESS_LINE_1_VALUE + ) + assert actual_structured_postal_address_profile[TOWN_CITY_KEY] == TOWN_CITY_VALUE + assert ( + actual_structured_postal_address_profile[POSTAL_CODE_KEY] == POSTAL_CODE_VALUE + ) + assert ( + actual_structured_postal_address_profile[COUNTRY_ISO_KEY] == COUNTRY_ISO_VALUE + ) + assert actual_structured_postal_address_profile[COUNTRY_KEY] == COUNTRY_VALUE + + assert ( + actual_structured_postal_address_profile[config.KEY_FORMATTED_ADDRESS] + == formatted_address_json + ) + + +def test_set_address_to_be_formatted_address(): + structured_postal_address = {config.KEY_FORMATTED_ADDRESS: FORMATTED_ADDRESS_VALUE} + structured_postal_address_bytes = json.dumps(structured_postal_address).encode() + + profile = Profile( + create_attribute_list_with_structured_postal_address_field( + structured_postal_address_bytes + ) + ) + + assert profile.postal_address.value == FORMATTED_ADDRESS_VALUE + + +def test_document_images(): + document_images_attribute = attribute_fixture_parser.get_attribute_from_base64_text( + attribute_fixture_parser.ATTRIBUTE_DOCUMENT_IMAGES + ) + + attribute_list = list() + attribute_list.append(document_images_attribute) + + profile = Profile(attribute_list) + + document_images_attribute = profile.document_images + assert len(document_images_attribute.value) == 2 + image_helper.assert_is_expected_image( + document_images_attribute.value[0], "jpeg", "vWgD//2Q==" + ) + image_helper.assert_is_expected_image( + document_images_attribute.value[1], "jpeg", "38TVEH/9k=" + ) + + +def test_nested_multi_value(): + attribute_name = "nested_multi_value" + inner_multi_value = attribute_fixture_parser.parse_multi_value() + + outer_tuple = (inner_multi_value,) + + profile = Profile(profile_attributes=None) + profile.attributes[attribute_name] = Attribute( + name=attribute_name, value=outer_tuple, anchors=None + ) + + retrieved_multi_value = profile.get_attribute(attribute_name) + + assert isinstance(retrieved_multi_value.value, tuple) + + for item in retrieved_multi_value.value: + assert isinstance(item, tuple) + + image_helper.assert_is_expected_image( + retrieved_multi_value.value[0][0], "jpeg", "vWgD//2Q==" + ) + image_helper.assert_is_expected_image( + retrieved_multi_value.value[0][1], "jpeg", "38TVEH/9k=" + ) + + +def test_get_attribute_document_images(): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_DOCUMENT_IMAGES, + value=[], + anchors=None, + content_type=Protobuf.CT_MULTI_VALUE, + ) + + profile = Profile(attribute_list) + + assert ( + profile.get_attribute(config.ATTRIBUTE_DOCUMENT_IMAGES) + == profile.document_images + ) + + +def test_get_attribute_selfie(): + profile = Profile(create_attribute_list_with_selfie_field()) + + assert profile.get_attribute(config.ATTRIBUTE_SELFIE) == profile.selfie + + +def test_get_attribute_email_address(): + profile = Profile(create_attribute_list_with_email_field()) + + assert ( + profile.get_attribute(config.ATTRIBUTE_EMAIL_ADDRESS) == profile.email_address + ) + + +def test_get_attribute_returns_none(): + profile = Profile(None) + + assert profile.get_attribute(config.ATTRIBUTE_SELFIE) is None + + +def test_get_document_details_usa(): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_DOCUMENT_DETAILS, + value=USA_DOCUMENT_DETAILS.encode(), + anchors=None, + content_type=Protobuf.CT_STRING, + ) + profile = Profile(attribute_list) + document = profile.document_details.value + + assert document.document_type == DRIVING_LICENCE + assert document.issuing_country == USA_COUNTRY_ISO_VALUE + assert document.document_number == USA_DRIVING_LICENCE_NUMBER + assert document.expiration_date.isoformat() == EXPIRY_DATE + + +def test_get_document_details_india(): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_DOCUMENT_DETAILS, + value=INDIA_DOCUMENT_DETAILS.encode(), + anchors=None, + content_type=Protobuf.CT_STRING, + ) + profile = Profile(attribute_list) + document = profile.document_details.value + + assert document.document_type == DRIVING_LICENCE + assert document.issuing_country == INDIA_COUNTRY_ISO_VALUE + assert document.document_number == IND_DRIVING_LICENCE_NUMBER + assert document.expiration_date.isoformat() == EXPIRY_DATE From bd1f5a23b08175fe828cfe6fa8f6673cfd41fb4e Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Mon, 12 Aug 2019 15:47:31 +0100 Subject: [PATCH 72/89] SDK-826: Add support for disabling SSL verification --- yoti_python_sdk/__init__.py | 12 +++++++++++- yoti_python_sdk/client.py | 7 ++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py index 13189ff3..9983177b 100644 --- a/yoti_python_sdk/__init__.py +++ b/yoti_python_sdk/__init__.py @@ -9,6 +9,7 @@ "YOTI_API_URL": "https://api.yoti.com", "YOTI_API_PORT": 443, "YOTI_API_VERSION": "v1", + "YOTI_API_VERIFY_SSL": "true" } main_ns = {} @@ -28,4 +29,13 @@ YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION ) -__all__ = ["Client", __version__] +YOTI_API_VERIFY_SSL = environ.get("YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"]) +if YOTI_API_VERIFY_SSL == "false": + YOTI_API_VERIFY_SSL = False +else: + YOTI_API_VERIFY_SSL = True + +__all__ = [ + "Client", + __version__ +] diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index c72e6979..a7c98119 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -23,6 +23,7 @@ X_YOTI_SDK_VERSION, JSON_CONTENT_TYPE, ) +from . import YOTI_API_VERIFY_SSL NO_KEY_FILE_SPECIFIED_ERROR = ( "Please specify the correct private key file " @@ -103,7 +104,7 @@ def make_request(self, http_method, endpoint, body): url = yoti_python_sdk.YOTI_API_ENDPOINT + endpoint headers = self.__get_request_headers(endpoint, http_method, body) - response = requests.request(http_method, url, headers=headers, data=body) + response = requests.request(http_method, url, headers=headers, data=body, verify=YOTI_API_VERIFY_SSL) return response @property @@ -135,7 +136,7 @@ def __make_activity_details_request( path = self.__endpoint.get_activity_details_request_path(decrypted_token) url = yoti_python_sdk.YOTI_API_ENDPOINT + path headers = self.__get_request_headers(path, http_method, content) - response = requests.get(url=url, headers=headers) + response = requests.get(url=url, headers=headers, verify=YOTI_API_VERIFY_SSL) self.http_error_handler( response, {"default": "Unsuccessful Yoti API call: {1}"} @@ -150,7 +151,7 @@ def __make_aml_check_request(self, http_method, aml_profile): url = yoti_python_sdk.YOTI_API_ENDPOINT + path headers = self.__get_request_headers(path, http_method, aml_profile_bytes) - response = requests.post(url=url, headers=headers, data=aml_profile_bytes) + response = requests.post(url=url, headers=headers, data=aml_profile_bytes, verify=YOTI_API_VERIFY_SSL) self.http_error_handler( response, {"default": "Unsuccessful Yoti API call: {1}"} From 22c078fcf259fcbb2d93985e4bed3198db490dc7 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Mon, 12 Aug 2019 15:57:13 +0100 Subject: [PATCH 73/89] SDK-826: Change how variable is imported --- yoti_python_sdk/client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index a7c98119..c584f7d2 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -23,7 +23,6 @@ X_YOTI_SDK_VERSION, JSON_CONTENT_TYPE, ) -from . import YOTI_API_VERIFY_SSL NO_KEY_FILE_SPECIFIED_ERROR = ( "Please specify the correct private key file " @@ -104,7 +103,7 @@ def make_request(self, http_method, endpoint, body): url = yoti_python_sdk.YOTI_API_ENDPOINT + endpoint headers = self.__get_request_headers(endpoint, http_method, body) - response = requests.request(http_method, url, headers=headers, data=body, verify=YOTI_API_VERIFY_SSL) + response = requests.request(http_method, url, headers=headers, data=body, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL) return response @property @@ -136,7 +135,7 @@ def __make_activity_details_request( path = self.__endpoint.get_activity_details_request_path(decrypted_token) url = yoti_python_sdk.YOTI_API_ENDPOINT + path headers = self.__get_request_headers(path, http_method, content) - response = requests.get(url=url, headers=headers, verify=YOTI_API_VERIFY_SSL) + response = requests.get(url=url, headers=headers, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL) self.http_error_handler( response, {"default": "Unsuccessful Yoti API call: {1}"} @@ -151,7 +150,7 @@ def __make_aml_check_request(self, http_method, aml_profile): url = yoti_python_sdk.YOTI_API_ENDPOINT + path headers = self.__get_request_headers(path, http_method, aml_profile_bytes) - response = requests.post(url=url, headers=headers, data=aml_profile_bytes, verify=YOTI_API_VERIFY_SSL) + response = requests.post(url=url, headers=headers, data=aml_profile_bytes, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL) self.http_error_handler( response, {"default": "Unsuccessful Yoti API call: {1}"} From c1b98580f48e45093aab87c21789296836322164 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Mon, 12 Aug 2019 16:05:05 +0100 Subject: [PATCH 74/89] SDK-826: Amend tests to include verify parameter in mock calls --- yoti_python_sdk/tests/test_client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yoti_python_sdk/tests/test_client.py b/yoti_python_sdk/tests/test_client.py index 18769183..8fd91c5b 100644 --- a/yoti_python_sdk/tests/test_client.py +++ b/yoti_python_sdk/tests/test_client.py @@ -164,7 +164,7 @@ def test_requesting_activity_details_with_correct_data( activity_details = client.get_activity_details(encrypted_request_token) mock_get.assert_called_once_with( - url=expected_activity_details_url, headers=expected_get_headers + url=expected_activity_details_url, headers=expected_get_headers, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL ) assert isinstance(activity_details, ActivityDetails) @@ -214,7 +214,7 @@ def test_requesting_activity_details_with_null_profile( activity_details = client.get_activity_details(encrypted_request_token) mock_get.assert_called_once_with( - url=expected_activity_details_url, headers=expected_get_headers + url=expected_activity_details_url, headers=expected_get_headers, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL ) assert ( activity_details.user_id @@ -243,7 +243,7 @@ def test_requesting_activity_details_with_empty_profile( activity_details = client.get_activity_details(encrypted_request_token) mock_get.assert_called_once_with( - url=expected_activity_details_url, headers=expected_get_headers + url=expected_activity_details_url, headers=expected_get_headers, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL ) assert ( activity_details.user_id @@ -272,7 +272,7 @@ def test_requesting_activity_details_with_missing_profile( activity_details = client.get_activity_details(encrypted_request_token) mock_get.assert_called_once_with( - url=expected_activity_details_url, headers=expected_get_headers + url=expected_activity_details_url, headers=expected_get_headers, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL ) assert ( activity_details.user_id @@ -345,7 +345,7 @@ def test_perform_aml_check_details_with_correct_data( aml_profile_bytes = aml_profile_json.encode() mock_post.assert_called_once_with( - url=expected_aml_url, headers=expected_post_headers, data=aml_profile_bytes + url=expected_aml_url, headers=expected_post_headers, data=aml_profile_bytes, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL ) assert isinstance(aml_result, aml.AmlResult) From dd403264a8c17ae3cf2cff3bfafb69d0d36a3538 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Mon, 12 Aug 2019 17:15:04 +0100 Subject: [PATCH 75/89] SDK-826: Add environment variable to allow overriding of whole endpoint --- yoti_python_sdk/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py index 9983177b..f0a5c8a2 100644 --- a/yoti_python_sdk/__init__.py +++ b/yoti_python_sdk/__init__.py @@ -21,6 +21,7 @@ with open(ver_path) as ver_file: exec(ver_file.read(), main_ns) +<<<<<<< HEAD __version__ = main_ns["__version__"] YOTI_API_URL = environ.get("YOTI_API_URL", DEFAULTS["YOTI_API_URL"]) YOTI_API_PORT = environ.get("YOTI_API_PORT", DEFAULTS["YOTI_API_PORT"]) @@ -28,6 +29,13 @@ YOTI_API_ENDPOINT = "{0}:{1}/api/{2}".format( YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION ) +======= +__version__ = main_ns['__version__'] +YOTI_API_URL = environ.get('YOTI_API_URL', DEFAULTS['YOTI_API_URL']) +YOTI_API_PORT = environ.get('YOTI_API_PORT', DEFAULTS['YOTI_API_PORT']) +YOTI_API_VERSION = environ.get('YOTI_API_VERSION', DEFAULTS['YOTI_API_VERSION']) +YOTI_API_ENDPOINT = environ.get('YOTI_API_ENDPOINT', '{0}:{1}/api/{2}'.format(YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION)) +>>>>>>> SDK-826: Add environment variable to allow overriding of whole endpoint YOTI_API_VERIFY_SSL = environ.get("YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"]) if YOTI_API_VERIFY_SSL == "false": From c7662dfb3f0f3be172d86bf0c4a37594b3c5af68 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 13 Aug 2019 11:43:42 +0100 Subject: [PATCH 76/89] SDK-826: Update tests to use verify SSL from environment variable --- yoti_python_sdk/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py index f0a5c8a2..9983177b 100644 --- a/yoti_python_sdk/__init__.py +++ b/yoti_python_sdk/__init__.py @@ -21,7 +21,6 @@ with open(ver_path) as ver_file: exec(ver_file.read(), main_ns) -<<<<<<< HEAD __version__ = main_ns["__version__"] YOTI_API_URL = environ.get("YOTI_API_URL", DEFAULTS["YOTI_API_URL"]) YOTI_API_PORT = environ.get("YOTI_API_PORT", DEFAULTS["YOTI_API_PORT"]) @@ -29,13 +28,6 @@ YOTI_API_ENDPOINT = "{0}:{1}/api/{2}".format( YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION ) -======= -__version__ = main_ns['__version__'] -YOTI_API_URL = environ.get('YOTI_API_URL', DEFAULTS['YOTI_API_URL']) -YOTI_API_PORT = environ.get('YOTI_API_PORT', DEFAULTS['YOTI_API_PORT']) -YOTI_API_VERSION = environ.get('YOTI_API_VERSION', DEFAULTS['YOTI_API_VERSION']) -YOTI_API_ENDPOINT = environ.get('YOTI_API_ENDPOINT', '{0}:{1}/api/{2}'.format(YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION)) ->>>>>>> SDK-826: Add environment variable to allow overriding of whole endpoint YOTI_API_VERIFY_SSL = environ.get("YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"]) if YOTI_API_VERIFY_SSL == "false": From 8e5a698c6473843c7602c822454c8f512e3fde03 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 13 Aug 2019 15:31:14 +0100 Subject: [PATCH 77/89] SDK-826: Update verify ssl logic to check for false in lowercase --- yoti_python_sdk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py index 9983177b..3199ce22 100644 --- a/yoti_python_sdk/__init__.py +++ b/yoti_python_sdk/__init__.py @@ -30,7 +30,7 @@ ) YOTI_API_VERIFY_SSL = environ.get("YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"]) -if YOTI_API_VERIFY_SSL == "false": +if YOTI_API_VERIFY_SSL.lower() == "false": YOTI_API_VERIFY_SSL = False else: YOTI_API_VERIFY_SSL = True From faf21a09cb5da4c51e2146f98839eb16a50dd5e4 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Tue, 13 Aug 2019 10:51:23 +0100 Subject: [PATCH 78/89] SDK-1129: Add clientSdkId to index.html for example projects --- .../yoti_example_django/yoti_example/templates/index.html | 1 + examples/yoti_example_django/yoti_example/views.py | 4 +++- examples/yoti_example_flask/app.py | 4 +++- examples/yoti_example_flask/templates/index.html | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/yoti_example_django/yoti_example/templates/index.html b/examples/yoti_example_django/yoti_example/templates/index.html index 832adc15..3743cdc2 100644 --- a/examples/yoti_example_django/yoti_example/templates/index.html +++ b/examples/yoti_example_django/yoti_example/templates/index.html @@ -70,6 +70,7 @@

The Yoti app is free to download and use:The Yoti app is free to download and use: Date: Tue, 13 Aug 2019 16:05:33 +0100 Subject: [PATCH 79/89] SDK-1129: Normalise indentation in example projects html files --- .../yoti_example_django/yoti_example/templates/index.html | 4 ++-- examples/yoti_example_flask/templates/index.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/yoti_example_django/yoti_example/templates/index.html b/examples/yoti_example_django/yoti_example/templates/index.html index 3743cdc2..26070ac6 100644 --- a/examples/yoti_example_django/yoti_example/templates/index.html +++ b/examples/yoti_example_django/yoti_example/templates/index.html @@ -70,8 +70,8 @@

The Yoti app is free to download and use:The Yoti app is free to download and use: Date: Wed, 14 Aug 2019 12:03:06 +0100 Subject: [PATCH 80/89] Update version numbers for 2.8.0 release --- examples/aml/requirements.txt | 2 +- examples/yoti_example_django/requirements.in | 2 +- examples/yoti_example_django/requirements.txt | 2 +- examples/yoti_example_flask/requirements.in | 2 +- examples/yoti_example_flask/requirements.txt | 2 +- yoti_python_sdk/version.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/aml/requirements.txt b/examples/aml/requirements.txt index e0308bc2..e0bb5113 100644 --- a/examples/aml/requirements.txt +++ b/examples/aml/requirements.txt @@ -1,2 +1,2 @@ -yoti>=2.7.0 +yoti>=2.8.0 python-dotenv>=0.7.1 diff --git a/examples/yoti_example_django/requirements.in b/examples/yoti_example_django/requirements.in index 6495580e..bac7e98d 100644 --- a/examples/yoti_example_django/requirements.in +++ b/examples/yoti_example_django/requirements.in @@ -4,4 +4,4 @@ django-sslserver>=0.2.0 python-dotenv>=0.7.1 requests>=2.20.0 urllib3>=1.24.2 -yoti>=2.7.0 +yoti>=2.8.0 diff --git a/examples/yoti_example_django/requirements.txt b/examples/yoti_example_django/requirements.txt index 2a96fe14..19059855 100644 --- a/examples/yoti_example_django/requirements.txt +++ b/examples/yoti_example_django/requirements.txt @@ -23,7 +23,7 @@ requests==2.21.0 six==1.11.0 # via cryptography, protobuf, pyopenssl sqlparse==0.3.0 # via django urllib3==1.24.2 -yoti==2.7.0 +yoti==2.8.0 # The following packages are considered to be unsafe in a requirements file: # setuptools==41.1.0 # via django-sslserver, protobuf diff --git a/examples/yoti_example_flask/requirements.in b/examples/yoti_example_flask/requirements.in index 83e637ad..59378ead 100644 --- a/examples/yoti_example_flask/requirements.in +++ b/examples/yoti_example_flask/requirements.in @@ -5,4 +5,4 @@ pyopenssl>=19.0.0 python-dotenv>=0.7.1 requests>=2.20.0 urllib3>=1.24.2 -yoti>=2.7.0 +yoti>=2.8.0 diff --git a/examples/yoti_example_flask/requirements.txt b/examples/yoti_example_flask/requirements.txt index 97b15b25..284ba853 100644 --- a/examples/yoti_example_flask/requirements.txt +++ b/examples/yoti_example_flask/requirements.txt @@ -25,7 +25,7 @@ requests==2.21.0 six==1.11.0 # via cryptography, protobuf, pyopenssl urllib3==1.24.2 werkzeug==0.14.1 # via flask -yoti==2.7.0 +yoti==2.8.0 # The following packages are considered to be unsafe in a requirements file: # setuptools==41.1.0 # via protobuf diff --git a/yoti_python_sdk/version.py b/yoti_python_sdk/version.py index 70e0898b..71bce917 100644 --- a/yoti_python_sdk/version.py +++ b/yoti_python_sdk/version.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "2.7.1" +__version__ = "2.8.0" From eba814c1cb723d238221e01d050c4f095cf74b4e Mon Sep 17 00:00:00 2001 From: Emma Smith <52929061+emmas-yoti@users.noreply.github.com> Date: Wed, 14 Aug 2019 16:07:19 +0100 Subject: [PATCH 81/89] SDK-1031: Use UNKNOWN_ANCHOR_TYPE for initialization of anchor Co-Authored-By: Ed Harrod --- yoti_python_sdk/anchor.py | 2 +- yoti_python_sdk/client.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/yoti_python_sdk/anchor.py b/yoti_python_sdk/anchor.py index a0cfe3f0..f3327e91 100644 --- a/yoti_python_sdk/anchor.py +++ b/yoti_python_sdk/anchor.py @@ -19,7 +19,7 @@ class Anchor: def __init__( self, - anchor_type="Unknown", + anchor_type=UNKNOWN_ANCHOR_TYPE, sub_type="", value="", signed_timestamp=None, diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index c584f7d2..0164caf5 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -100,7 +100,6 @@ def perform_aml_check(self, aml_profile): return aml.AmlResult(response.text) def make_request(self, http_method, endpoint, body): - url = yoti_python_sdk.YOTI_API_ENDPOINT + endpoint headers = self.__get_request_headers(endpoint, http_method, body) response = requests.request(http_method, url, headers=headers, data=body, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL) From 065198f3f44416e4a66500abe9eede4c024b5c41 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Wed, 14 Aug 2019 18:00:43 +0100 Subject: [PATCH 82/89] SDK-722: Rename methods from wanted_attribute_builder to conform to PEP8 --- .../policy/dynamic_policy_builder.py | 6 ++--- .../policy/wanted_attribute_builder.py | 25 ++++++++----------- .../policy/test_dynamic_policy_builder.py | 2 +- .../policy/test_wanted_attribute_builder.py | 2 +- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/dynamic_policy_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/dynamic_policy_builder.py index 264a883c..3c5a2974 100644 --- a/yoti_python_sdk/dynamic_sharing_service/policy/dynamic_policy_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/policy/dynamic_policy_builder.py @@ -39,7 +39,7 @@ def with_wanted_attribute(self, wanted_attribute): """ def with_wanted_attribute_by_name(self, wanted_name): - attribute = WantedAttributeBuilder().withName(wanted_name).build() + attribute = WantedAttributeBuilder().with_name(wanted_name).build() return self.with_wanted_attribute(attribute) def with_family_name(self): @@ -57,8 +57,8 @@ def with_date_of_birth(self): def with_age_derived_attribute(self, derivation): attribute = ( WantedAttributeBuilder() - .withName(config.ATTRIBUTE_DATE_OF_BIRTH) - .withDerivation(derivation) + .with_name(config.ATTRIBUTE_DATE_OF_BIRTH) + .with_derivation(derivation) .build() ) return self.with_wanted_attribute(attribute) diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/wanted_attribute_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/wanted_attribute_builder.py index 2b16cda2..90d77165 100644 --- a/yoti_python_sdk/dynamic_sharing_service/policy/wanted_attribute_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/policy/wanted_attribute_builder.py @@ -10,25 +10,22 @@ class WantedAttributeBuilder(object): def __init__(self): self.__attribute = {} - """ - @param name Sets name - """ - - def withName(self, name): + def with_name(self, name): + """ + :param name: Sets name + """ self.__attribute["name"] = name return self - """ - @param derivation Sets derivation - """ - - def withDerivation(self, derivation): + def with_derivation(self, derivation): + """ + :param derivation: Sets derivation + """ self.__attribute["derivation"] = derivation return self - """ - @return The wanted attribute object - """ - def build(self): + """ + :return: The wanted attribute object + """ return self.__attribute.copy() diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_dynamic_policy_builder.py b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_dynamic_policy_builder.py index da87c9cd..e53edbdc 100644 --- a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_dynamic_policy_builder.py +++ b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_dynamic_policy_builder.py @@ -11,7 +11,7 @@ def test_an_attribute_can_only_exist_once(): NAME = "Test name" - wanted_attribute = WantedAttributeBuilder().withName(NAME).build() + wanted_attribute = WantedAttributeBuilder().with_name(NAME).build() policy = ( DynamicPolicyBuilder() diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_wanted_attribute_builder.py b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_wanted_attribute_builder.py index d720edd4..e061460f 100644 --- a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_wanted_attribute_builder.py +++ b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_wanted_attribute_builder.py @@ -9,7 +9,7 @@ def test_build(): builder = WantedAttributeBuilder() - attribute = builder.withName(NAME).withDerivation(DERIVATION).build() + attribute = builder.with_name(NAME).with_derivation(DERIVATION).build() assert attribute["name"] == NAME assert attribute["derivation"] == DERIVATION From 04bf568446bdfa844e4123574ef3a7d733a2d9db Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 13 Aug 2019 14:52:54 +0100 Subject: [PATCH 83/89] SDK-1098: Add initial support for getting application profile details from activity details --- yoti_python_sdk/activity_details.py | 10 ++++-- yoti_python_sdk/client.py | 7 +++- yoti_python_sdk/config.py | 4 +++ yoti_python_sdk/profile.py | 52 ++++++++++++++++++++++------ yoti_python_sdk/protobuf/protobuf.py | 13 +++++++ 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/yoti_python_sdk/activity_details.py b/yoti_python_sdk/activity_details.py index baac8c21..bac495b1 100644 --- a/yoti_python_sdk/activity_details.py +++ b/yoti_python_sdk/activity_details.py @@ -6,14 +6,15 @@ from datetime import datetime from yoti_python_sdk import attribute_parser, config -from yoti_python_sdk.profile import Profile +from yoti_python_sdk.profile import Profile, ApplicationProfile class ActivityDetails: - def __init__(self, receipt, decrypted_profile=None): + def __init__(self, receipt, decrypted_profile=None, decrypted_application_profile=None): self.decrypted_profile = decrypted_profile self.user_profile = {} # will be removed in v3.0.0 self.base64_selfie_uri = None + self.application_profile = None if decrypted_profile and hasattr(decrypted_profile, "attributes"): decrypted_profile_attributes = decrypted_profile.attributes @@ -55,6 +56,11 @@ def __init__(self, receipt, decrypted_profile=None): ) self.ensure_postal_address() + + + if decrypted_application_profile and hasattr(decrypted_application_profile, "attributes"): + decrypted_application_profile_attributes = decrypted_application_profile.attributes + self.application_profile = ApplicationProfile(decrypted_application_profile_attributes) self.__remember_me_id = receipt.get("remember_me_id") self.parent_remember_me_id = receipt.get("parent_remember_me_id") diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index 0164caf5..9fce118b 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -78,6 +78,7 @@ def get_activity_details(self, encrypted_request_token): receipt = json.loads(response.text).get("receipt") encrypted_data = proto.current_user(receipt) + encrypted_application_profile = proto.current_application(receipt) if not encrypted_data: return ActivityDetails(receipt) @@ -86,8 +87,12 @@ def get_activity_details(self, encrypted_request_token): decrypted_data = self.__crypto.decipher( unwrapped_key, encrypted_data.iv, encrypted_data.cipher_text ) + decrypted_application_data = self.__crypto.decipher( + unwrapped_key, encrypted_application_profile.iv, encrypted_application_profile.cipher_text + ) + attribute_list = proto.attribute_list(decrypted_data) - return ActivityDetails(receipt, attribute_list) + return ActivityDetails(receipt, attribute_list, decrypted_application_profile=decrypted_application_data) def perform_aml_check(self, aml_profile): if aml_profile is None: diff --git a/yoti_python_sdk/config.py b/yoti_python_sdk/config.py index 758db08a..06e430ad 100644 --- a/yoti_python_sdk/config.py +++ b/yoti_python_sdk/config.py @@ -16,6 +16,10 @@ ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS = "structured_postal_address" ATTRIBUTE_DOCUMENT_IMAGES = "document_images" ATTRIBUTE_DOCUMENT_DETAILS = "document_details" +ATTRIBUTE_APPLICATION_NAME = "application_name" +ATTRIBUTE_APPLICATION_LOGO = "application_logo" +ATTRIBUTE_APPLICATION_URL = "application_url" +ATTRIBUTE_APPLICATION_RECEIPT_BGCOLOR = "application_receipt_bgcolor" ANCHOR_SOURCE = "SOURCE" ANCHOR_VERIFIER = "VERIFIER" KEY_AGE_VERIFIED = "is_age_verified" diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index 19317e03..102fbde3 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -7,8 +7,8 @@ from yoti_python_sdk.image import Image from yoti_python_sdk import document_details +class BaseProfile: -class Profile: def __init__(self, profile_attributes): self.attributes = {} @@ -44,7 +44,21 @@ def __init__(self, profile_attributes): ) ) - self.ensure_postal_address() + def get_attribute(self, attribute_name): + """retrieves an attribute based on its name + :param attribute_name: + :return: Attribute + """ + if attribute_name in self.attributes: + return self.attributes.get(attribute_name) + else: + return None + + +class Profile(BaseProfile): + def __init__(self, profile_attributes): + super(Profile, self).__init__(profile_attributes) + self.ensure_postal_address() @property def date_of_birth(self): @@ -145,15 +159,6 @@ def document_images(self): def document_details(self): return self.get_attribute(config.ATTRIBUTE_DOCUMENT_DETAILS) - def get_attribute(self, attribute_name): - """retrieves an attribute based on its name - :param attribute_name: - :return: Attribute - """ - if attribute_name in self.attributes: - return self.attributes.get(attribute_name) - else: - return None def ensure_postal_address(self): if ( @@ -173,3 +178,28 @@ def ensure_postal_address(self): formatted_address, structured_postal_address.anchors, ) + +def ApplicationProfile(BaseProfile): + + def __init__(self, profile_attributes): + super(ApplicationProfile, self).__init__(profile_attributes) + + @property + def application_name(self): + """ + application_name is the name of the application set in Yoti Hub + :return: Attribute(str) + """ + return self.get_attribute(config.ATTRIBUTE_APPLICATION_NAME) + + @property + def application_url(self): + return self.get_attribute(config.ATTRIBUTE_APPLICATION_URL) + + @property + def application_logo(self): + return self.get_attribute(config.ATTRIBUTE_APPLICATION_LOGO) + + @property + def application_receipt_bg_color(self): + return self.get_attribute(config.ATTRIBUTE_APPLICATION_RECEIPT_BGCOLOR) diff --git a/yoti_python_sdk/protobuf/protobuf.py b/yoti_python_sdk/protobuf/protobuf.py index 48a02135..14dd15db 100644 --- a/yoti_python_sdk/protobuf/protobuf.py +++ b/yoti_python_sdk/protobuf/protobuf.py @@ -28,6 +28,19 @@ def current_user(receipt): merged_user.MergeFromString(decoded_profile_content) return merged_user + @staticmethod + def current_application(receipt): + if receipt.get("profile_content") is None or receipt.get("profile_content") == '': + return None + + application_content = receipt["profile_content"] + decoded_application_content = base64.b64decode(application_content) + + merged_application = EncryptedData_pb2.EncryptedData() + merged_application.MergeFromString(decoded_application_content) + + return merged_application + @staticmethod def attribute_list(data): attribute_list = List_pb2.AttributeList() From 0e28d7eb3aa7a30f0c648e069128c19e43d35622 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 13 Aug 2019 15:13:38 +0100 Subject: [PATCH 84/89] SDK-1098: Change Profile and Application profile to use old inheritance syntax when calling supers constructor --- yoti_python_sdk/__init__.py | 4 ++-- yoti_python_sdk/profile.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py index 3199ce22..8486f9a9 100644 --- a/yoti_python_sdk/__init__.py +++ b/yoti_python_sdk/__init__.py @@ -25,9 +25,9 @@ YOTI_API_URL = environ.get("YOTI_API_URL", DEFAULTS["YOTI_API_URL"]) YOTI_API_PORT = environ.get("YOTI_API_PORT", DEFAULTS["YOTI_API_PORT"]) YOTI_API_VERSION = environ.get("YOTI_API_VERSION", DEFAULTS["YOTI_API_VERSION"]) -YOTI_API_ENDPOINT = "{0}:{1}/api/{2}".format( +YOTI_API_ENDPOINT = environ.get("YOTI_API_ENDPOINT", "{0}:{1}/api/{2}".format( YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION -) +)) YOTI_API_VERIFY_SSL = environ.get("YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"]) if YOTI_API_VERIFY_SSL.lower() == "false": diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index 102fbde3..accc2364 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -57,7 +57,7 @@ def get_attribute(self, attribute_name): class Profile(BaseProfile): def __init__(self, profile_attributes): - super(Profile, self).__init__(profile_attributes) + BaseProfile.__init__(self, profile_attributes) self.ensure_postal_address() @property @@ -182,7 +182,7 @@ def ensure_postal_address(self): def ApplicationProfile(BaseProfile): def __init__(self, profile_attributes): - super(ApplicationProfile, self).__init__(profile_attributes) + BaseProfile.__init__(self, profile_attributes) @property def application_name(self): From ba1fa7109bde3d59091782d93ed3fe2a5eee8fa9 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 14 Aug 2019 11:48:58 +0100 Subject: [PATCH 85/89] SDK-1098: Update ApplicationProfile syntax and parse encrypted data into attribute list: --- yoti_python_sdk/activity_details.py | 5 ++--- yoti_python_sdk/client.py | 3 ++- yoti_python_sdk/profile.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/yoti_python_sdk/activity_details.py b/yoti_python_sdk/activity_details.py index bac495b1..ddef67f4 100644 --- a/yoti_python_sdk/activity_details.py +++ b/yoti_python_sdk/activity_details.py @@ -11,10 +11,10 @@ class ActivityDetails: def __init__(self, receipt, decrypted_profile=None, decrypted_application_profile=None): - self.decrypted_profile = decrypted_profile + self.decrypted_profile = decrypted_profile # TODO: This isn't used, can we remove it? self.user_profile = {} # will be removed in v3.0.0 self.base64_selfie_uri = None - self.application_profile = None + self.decrypted_application_profile = decrypted_application_profile if decrypted_profile and hasattr(decrypted_profile, "attributes"): decrypted_profile_attributes = decrypted_profile.attributes @@ -56,7 +56,6 @@ def __init__(self, receipt, decrypted_profile=None, decrypted_application_profil ) self.ensure_postal_address() - if decrypted_application_profile and hasattr(decrypted_application_profile, "attributes"): decrypted_application_profile_attributes = decrypted_application_profile.attributes diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index 9fce118b..83ea326c 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -92,7 +92,8 @@ def get_activity_details(self, encrypted_request_token): ) attribute_list = proto.attribute_list(decrypted_data) - return ActivityDetails(receipt, attribute_list, decrypted_application_profile=decrypted_application_data) + application_attribute_list = proto.attribute_list(decrypted_application_data) + return ActivityDetails(receipt, attribute_list, decrypted_application_profile=application_attribute_list) def perform_aml_check(self, aml_profile): if aml_profile is None: diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index accc2364..6d70adee 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -7,7 +7,7 @@ from yoti_python_sdk.image import Image from yoti_python_sdk import document_details -class BaseProfile: +class BaseProfile(object): def __init__(self, profile_attributes): self.attributes = {} @@ -57,7 +57,7 @@ def get_attribute(self, attribute_name): class Profile(BaseProfile): def __init__(self, profile_attributes): - BaseProfile.__init__(self, profile_attributes) + super(Profile, self).__init__(profile_attributes) self.ensure_postal_address() @property @@ -179,10 +179,10 @@ def ensure_postal_address(self): structured_postal_address.anchors, ) -def ApplicationProfile(BaseProfile): +class ApplicationProfile(BaseProfile): def __init__(self, profile_attributes): - BaseProfile.__init__(self, profile_attributes) + super(ApplicationProfile, self).__init__(profile_attributes) @property def application_name(self): From bcf8dd3edfb3084667f0fe06e4d5f06179c01de5 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 14 Aug 2019 15:07:32 +0100 Subject: [PATCH 86/89] SDK-1098: Update tests for checking ApplicationProfile functionality and rename some internal variables to make it more clear which profile data is which --- yoti_python_sdk/client.py | 12 +++-- yoti_python_sdk/profile.py | 16 ++++++- yoti_python_sdk/tests/test_profile.py | 67 ++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index 83ea326c..b5d7a6a3 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -84,16 +84,20 @@ def get_activity_details(self, encrypted_request_token): return ActivityDetails(receipt) unwrapped_key = self.__crypto.decrypt_token(receipt["wrapped_receipt_key"]) - decrypted_data = self.__crypto.decipher( + + decrypted_profile_data = self.__crypto.decipher( unwrapped_key, encrypted_data.iv, encrypted_data.cipher_text ) decrypted_application_data = self.__crypto.decipher( unwrapped_key, encrypted_application_profile.iv, encrypted_application_profile.cipher_text ) - attribute_list = proto.attribute_list(decrypted_data) - application_attribute_list = proto.attribute_list(decrypted_application_data) - return ActivityDetails(receipt, attribute_list, decrypted_application_profile=application_attribute_list) + user_profile_attribute_list = proto.attribute_list(decrypted_profile_data) + application_profile_attribute_list = proto.attribute_list(decrypted_application_data) + + return ActivityDetails( + receipt, user_profile_attribute_list, decrypted_application_profile=application_profile_attribute_list + ) def perform_aml_check(self, aml_profile): if aml_profile is None: diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index 6d70adee..4a10d498 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -7,6 +7,7 @@ from yoti_python_sdk.image import Image from yoti_python_sdk import document_details + class BaseProfile(object): def __init__(self, profile_attributes): @@ -45,7 +46,8 @@ def __init__(self, profile_attributes): ) def get_attribute(self, attribute_name): - """retrieves an attribute based on its name + """ + retrieves an attribute based on its name :param attribute_name: :return: Attribute """ @@ -194,12 +196,24 @@ def application_name(self): @property def application_url(self): + """ + application_url is the url of the application set in Yoti Hub + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_APPLICATION_URL) @property def application_logo(self): + """ + application_logo is the Image of the application logo set in Yoti Hub + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_APPLICATION_LOGO) @property def application_receipt_bg_color(self): + """ + application_receipt_bg_color is the background color of the application set in Yoti Hub + :return: Attribute(str) + """ return self.get_attribute(config.ATTRIBUTE_APPLICATION_RECEIPT_BGCOLOR) diff --git a/yoti_python_sdk/tests/test_profile.py b/yoti_python_sdk/tests/test_profile.py index 18573af1..78be7e59 100644 --- a/yoti_python_sdk/tests/test_profile.py +++ b/yoti_python_sdk/tests/test_profile.py @@ -7,10 +7,11 @@ from yoti_python_sdk import config from yoti_python_sdk.attribute import Attribute -from yoti_python_sdk.profile import Profile +from yoti_python_sdk.profile import Profile, ApplicationProfile from yoti_python_sdk.protobuf.protobuf import Protobuf from yoti_python_sdk.tests import attribute_fixture_parser, image_helper from yoti_python_sdk.tests.protobuf_attribute import ProtobufAttribute +from yoti_python_sdk.image import Image ADDRESS_FORMAT_KEY = "address_format" ADDRESS_FORMAT_VALUE = 1 @@ -92,6 +93,15 @@ def create_attribute_list_with_selfie_field(): ) +def create_attribute_list_with_application_logo(): + return create_single_attribute_list( + name=config.ATTRIBUTE_APPLICATION_LOGO, + value="base64(┛ಠ_ಠ)┛彡┻━┻", + anchors=None, + content_type=Protobuf.CT_JPEG, + ) + + def create_attribute_list_with_email_field(): return create_single_attribute_list( name=config.ATTRIBUTE_EMAIL_ADDRESS, @@ -587,3 +597,58 @@ def test_get_document_details_india(): assert document.issuing_country == INDIA_COUNTRY_ISO_VALUE assert document.document_number == IND_DRIVING_LICENCE_NUMBER assert document.expiration_date.isoformat() == EXPIRY_DATE + + +def test_create_application_profile_with_name(): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_APPLICATION_NAME, + value="yoti-sdk-test", + anchors=None, + content_type=Protobuf.CT_STRING + ) + + app_profile = ApplicationProfile(attribute_list) + + assert (app_profile.get_attribute(config.ATTRIBUTE_APPLICATION_NAME) == app_profile.application_name) + assert isinstance(app_profile, ApplicationProfile) + + +def test_create_application_profile_with_url(): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_APPLICATION_URL, + value="https://yoti.com", + anchors=None, + content_type=Protobuf.CT_STRING + ) + + app_profile = ApplicationProfile(attribute_list) + + assert (app_profile.get_attribute(config.ATTRIBUTE_APPLICATION_URL) == app_profile.application_url) + assert isinstance(app_profile, ApplicationProfile) + + +def test_create_application_profile_with_receipt_bgcolor(): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_APPLICATION_RECEIPT_BGCOLOR, + value="#FFFFFF", + anchors=None, + content_type=Protobuf.CT_STRING + ) + + app_profile = ApplicationProfile(attribute_list) + + assert (app_profile.get_attribute(config.ATTRIBUTE_APPLICATION_RECEIPT_BGCOLOR) == app_profile.application_receipt_bg_color) + assert isinstance(app_profile, ApplicationProfile) + + +def test_create_application_profile_with_logo(): + attribute_list = create_attribute_list_with_application_logo() + + app_profile = ApplicationProfile(attribute_list) + app_logo = app_profile.application_logo + + assert isinstance(app_logo.value, Image) + assert (app_profile.get_attribute(config.ATTRIBUTE_APPLICATION_LOGO) == app_profile.application_logo) + assert isinstance(app_profile, ApplicationProfile) + + From 216de918e13451483abb1b006df3da0a3f4cecad Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 14 Aug 2019 15:33:38 +0100 Subject: [PATCH 87/89] SDK-1098: Add comments to the helper methods in ApplicationProfile --- yoti_python_sdk/activity_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti_python_sdk/activity_details.py b/yoti_python_sdk/activity_details.py index ddef67f4..8359dab8 100644 --- a/yoti_python_sdk/activity_details.py +++ b/yoti_python_sdk/activity_details.py @@ -11,7 +11,7 @@ class ActivityDetails: def __init__(self, receipt, decrypted_profile=None, decrypted_application_profile=None): - self.decrypted_profile = decrypted_profile # TODO: This isn't used, can we remove it? + self.decrypted_profile = decrypted_profile self.user_profile = {} # will be removed in v3.0.0 self.base64_selfie_uri = None self.decrypted_application_profile = decrypted_application_profile From ea945643c7596212a8467e060e2cc5d20ba358b9 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 11:13:39 +0100 Subject: [PATCH 88/89] SDK-1098: Change client to create ActivityDetails with named parameters and update .gitignore --- .gitignore | 4 +++- yoti_python_sdk/client.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4391dcbf..538ee107 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,7 @@ htmlcov/ .coverage.* .cache nosetests.xml -coverage.xml +coverage.* *,cover .hypothesis/ @@ -102,3 +102,5 @@ examples/yoti_example_flask/static/YotiSelfie.jpg #.pem files for examples examples/yoti_example_django/*.pem examples/yoti_example_flask/*.pem + +.scannerwork \ No newline at end of file diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index b5d7a6a3..3d82bed5 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -96,7 +96,7 @@ def get_activity_details(self, encrypted_request_token): application_profile_attribute_list = proto.attribute_list(decrypted_application_data) return ActivityDetails( - receipt, user_profile_attribute_list, decrypted_application_profile=application_profile_attribute_list + receipt=receipt, decrypted_profile=user_profile_attribute_list, decrypted_application_profile=application_profile_attribute_list ) def perform_aml_check(self, aml_profile): From dee3c0504a34698400e043a5de5c3fddd47c3c76 Mon Sep 17 00:00:00 2001 From: Emma Smith <52929061+emmas-yoti@users.noreply.github.com> Date: Wed, 21 Aug 2019 10:54:52 +0100 Subject: [PATCH 89/89] Update version number to 2.8.1 --- yoti_python_sdk/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti_python_sdk/version.py b/yoti_python_sdk/version.py index 71bce917..40720561 100644 --- a/yoti_python_sdk/version.py +++ b/yoti_python_sdk/version.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "2.8.0" +__version__ = "2.8.1"