From e22741e47116eba4ccd081cffd8b10b9b3dce50f Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 13:53:49 +0100 Subject: [PATCH 01/74] SDK-1135: Change parameters to not initialise objects within default values --- yoti_python_sdk/anchor.py | 15 ++++++++++++--- yoti_python_sdk/attribute.py | 6 +++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/yoti_python_sdk/anchor.py b/yoti_python_sdk/anchor.py index fd1f18ea..924330ae 100644 --- a/yoti_python_sdk/anchor.py +++ b/yoti_python_sdk/anchor.py @@ -19,12 +19,21 @@ class Anchor: def __init__( self, - anchor_type=UNKNOWN_ANCHOR_TYPE, - sub_type="", - value="", + anchor_type=None, + sub_type=None, + value=None, signed_timestamp=None, origin_server_certs=None, ): + if sub_type is None: + sub_type = "" + + if value is None: + value = "" + + if anchor_type is None: + anchor_type = UNKNOWN_ANCHOR_TYPE + self.__anchor_type = anchor_type self.__sub_type = sub_type self.__value = value diff --git a/yoti_python_sdk/attribute.py b/yoti_python_sdk/attribute.py index c933154a..5a5a5bf7 100644 --- a/yoti_python_sdk/attribute.py +++ b/yoti_python_sdk/attribute.py @@ -2,7 +2,11 @@ class Attribute: - def __init__(self, name="", value="", anchors=None): + def __init__(self, name=None, value=None, anchors=None): + if name is None: + name = "" + if value is None: + value = "" if anchors is None: anchors = {} self.__name = name From 602a26862e95d50df31b8fb83e6722087e0e418e Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Wed, 21 Aug 2019 16:01:00 +0100 Subject: [PATCH 02/74] SDK-1123: Add Source Constraints to policy builders Also add optional and accept_self_asserted for wanted attributes --- .../dynamic_scenario_builder.py | 35 +++--- .../policy/dynamic_policy_builder.py | 117 +++++++++++------- .../policy/source_constraint_builder.py | 76 ++++++++++++ .../policy/wanted_anchor_builder.py | 33 +++++ .../policy/wanted_attribute_builder.py | 38 +++++- 5 files changed, 232 insertions(+), 67 deletions(-) create mode 100644 yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py create mode 100644 yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py diff --git a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py index 49400752..e181c3a9 100644 --- a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py @@ -10,35 +10,38 @@ def __init__(self): "policy": DynamicPolicyBuilder().build(), "extensions": [], "callback_endpoint": "", + "auto_allow": False, } - """ - @param policy A DynamicPolicy defining the attributes to be shared - """ - def with_policy(self, policy): + """ + :param policy: A DynamicPolicy defining the attributes to be shared + """ self.__scenario["policy"] = policy return self - """ - @param extension An extension to be activated for the scenario - """ - def with_extension(self, extension): + """ + :param extension: An extension to be activated for the scenario + """ self.__scenario["extensions"].append(extension) return self - """ - @param callback_endpoint A string with the callback endpoint - """ - def with_callback_endpoint(self, callback_endpoint): + """ + :param callback_endpoint: A string with the callback endpoint + """ self.__scenario["callback_endpoint"] = callback_endpoint return self - """ - @return Dictionary representation of dynamic scenario - """ + def with_auto_allow(self, value=True): + self.__scenario["auto_allow"] = value + return self def build(self): - return self.__scenario.copy() + """ + :returns: Dictionary representation of dynamic scenario + """ + scenario = self.__scenario.copy() + scenario["extensions"] = scenario["extensions"].copy() + return scenario 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 3c5a2974..bf03ad8a 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 @@ -7,12 +7,12 @@ from .wanted_attribute_builder import WantedAttributeBuilder -""" -Builder for DynamicPolicy -""" - class DynamicPolicyBuilder(object): + """ + Builder for DynamicPolicy + """ + SELFIE_AUTH_TYPE = 1 PIN_AUTH_TYPE = 2 @@ -21,11 +21,10 @@ def __init__(self): self.__wanted_auth_types = {} self.__is_wanted_remember_me = False - """ - @param wanted_attribute - """ - def with_wanted_attribute(self, wanted_attribute): + """ + @param wanted_attribute + """ key = ( wanted_attribute["derivation"] if wanted_attribute.get("derivation", False) @@ -34,68 +33,94 @@ def with_wanted_attribute(self, wanted_attribute): self.__wanted_attributes[key] = wanted_attribute return self - """ - @param wanted_name The name of the attribute to include - """ + def __attribute_keyword_parser(self, attributeBuilder, **kwargs): + constraints = kwargs.get("constraints", False) + if constraints: + attributeBuilder.with_constraint(constraints) - def with_wanted_attribute_by_name(self, wanted_name): - attribute = WantedAttributeBuilder().with_name(wanted_name).build() - return self.with_wanted_attribute(attribute) + def with_wanted_attribute_by_name(self, wanted_name, **kwargs): + """ + @param wanted_name The name of the attribute to include + """ + attributeBuilder = WantedAttributeBuilder().with_name(wanted_name) + self.__attribute_keyword_parser(attributeBuilder, **kwargs) - def with_family_name(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_FAMILY_NAME) + return self.with_wanted_attribute(attributeBuilder.build()) - def with_given_names(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_GIVEN_NAMES) + def with_family_name(self, **kwargs): + return self.with_wanted_attribute_by_name( + config.ATTRIBUTE_FAMILY_NAME, **kwargs + ) + + def with_given_names(self, **kwargs): + return self.with_wanted_attribute_by_name( + config.ATTRIBUTE_GIVEN_NAMES, **kwargs + ) - def with_full_name(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_FULL_NAME) + def with_full_name(self, **kwargs): + return self.with_wanted_attribute_by_name(config.ATTRIBUTE_FULL_NAME, **kwargs) - def with_date_of_birth(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_DATE_OF_BIRTH) + def with_date_of_birth(self, **kwargs): + return self.with_wanted_attribute_by_name( + config.ATTRIBUTE_DATE_OF_BIRTH, **kwargs + ) - def with_age_derived_attribute(self, derivation): - attribute = ( + def with_age_derived_attribute(self, derivation, **kwargs): + attributeBuilder = ( WantedAttributeBuilder() .with_name(config.ATTRIBUTE_DATE_OF_BIRTH) .with_derivation(derivation) - .build() ) - return self.with_wanted_attribute(attribute) + self.__attribute_keyword_parser(attributeBuilder, **kwargs) + return self.with_wanted_attribute(attributeBuilder.build()) - def with_age_over(self, age): + def with_age_over(self, age, **kwargs): assert self.__is_number(age) - return self.with_age_derived_attribute(config.ATTRIBUTE_AGE_OVER + str(age)) + return self.with_age_derived_attribute( + config.ATTRIBUTE_AGE_OVER + str(age), **kwargs + ) - def with_age_under(self, age): + def with_age_under(self, age, **kwargs): assert self.__is_number(age) - return self.with_age_derived_attribute(config.ATTRIBUTE_AGE_UNDER + str(age)) + return self.with_age_derived_attribute( + config.ATTRIBUTE_AGE_UNDER + str(age), **kwargs + ) - def with_gender(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_GENDER) + def with_gender(self, **kwargs): + return self.with_wanted_attribute_by_name(config.ATTRIBUTE_GENDER, **kwargs) - def with_postal_address(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_POSTAL_ADDRESS) + def with_postal_address(self, **kwargs): + return self.with_wanted_attribute_by_name( + config.ATTRIBUTE_POSTAL_ADDRESS, **kwargs + ) - def with_structured_postal_address(self): + def with_structured_postal_address(self, **kwargs): return self.with_wanted_attribute_by_name( - config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS + config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS, **kwargs ) - def with_nationality(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_NATIONALITY) + def with_nationality(self, **kwargs): + return self.with_wanted_attribute_by_name( + config.ATTRIBUTE_NATIONALITY, **kwargs + ) - def with_phone_number(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_PHONE_NUMBER) + def with_phone_number(self, **kwargs): + return self.with_wanted_attribute_by_name( + config.ATTRIBUTE_PHONE_NUMBER, **kwargs + ) - def with_selfie(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_SELFIE) + def with_selfie(self, **kwargs): + return self.with_wanted_attribute_by_name(config.ATTRIBUTE_SELFIE, **kwargs) - def with_email(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_EMAIL_ADDRESS) + def with_email(self, **kwargs): + return self.with_wanted_attribute_by_name( + config.ATTRIBUTE_EMAIL_ADDRESS, **kwargs + ) - def with_document_details(self): - return self.with_wanted_attribute_by_name(config.ATTRIBUTE_DOCUMENT_DETAILS) + def with_document_details(self, **kwargs): + return self.with_wanted_attribute_by_name( + config.ATTRIBUTE_DOCUMENT_DETAILS, **kwargs + ) """ @param wanted_auth_type diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py new file mode 100644 index 00000000..ac6f0a89 --- /dev/null +++ b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from wanted_anchor_builder import ( + WantedAnchorBuilder, + PASSPORT, + PASS_CARD, + NATIONAL_ID, + DRIVING_LICENCE, +) + + +class SourceConstraintBuilder(object): + def __init__(self): + self.__soft_preference = False + self.__anchors = [] + + def with_soft_preference(self, value=True): + """ + :param value: If true, this will be treated as a soft preference, otherwise + this will be treated as a hard requirement + """ + self.__soft_preference = value + return self + + def with_anchor(self, anchor): + """ + :param anchor: Add an anchor to the source constraint. You should prefer + use one of the other helper methods instead of this one. + """ + self.__anchors.append(anchor) + return self + + def with_anchor_by_name(self, value, subtype=""): + """ + :param value: The type of anchor wanted, represented by a string + :param subtype: The subtype information for the anchor as a string + """ + anchor = WantedAnchorBuilder().with_value(value).with_subtype(subtype).build() + return self.with_anchor(anchor) + + def with_passport(self, subtype=""): + """ + :param subtype: Subtype information as a string + """ + return self.with_anchor_by_name(PASSPORT, subtype) + + def with_national_id(self, subtype=""): + """ + :param subtype: Subtype information as a string + """ + return self.with_anchor_by_name(NATIONAL_ID, subtype) + + def with_passcard(self, subtype=""): + """ + :param subtype: Subtype information as a string + """ + return self.with_anchor_by_name(PASS_CARD, subtype) + + def with_driving_licence(self, subtype=""): + """ + :param subtype: Subtype information as a string + """ + return self.with_anchor_by_name(DRIVING_LICENCE, subtype) + + def build(self): + """ + :returns: A dict describing the source constraint + """ + return { + "type": "SOURCE", + "preferred_sources": { + "soft_preference": self.__soft_preference, + "anchors": self.__anchors, + }, + } diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py new file mode 100644 index 00000000..926c6351 --- /dev/null +++ b/yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +PASSPORT = "PASSPORT" +NATIONAL_ID = "NATIONAL_ID" +PASS_CARD = "PASS_CARD" +DRIVING_LICENCE = "DRIVING_LICENCE" + + +class WantedAnchorBuilder(object): + def __init__(self): + self.__name = "" + self.__subtype = "" + + def with_value(self, name): + """ + :param name: The type of anchor as a string + """ + self.__name = name + return self + + def with_subtype(self, subtype): + """ + :param subtype: Subtype information as a string + """ + self.__subtype = subtype + return self + + def build(self): + """ + :returns: A dict containing the anchor specification + """ + return {"name": self.__name, "sub_type": self.__subtype} 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 90d77165..a0876284 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 @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -""" -Builder for WantedAttribute -""" - class WantedAttributeBuilder(object): + """ + Builder for WantedAttribute + """ + def __init__(self): self.__attribute = {} + self.__constraints = [] def with_name(self, name): """ @@ -24,8 +25,35 @@ def with_derivation(self, derivation): self.__attribute["derivation"] = derivation return self + def with_self_asserted(self, value=True): + """ + :param value: True if self-asserted details are allowed + """ + self.__attribute["accept_self_asserted"] + return self + + def with_optional(self, value=True): + """ + :param value: True if this user attribute is optional in the returned receipt + """ + self.__attribute["optional"] = value + return self + + def with_constraint(self, constraint): + """ + :param constraint: Adds a constraint (e.g. a source constraint) to the + wanted attribute + """ + if isinstance(constraint, list): + self.__constraints.extend(constraint) + else: + self.__constraints.append(constraint) + return self + def build(self): """ :return: The wanted attribute object """ - return self.__attribute.copy() + attribute = self.__attribute.copy() + attribute["constraints"] = self.__constraints.copy() + return attribute From 57d9a09555042b9ea82157e2760a8a720e584c02 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Wed, 21 Aug 2019 16:39:19 +0100 Subject: [PATCH 03/74] SDK-1123: Add unit tests for source constraint features --- .../policy/source_constraint_builder.py | 2 +- .../policy/wanted_attribute_builder.py | 4 +- .../policy/test_dynamic_policy_builder.py | 9 ++++ .../policy/test_source_constraint_builder.py | 38 +++++++++++++++ .../policy/test_wanted_anchor_builder.py | 17 +++++++ .../policy/test_wanted_attribute_builder.py | 47 +++++++++++++++++++ .../test_dynamic_scenario_builder.py | 11 +++++ 7 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py create mode 100644 yoti_python_sdk/tests/dynamic_sharing_service/policy/test_wanted_anchor_builder.py diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py index ac6f0a89..e3d457c3 100644 --- a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from wanted_anchor_builder import ( +from .wanted_anchor_builder import ( WantedAnchorBuilder, PASSPORT, PASS_CARD, 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 a0876284..a6557fa4 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 @@ -25,11 +25,11 @@ def with_derivation(self, derivation): self.__attribute["derivation"] = derivation return self - def with_self_asserted(self, value=True): + def with_accept_self_asserted(self, value=True): """ :param value: True if self-asserted details are allowed """ - self.__attribute["accept_self_asserted"] + self.__attribute["accept_self_asserted"] = value return self def with_optional(self, value=True): 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 e53edbdc..d383962d 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 @@ -4,6 +4,9 @@ from yoti_python_sdk.dynamic_sharing_service.policy.wanted_attribute_builder import ( WantedAttributeBuilder, ) +from yoti_python_sdk.dynamic_sharing_service.policy.source_constraint_builder import ( + SourceConstraintBuilder, +) from yoti_python_sdk import config @@ -119,3 +122,9 @@ def test_auth_types_can_exist_only_once(): assert len(policy["wanted_auth_types"]) == 1 assert DynamicPolicyBuilder.SELFIE_AUTH_TYPE not in policy["wanted_auth_types"] assert DynamicPolicyBuilder.PIN_AUTH_TYPE in policy["wanted_auth_types"] + + +def test_attributes_with_constraints(): + constraint = SourceConstraintBuilder().with_national_id().build() + policy = DynamicPolicyBuilder().with_nationality(constraints=constraint).build() + assert len(policy["wanted"][0]["constraints"]) == 1 diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py new file mode 100644 index 00000000..c866f862 --- /dev/null +++ b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py @@ -0,0 +1,38 @@ +from yoti_python_sdk.dynamic_sharing_service.policy.source_constraint_builder import ( + SourceConstraintBuilder, +) +from yoti_python_sdk.dynamic_sharing_service.policy.wanted_anchor_builder import ( + DRIVING_LICENCE, + PASSPORT, +) + + +def test_build(): + constraint = SourceConstraintBuilder().build() + + assert constraint["type"] == "SOURCE" + assert not constraint["preferred_sources"]["soft_preference"] + assert constraint["preferred_sources"]["anchors"] == [] + + +def test_with_driving_licence(): + constraint = SourceConstraintBuilder().with_driving_licence().build() + + anchors = constraint["preferred_sources"]["anchors"] + assert len(anchors) == 1 + assert DRIVING_LICENCE in [a["name"] for a in anchors] + + +def test_with_soft_preference(): + constraint = ( + SourceConstraintBuilder() + .with_passport() + .with_driving_licence() + .with_soft_preference() + .build() + ) + anchors = constraint["preferred_sources"]["anchors"] + assert len(anchors) == 2 + assert DRIVING_LICENCE in [a["name"] for a in anchors] + assert PASSPORT in [a["name"] for a in anchors] + assert constraint["preferred_sources"]["soft_preference"] diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_wanted_anchor_builder.py b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_wanted_anchor_builder.py new file mode 100644 index 00000000..b30c4eef --- /dev/null +++ b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_wanted_anchor_builder.py @@ -0,0 +1,17 @@ +from yoti_python_sdk.dynamic_sharing_service.policy.wanted_anchor_builder import ( + WantedAnchorBuilder, +) + + +def test_build(): + TEST_VALUE = "TEST VALUE" + TEST_SUB_TYPE = "TEST SUB TYPE" + + builder = WantedAnchorBuilder() + builder.with_value(TEST_VALUE) + builder.with_subtype(TEST_SUB_TYPE) + + anchor = builder.build() + + assert anchor["name"] == TEST_VALUE + assert anchor["sub_type"] == TEST_SUB_TYPE 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 e061460f..787f5886 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 @@ -1,6 +1,9 @@ from yoti_python_sdk.dynamic_sharing_service.policy.wanted_attribute_builder import ( WantedAttributeBuilder, ) +from yoti_python_sdk.dynamic_sharing_service.policy.source_constraint_builder import ( + SourceConstraintBuilder, +) def test_build(): @@ -13,3 +16,47 @@ def test_build(): assert attribute["name"] == NAME assert attribute["derivation"] == DERIVATION + + +def test_with_constraint(): + constraint = SourceConstraintBuilder().with_driving_licence().build() + attribute = ( + WantedAttributeBuilder() + .with_name("test name") + .with_constraint(constraint) + .build() + ) + + constraints = attribute["constraints"] + assert len(constraints) == 1 + assert len(constraints[0]["preferred_sources"]["anchors"]) == 1 + + +def test_with_multiple_constraints(): + constraintA = SourceConstraintBuilder().with_driving_licence().build() + constraintB = SourceConstraintBuilder().with_passport().build() + + attribute = ( + WantedAttributeBuilder() + .with_name("test name") + .with_constraint([constraintA, constraintB]) + .build() + ) + + constraints = attribute["constraints"] + assert len(constraints) == 2 + + +def test_optional(): + attribute = WantedAttributeBuilder().with_name("test name").with_optional().build() + assert attribute["optional"] + + +def test_acccept_self_assert(): + attribute = ( + WantedAttributeBuilder() + .with_name("test name") + .with_accept_self_asserted() + .build() + ) + assert attribute["accept_self_asserted"] diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/test_dynamic_scenario_builder.py b/yoti_python_sdk/tests/dynamic_sharing_service/test_dynamic_scenario_builder.py index a0103d63..8ff3222d 100644 --- a/yoti_python_sdk/tests/dynamic_sharing_service/test_dynamic_scenario_builder.py +++ b/yoti_python_sdk/tests/dynamic_sharing_service/test_dynamic_scenario_builder.py @@ -31,3 +31,14 @@ def test_build_scenario(): assert EXTENSION1 in scenario["extensions"] assert EXTENSION2 in scenario["extensions"] assert scenario["callback_endpoint"] == CALLBACK_ENDPOINT + + +def test_auto_allow(): + scenario = ( + DynamicScenarioBuilder() + .with_policy(DynamicPolicyBuilder().with_full_name().build()) + .with_auto_allow() + .build() + ) + + assert scenario["auto_allow"] From 0d0aa1af1d855021c6500837fcd2194d36aca155 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Wed, 21 Aug 2019 17:10:58 +0100 Subject: [PATCH 04/74] SDK-1123: Add source-constraint page to example projects --- .../yoti_example_django/yoti_example/urls.py | 7 +++- .../yoti_example_django/yoti_example/views.py | 33 ++++++++++++++++++- examples/yoti_example_flask/app.py | 31 ++++++++++++++++- .../policy/__init__.py | 11 +++++-- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/examples/yoti_example_django/yoti_example/urls.py b/examples/yoti_example_django/yoti_example/urls.py index 4aa44902..f8063333 100644 --- a/examples/yoti_example_django/yoti_example/urls.py +++ b/examples/yoti_example_django/yoti_example/urls.py @@ -16,11 +16,16 @@ from django.conf.urls import url from django.contrib import admin -from .views import IndexView, AuthView, DynamicShareView +from .views import IndexView, AuthView, DynamicShareView, SourceConstraintsView urlpatterns = [ url(r"^$", IndexView.as_view(), name="index"), url(r"^yoti/auth/$", AuthView.as_view(), name="auth"), url(r"^admin/", admin.site.urls), url(r"^dynamic-share/$", DynamicShareView.as_view(), name="dynamic-share"), + url( + r"^source-constraint/$", + SourceConstraintsView.as_view(), + name="source-constraints", + ), ] diff --git a/examples/yoti_example_django/yoti_example/views.py b/examples/yoti_example_django/yoti_example/views.py index 7b908f77..d04cd880 100644 --- a/examples/yoti_example_django/yoti_example/views.py +++ b/examples/yoti_example_django/yoti_example/views.py @@ -7,7 +7,10 @@ DynamicScenarioBuilder, create_share_url, ) -from yoti_python_sdk.dynamic_sharing_service.policy import DynamicPolicyBuilder +from yoti_python_sdk.dynamic_sharing_service.policy import ( + DynamicPolicyBuilder, + SourceConstraintBuilder, +) load_dotenv(find_dotenv()) @@ -53,6 +56,34 @@ def get(self, request, *args, **kwargs): return self.render_to_response(context) +class SourceConstraintsView(TemplateView): + template_name = "dynamic-share.html" + + def get(self, request, *args, **kwargs): + client = Client(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH) + constraint = ( + SourceConstraintBuilder().with_driving_licence().with_passport().build() + ) + policy = ( + DynamicPolicyBuilder() + .with_full_name(constraints=constraint) + .with_structured_postal_address(constraints=constraint) + .build() + ) + scenario = ( + DynamicScenarioBuilder() + .with_policy(policy) + .with_callback_endpoint("/yoti/auth") + .build() + ) + share = create_share_url(client, scenario) + context = { + "yoti_client_sdk_id": YOTI_CLIENT_SDK_ID, + "yoti_share_url": share.share_url, + } + return self.render_to_response(context) + + class AuthView(TemplateView): template_name = "profile.html" diff --git a/examples/yoti_example_flask/app.py b/examples/yoti_example_flask/app.py index e6ade6c3..b4b0cde1 100644 --- a/examples/yoti_example_flask/app.py +++ b/examples/yoti_example_flask/app.py @@ -6,7 +6,10 @@ from flask import Flask, render_template, request from yoti_python_sdk import Client -from yoti_python_sdk.dynamic_sharing_service.policy import DynamicPolicyBuilder +from yoti_python_sdk.dynamic_sharing_service.policy import ( + DynamicPolicyBuilder, + SourceConstraintBuilder, +) from yoti_python_sdk.dynamic_sharing_service import DynamicScenarioBuilder from yoti_python_sdk.dynamic_sharing_service import create_share_url @@ -52,6 +55,32 @@ def dynamic_share(): ) +@app.route("/source-constraints") +def source_constraints(): + client = Client(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH) + constraint = ( + SourceConstraintBuilder().with_driving_licence().with_passport().build() + ) + policy = ( + DynamicPolicyBuilder() + .with_full_name(constraints=constraint) + .with_structured_postal_address(constraints=constraint) + .build() + ) + scenario = ( + DynamicScenarioBuilder() + .with_policy(policy) + .with_callback_endpoint("/yoti/auth") + .build() + ) + share = create_share_url(client, scenario) + return render_template( + "dynamic-share.html", + yoti_client_sdk_id=YOTI_CLIENT_SDK_ID, + yoti_share_url=share.share_url, + ) + + @app.route("/yoti/auth") def auth(): client = Client(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH) diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/__init__.py b/yoti_python_sdk/dynamic_sharing_service/policy/__init__.py index 17ebaaf9..2cbd92ad 100644 --- a/yoti_python_sdk/dynamic_sharing_service/policy/__init__.py +++ b/yoti_python_sdk/dynamic_sharing_service/policy/__init__.py @@ -1,6 +1,13 @@ # -*- coding: utf-8 -*- -from .dynamic_policy_builder import DynamicPolicyBuilder from .wanted_attribute_builder import WantedAttributeBuilder +from .wanted_anchor_builder import WantedAnchorBuilder +from .source_constraint_builder import SourceConstraintBuilder +from .dynamic_policy_builder import DynamicPolicyBuilder -__all__ = ["DynamicPolicyBuilder", "WantedAttributeBuilder"] +__all__ = [ + "DynamicPolicyBuilder", + "WantedAttributeBuilder", + "WantedAnchorBuilder", + "SourceConstraintBuilder", +] From 4c0f56cbe573a1d291bc71076d41d220afecf8a4 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Wed, 21 Aug 2019 17:37:30 +0100 Subject: [PATCH 05/74] SDK-1123: Use slice to copy instead of relying on list.copy() Python2 does not have list.copy, so use slicing instead to create a copy of lists --- .../dynamic_sharing_service/dynamic_scenario_builder.py | 2 +- .../dynamic_sharing_service/policy/wanted_attribute_builder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py index e181c3a9..e83f0abb 100644 --- a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py @@ -43,5 +43,5 @@ def build(self): :returns: Dictionary representation of dynamic scenario """ scenario = self.__scenario.copy() - scenario["extensions"] = scenario["extensions"].copy() + scenario["extensions"] = scenario["extensions"][:] return scenario 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 a6557fa4..3aa9e64e 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 @@ -55,5 +55,5 @@ def build(self): :return: The wanted attribute object """ attribute = self.__attribute.copy() - attribute["constraints"] = self.__constraints.copy() + attribute["constraints"] = self.__constraints[:] return attribute From c9c388278270608a6f691c6e009f9fafc7ea9f47 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Fri, 23 Aug 2019 12:32:05 +0100 Subject: [PATCH 06/74] SDK-1123: Remove auto_allow from dynamic scenario --- .../dynamic_scenario_builder.py | 5 ----- .../test_dynamic_scenario_builder.py | 11 ----------- 2 files changed, 16 deletions(-) diff --git a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py index e83f0abb..059eb210 100644 --- a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py @@ -10,7 +10,6 @@ def __init__(self): "policy": DynamicPolicyBuilder().build(), "extensions": [], "callback_endpoint": "", - "auto_allow": False, } def with_policy(self, policy): @@ -34,10 +33,6 @@ def with_callback_endpoint(self, callback_endpoint): self.__scenario["callback_endpoint"] = callback_endpoint return self - def with_auto_allow(self, value=True): - self.__scenario["auto_allow"] = value - return self - def build(self): """ :returns: Dictionary representation of dynamic scenario diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/test_dynamic_scenario_builder.py b/yoti_python_sdk/tests/dynamic_sharing_service/test_dynamic_scenario_builder.py index 8ff3222d..a0103d63 100644 --- a/yoti_python_sdk/tests/dynamic_sharing_service/test_dynamic_scenario_builder.py +++ b/yoti_python_sdk/tests/dynamic_sharing_service/test_dynamic_scenario_builder.py @@ -31,14 +31,3 @@ def test_build_scenario(): assert EXTENSION1 in scenario["extensions"] assert EXTENSION2 in scenario["extensions"] assert scenario["callback_endpoint"] == CALLBACK_ENDPOINT - - -def test_auto_allow(): - scenario = ( - DynamicScenarioBuilder() - .with_policy(DynamicPolicyBuilder().with_full_name().build()) - .with_auto_allow() - .build() - ) - - assert scenario["auto_allow"] From 4fd8bd93b95bf640c824696cedca2391f29c6350 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Fri, 23 Aug 2019 12:36:33 +0100 Subject: [PATCH 07/74] SDK-1123: Move Anchor names to config.py --- yoti_python_sdk/config.py | 4 ++++ .../policy/source_constraint_builder.py | 10 +++------- .../policy/wanted_anchor_builder.py | 5 ----- .../policy/test_source_constraint_builder.py | 5 +---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/yoti_python_sdk/config.py b/yoti_python_sdk/config.py index 06e430ad..541f39a3 100644 --- a/yoti_python_sdk/config.py +++ b/yoti_python_sdk/config.py @@ -30,3 +30,7 @@ X_YOTI_SDK = "X-Yoti-SDK" X_YOTI_SDK_VERSION = X_YOTI_SDK + "-Version" JSON_CONTENT_TYPE = "application/json" +PASSPORT = "PASSPORT" +NATIONAL_ID = "NATIONAL_ID" +PASS_CARD = "PASS_CARD" +DRIVING_LICENCE = "DRIVING_LICENCE" diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py index e3d457c3..84d4db78 100644 --- a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py @@ -1,13 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from .wanted_anchor_builder import ( - WantedAnchorBuilder, - PASSPORT, - PASS_CARD, - NATIONAL_ID, - DRIVING_LICENCE, -) +from yoti_python_sdk.config import PASSPORT, PASS_CARD, NATIONAL_ID, DRIVING_LICENCE + +from .wanted_anchor_builder import WantedAnchorBuilder class SourceConstraintBuilder(object): diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py index 926c6351..47c05c81 100644 --- a/yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py @@ -1,11 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -PASSPORT = "PASSPORT" -NATIONAL_ID = "NATIONAL_ID" -PASS_CARD = "PASS_CARD" -DRIVING_LICENCE = "DRIVING_LICENCE" - class WantedAnchorBuilder(object): def __init__(self): diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py index c866f862..bd59e33f 100644 --- a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py +++ b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py @@ -1,10 +1,7 @@ from yoti_python_sdk.dynamic_sharing_service.policy.source_constraint_builder import ( SourceConstraintBuilder, ) -from yoti_python_sdk.dynamic_sharing_service.policy.wanted_anchor_builder import ( - DRIVING_LICENCE, - PASSPORT, -) +from yoti_python_sdk.config import DRIVING_LICENCE, PASSPORT def test_build(): From 18b4f37847356aff28a67f8f4a25dca9475187a9 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Tue, 27 Aug 2019 16:12:50 +0100 Subject: [PATCH 08/74] SDK-1123: Remove optional from wanted attribute builder Feature not supported at this time --- .../policy/wanted_attribute_builder.py | 7 ------- .../policy/test_wanted_attribute_builder.py | 5 ----- 2 files changed, 12 deletions(-) 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 3aa9e64e..7cacf7f6 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 @@ -32,13 +32,6 @@ def with_accept_self_asserted(self, value=True): self.__attribute["accept_self_asserted"] = value return self - def with_optional(self, value=True): - """ - :param value: True if this user attribute is optional in the returned receipt - """ - self.__attribute["optional"] = value - return self - def with_constraint(self, constraint): """ :param constraint: Adds a constraint (e.g. a source constraint) to the 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 787f5886..1c273306 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 @@ -47,11 +47,6 @@ def test_with_multiple_constraints(): assert len(constraints) == 2 -def test_optional(): - attribute = WantedAttributeBuilder().with_name("test name").with_optional().build() - assert attribute["optional"] - - def test_acccept_self_assert(): attribute = ( WantedAttributeBuilder() From e199763592ec874336968d757f77b34c686ce97b Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Tue, 27 Aug 2019 16:19:01 +0100 Subject: [PATCH 09/74] SDK-1123: Rename anchor constants to ANCHOR_VALUE_X --- yoti_python_sdk/config.py | 8 ++++---- .../policy/source_constraint_builder.py | 19 ++++++++++++------- .../policy/test_source_constraint_builder.py | 8 ++++---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/yoti_python_sdk/config.py b/yoti_python_sdk/config.py index 541f39a3..3a254e11 100644 --- a/yoti_python_sdk/config.py +++ b/yoti_python_sdk/config.py @@ -30,7 +30,7 @@ X_YOTI_SDK = "X-Yoti-SDK" X_YOTI_SDK_VERSION = X_YOTI_SDK + "-Version" JSON_CONTENT_TYPE = "application/json" -PASSPORT = "PASSPORT" -NATIONAL_ID = "NATIONAL_ID" -PASS_CARD = "PASS_CARD" -DRIVING_LICENCE = "DRIVING_LICENCE" +ANCHOR_VALUE_PASSPORT = "PASSPORT" +ANCHOR_VALUE_NATIONAL_ID = "NATIONAL_ID" +ANCHOR_VALUE_PASS_CARD = "PASS_CARD" +ANCHOR_VALUE_DRIVING_LICENCE = "DRIVING_LICENCE" diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py index 84d4db78..e86367f1 100644 --- a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py @@ -1,7 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from yoti_python_sdk.config import PASSPORT, PASS_CARD, NATIONAL_ID, DRIVING_LICENCE +from yoti_python_sdk.config import ( + ANCHOR_VALUE_PASSPORT, + ANCHOR_VALUE_PASS_CARD, + ANCHOR_VALUE_NATIONAL_ID, + ANCHOR_VALUE_DRIVING_LICENCE, +) from .wanted_anchor_builder import WantedAnchorBuilder @@ -21,8 +26,8 @@ def with_soft_preference(self, value=True): def with_anchor(self, anchor): """ - :param anchor: Add an anchor to the source constraint. You should prefer - use one of the other helper methods instead of this one. + :param anchor: Add an anchor to the source constraint. + It is recommended to use the other helper methods isntead of this one """ self.__anchors.append(anchor) return self @@ -39,25 +44,25 @@ def with_passport(self, subtype=""): """ :param subtype: Subtype information as a string """ - return self.with_anchor_by_name(PASSPORT, subtype) + return self.with_anchor_by_name(ANCHOR_VALUE_PASSPORT, subtype) def with_national_id(self, subtype=""): """ :param subtype: Subtype information as a string """ - return self.with_anchor_by_name(NATIONAL_ID, subtype) + return self.with_anchor_by_name(ANCHOR_VALUE_NATIONAL_ID, subtype) def with_passcard(self, subtype=""): """ :param subtype: Subtype information as a string """ - return self.with_anchor_by_name(PASS_CARD, subtype) + return self.with_anchor_by_name(ANCHOR_VALUE_PASS_CARD, subtype) def with_driving_licence(self, subtype=""): """ :param subtype: Subtype information as a string """ - return self.with_anchor_by_name(DRIVING_LICENCE, subtype) + return self.with_anchor_by_name(ANCHOR_VALUE_DRIVING_LICENCE, subtype) def build(self): """ diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py index bd59e33f..689fc003 100644 --- a/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py +++ b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py @@ -1,7 +1,7 @@ from yoti_python_sdk.dynamic_sharing_service.policy.source_constraint_builder import ( SourceConstraintBuilder, ) -from yoti_python_sdk.config import DRIVING_LICENCE, PASSPORT +from yoti_python_sdk.config import ANCHOR_VALUE_DRIVING_LICENCE, ANCHOR_VALUE_PASSPORT def test_build(): @@ -17,7 +17,7 @@ def test_with_driving_licence(): anchors = constraint["preferred_sources"]["anchors"] assert len(anchors) == 1 - assert DRIVING_LICENCE in [a["name"] for a in anchors] + assert ANCHOR_VALUE_DRIVING_LICENCE in [a["name"] for a in anchors] def test_with_soft_preference(): @@ -30,6 +30,6 @@ def test_with_soft_preference(): ) anchors = constraint["preferred_sources"]["anchors"] assert len(anchors) == 2 - assert DRIVING_LICENCE in [a["name"] for a in anchors] - assert PASSPORT in [a["name"] for a in anchors] + assert ANCHOR_VALUE_DRIVING_LICENCE in [a["name"] for a in anchors] + assert ANCHOR_VALUE_PASSPORT in [a["name"] for a in anchors] assert constraint["preferred_sources"]["soft_preference"] From b7f87e3d733caed331a3f86928f5877bdfc78d48 Mon Sep 17 00:00:00 2001 From: Emma Smith Date: Tue, 27 Aug 2019 16:20:05 +0100 Subject: [PATCH 10/74] SDK-1123: Modify documentation for DynamicScenario.with_policy to be shared -> to be requested --- .../dynamic_sharing_service/dynamic_scenario_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py index 059eb210..854387da 100644 --- a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py @@ -14,7 +14,7 @@ def __init__(self): def with_policy(self, policy): """ - :param policy: A DynamicPolicy defining the attributes to be shared + :param policy: A DynamicPolicy defining the attributes to be requested """ self.__scenario["policy"] = policy return self From 8fad1c374d628006b2071cb522aff0ac2607e347 Mon Sep 17 00:00:00 2001 From: Emma Smith <52929061+emmas-yoti@users.noreply.github.com> Date: Thu, 29 Aug 2019 11:10:31 +0100 Subject: [PATCH 11/74] SDK-1123: Fix typo in source_constraint_builder.py Co-Authored-By: Ed Harrod --- .../dynamic_sharing_service/policy/source_constraint_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py index e86367f1..273afbfc 100644 --- a/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py @@ -27,7 +27,7 @@ def with_soft_preference(self, value=True): def with_anchor(self, anchor): """ :param anchor: Add an anchor to the source constraint. - It is recommended to use the other helper methods isntead of this one + It is recommended to use the other helper methods instead of this one """ self.__anchors.append(anchor) return self From befdf83a8a0b81d43265b5d56981099288f0502f Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 12:11:47 +0100 Subject: [PATCH 12/74] SDK-1164: Update README to include details of the application profile available on activity details --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 797b464a..5ebe3139 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,11 @@ Running the tests is done by the following process, ensuring you are using Pytho * [X] Address `postal_address` * [X] Gender `gender` * [X] Nationality `nationality` + * [X] Application Profile `application_profile` + * [X] Name `application_name` + * [X] URL `application_url` + * [X] Logo `application_logo` + * [X] Receipt Background Color `application_receipt_bg_color` * [X] Base64 Selfie URI `base64_selfie_uri` ## Support From d8f2cdc033086ec926dedb3ccbff22617980fcfa Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 14 Aug 2019 18:30:39 +0100 Subject: [PATCH 13/74] SDK-1132: Add code coverage and testing for Sonaqube --- .coveragerc | 4 +- .gitignore | 2 +- .pylintrc | 2 + pytest.ini | 4 +- sonar-project.properties | 9 +++ yoti_python_sdk/tests/conftest.py | 16 ++--- .../dynamic_sharing_service/test_share_url.py | 4 +- .../tests/test_activity_details.py | 70 +++++++++---------- yoti_python_sdk/tests/test_aml.py | 2 +- .../tests/test_attribute_parser.py | 14 ++-- yoti_python_sdk/tests/test_client.py | 18 ++--- 11 files changed, 79 insertions(+), 66 deletions(-) create mode 100644 .pylintrc create mode 100644 sonar-project.properties diff --git a/.coveragerc b/.coveragerc index 571f8bb9..bbd92e24 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,4 @@ [run] -omit=yoti/tests/* \ No newline at end of file +omit = + yoti_python_sdk/tests/** + examples/** \ No newline at end of file diff --git a/.gitignore b/.gitignore index 538ee107..e7fba6cc 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,4 @@ examples/yoti_example_flask/static/YotiSelfie.jpg examples/yoti_example_django/*.pem examples/yoti_example_flask/*.pem -.scannerwork \ No newline at end of file +.scannerwork diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..407cd4b1 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +[MASTER] +ignore=yoti_python_sdk/tests/**,examples/** \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index 7cf4cee5..8ca9e032 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -testpaths = yoti_python_sdk/tests -norecursedirs = venv +testpaths = yoti_python_sdk/tests/ +norecursedirs = venv \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..4b3ccdc7 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,9 @@ +sonar.projectKey = yoti-web-sdk:python +sonar.projectName = python-sdk +sonar.projectVersion = 2.8.0 +sonar.language = py +sonar.exclusions=yoti_python_sdk/tests/**,examples/** +sonar.src=yoti_python_sdk/** + +sonar.python.pylint.reportPath=coverage.out +sonar.python.coverage.reportPath=coverage.xml \ No newline at end of file diff --git a/yoti_python_sdk/tests/conftest.py b/yoti_python_sdk/tests/conftest.py index c7c87b77..e478886b 100644 --- a/yoti_python_sdk/tests/conftest.py +++ b/yoti_python_sdk/tests/conftest.py @@ -63,22 +63,22 @@ def timestamp(): @pytest.fixture(scope="module") -def successful_receipt(): +def successful_receipt(user_id, parent_remember_me_id, receipt_id, timestamp): return { - "remember_me_id": user_id(), - "parent_remember_me_id": parent_remember_me_id(), - "receipt_id": receipt_id(), - "timestamp": timestamp(), + "remember_me_id": user_id, + "parent_remember_me_id": parent_remember_me_id, + "receipt_id": receipt_id, + "timestamp": timestamp, "sharing_outcome": "SUCCESS", } @pytest.fixture(scope="module") -def failure_receipt(): +def failure_receipt(user_id, timestamp): return { - "remember_me_id": user_id(), + "remember_me_id": user_id, "sharing_outcome": "FAILURE", - "timestamp": timestamp(), + "timestamp": timestamp, } diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/test_share_url.py b/yoti_python_sdk/tests/dynamic_sharing_service/test_share_url.py index 11f6d3d4..36c63f01 100644 --- a/yoti_python_sdk/tests/dynamic_sharing_service/test_share_url.py +++ b/yoti_python_sdk/tests/dynamic_sharing_service/test_share_url.py @@ -48,7 +48,7 @@ def test_create_share_url_invalid_json(mock_uuid4, mock_time, mock_get): with pytest.raises(RuntimeError) as err: share_url.create_share_url(yoti_client, dynamic_scenario) - assert share_url.INVALID_DATA in str(err) + assert share_url.INVALID_DATA in str(err.value) @mock.patch( @@ -62,4 +62,4 @@ def test_create_share_url_app_not_found(mock_uuid4, mock_time, mock_get): with pytest.raises(RuntimeError) as err: share_url.create_share_url(yoti_client, dynamic_scenario) - assert share_url.APPLICATION_NOT_FOUND in str(err) + assert share_url.APPLICATION_NOT_FOUND in str(err.value) diff --git a/yoti_python_sdk/tests/test_activity_details.py b/yoti_python_sdk/tests/test_activity_details.py index 1337255f..1735b030 100644 --- a/yoti_python_sdk/tests/test_activity_details.py +++ b/yoti_python_sdk/tests/test_activity_details.py @@ -98,8 +98,8 @@ def create_structured_postal_address_field(activity_details, json_address_value) activity_details.field.content_type = Protobuf.CT_JSON -def test_try_parse_age_verified_both_missing_not_parsed(): - activity_details = ActivityDetails(successful_receipt()) +def test_try_parse_age_verified_both_missing_not_parsed(successful_receipt): + activity_details = ActivityDetails(successful_receipt) field = None ActivityDetails.try_parse_age_verified_field(activity_details, field) @@ -108,52 +108,52 @@ def test_try_parse_age_verified_both_missing_not_parsed(): ) -def test_failure_receipt_handled(): - activity_details = ActivityDetails(failure_receipt()) +def test_failure_receipt_handled(failure_receipt, user_id): + activity_details = ActivityDetails(failure_receipt) - assert activity_details.user_id == user_id() - assert activity_details.remember_me_id == user_id() + assert activity_details.user_id == user_id + assert activity_details.remember_me_id == user_id assert activity_details.outcome == "FAILURE" assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) -def test_missing_values_handled(): - activity_details = ActivityDetails(no_values_receipt()) +def test_missing_values_handled(no_values_receipt): + activity_details = ActivityDetails(no_values_receipt) assert isinstance(activity_details, ActivityDetails) -def test_remember_me_id_empty(): - activity_details = ActivityDetails(empty_strings()) +def test_remember_me_id_empty(empty_strings): + activity_details = ActivityDetails(empty_strings) assert activity_details.user_id == "" assert activity_details.remember_me_id == "" assert isinstance(activity_details, ActivityDetails) -def test_remember_me_id_valid(): - activity_details = ActivityDetails(successful_receipt()) +def test_remember_me_id_valid(successful_receipt, user_id): + activity_details = ActivityDetails(successful_receipt) - assert activity_details.user_id == user_id() - assert activity_details.remember_me_id == user_id() + assert activity_details.user_id == user_id + assert activity_details.remember_me_id == user_id -def test_parent_remember_me_id_empty(): - activity_details = ActivityDetails(empty_strings()) +def test_parent_remember_me_id_empty(empty_strings): + activity_details = ActivityDetails(empty_strings) assert activity_details.user_id == "" assert activity_details.remember_me_id == "" assert isinstance(activity_details, ActivityDetails) -def test_parent_remember_me_id_valid(): - activity_details = ActivityDetails(successful_receipt()) +def test_parent_remember_me_id_valid(successful_receipt, parent_remember_me_id): + activity_details = ActivityDetails(successful_receipt) - assert activity_details.parent_remember_me_id == parent_remember_me_id() + assert activity_details.parent_remember_me_id == parent_remember_me_id -def test_try_parse_age_verified_field_age_over(): - activity_details = ActivityDetails(successful_receipt()) +def test_try_parse_age_verified_field_age_over(successful_receipt): + activity_details = ActivityDetails(successful_receipt) create_age_verified_field(activity_details, True, "true".encode(), 18) ActivityDetails.try_parse_age_verified_field( @@ -162,8 +162,8 @@ def test_try_parse_age_verified_field_age_over(): assert activity_details.user_profile[config.KEY_AGE_VERIFIED] is True -def test_try_parse_age_verified_field_age_under(): - activity_details = ActivityDetails(successful_receipt()) +def test_try_parse_age_verified_field_age_under(successful_receipt): + activity_details = ActivityDetails(successful_receipt) create_age_verified_field(activity_details, False, "false".encode(), 55) ActivityDetails.try_parse_age_verified_field( @@ -172,8 +172,8 @@ def test_try_parse_age_verified_field_age_under(): assert activity_details.user_profile[config.KEY_AGE_VERIFIED] is False -def test_try_parse_age_verified_field_non_bool_value_not_parsed(): - activity_details = ActivityDetails(successful_receipt()) +def test_try_parse_age_verified_field_non_bool_value_not_parsed(successful_receipt): + activity_details = ActivityDetails(successful_receipt) create_age_verified_field(activity_details, True, "18".encode(), 18) sys.stdout = open(os.devnull, "w") # disable print ActivityDetails.try_parse_age_verified_field( @@ -185,8 +185,8 @@ def test_try_parse_age_verified_field_non_bool_value_not_parsed(): ) -def test_try_parse_structured_postal_address_uk(): - activity_details = ActivityDetails(successful_receipt()) +def test_try_parse_structured_postal_address_uk(successful_receipt): + activity_details = ActivityDetails(successful_receipt) structured_postal_address = { ADDRESS_FORMAT_KEY: ADDRESS_FORMAT_VALUE, BUILDING_NUMBER_KEY: BUILDING_NUMBER_VALUE, @@ -247,8 +247,8 @@ def test_try_parse_structured_postal_address_uk(): ) -def test_try_parse_structured_postal_address_india(): - activity_details = ActivityDetails(successful_receipt()) +def test_try_parse_structured_postal_address_india(successful_receipt): + activity_details = ActivityDetails(successful_receipt) structured_postal_address = { ADDRESS_FORMAT_KEY: INDIA_FORMAT_VALUE, CARE_OF_KEY: CARE_OF_VALUE, @@ -322,8 +322,8 @@ def test_try_parse_structured_postal_address_india(): ) -def test_try_parse_structured_postal_address_usa(): - activity_details = ActivityDetails(successful_receipt()) +def test_try_parse_structured_postal_address_usa(successful_receipt): + activity_details = ActivityDetails(successful_receipt) structured_postal_address = { ADDRESS_FORMAT_KEY: USA_FORMAT_VALUE, ADDRESS_LINE_1_KEY: ADDRESS_LINE_1_VALUE, @@ -383,13 +383,13 @@ def test_try_parse_structured_postal_address_usa(): ) -def test_try_parse_structured_postal_address_nested_json(): +def test_try_parse_structured_postal_address_nested_json(successful_receipt): formatted_address_json = { "item1": [[1, "a1"], [2, "a2"]], "item2": [[3, "b3"], [4, "b4"]], } - activity_details = ActivityDetails(successful_receipt()) + activity_details = ActivityDetails(successful_receipt) structured_postal_address = { ADDRESS_FORMAT_KEY: ADDRESS_FORMAT_VALUE, BUILDING_NUMBER_KEY: BUILDING_NUMBER_VALUE, @@ -451,8 +451,8 @@ def test_try_parse_structured_postal_address_nested_json(): ) -def test_set_address_to_be_formatted_address(): - activity_details = ActivityDetails(successful_receipt()) +def test_set_address_to_be_formatted_address(successful_receipt): + activity_details = ActivityDetails(successful_receipt) structured_postal_address = {config.KEY_FORMATTED_ADDRESS: FORMATTED_ADDRESS_VALUE} structured_postal_address_json = json.dumps(structured_postal_address) diff --git a/yoti_python_sdk/tests/test_aml.py b/yoti_python_sdk/tests/test_aml.py index 77cf1947..075cc948 100644 --- a/yoti_python_sdk/tests/test_aml.py +++ b/yoti_python_sdk/tests/test_aml.py @@ -20,7 +20,7 @@ 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" - assert expected_error in str(exc) + assert expected_error in str(exc.value) def test_getting_aml_result_with_missing_value(): diff --git a/yoti_python_sdk/tests/test_attribute_parser.py b/yoti_python_sdk/tests/test_attribute_parser.py index 071ac175..9caaab91 100644 --- a/yoti_python_sdk/tests/test_attribute_parser.py +++ b/yoti_python_sdk/tests/test_attribute_parser.py @@ -19,9 +19,9 @@ def proto(): @pytest.mark.parametrize( "content_type, expected_value", [ - (proto().CT_STRING, STRING_VALUE), - (proto().CT_DATE, STRING_VALUE), - (proto().CT_INT, INT_VALUE), + (protobuf.Protobuf.CT_STRING, STRING_VALUE), + (protobuf.Protobuf.CT_DATE, STRING_VALUE), + (protobuf.Protobuf.CT_INT, INT_VALUE), ], ) def test_attribute_parser_values_based_on_content_type(content_type, expected_value): @@ -48,13 +48,13 @@ def test_attribute_parser_values_based_on_other_content_types(proto): logger.propagate = True -def test_png_image_value_based_on_content_type(): - result = attribute_parser.value_based_on_content_type(BYTE_VALUE, proto().CT_PNG) +def test_png_image_value_based_on_content_type(proto): + result = attribute_parser.value_based_on_content_type(BYTE_VALUE, proto.CT_PNG) assert result.data == BYTE_VALUE assert result.content_type == "png" -def test_jpeg_image_value_based_on_content_type(): - result = attribute_parser.value_based_on_content_type(BYTE_VALUE, proto().CT_JPEG) +def test_jpeg_image_value_based_on_content_type(proto): + result = attribute_parser.value_based_on_content_type(BYTE_VALUE, proto.CT_JPEG) assert result.data == BYTE_VALUE assert result.content_type == "jpeg" diff --git a/yoti_python_sdk/tests/test_client.py b/yoti_python_sdk/tests/test_client.py index 8fd91c5b..72ef7c0f 100644 --- a/yoti_python_sdk/tests/test_client.py +++ b/yoti_python_sdk/tests/test_client.py @@ -119,8 +119,8 @@ 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) + assert expected_error in str(exc.value) + assert str(key_file) in str(exc.value) @pytest.mark.parametrize("key_file", INVALID_KEY_FILES) @@ -130,9 +130,9 @@ def test_creating_client_instance_with_invalid_key_file_env(key_file): 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) + assert expected_error in str(exc.value) + assert expected_error_source in str(exc.value) + assert str(key_file) in str(exc.value) def test_creating_client_instance_with_invalid_key_file_env_but_valid_key_file_arg(): @@ -145,8 +145,8 @@ def test_creating_client_instance_with_valid_key_file_env_but_invalid_key_file_a 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) + assert expected_error in str(exc.value) + assert str(INVALID_KEY_FILE_PATH) in str(exc.value) @mock.patch("requests.get", side_effect=mocked_requests_get) @@ -360,7 +360,7 @@ def test_perform_aml_check_with_null_profile(client): with pytest.raises(TypeError) as exc: client.perform_aml_check(aml_profile) expected_error = "aml_profile not set" - assert expected_error in str(exc) + assert expected_error in str(exc.value) @mock.patch("requests.post", side_effect=mocked_requests_post_aml_profile_not_found) @@ -378,4 +378,4 @@ def test_perform_aml_check_with_unsuccessful_call( with pytest.raises(RuntimeError) as exc: client.perform_aml_check(aml_profile) expected_error = "Unsuccessful Yoti API call:" - assert expected_error in str(exc) + assert expected_error in str(exc.value) From 40b2de89587c6b7a3e526ba5dfed0a6c767ad295 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 09:31:59 +0100 Subject: [PATCH 14/74] SDK-1132: Update pytest version and add pytest-cov and pylint as dependencies --- requirements.in | 4 +++- requirements.txt | 27 ++++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/requirements.in b/requirements.in index 6ec178ff..fcf4a3e5 100644 --- a/requirements.in +++ b/requirements.in @@ -7,7 +7,9 @@ mock==2.0.0 pbr==1.10.0 protobuf==3.7.0 pyopenssl==18.0.0 -pytest==3.3.2 +pytest==5.0.1 +pytest-cov==2.7.1 +pylint==2.3.1 pytz==2018.9 requests>=2.20.0 urllib3>=1.24.2 diff --git a/requirements.txt b/requirements.txt index f4c72ff6..a9ef5c9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,40 +7,53 @@ asn1==2.2.0 asn1crypto==0.24.0 # via cryptography aspy.yaml==1.3.0 # via pre-commit -attrs==18.1.0 # via pytest +astroid==2.2.5 # via pylint +atomicwrites==1.3.0 # via pytest +attrs==18.1.0 # via packaging, 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 +coverage==4.5.4 # via pytest-cov 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-metadata==0.18 # via pluggy, pre-commit, pytest importlib-resources==1.0.2 # via pre-commit +isort==4.3.21 # via pylint itsdangerous==0.24 +lazy-object-proxy==1.4.1 # via astroid +mccabe==0.6.1 # via pylint mock==2.0.0 +more-itertools==7.2.0 # via pytest nodeenv==1.3.3 # via pre-commit +packaging==19.1 # via pytest pbr==1.10.0 -pluggy==0.6.0 # via pytest +pluggy==0.12.0 # via pytest pre-commit==1.17.0 protobuf==3.7.0 py==1.5.3 # via pytest pycparser==2.18 # via cffi +pylint==2.3.1 pyopenssl==18.0.0 -pytest==3.3.2 +pyparsing==2.4.2 # via packaging +pytest-cov==2.7.1 +pytest==5.0.1 pytz==2018.9 pyyaml==5.1.1 # via aspy.yaml, pre-commit requests==2.21.0 -six==1.10.0 # via cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest +six==1.10.0 # via astroid, cfgv, cryptography, mock, packaging, pre-commit, protobuf, pyopenssl toml==0.10.0 # via pre-commit +typed-ast==1.4.0 # via astroid urllib3==1.24.2 virtualenv==15.2.0 +wcwidth==0.1.7 # via pytest wheel==0.24.0 -wrapt==1.11.2 # via deprecated +wrapt==1.11.2 # via astroid, deprecated zipp==0.5.2 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -# setuptools==41.1.0 # via protobuf, pytest +# setuptools==41.1.0 # via protobuf From ff0ccd8b329b36dd8e20d7424b8b6936e1b35d5d Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 09:40:09 +0100 Subject: [PATCH 15/74] SDK-1132: Update pytest version to try and make it compatible with older python versions --- requirements.in | 2 +- requirements.txt | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/requirements.in b/requirements.in index fcf4a3e5..a33531fe 100644 --- a/requirements.in +++ b/requirements.in @@ -7,7 +7,7 @@ mock==2.0.0 pbr==1.10.0 protobuf==3.7.0 pyopenssl==18.0.0 -pytest==5.0.1 +pytest==3.6 pytest-cov==2.7.1 pylint==2.3.1 pytz==2018.9 diff --git a/requirements.txt b/requirements.txt index a9ef5c9e..f5c0291b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ asn1crypto==0.24.0 # via cryptography aspy.yaml==1.3.0 # via pre-commit astroid==2.2.5 # via pylint atomicwrites==1.3.0 # via pytest -attrs==18.1.0 # via packaging, pytest +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 @@ -21,7 +21,7 @@ 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 pluggy, pre-commit, pytest +importlib-metadata==0.18 # via pre-commit importlib-resources==1.0.2 # via pre-commit isort==4.3.21 # via pylint itsdangerous==0.24 @@ -30,30 +30,27 @@ mccabe==0.6.1 # via pylint mock==2.0.0 more-itertools==7.2.0 # via pytest nodeenv==1.3.3 # via pre-commit -packaging==19.1 # via pytest pbr==1.10.0 -pluggy==0.12.0 # via pytest +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 pylint==2.3.1 pyopenssl==18.0.0 -pyparsing==2.4.2 # via packaging pytest-cov==2.7.1 -pytest==5.0.1 +pytest==3.6.0 pytz==2018.9 pyyaml==5.1.1 # via aspy.yaml, pre-commit requests==2.21.0 -six==1.10.0 # via astroid, cfgv, cryptography, mock, packaging, pre-commit, protobuf, pyopenssl +six==1.10.0 # via astroid, cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest toml==0.10.0 # via pre-commit typed-ast==1.4.0 # via astroid urllib3==1.24.2 virtualenv==15.2.0 -wcwidth==0.1.7 # via pytest wheel==0.24.0 wrapt==1.11.2 # via astroid, deprecated zipp==0.5.2 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -# setuptools==41.1.0 # via protobuf +# setuptools==41.1.0 # via protobuf, pytest From 7e894ad0e8afab37bd554b8f6dd9ffbf4787a199 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 09:53:11 +0100 Subject: [PATCH 16/74] SDK-826: Remove pytest-cov and pylint due to python 2.7 incompatability --- requirements.in | 4 +--- requirements.txt | 16 +++------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/requirements.in b/requirements.in index a33531fe..6ec178ff 100644 --- a/requirements.in +++ b/requirements.in @@ -7,9 +7,7 @@ mock==2.0.0 pbr==1.10.0 protobuf==3.7.0 pyopenssl==18.0.0 -pytest==3.6 -pytest-cov==2.7.1 -pylint==2.3.1 +pytest==3.3.2 pytz==2018.9 requests>=2.20.0 urllib3>=1.24.2 diff --git a/requirements.txt b/requirements.txt index f5c0291b..f4c72ff6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,15 +7,12 @@ asn1==2.2.0 asn1crypto==0.24.0 # via cryptography aspy.yaml==1.3.0 # via pre-commit -astroid==2.2.5 # via pylint -atomicwrites==1.3.0 # via pytest 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 -coverage==4.5.4 # via pytest-cov cryptography==2.4.1 deprecated==1.2.6 future==0.15.2 @@ -23,12 +20,8 @@ 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 -isort==4.3.21 # via pylint itsdangerous==0.24 -lazy-object-proxy==1.4.1 # via astroid -mccabe==0.6.1 # via pylint mock==2.0.0 -more-itertools==7.2.0 # via pytest nodeenv==1.3.3 # via pre-commit pbr==1.10.0 pluggy==0.6.0 # via pytest @@ -36,20 +29,17 @@ pre-commit==1.17.0 protobuf==3.7.0 py==1.5.3 # via pytest pycparser==2.18 # via cffi -pylint==2.3.1 pyopenssl==18.0.0 -pytest-cov==2.7.1 -pytest==3.6.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 astroid, cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest +six==1.10.0 # via cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest toml==0.10.0 # via pre-commit -typed-ast==1.4.0 # via astroid urllib3==1.24.2 virtualenv==15.2.0 wheel==0.24.0 -wrapt==1.11.2 # via astroid, deprecated +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: From af85db5009dd9c32c1ab09796aae99ea83aa2d61 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 14:51:09 +0100 Subject: [PATCH 17/74] SDK-1132: Update sonarqube properties file --- sonar-project.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 4b3ccdc7..889cb982 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,6 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 -sonar.language = py sonar.exclusions=yoti_python_sdk/tests/**,examples/** sonar.src=yoti_python_sdk/** From 7a7f0f58ca41b69a5cccf7379395c8730182cb66 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 15:01:31 +0100 Subject: [PATCH 18/74] SDK-1132: Further update sonar scanner properties file --- sonar-project.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sonar-project.properties b/sonar-project.properties index 889cb982..ec1d5465 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,8 +1,13 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 +sonar.language = py sonar.exclusions=yoti_python_sdk/tests/**,examples/** sonar.src=yoti_python_sdk/** +sonar.links.scm = https://github.com/getyoti/yoti-python-sdk + +sonar.tests = yoti_python_sdk/tests +sonar.test.inclusions = **/test_*.py sonar.python.pylint.reportPath=coverage.out sonar.python.coverage.reportPath=coverage.xml \ No newline at end of file From aacbd6e9744ab4be63f7a5d94da1706122ded61b Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 15:10:47 +0100 Subject: [PATCH 19/74] SDK-1132: Further update sonar scanner properties file --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index ec1d5465..2b6d1256 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -10,4 +10,4 @@ sonar.tests = yoti_python_sdk/tests sonar.test.inclusions = **/test_*.py sonar.python.pylint.reportPath=coverage.out -sonar.python.coverage.reportPath=coverage.xml \ No newline at end of file +sonar.python.coverage.reportPaths=coverage.xml \ No newline at end of file From 87393e145ae2bb7e5695487675650847721c00f1 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 15:28:45 +0100 Subject: [PATCH 20/74] SDK-826: Remove src options from sonarqube properties file --- sonar-project.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 2b6d1256..425623f1 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,6 @@ sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 sonar.language = py sonar.exclusions=yoti_python_sdk/tests/**,examples/** -sonar.src=yoti_python_sdk/** sonar.links.scm = https://github.com/getyoti/yoti-python-sdk sonar.tests = yoti_python_sdk/tests From 1b5858301ce645f0f1b3132b0f6a1552bf71c78e Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 15:59:46 +0100 Subject: [PATCH 21/74] SDK-1132: Update sonarqube properties file --- sonar-project.properties | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 425623f1..2a1acd23 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,10 +3,6 @@ sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 sonar.language = py sonar.exclusions=yoti_python_sdk/tests/**,examples/** -sonar.links.scm = https://github.com/getyoti/yoti-python-sdk - -sonar.tests = yoti_python_sdk/tests -sonar.test.inclusions = **/test_*.py sonar.python.pylint.reportPath=coverage.out sonar.python.coverage.reportPaths=coverage.xml \ No newline at end of file From 38c219a1a0bf0d6bd5e175c56125199787fa09b2 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 16:34:18 +0100 Subject: [PATCH 22/74] SDK-1132: Further update sonarqube properties file --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 2a1acd23..67a70e7c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 -sonar.language = py +sonar.src = yoti_python_sdk sonar.exclusions=yoti_python_sdk/tests/**,examples/** sonar.python.pylint.reportPath=coverage.out From fe868f74da09388e342f70e86d45cc301c3f35a5 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 16:51:41 +0100 Subject: [PATCH 23/74] SDK-1132: Further update sonarqube properties file --- sonar-project.properties | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 67a70e7c..3b99b387 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,8 +1,9 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 -sonar.src = yoti_python_sdk -sonar.exclusions=yoti_python_sdk/tests/**,examples/** +sonar.src = ./yoti_python_sdk +sonar.exclusions=./yoti_python_sdk/tests/**,./examples/** -sonar.python.pylint.reportPath=coverage.out -sonar.python.coverage.reportPaths=coverage.xml \ No newline at end of file +sonar.python.pylint.reportPath=./coverage.out +sonar.python.coverage.reportPaths=./coverage.xml +sonar.verbose = true \ No newline at end of file From d829d3980a5538c583a2746b8f2eb77c2f0970ff Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 17:11:53 +0100 Subject: [PATCH 24/74] SDK-1132: Further update sonarqube properties file --- sonar-project.properties | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 3b99b387..118bb095 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,9 +1,9 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 -sonar.src = ./yoti_python_sdk -sonar.exclusions=./yoti_python_sdk/tests/**,./examples/** +sonar.exclusions=yoti_python_sdk/tests/**,examples/** +sonar.src=yoti_python_sdk -sonar.python.pylint.reportPath=./coverage.out -sonar.python.coverage.reportPaths=./coverage.xml +sonar.python.pylint.reportPath=coverage.out +sonar.python.coverage.reportPath=coverage.xml sonar.verbose = true \ No newline at end of file From 2acfa5f840c6f7d0594f85a59e628f7e570195ab Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 16 Aug 2019 10:10:09 +0100 Subject: [PATCH 25/74] SDK-1132: Update sonarqube properties file --- sonar-project.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 118bb095..3724986b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,5 +5,4 @@ sonar.exclusions=yoti_python_sdk/tests/**,examples/** sonar.src=yoti_python_sdk sonar.python.pylint.reportPath=coverage.out -sonar.python.coverage.reportPath=coverage.xml sonar.verbose = true \ No newline at end of file From 5b2ebfd8c00c041c72bf5b2065507915e1b1a4b2 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 16 Aug 2019 10:52:10 +0100 Subject: [PATCH 26/74] SDK-1132: Update sonar.sources param --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 3724986b..22662f10 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,7 +2,7 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 sonar.exclusions=yoti_python_sdk/tests/**,examples/** -sonar.src=yoti_python_sdk +sonar.sources=./yoti_python_sdk sonar.python.pylint.reportPath=coverage.out sonar.verbose = true \ No newline at end of file From f1d21dc067a0a0f5a751c47e0ca2cec2d7a2b5f5 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 16 Aug 2019 12:23:50 +0100 Subject: [PATCH 27/74] SDK-1132: Update .gitignore and sonarqube properties file --- .gitignore | 1 + sonar-project.properties | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e7fba6cc..8a197872 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ htmlcov/ .cache nosetests.xml coverage.* +coverage-reports *,cover .hypothesis/ diff --git a/sonar-project.properties b/sonar-project.properties index 22662f10..0f1a29b6 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,7 +2,6 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk sonar.projectVersion = 2.8.0 sonar.exclusions=yoti_python_sdk/tests/**,examples/** -sonar.sources=./yoti_python_sdk sonar.python.pylint.reportPath=coverage.out sonar.verbose = true \ No newline at end of file From a2ecbeff87421125cd63712ce001b255d4eb78f4 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Mon, 2 Sep 2019 12:29:19 +0100 Subject: [PATCH 28/74] SDK-1132: Update sonaqube properties file to point to latest version of SDK --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 0f1a29b6..a8c26a58 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk -sonar.projectVersion = 2.8.0 +sonar.projectVersion = 2.8.2 sonar.exclusions=yoti_python_sdk/tests/**,examples/** sonar.python.pylint.reportPath=coverage.out From 58ec10a809f575ae025a16fd05c51778cd5550fd Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 22 Aug 2019 16:18:15 +0100 Subject: [PATCH 29/74] SDK-573: Implement AgeVerification class which supports methods for getting age verifications by number --- yoti_python_sdk/age_verification.py | 27 +++++++++++++++++ yoti_python_sdk/profile.py | 29 ++++++++++++++++--- .../tests/test_age_verification.py | 0 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 yoti_python_sdk/age_verification.py create mode 100644 yoti_python_sdk/tests/test_age_verification.py diff --git a/yoti_python_sdk/age_verification.py b/yoti_python_sdk/age_verification.py new file mode 100644 index 00000000..d36952d9 --- /dev/null +++ b/yoti_python_sdk/age_verification.py @@ -0,0 +1,27 @@ +from yoti_python_sdk.attribute import Attribute + + +class AgeVerification(object): + def __init__(self, derived_attribute: Attribute): + self.__derived_attribute = derived_attribute + + split = derived_attribute.name.split(":") + self.__check_type = split[0] + self.__age_verified = int(split[1]) + self.__result = bool(derived_attribute.value) + + @property + def age(self): + return self.__age_verified + + @property + def check_type(self): + return self.__check_type + + @property + def result(self): + return self.__result + + @property + def attribute(self): + return self.__derived_attribute diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index 4a10d498..ddcd6ca8 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- import logging -from yoti_python_sdk import attribute_parser, config, multivalue +from yoti_python_sdk import attribute_parser, config, multivalue, document_details 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 +from yoti_python_sdk.age_verification import AgeVerification class BaseProfile(object): - def __init__(self, profile_attributes): self.attributes = {} + self.verifications = None if profile_attributes: for field in profile_attributes: @@ -161,6 +161,27 @@ def document_images(self): def document_details(self): return self.get_attribute(config.ATTRIBUTE_DOCUMENT_DETAILS) + def get_age_verifications(self): + self.__find_all_age_verifications() + return [self.verifications[key] for key in self.verifications.keys()] + + def find_age_over_verification(self, age): + self.__find_all_age_verifications() + return self.verifications[config.ATTRIBUTE_AGE_OVER + str(age)] + + def find_age_under_verification(self, age): + self.__find_all_age_verifications() + return self.verifications[config.ATTRIBUTE_AGE_UNDER + str(age)] + + def __find_all_age_verifications(self): + if self.verifications is None: + self.verifications = {} + for attribute in self.attributes: + if ( + config.ATTRIBUTE_AGE_OVER in attribute.name + or config.ATTRIBUTE_AGE_UNDER in attribute.name + ): + self.verifications[attribute.name] = AgeVerification(attribute) def ensure_postal_address(self): if ( @@ -185,7 +206,7 @@ def ensure_postal_address(self): class ApplicationProfile(BaseProfile): def __init__(self, profile_attributes): super(ApplicationProfile, self).__init__(profile_attributes) - + @property def application_name(self): """ diff --git a/yoti_python_sdk/tests/test_age_verification.py b/yoti_python_sdk/tests/test_age_verification.py new file mode 100644 index 00000000..e69de29b From 65279baae20f53d4db908634e4d6da309afa6c20 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 22 Aug 2019 17:13:04 +0100 Subject: [PATCH 30/74] SDK-573: Fix syntax for Python 2.7 support and add some unit tests for age verification --- yoti_python_sdk/age_verification.py | 6 +++- .../tests/test_age_verification.py | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/yoti_python_sdk/age_verification.py b/yoti_python_sdk/age_verification.py index d36952d9..45c927e1 100644 --- a/yoti_python_sdk/age_verification.py +++ b/yoti_python_sdk/age_verification.py @@ -8,7 +8,11 @@ def __init__(self, derived_attribute: Attribute): split = derived_attribute.name.split(":") self.__check_type = split[0] self.__age_verified = int(split[1]) - self.__result = bool(derived_attribute.value) + + if derived_attribute.value == "true": + self.__result = True + elif derived_attribute.value == "false": + self.__result = False @property def age(self): diff --git a/yoti_python_sdk/tests/test_age_verification.py b/yoti_python_sdk/tests/test_age_verification.py index e69de29b..f638208f 100644 --- a/yoti_python_sdk/tests/test_age_verification.py +++ b/yoti_python_sdk/tests/test_age_verification.py @@ -0,0 +1,30 @@ +from yoti_python_sdk.attribute import Attribute +from yoti_python_sdk.age_verification import AgeVerification +from yoti_python_sdk import config +import pytest + + +@pytest.fixture(scope="module") +def age_over_attribute(): + return Attribute(config.ATTRIBUTE_AGE_OVER + "18", "true", None) + + +@pytest.fixture(scope="module") +def age_under_attribute(): + return Attribute(config.ATTRIBUTE_AGE_UNDER + "18", "false", None) + + +def test_create_age_verification_from_age_over_attribute(age_over_attribute): + age_verification = AgeVerification(age_over_attribute) + + assert age_verification.age == 18 + assert age_verification.check_type in config.ATTRIBUTE_AGE_OVER + assert age_verification.result is True + + +def test_create_age_verification_from_age_under_attribute(age_under_attribute): + age_verification = AgeVerification(age_under_attribute) + + assert age_verification.age == 18 + assert age_verification.check_type in config.ATTRIBUTE_AGE_UNDER + assert age_verification.result is False From 3d51d78f58a2e05c652e6739e792fafe9ac3e3e0 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 22 Aug 2019 17:15:45 +0100 Subject: [PATCH 31/74] SDK-573: Fix syntax for Python 2.7 support and add some unit tests for age verification --- yoti_python_sdk/age_verification.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/yoti_python_sdk/age_verification.py b/yoti_python_sdk/age_verification.py index 45c927e1..3bec5de5 100644 --- a/yoti_python_sdk/age_verification.py +++ b/yoti_python_sdk/age_verification.py @@ -1,8 +1,5 @@ -from yoti_python_sdk.attribute import Attribute - - class AgeVerification(object): - def __init__(self, derived_attribute: Attribute): + def __init__(self, derived_attribute): self.__derived_attribute = derived_attribute split = derived_attribute.name.split(":") From 1251c0efd1841866d535ef7d2d9388150853ff7c Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 22 Aug 2019 18:25:01 +0100 Subject: [PATCH 32/74] SDk-573: Add custom exception when a malformed age verification attribute is parsed: --- yoti_python_sdk/age_verification.py | 16 +++++++++++++++- yoti_python_sdk/exceptions.py | 2 ++ yoti_python_sdk/tests/test_age_verification.py | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 yoti_python_sdk/exceptions.py diff --git a/yoti_python_sdk/age_verification.py b/yoti_python_sdk/age_verification.py index 3bec5de5..96cd0fad 100644 --- a/yoti_python_sdk/age_verification.py +++ b/yoti_python_sdk/age_verification.py @@ -1,9 +1,23 @@ +from yoti_python_sdk.exceptions import MalformedAgeVerificationException +from yoti_python_sdk import config + + class AgeVerification(object): def __init__(self, derived_attribute): self.__derived_attribute = derived_attribute split = derived_attribute.name.split(":") - self.__check_type = split[0] + if len(split) != 2: + raise MalformedAgeVerificationException + + if ( + split[0] in config.ATTRIBUTE_AGE_OVER + or split[0] in config.ATTRIBUTE_AGE_UNDER + ): + self.__check_type = split[0] + else: + raise MalformedAgeVerificationException + self.__age_verified = int(split[1]) if derived_attribute.value == "true": diff --git a/yoti_python_sdk/exceptions.py b/yoti_python_sdk/exceptions.py new file mode 100644 index 00000000..e445e88b --- /dev/null +++ b/yoti_python_sdk/exceptions.py @@ -0,0 +1,2 @@ +class MalformedAgeVerificationException(Exception): + pass diff --git a/yoti_python_sdk/tests/test_age_verification.py b/yoti_python_sdk/tests/test_age_verification.py index f638208f..3f35a0c3 100644 --- a/yoti_python_sdk/tests/test_age_verification.py +++ b/yoti_python_sdk/tests/test_age_verification.py @@ -14,6 +14,16 @@ def age_under_attribute(): return Attribute(config.ATTRIBUTE_AGE_UNDER + "18", "false", None) +@pytest.mark.parametrize( + "age_verification_name", + [":age_over:18", "age_over:18:", "ageover:18", "age_over:", "age_over::18"], +) +def test_malformed_age_verification_attributes(age_verification_name): + with pytest.raises(Exception): + attribute = Attribute(age_verification_name, "true", None) + age_verification_name = AgeVerification(attribute) + + def test_create_age_verification_from_age_over_attribute(age_over_attribute): age_verification = AgeVerification(age_over_attribute) From ff6a5f0f27a5761b4bc51e20341519b79fa53947 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 10:31:03 +0100 Subject: [PATCH 33/74] SDK-573: Add more validation and error handling to AgeVerification class --- yoti_python_sdk/age_verification.py | 14 ++++++++------ yoti_python_sdk/tests/test_age_verification.py | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/yoti_python_sdk/age_verification.py b/yoti_python_sdk/age_verification.py index 96cd0fad..e181eb34 100644 --- a/yoti_python_sdk/age_verification.py +++ b/yoti_python_sdk/age_verification.py @@ -18,12 +18,14 @@ def __init__(self, derived_attribute): else: raise MalformedAgeVerificationException - self.__age_verified = int(split[1]) - - if derived_attribute.value == "true": - self.__result = True - elif derived_attribute.value == "false": - self.__result = False + try: + self.__age_verified = int(split[1]) + if derived_attribute.value == "true": + self.__result = True + elif derived_attribute.value == "false": + self.__result = False + except Exception: + raise MalformedAgeVerificationException @property def age(self): diff --git a/yoti_python_sdk/tests/test_age_verification.py b/yoti_python_sdk/tests/test_age_verification.py index 3f35a0c3..020205c3 100644 --- a/yoti_python_sdk/tests/test_age_verification.py +++ b/yoti_python_sdk/tests/test_age_verification.py @@ -1,5 +1,6 @@ from yoti_python_sdk.attribute import Attribute from yoti_python_sdk.age_verification import AgeVerification +from yoti_python_sdk.exceptions import MalformedAgeVerificationException from yoti_python_sdk import config import pytest @@ -19,7 +20,7 @@ def age_under_attribute(): [":age_over:18", "age_over:18:", "ageover:18", "age_over:", "age_over::18"], ) def test_malformed_age_verification_attributes(age_verification_name): - with pytest.raises(Exception): + with pytest.raises(MalformedAgeVerificationException): attribute = Attribute(age_verification_name, "true", None) age_verification_name = AgeVerification(attribute) From c5f49ff3e62f8f74069196ada08c5f594a6c5acd Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 10:47:43 +0100 Subject: [PATCH 34/74] SDK-573: Deprecate try_parse_age_verified_field method on ActivityDetails class --- yoti_python_sdk/activity_details.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/yoti_python_sdk/activity_details.py b/yoti_python_sdk/activity_details.py index 8359dab8..b027bf70 100644 --- a/yoti_python_sdk/activity_details.py +++ b/yoti_python_sdk/activity_details.py @@ -10,7 +10,9 @@ class ActivityDetails: - def __init__(self, receipt, decrypted_profile=None, decrypted_application_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 @@ -57,9 +59,15 @@ 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 - self.application_profile = ApplicationProfile(decrypted_application_profile_attributes) + 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") @@ -84,6 +92,7 @@ def user_id(self): def user_id(self, value): self.__remember_me_id = value + @deprecated def try_parse_age_verified_field(self, field): if field is not None: age_verified = attribute_parser.value_based_on_content_type( From 68299ff9174dd0be75480900df1e98a67840ee62 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 14:32:49 +0100 Subject: [PATCH 35/74] SDK-573: Add tests for helper methods in ActivityDetails for getting age verifications --- yoti_python_sdk/tests/test_profile.py | 93 ++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/yoti_python_sdk/tests/test_profile.py b/yoti_python_sdk/tests/test_profile.py index 78be7e59..8e59d367 100644 --- a/yoti_python_sdk/tests/test_profile.py +++ b/yoti_python_sdk/tests/test_profile.py @@ -8,6 +8,7 @@ from yoti_python_sdk import config from yoti_python_sdk.attribute import Attribute from yoti_python_sdk.profile import Profile, ApplicationProfile +from yoti_python_sdk.age_verification import AgeVerification 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 @@ -602,42 +603,51 @@ def test_get_document_details_india(): def test_create_application_profile_with_name(): attribute_list = create_single_attribute_list( name=config.ATTRIBUTE_APPLICATION_NAME, - value="yoti-sdk-test", + value="yoti-sdk-test".encode(), anchors=None, - content_type=Protobuf.CT_STRING + content_type=Protobuf.CT_STRING, ) app_profile = ApplicationProfile(attribute_list) - assert (app_profile.get_attribute(config.ATTRIBUTE_APPLICATION_NAME) == app_profile.application_name) + 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", + value="https://yoti.com".encode(), anchors=None, - content_type=Protobuf.CT_STRING + content_type=Protobuf.CT_STRING, ) app_profile = ApplicationProfile(attribute_list) - assert (app_profile.get_attribute(config.ATTRIBUTE_APPLICATION_URL) == app_profile.application_url) + 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", + value="#FFFFFF".encode(), anchors=None, - content_type=Protobuf.CT_STRING + 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 ( + app_profile.get_attribute(config.ATTRIBUTE_APPLICATION_RECEIPT_BGCOLOR) + == app_profile.application_receipt_bg_color + ) assert isinstance(app_profile, ApplicationProfile) @@ -648,7 +658,70 @@ def test_create_application_profile_with_logo(): 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 ( + app_profile.get_attribute(config.ATTRIBUTE_APPLICATION_LOGO) + == app_profile.application_logo + ) assert isinstance(app_profile, ApplicationProfile) +@pytest.mark.parametrize( + "attribute_value,expected_age_over,expected_value", + [("true", 18, True), ("true", 21, True), ("false", 18, False)], +) +def test_get_age_over_verification(attribute_value, expected_age_over, expected_value): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_AGE_OVER + str(expected_age_over), + value=attribute_value.encode(), + anchors=None, + content_type=Protobuf.CT_STRING, + ) + + human_profile = Profile(attribute_list) + print(human_profile.attributes) + + age_verifications = human_profile.get_age_verifications() + age_verification = human_profile.find_age_over_verification(expected_age_over) + + assert len(age_verifications) == 1 + assert isinstance(age_verification, AgeVerification) + assert age_verification.result is expected_value + + +@pytest.mark.parametrize( + "attribute_value,expected_age_under,expected_value", + [("true", 18, True), ("true", 21, True), ("false", 18, False)], +) +def test_get_age_under_verification( + attribute_value, expected_age_under, expected_value +): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_AGE_UNDER + str(expected_age_under), + value=attribute_value.encode(), + anchors=None, + content_type=Protobuf.CT_STRING, + ) + + human_profile = Profile(attribute_list) + print(human_profile.attributes) + + age_verifications = human_profile.get_age_verifications() + age_verification = human_profile.find_age_under_verification(expected_age_under) + + assert len(age_verifications) == 1 + assert isinstance(age_verification, AgeVerification) + assert age_verification.result is expected_value + + +def test_get_age_verifications(): + attribute_list = create_single_attribute_list( + name=config.ATTRIBUTE_AGE_UNDER + str(18), + value="true".encode(), + anchors=None, + content_type=Protobuf.CT_STRING, + ) + + human_profile = Profile(attribute_list) + age_verifications = human_profile.get_age_verifications() + + assert len(age_verifications) == 1 From 26685435a29f994e122e1cefb75e356c0629ab97 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 14:33:34 +0100 Subject: [PATCH 36/74] SDK-573: Fix __find_all_age_verifications which was looping through the keys rather than attribute objects --- yoti_python_sdk/profile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index ddcd6ca8..b541a67e 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -176,7 +176,8 @@ def find_age_under_verification(self, age): def __find_all_age_verifications(self): if self.verifications is None: self.verifications = {} - for attribute in self.attributes: + for key in self.attributes: + attribute = self.attributes[key] if ( config.ATTRIBUTE_AGE_OVER in attribute.name or config.ATTRIBUTE_AGE_UNDER in attribute.name From 54fdf6a42a30b0ddaf8efd6c709e1e18b737cb51 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Mon, 2 Sep 2019 12:10:54 +0100 Subject: [PATCH 37/74] SDK-573: Add check to make sure Attribute has value name as images are stored as strings currently --- yoti_python_sdk/profile.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index b541a67e..3dfb4693 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -178,11 +178,14 @@ def __find_all_age_verifications(self): self.verifications = {} for key in self.attributes: attribute = self.attributes[key] - if ( - config.ATTRIBUTE_AGE_OVER in attribute.name - or config.ATTRIBUTE_AGE_UNDER in attribute.name - ): - self.verifications[attribute.name] = AgeVerification(attribute) + if hasattr( + attribute, "name" + ): # This will be changed in v3 as selfie will be an object rather than a string + if ( + config.ATTRIBUTE_AGE_OVER in attribute.name + or config.ATTRIBUTE_AGE_UNDER in attribute.name + ): + self.verifications[attribute.name] = AgeVerification(attribute) def ensure_postal_address(self): if ( From 4e98a8f6c35a46e7d718d35496a749fa6d1b686b Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Mon, 2 Sep 2019 12:11:35 +0100 Subject: [PATCH 38/74] SDK-573: Update Django example project to use AgeVerification object --- .../templates/attribute_snippet.html | 17 ++++++++++++++++- .../yoti_example_django/yoti_example/views.py | 14 +++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) 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 0b5c894a..02683ece 100644 --- a/examples/yoti_example_django/yoti_example/templates/attribute_snippet.html +++ b/examples/yoti_example_django/yoti_example/templates/attribute_snippet.html @@ -6,7 +6,7 @@ {{ name }} - +
{% if prop.name == "document_images" %} @@ -34,6 +34,21 @@ {% endfor %} + {% elif prop.name == "age_verified" %} + + + + + + + + + + + + + +
Check Type{{ prop.value.check_type }}
Age{{ prop.value.age }}
Result{{ prop.value.result }}
{% else %} {{ prevalue }} {{ prop.value }} diff --git a/examples/yoti_example_django/yoti_example/views.py b/examples/yoti_example_django/yoti_example/views.py index d04cd880..3c04a4c4 100644 --- a/examples/yoti_example_django/yoti_example/views.py +++ b/examples/yoti_example_django/yoti_example/views.py @@ -102,10 +102,18 @@ def get(self, request, *args, **kwargs): context["receipt_id"] = getattr(activity_details, "receipt_id") context["timestamp"] = getattr(activity_details, "timestamp") - # change this string according to the age condition defined in Yoti Hub - age_verified = profile.get_attribute("age_over:18") + # change this number according to the age condition defined in Yoti Hub + age_verified = profile.find_age_over_verification(18) + + # Age verification objects don't have the same properties as an attribute, + # so for this example we had to mock an object with the same properties if age_verified is not None: - context["age_verified"] = age_verified + context["age_verified"] = { + "name": "age_verified", + "value": age_verified, + "sources": age_verified.attribute.sources, + "verifiers": age_verified.attribute.verifiers, + } selfie = context.get("selfie") if selfie is not None: From f3c422a0217417e021b9f0074796fa1bf2160c8d Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Mon, 2 Sep 2019 12:17:05 +0100 Subject: [PATCH 39/74] SDK-573: Update example Flask application with new AgeVerification properties --- examples/yoti_example_flask/app.py | 14 +++++++++++--- .../yoti_example_flask/templates/profile.html | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/examples/yoti_example_flask/app.py b/examples/yoti_example_flask/app.py index b4b0cde1..3a53249d 100644 --- a/examples/yoti_example_flask/app.py +++ b/examples/yoti_example_flask/app.py @@ -97,10 +97,18 @@ def auth(): context["receipt_id"] = getattr(activity_details, "receipt_id") context["timestamp"] = getattr(activity_details, "timestamp") - # change this string according to the age condition defined in Yoti Hub - age_verified = profile.get_attribute("age_over:18") + # change this number according to the age condition defined in Yoti Hub + age_verified = profile.find_age_over_verification(18) + + # Age verification objects don't have the same properties as an attribute, + # so for this example we had to mock an object with the same properties if age_verified is not None: - context["age_verified"] = age_verified + context["age_verified"] = { + "name": "age_verified", + "value": age_verified, + "sources": age_verified.attribute.sources, + "verifiers": age_verified.attribute.verifiers, + } selfie = context.get("selfie") if selfie is not None: diff --git a/examples/yoti_example_flask/templates/profile.html b/examples/yoti_example_flask/templates/profile.html index fe0136b2..5819341c 100644 --- a/examples/yoti_example_flask/templates/profile.html +++ b/examples/yoti_example_flask/templates/profile.html @@ -29,6 +29,23 @@ {% endmacro %} +{% macro parse_age_verification(prop) %} + + + + + + + + + + + + + +
Check Type{{ prop.value.check_type }}
Age{{ prop.value.age }}
Result{{ prop.value.result }}
+{% endmacro %} + {% macro attribute(name, icon, prop, prevalue="") %} {% if prop %} {% if prop.value %} @@ -48,6 +65,8 @@ {{ parse_structured_address(prop) }} {% elif prop.name == "document_details" %} {{ parse_document_details(prop) }} + {% elif prop.name == "age_verified" %} + {{ parse_age_verification(prop) }} {% else %} {{ prevalue }} {{ prop.value }} From 0d49915455fcea286d949712624a3237d77215e7 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 20 Aug 2019 16:14:52 +0100 Subject: [PATCH 40/74] SDK-602: Initial commit of Sandbox integration helper modules --- yoti_python_sdk/config.py | 1 + yoti_python_sdk/sandbox/__init__.py | 0 yoti_python_sdk/sandbox/anchor.py | 65 +++++++++++++++ yoti_python_sdk/sandbox/attribute.py | 67 +++++++++++++++ yoti_python_sdk/sandbox/client.py | 118 +++++++++++++++++++++++++++ yoti_python_sdk/sandbox/endpoint.py | 13 +++ yoti_python_sdk/sandbox/token.py | 94 +++++++++++++++++++++ 7 files changed, 358 insertions(+) create mode 100644 yoti_python_sdk/sandbox/__init__.py create mode 100644 yoti_python_sdk/sandbox/anchor.py create mode 100644 yoti_python_sdk/sandbox/attribute.py create mode 100644 yoti_python_sdk/sandbox/client.py create mode 100644 yoti_python_sdk/sandbox/endpoint.py create mode 100644 yoti_python_sdk/sandbox/token.py diff --git a/yoti_python_sdk/config.py b/yoti_python_sdk/config.py index 3a254e11..6500f78c 100644 --- a/yoti_python_sdk/config.py +++ b/yoti_python_sdk/config.py @@ -30,6 +30,7 @@ X_YOTI_SDK = "X-Yoti-SDK" X_YOTI_SDK_VERSION = X_YOTI_SDK + "-Version" JSON_CONTENT_TYPE = "application/json" + ANCHOR_VALUE_PASSPORT = "PASSPORT" ANCHOR_VALUE_NATIONAL_ID = "NATIONAL_ID" ANCHOR_VALUE_PASS_CARD = "PASS_CARD" diff --git a/yoti_python_sdk/sandbox/__init__.py b/yoti_python_sdk/sandbox/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/sandbox/anchor.py b/yoti_python_sdk/sandbox/anchor.py new file mode 100644 index 00000000..20868eb8 --- /dev/null +++ b/yoti_python_sdk/sandbox/anchor.py @@ -0,0 +1,65 @@ +from yoti_python_sdk import config + + +class SandboxAnchor(object): + def __init__(self, anchor_type=None, sub_type=None, value=None, timestamp=None): + if anchor_type is None: + anchor_type = config.UNKNOWN_ANCHOR_TYPE + if sub_type is None: + sub_type = "" + if value is None: + value = "" + + self.__anchor_type = anchor_type + self.__sub_type = sub_type + self.__value = value + self.__timestamp = timestamp + + @property + def anchor_type(self): + return self.__anchor_type + + @property + def sub_type(self): + return self.__sub_type + + @property + def value(self): + return self.__value + + @property + def timestamp(self): + return self.__timestamp + + @staticmethod + def builder(self): + return SandboxAnchorBuilder() + + +class SandboxAnchorBuilder(object): + def __init__(self): + self.__type = None + self.__value = None + self.__sub_type = None + self.__timestamp = None + + def with_type(self, type): + self.__type = type + return self + + def with_value(self, value): + self.__value = value + return self + + def with_sub_type(self, sub_type): + self.__sub_type = sub_type + return self + + def with_timestamp(self, timestamp): + self.__timestamp = timestamp + return self + + def build(self): + return SandboxAnchor( + self.__type, self.__sub_type, self.__value, self.__timestamp + ) diff --git a/yoti_python_sdk/sandbox/attribute.py b/yoti_python_sdk/sandbox/attribute.py new file mode 100644 index 00000000..360abf0b --- /dev/null +++ b/yoti_python_sdk/sandbox/attribute.py @@ -0,0 +1,67 @@ +from yoti_python_sdk import config + + +class SandboxAttribute(object): + def __init__(self, name=None, value=None, anchors=None): + if name is None: + name = "" + + if value is None: + value = "" + + 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) + ) + + @staticmethod + def builder(): + return SandboxAttribute() + + +class SandboxAttributeBuilder(object): + def __init__(self): + self.__name = None + self.__value = None + self.__anchors = None + + def with_name(self, name): + self.__name = name + return self + + def with_value(self, value): + self.__value = value + return self + + def with_anchors(self, anchors): + self.__anchors = anchors + return self + + def build(self): + return SandboxAttribute(self.__name, self.__value, self.__anchors) diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py new file mode 100644 index 00000000..e870b114 --- /dev/null +++ b/yoti_python_sdk/sandbox/client.py @@ -0,0 +1,118 @@ +from yoti_python_sdk.sandbox.endpoint import SandboxEndpoint +from yoti_python_sdk.sandbox.token import YotiTokenRequest +from cryptography.fernet import base64 +from os.path import expanduser, isfile +from past.builtins import basestring + +import json +import requests +import yoti_python_sdk +from yoti_python_sdk.config import ( + X_YOTI_AUTH_KEY, + X_YOTI_AUTH_DIGEST, + X_YOTI_SDK, + SDK_IDENTIFIER, + X_YOTI_SDK_VERSION, + JSON_CONTENT_TYPE, +) + + +class SandboxClient(object): + + HTTP_SUPPORTED_METHODS = ["POST", "PUT", "PATCH", "GET", "DELETE"] + + def __init__(self, sdk_id, pem_file): + self.sdk_id = sdk_id + self.__endpoint = SandboxEndpoint(sdk_id) + self.__crypto = SandboxClient.__read_pem_file( + pem_file, "failed in SandboxClient __init__" + ) + + def setup_sharing_profile(self, request_token: YotiTokenRequest): + # request_path = self.__endpoint.get_sandbox_path() + payload = json.dumps(request_token.__dict__) + print(payload) + + @staticmethod + def builder(): + return SandboxClientBuilder() + + @staticmethod + def post(url, key, content): + payload = json.dumps(content) + payload_bytes = payload.encode() + headers = SandboxClient.__get_request_headers(url, "POST", payload_bytes, key) + return requests.post( + yoti_python_sdk.YOTI_API_ENDPOINT + url, + payload_bytes, + headers=headers, + verify=False, + ) + + @staticmethod + def __get_request_headers(path, http_method, content, crypto): + request = SandboxClient.__create_request(http_method, path, content) + sdk_version = yoti_python_sdk.__version__ + + return { + X_YOTI_AUTH_KEY: crypto.get_public_key(), + X_YOTI_AUTH_DIGEST: crypto.sign(request), + X_YOTI_SDK: SDK_IDENTIFIER, + X_YOTI_SDK_VERSION: "{0}-{1}".format(SDK_IDENTIFIER, sdk_version), + "Content-Type": JSON_CONTENT_TYPE, + "Accept": JSON_CONTENT_TYPE, + } + + @staticmethod + def __read_pem_file(key_file_path, error_source): + try: + key_file_path = expanduser(key_file_path) + + if not isinstance(key_file_path, basestring) or not isfile(key_file_path): + raise IOError("File not found: {0}".format(key_file_path)) + with open(key_file_path, "rb") as pem_file: + return pem_file.read().strip() + except (AttributeError, IOError, TypeError, OSError) as exc: + error = 'Could not read private key file: "{0}", passed as: {1} '.format( + key_file_path, error_source + ) + exception = "{0}: {1}".format(type(exc).__name__, exc) + raise RuntimeError("{0}: {1}".format(error, exception)) + + @staticmethod + def __create_request(http_method, path, content): + if http_method not in SandboxClient.HTTP_SUPPORTED_METHODS: + raise ValueError( + "{} is not in the list of supported methods: {}".format( + http_method, SandboxClient.HTTP_SUPPORTED_METHODS + ) + ) + + request = "{}&{}".format(http_method, path) + + if content is not None: + b64encoded = base64.b64encode(content) + b64ascii = b64encoded.decode("ascii") + request += "&" + b64ascii + + return request + + +class SandboxClientBuilder(object): + def __init__(self): + self.__sdkId = None + self.__pem_file = None + + def for_application(self, sdkId): + self.__sdkId = sdkId + return self + + def with_pem_file(self, pem_file): + self.__pem_file = pem_file + return self + + def build(self): + if self.__sdkId is None or self.__pem_file is None: + raise ValueError("SDK ID and/or pem file must not be None") + + return SandboxClient(self.__sdkId, self.__pem_file) diff --git a/yoti_python_sdk/sandbox/endpoint.py b/yoti_python_sdk/sandbox/endpoint.py new file mode 100644 index 00000000..6909bc26 --- /dev/null +++ b/yoti_python_sdk/sandbox/endpoint.py @@ -0,0 +1,13 @@ +from yoti_python_sdk.endpoint import Endpoint + + +class SandboxEndpoint(Endpoint): + def __init__(self, sdk_id): + super(SandboxEndpoint, self).__init__(sdk_id) + + def get_sandbox_path(self): + return "/apps/{sdk_id}/tokens?timestamp={timestamp}&nonce={nonce}".format( + sdk_id=self.sdk_id, + nonce=self.__create_nonce(), + timestamp=self.__create_timestamp(), + ) diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py new file mode 100644 index 00000000..78c54f9a --- /dev/null +++ b/yoti_python_sdk/sandbox/token.py @@ -0,0 +1,94 @@ +from yoti_python_sdk.sandbox.attribute import SandboxAttribute +from yoti_python_sdk import config +from cryptography.fernet import base64 + + +class YotiTokenRequest(object): + def __init__(self, remember_me_id, sandbox_attributes=None): + + if sandbox_attributes is None: + sandbox_attributes = [] + + self.remember_me_id = remember_me_id + self.sandbox_attributes = sandbox_attributes + + def __dict__(self): + return { + "remember_me_id": self.remember_me_id, + "profile_attributes": self.sandbox_attributes, + } + + @staticmethod + def builder(): + return YotiTokenRequestBuilder() + + +class YotiTokenRequestBuilder(object): + def __init__(self): + self.remember_me_id = None + self.attributes = [] + + def with_remember_me_id(self, remember_me_id): + self.remember_me_id = remember_me_id + return self + + def with_attribute(self, sandbox_attribute): + self.attributes.append(sandbox_attribute) + return self + + def with_given_names(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_GIVEN_NAMES, value, anchors) + + def with_family_name(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_FAMILY_NAME, value, anchors) + + def with_full_name(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_FULL_NAME, value, anchors) + + def with_date_of_birth(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_DATE_OF_BIRTH, value, anchors) + + def with_age_over(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_AGE_OVER, value, anchors) + + def with_age_under(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_AGE_UNDER, value, anchors) + + def with_gender(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_GENDER, value, anchors) + + def with_phone_number(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_PHONE_NUMBER, value, anchors) + + def with_nationality(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_NATIONALITY, value, anchors) + + def with_postal_address(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_POSTAL_ADDRESS, value, anchors) + + def with_structured_postal_address(self, value, anchors=None): + return self.__create_attribute( + config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS, value, anchors + ) + + def with_selfie(self, value, anchors=None): + base64_selfie = base64.b64decode(value).decode("utf-8") + return self.with_base64_selfie(base64_selfie, anchors) + + def with_base64_selfie(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_SELFIE, value, anchors) + + def with_email_address(self, value, anchors=None): + return self.__create_attribute(config.ATTRIBUTE_EMAIL_ADDRESS, value, anchors) + + def with_document_details(self, value, anchors=None): + return self.__create_attribute( + config.ATTRIBUTE_DOCUMENT_DETAILS, value, anchors + ) + + def build(self): + return YotiTokenRequest(self.remember_me_id, self.attributes) + + @staticmethod + def __create_attribute(self, name, value, anchors=None): + return SandboxAttribute(name, value, anchors) From 09d139337046e258209e624abcfa3e60ecd0e37a Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 20 Aug 2019 16:47:41 +0100 Subject: [PATCH 41/74] SDK-602: Update SandboxClient to do a request in setupShareProfile --- yoti_python_sdk/sandbox/client.py | 5 +++-- yoti_python_sdk/tests/sandbox/test_sandbox_client.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 yoti_python_sdk/tests/sandbox/test_sandbox_client.py diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py index e870b114..18da94b0 100644 --- a/yoti_python_sdk/sandbox/client.py +++ b/yoti_python_sdk/sandbox/client.py @@ -29,9 +29,10 @@ def __init__(self, sdk_id, pem_file): ) def setup_sharing_profile(self, request_token: YotiTokenRequest): - # request_path = self.__endpoint.get_sandbox_path() + request_path = self.__endpoint.get_sandbox_path() payload = json.dumps(request_token.__dict__) - print(payload) + response = SandboxClient.post(request_path, self.__crypto, payload) + return response.status_code @staticmethod def builder(): diff --git a/yoti_python_sdk/tests/sandbox/test_sandbox_client.py b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py new file mode 100644 index 00000000..9b9f3471 --- /dev/null +++ b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py @@ -0,0 +1,9 @@ +from yoti_python_sdk.sandbox.client import SandboxClient + +import pytest + + +def test_builder_should_throw_exception_for_missing_sdk_id(): + builder = SandboxClient.builder() + with pytest.raises(ValueError): + builder.build() From 0bc45f8b5bc2ce0cf9787c428960405b1710d636 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 20 Aug 2019 18:04:15 +0100 Subject: [PATCH 42/74] SDK-602: Add tests for SandboxClient and remove syntax causing Python 2.7 errors in Travis --- yoti_python_sdk/sandbox/client.py | 3 +- yoti_python_sdk/sandbox/token.py | 9 +++++ .../tests/sandbox/test_sandbox_client.py | 39 ++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py index 18da94b0..17477242 100644 --- a/yoti_python_sdk/sandbox/client.py +++ b/yoti_python_sdk/sandbox/client.py @@ -1,5 +1,4 @@ from yoti_python_sdk.sandbox.endpoint import SandboxEndpoint -from yoti_python_sdk.sandbox.token import YotiTokenRequest from cryptography.fernet import base64 from os.path import expanduser, isfile from past.builtins import basestring @@ -28,7 +27,7 @@ def __init__(self, sdk_id, pem_file): pem_file, "failed in SandboxClient __init__" ) - def setup_sharing_profile(self, request_token: YotiTokenRequest): + def setup_sharing_profile(self, request_token): request_path = self.__endpoint.get_sandbox_path() payload = json.dumps(request_token.__dict__) response = SandboxClient.post(request_path, self.__crypto, payload) diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py index 78c54f9a..97169058 100644 --- a/yoti_python_sdk/sandbox/token.py +++ b/yoti_python_sdk/sandbox/token.py @@ -3,6 +3,15 @@ from cryptography.fernet import base64 +class YotiTokenResponse(object): + def __init__(self, token): + self.__token = token + + @property + def token(self): + return self.__token + + class YotiTokenRequest(object): def __init__(self, remember_me_id, sandbox_attributes=None): diff --git a/yoti_python_sdk/tests/sandbox/test_sandbox_client.py b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py index 9b9f3471..4c3af780 100644 --- a/yoti_python_sdk/tests/sandbox/test_sandbox_client.py +++ b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py @@ -1,9 +1,44 @@ from yoti_python_sdk.sandbox.client import SandboxClient +from yoti_python_sdk.sandbox.token import YotiTokenRequest, YotiTokenResponse +from yoti_python_sdk.tests.conftest import PEM_FILE_PATH +from unittest.mock import patch import pytest -def test_builder_should_throw_exception_for_missing_sdk_id(): - builder = SandboxClient.builder() +def test_builder_should_throw_error_for_missing_sdk_id(): + builder = SandboxClient.builder().with_pem_file("some_pem.pem") with pytest.raises(ValueError): builder.build() + + +def test_builder_should_throw_error_for_missing_pem_file(): + builder = SandboxClient.builder().for_application("my_application") + with pytest.raises(ValueError): + builder.build() + + +def test_builder_should_build_client(): + client = ( + SandboxClient.builder() + .for_application("some_app") + .with_pem_file(PEM_FILE_PATH) + .build() + ) + + assert client.sdk_id == "some_app" + assert isinstance(client, SandboxClient) + + +@patch("yoti_python_sdk.sandbox.client.SandboxClient") +def test_client_should_return_token_from_sandbox(sandbox_client_mock): + sandbox_client_mock.setup_profile_share.return_value = YotiTokenResponse( + "some-token" + ) + + token_request = ( + YotiTokenRequest.builder().with_remember_me_id("remember_me_pls").build() + ) + response = sandbox_client_mock.setup_profile_share(token_request) + + assert response.token == "some-token" From f144746b684ec3a3aa3f5947ed7c4e4dac8e98e0 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 21 Aug 2019 09:36:44 +0100 Subject: [PATCH 43/74] SDK-602: Add try catch to import of mock (different between python 2 and 3) --- yoti_python_sdk/tests/sandbox/test_sandbox_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yoti_python_sdk/tests/sandbox/test_sandbox_client.py b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py index 4c3af780..74529d21 100644 --- a/yoti_python_sdk/tests/sandbox/test_sandbox_client.py +++ b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py @@ -1,7 +1,11 @@ from yoti_python_sdk.sandbox.client import SandboxClient from yoti_python_sdk.sandbox.token import YotiTokenRequest, YotiTokenResponse from yoti_python_sdk.tests.conftest import PEM_FILE_PATH -from unittest.mock import patch + +try: + from unittest.mock import patch +except ImportError: + from mock import patch import pytest From e277aaecc078ea3ea7c4b5c3e42e769d6562fe3e Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 21 Aug 2019 09:45:38 +0100 Subject: [PATCH 44/74] SDK-602: Move anchor constants back out of config module until major release: --- yoti_python_sdk/config.py | 1 + yoti_python_sdk/sandbox/anchor.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/yoti_python_sdk/config.py b/yoti_python_sdk/config.py index 6500f78c..8b3b88c0 100644 --- a/yoti_python_sdk/config.py +++ b/yoti_python_sdk/config.py @@ -35,3 +35,4 @@ ANCHOR_VALUE_NATIONAL_ID = "NATIONAL_ID" ANCHOR_VALUE_PASS_CARD = "PASS_CARD" ANCHOR_VALUE_DRIVING_LICENCE = "DRIVING_LICENCE" + diff --git a/yoti_python_sdk/sandbox/anchor.py b/yoti_python_sdk/sandbox/anchor.py index 20868eb8..fe28aff0 100644 --- a/yoti_python_sdk/sandbox/anchor.py +++ b/yoti_python_sdk/sandbox/anchor.py @@ -1,10 +1,15 @@ -from yoti_python_sdk import config +UNKNOWN_EXTENSION = "" +SOURCE_EXTENSION = "1.3.6.1.4.1.47127.1.1.1" +VERIFIER_EXTENSION = "1.3.6.1.4.1.47127.1.1.2" + +UNKNOWN_ANCHOR_TYPE = "Unknown" +UNKNOWN_ANCHOR_VALUE = "" class SandboxAnchor(object): def __init__(self, anchor_type=None, sub_type=None, value=None, timestamp=None): if anchor_type is None: - anchor_type = config.UNKNOWN_ANCHOR_TYPE + anchor_type = UNKNOWN_ANCHOR_TYPE if sub_type is None: sub_type = "" if value is None: From 908fbf0d401d0dcc70fd1b58f4edefa4c3b09868 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 21 Aug 2019 10:09:58 +0100 Subject: [PATCH 45/74] SDK-826: Implement __create_nonce and __create_timestamp into SandboxEndpoint due to being unavailable from Endpoint --- yoti_python_sdk/sandbox/client.py | 4 +++- yoti_python_sdk/sandbox/endpoint.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py index 17477242..90c8da19 100644 --- a/yoti_python_sdk/sandbox/client.py +++ b/yoti_python_sdk/sandbox/client.py @@ -14,6 +14,7 @@ X_YOTI_SDK_VERSION, JSON_CONTENT_TYPE, ) +from yoti_python_sdk.sandbox.token import YotiTokenResponse class SandboxClient(object): @@ -31,7 +32,8 @@ def setup_sharing_profile(self, request_token): request_path = self.__endpoint.get_sandbox_path() payload = json.dumps(request_token.__dict__) response = SandboxClient.post(request_path, self.__crypto, payload) - return response.status_code + response_payload = response.json() + return YotiTokenResponse(response_payload["token"]) @staticmethod def builder(): diff --git a/yoti_python_sdk/sandbox/endpoint.py b/yoti_python_sdk/sandbox/endpoint.py index 6909bc26..e671777a 100644 --- a/yoti_python_sdk/sandbox/endpoint.py +++ b/yoti_python_sdk/sandbox/endpoint.py @@ -1,4 +1,6 @@ from yoti_python_sdk.endpoint import Endpoint +import time +import uuid class SandboxEndpoint(Endpoint): @@ -11,3 +13,11 @@ def get_sandbox_path(self): nonce=self.__create_nonce(), timestamp=self.__create_timestamp(), ) + + @staticmethod + def __create_nonce(): + return uuid.uuid4() + + @staticmethod + def __create_timestamp(): + return int(time.time() * 1000) From 7056e67a8c68ff1d384b013f1d72b529cd821722 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 21 Aug 2019 11:39:14 +0100 Subject: [PATCH 46/74] SDK-602: Update SandboxClient to parse into Crypto object, and update default error messages to actually display the messages --- yoti_python_sdk/client.py | 35 ++++++++++++++++++++++++------- yoti_python_sdk/sandbox/client.py | 23 +++++++++++++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index 3d82bed5..0352ecdf 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -89,14 +89,20 @@ def get_activity_details(self, encrypted_request_token): 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 + unwrapped_key, + encrypted_application_profile.iv, + encrypted_application_profile.cipher_text, ) user_profile_attribute_list = proto.attribute_list(decrypted_profile_data) - application_profile_attribute_list = proto.attribute_list(decrypted_application_data) + application_profile_attribute_list = proto.attribute_list( + decrypted_application_data + ) return ActivityDetails( - receipt=receipt, decrypted_profile=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): @@ -112,7 +118,13 @@ def perform_aml_check(self, aml_profile): 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) + response = requests.request( + http_method, + url, + headers=headers, + data=body, + verify=yoti_python_sdk.YOTI_API_VERIFY_SSL, + ) return response @property @@ -144,10 +156,12 @@ 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_python_sdk.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}"} + response, {"default": "Unsuccessful Yoti API call: {} {}"} ) return response @@ -159,10 +173,15 @@ 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_python_sdk.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}"} + response, {"default": "Unsuccessful Yoti API call: {} {}"} ) return response diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py index 90c8da19..b06ce507 100644 --- a/yoti_python_sdk/sandbox/client.py +++ b/yoti_python_sdk/sandbox/client.py @@ -14,7 +14,17 @@ X_YOTI_SDK_VERSION, JSON_CONTENT_TYPE, ) -from yoti_python_sdk.sandbox.token import YotiTokenResponse +from yoti_python_sdk.sandbox.token import YotiTokenRequest, YotiTokenResponse +from yoti_python_sdk.crypto import Crypto +from json import JSONEncoder + + +class SandboxEncoder(JSONEncoder): + def default(self, o): + if isinstance(o, YotiTokenRequest): + return o.__dict__() + + return json.JSONEncoder.default(self, o) class SandboxClient(object): @@ -24,14 +34,17 @@ class SandboxClient(object): def __init__(self, sdk_id, pem_file): self.sdk_id = sdk_id self.__endpoint = SandboxEndpoint(sdk_id) - self.__crypto = SandboxClient.__read_pem_file( + + pem_data = SandboxClient.__read_pem_file( pem_file, "failed in SandboxClient __init__" ) + self.__crypto = Crypto(pem_data) + def setup_sharing_profile(self, request_token): request_path = self.__endpoint.get_sandbox_path() - payload = json.dumps(request_token.__dict__) - response = SandboxClient.post(request_path, self.__crypto, payload) + response = SandboxClient.post(request_path, self.__crypto, request_token) + print(response.text) response_payload = response.json() return YotiTokenResponse(response_payload["token"]) @@ -41,7 +54,7 @@ def builder(): @staticmethod def post(url, key, content): - payload = json.dumps(content) + payload = json.dumps(content, cls=SandboxEncoder) payload_bytes = payload.encode() headers = SandboxClient.__get_request_headers(url, "POST", payload_bytes, key) return requests.post( From 627c185cb74195b1418ed709931911c9e54edd9e Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 21 Aug 2019 16:53:59 +0100 Subject: [PATCH 47/74] SDK-602: Add statements to parse different objects when converting to a JSON payload --- yoti_python_sdk/sandbox/anchor.py | 10 ++++- yoti_python_sdk/sandbox/attribute.py | 5 ++- yoti_python_sdk/sandbox/client.py | 26 +++++++----- yoti_python_sdk/sandbox/token.py | 62 +++++++++++++++++++++------- 4 files changed, 74 insertions(+), 29 deletions(-) diff --git a/yoti_python_sdk/sandbox/anchor.py b/yoti_python_sdk/sandbox/anchor.py index fe28aff0..8f544dc5 100644 --- a/yoti_python_sdk/sandbox/anchor.py +++ b/yoti_python_sdk/sandbox/anchor.py @@ -36,8 +36,16 @@ def value(self): def timestamp(self): return self.__timestamp + def __dict__(self): + return { + "type": self.anchor_type, + "value": self.value, + "sub_type": self.sub_type, + "timestamp": self.timestamp, + } + @staticmethod - def builder(self): + def builder(): return SandboxAnchorBuilder() diff --git a/yoti_python_sdk/sandbox/attribute.py b/yoti_python_sdk/sandbox/attribute.py index 360abf0b..67eb7cec 100644 --- a/yoti_python_sdk/sandbox/attribute.py +++ b/yoti_python_sdk/sandbox/attribute.py @@ -10,7 +10,7 @@ def __init__(self, name=None, value=None, anchors=None): value = "" if anchors is None: - anchors = {} + anchors = [] self.__name = name self.__value = value @@ -40,6 +40,9 @@ def verifiers(self): filter(lambda a: a.anchor_type == config.ANCHOR_VERIFIER, self.__anchors) ) + def __dict__(self): + return {"name": self.name, "value": self.value, "anchors": self.anchors} + @staticmethod def builder(): return SandboxAttribute() diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py index b06ce507..4a3fbae9 100644 --- a/yoti_python_sdk/sandbox/client.py +++ b/yoti_python_sdk/sandbox/client.py @@ -15,6 +15,8 @@ JSON_CONTENT_TYPE, ) from yoti_python_sdk.sandbox.token import YotiTokenRequest, YotiTokenResponse +from yoti_python_sdk.sandbox.attribute import SandboxAttribute +from yoti_python_sdk.sandbox.anchor import SandboxAnchor from yoti_python_sdk.crypto import Crypto from json import JSONEncoder @@ -23,6 +25,10 @@ class SandboxEncoder(JSONEncoder): def default(self, o): if isinstance(o, YotiTokenRequest): return o.__dict__() + if isinstance(o, SandboxAttribute): + return o.__dict__() + if isinstance(o, SandboxAnchor): + return o.__dict__() return json.JSONEncoder.default(self, o) @@ -31,9 +37,10 @@ class SandboxClient(object): HTTP_SUPPORTED_METHODS = ["POST", "PUT", "PATCH", "GET", "DELETE"] - def __init__(self, sdk_id, pem_file): + def __init__(self, sdk_id, pem_file, sandbox_url): self.sdk_id = sdk_id self.__endpoint = SandboxEndpoint(sdk_id) + self.__sandbox_url = sandbox_url pem_data = SandboxClient.__read_pem_file( pem_file, "failed in SandboxClient __init__" @@ -43,8 +50,9 @@ def __init__(self, sdk_id, pem_file): def setup_sharing_profile(self, request_token): request_path = self.__endpoint.get_sandbox_path() - response = SandboxClient.post(request_path, self.__crypto, request_token) - print(response.text) + response = SandboxClient.post( + self.__sandbox_url, request_path, self.__crypto, request_token + ) response_payload = response.json() return YotiTokenResponse(response_payload["token"]) @@ -53,16 +61,12 @@ def builder(): return SandboxClientBuilder() @staticmethod - def post(url, key, content): + def post(host, path, key, content): payload = json.dumps(content, cls=SandboxEncoder) + print(payload) payload_bytes = payload.encode() - headers = SandboxClient.__get_request_headers(url, "POST", payload_bytes, key) - return requests.post( - yoti_python_sdk.YOTI_API_ENDPOINT + url, - payload_bytes, - headers=headers, - verify=False, - ) + headers = SandboxClient.__get_request_headers(path, "POST", payload_bytes, key) + return requests.post(host + path, payload_bytes, headers=headers, verify=False) @staticmethod def __get_request_headers(path, http_method, content, crypto): diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py index 97169058..b6a937f1 100644 --- a/yoti_python_sdk/sandbox/token.py +++ b/yoti_python_sdk/sandbox/token.py @@ -13,7 +13,10 @@ def token(self): class YotiTokenRequest(object): - def __init__(self, remember_me_id, sandbox_attributes=None): + def __init__(self, remember_me_id=None, sandbox_attributes=None): + + if remember_me_id is None: + remember_me_id = "" if sandbox_attributes is None: sandbox_attributes = [] @@ -46,58 +49,85 @@ def with_attribute(self, sandbox_attribute): return self def with_given_names(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_GIVEN_NAMES, value, anchors) + attribute = self.__create_attribute( + config.ATTRIBUTE_GIVEN_NAMES, value, anchors + ) + return self.with_attribute(attribute) def with_family_name(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_FAMILY_NAME, value, anchors) + attribute = self.__create_attribute( + config.ATTRIBUTE_FAMILY_NAME, value, anchors + ) + return self.with_attribute(attribute) def with_full_name(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_FULL_NAME, value, anchors) + attribute = self.__create_attribute(config.ATTRIBUTE_FULL_NAME, value, anchors) + return self.with_attribute(attribute) def with_date_of_birth(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_DATE_OF_BIRTH, value, anchors) + attribute = self.__create_attribute( + config.ATTRIBUTE_DATE_OF_BIRTH, value, anchors + ) + return self.with_attribute(attribute) def with_age_over(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_AGE_OVER, value, anchors) + attribute = self.__create_attribute(config.ATTRIBUTE_AGE_OVER, value, anchors) + return self.with_attribute(attribute) def with_age_under(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_AGE_UNDER, value, anchors) + attribute = self.__create_attribute(config.ATTRIBUTE_AGE_UNDER, value, anchors) + return self.with_attribute(attribute) def with_gender(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_GENDER, value, anchors) + attribute = self.__create_attribute(config.ATTRIBUTE_GENDER, value, anchors) + return self.with_attribute(attribute) def with_phone_number(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_PHONE_NUMBER, value, anchors) + attribute = self.__create_attribute( + config.ATTRIBUTE_PHONE_NUMBER, value, anchors + ) + return self.with_attribute(attribute) def with_nationality(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_NATIONALITY, value, anchors) + attribute = self.__create_attribute( + config.ATTRIBUTE_NATIONALITY, value, anchors + ) + return self.with_attribute(attribute) def with_postal_address(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_POSTAL_ADDRESS, value, anchors) + attribute = self.__create_attribute( + config.ATTRIBUTE_POSTAL_ADDRESS, value, anchors + ) + return self.with_attribute(attribute) def with_structured_postal_address(self, value, anchors=None): - return self.__create_attribute( + attribute = self.__create_attribute( config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS, value, anchors ) + return self.with_attribute(attribute) def with_selfie(self, value, anchors=None): base64_selfie = base64.b64decode(value).decode("utf-8") return self.with_base64_selfie(base64_selfie, anchors) def with_base64_selfie(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_SELFIE, value, anchors) + attribute = self.__create_attribute(config.ATTRIBUTE_SELFIE, value, anchors) + return self.with_attribute(attribute) def with_email_address(self, value, anchors=None): - return self.__create_attribute(config.ATTRIBUTE_EMAIL_ADDRESS, value, anchors) + attribute = self.__create_attribute( + config.ATTRIBUTE_EMAIL_ADDRESS, value, anchors + ) + return self.with_attribute(attribute) def with_document_details(self, value, anchors=None): - return self.__create_attribute( + attribute = self.__create_attribute( config.ATTRIBUTE_DOCUMENT_DETAILS, value, anchors ) + return self.with_attribute(attribute) def build(self): return YotiTokenRequest(self.remember_me_id, self.attributes) - @staticmethod def __create_attribute(self, name, value, anchors=None): return SandboxAttribute(name, value, anchors) From 604450d3ce3d6c68ea9a161fde4fe9de022a61c2 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 22 Aug 2019 12:44:23 +0100 Subject: [PATCH 48/74] SDK-602: Change YotiRequestToken to base64encode and decode using utf-8 in with_selfie --- yoti_python_sdk/sandbox/token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py index b6a937f1..a62dc7e3 100644 --- a/yoti_python_sdk/sandbox/token.py +++ b/yoti_python_sdk/sandbox/token.py @@ -1,6 +1,6 @@ from yoti_python_sdk.sandbox.attribute import SandboxAttribute from yoti_python_sdk import config -from cryptography.fernet import base64 +import base64 class YotiTokenResponse(object): @@ -107,7 +107,7 @@ def with_structured_postal_address(self, value, anchors=None): return self.with_attribute(attribute) def with_selfie(self, value, anchors=None): - base64_selfie = base64.b64decode(value).decode("utf-8") + base64_selfie = base64.b64encode(value).decode("utf-8") return self.with_base64_selfie(base64_selfie, anchors) def with_base64_selfie(self, value, anchors=None): From 6c1492d7af41d19a20cf594ea9cdf1f6e69d1dd8 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 22 Aug 2019 15:33:34 +0100 Subject: [PATCH 49/74] SDK-602: Implement support for SandboxAgeVerification --- yoti_python_sdk/sandbox/age_verification.py | 57 +++++++++++++++++++++ yoti_python_sdk/sandbox/attribute.py | 28 ++++++++-- yoti_python_sdk/sandbox/token.py | 9 +--- 3 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 yoti_python_sdk/sandbox/age_verification.py diff --git a/yoti_python_sdk/sandbox/age_verification.py b/yoti_python_sdk/sandbox/age_verification.py new file mode 100644 index 00000000..80cc4ea0 --- /dev/null +++ b/yoti_python_sdk/sandbox/age_verification.py @@ -0,0 +1,57 @@ +from yoti_python_sdk.sandbox.attribute import SandboxAttribute +from yoti_python_sdk import config + + +class SandboxAgeVerification(object): + def __init__(self, date_of_birth, supported_age_derivation, anchors=None): + + if anchors is None: + anchors = [] + + self.__date_of_birth = date_of_birth + self.__supported_age_derivation = supported_age_derivation + self.__anchors = anchors + + def to_attribute(self): + return ( + SandboxAttribute.builder() + .with_name(config.ATTRIBUTE_DATE_OF_BIRTH) + .with_value(self.__date_of_birth) + .with_derivation(self.__supported_age_derivation) + .with_anchors(self.__anchors) + .build() + ) + + @staticmethod + def builder(): + return SandboxAgeVerificationBuilder() + + +class SandboxAgeVerificationBuilder(object): + def __init__(self): + self.__date_of_birth = None + self.__derivation = None + self.__anchors = None + + def with_date_of_birth(self, date_of_birth): + self.__date_of_birth = date_of_birth + return self + + def with_derivation(self, derivation): + self.__derivation = derivation + return self + + def with_age_over(self, age_over): + return self.with_derivation(config.ATTRIBUTE_AGE_OVER + str(age_over)) + + def with_age_under(self, age_under): + return self.with_derivation(config.ATTRIBUTE_AGE_UNDER + str(age_under)) + + def with_anchors(self, anchors): + self.__anchors = anchors + return self + + def build(self): + return SandboxAgeVerification( + self.__date_of_birth, self.__derivation, self.__anchors + ) diff --git a/yoti_python_sdk/sandbox/attribute.py b/yoti_python_sdk/sandbox/attribute.py index 67eb7cec..e4d359ad 100644 --- a/yoti_python_sdk/sandbox/attribute.py +++ b/yoti_python_sdk/sandbox/attribute.py @@ -2,7 +2,7 @@ class SandboxAttribute(object): - def __init__(self, name=None, value=None, anchors=None): + def __init__(self, name=None, value=None, anchors=None, derivation=None): if name is None: name = "" @@ -12,9 +12,13 @@ def __init__(self, name=None, value=None, anchors=None): if anchors is None: anchors = [] + if derivation is None: + derivation = "" + self.__name = name self.__value = value self.__anchors = anchors + self.__derivation = derivation @property def name(self): @@ -40,12 +44,21 @@ def verifiers(self): filter(lambda a: a.anchor_type == config.ANCHOR_VERIFIER, self.__anchors) ) + @property + def derivation(self): + return self.__derivation + def __dict__(self): - return {"name": self.name, "value": self.value, "anchors": self.anchors} + return { + "name": self.name, + "value": self.value, + "anchors": self.anchors, + "derivation": self.derivation, + } @staticmethod def builder(): - return SandboxAttribute() + return SandboxAttributeBuilder() class SandboxAttributeBuilder(object): @@ -53,6 +66,7 @@ def __init__(self): self.__name = None self.__value = None self.__anchors = None + self.__derivation = None def with_name(self, name): self.__name = name @@ -66,5 +80,11 @@ def with_anchors(self, anchors): self.__anchors = anchors return self + def with_derivation(self, derivation): + self.__derivation = derivation + return self + def build(self): - return SandboxAttribute(self.__name, self.__value, self.__anchors) + return SandboxAttribute( + self.__name, self.__value, self.__anchors, self.__derivation + ) diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py index a62dc7e3..60d41b96 100644 --- a/yoti_python_sdk/sandbox/token.py +++ b/yoti_python_sdk/sandbox/token.py @@ -70,13 +70,8 @@ def with_date_of_birth(self, value, anchors=None): ) return self.with_attribute(attribute) - def with_age_over(self, value, anchors=None): - attribute = self.__create_attribute(config.ATTRIBUTE_AGE_OVER, value, anchors) - return self.with_attribute(attribute) - - def with_age_under(self, value, anchors=None): - attribute = self.__create_attribute(config.ATTRIBUTE_AGE_UNDER, value, anchors) - return self.with_attribute(attribute) + def with_age_verification(self, age_verification): + return self.with_attribute(age_verification.to_attribute()) def with_gender(self, value, anchors=None): attribute = self.__create_attribute(config.ATTRIBUTE_GENDER, value, anchors) From fdd5eb793f36281803bd001b0d809ebd5cef157b Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 22 Aug 2019 15:54:29 +0100 Subject: [PATCH 50/74] SDK-602: Update SandboxClientBuilder to instantiate SandboxClient with sandbox url --- yoti_python_sdk/sandbox/client.py | 23 +++++++++++++------ .../tests/sandbox/test_sandbox_client.py | 1 + 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py index 4a3fbae9..16dd5399 100644 --- a/yoti_python_sdk/sandbox/client.py +++ b/yoti_python_sdk/sandbox/client.py @@ -119,19 +119,28 @@ def __create_request(http_method, path, content): class SandboxClientBuilder(object): def __init__(self): - self.__sdkId = None + self.__sdk_id = None self.__pem_file = None + self.__sandbox_url = None - def for_application(self, sdkId): - self.__sdkId = sdkId + def for_application(self, sdk_id): + self.__sdk_id = sdk_id return self def with_pem_file(self, pem_file): self.__pem_file = pem_file return self - def build(self): - if self.__sdkId is None or self.__pem_file is None: - raise ValueError("SDK ID and/or pem file must not be None") + def with_sandbox_url(self, sandbox_url): + self.__sandbox_url = sandbox_url + return self - return SandboxClient(self.__sdkId, self.__pem_file) + def build(self): + if ( + self.__sdk_id is None + or self.__pem_file is None + or self.__sandbox_url is None + ): + raise ValueError("SDK ID/PEM file/sandbox url must not be None") + + return SandboxClient(self.__sdk_id, self.__pem_file, self.__sandbox_url) diff --git a/yoti_python_sdk/tests/sandbox/test_sandbox_client.py b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py index 74529d21..faa62c17 100644 --- a/yoti_python_sdk/tests/sandbox/test_sandbox_client.py +++ b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py @@ -27,6 +27,7 @@ def test_builder_should_build_client(): SandboxClient.builder() .for_application("some_app") .with_pem_file(PEM_FILE_PATH) + .with_sandbox_url("https://localhost") .build() ) From 6d4ffe952804c8c35423fe49b971e3f70df7ffa3 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 5 Sep 2019 12:26:18 +0100 Subject: [PATCH 51/74] SDK-602: Update docstrings for sandbox modules --- yoti_python_sdk/sandbox/age_verification.py | 40 +++++++ yoti_python_sdk/sandbox/anchor.py | 54 +++++++++ yoti_python_sdk/sandbox/attribute.py | 64 ++++++++++ yoti_python_sdk/sandbox/client.py | 38 ++++++ yoti_python_sdk/sandbox/token.py | 125 +++++++++++++++++++- 5 files changed, 320 insertions(+), 1 deletion(-) diff --git a/yoti_python_sdk/sandbox/age_verification.py b/yoti_python_sdk/sandbox/age_verification.py index 80cc4ea0..2cb5882c 100644 --- a/yoti_python_sdk/sandbox/age_verification.py +++ b/yoti_python_sdk/sandbox/age_verification.py @@ -13,6 +13,11 @@ def __init__(self, date_of_birth, supported_age_derivation, anchors=None): self.__anchors = anchors def to_attribute(self): + """ + Converts the age verification object into an Attribute + + :return: Instance of SandboxAttribute + """ return ( SandboxAttribute.builder() .with_name(config.ATTRIBUTE_DATE_OF_BIRTH) @@ -24,6 +29,11 @@ def to_attribute(self): @staticmethod def builder(): + """ + Creates a sandbox age verification builder + + :return: Instance of SandboxAgeVerificationBuilder + """ return SandboxAgeVerificationBuilder() @@ -34,20 +44,50 @@ def __init__(self): self.__anchors = None def with_date_of_birth(self, date_of_birth): + """ + Set the date of birth on the builder + + :param str date_of_birth: the date of birth + :return: the updated builder + """ self.__date_of_birth = date_of_birth return self def with_derivation(self, derivation): + """ + Set the derivation of the age verification + + :param str derivation: the derivation + :return: the updated builder + """ self.__derivation = derivation return self def with_age_over(self, age_over): + """ + Set the age over value of the age verification + + :param int age_over: the age over value + :return: the updated builder + """ return self.with_derivation(config.ATTRIBUTE_AGE_OVER + str(age_over)) def with_age_under(self, age_under): + """ + Set the age under value of the age verification + + :param int age_under: + :return: the updated builder + """ return self.with_derivation(config.ATTRIBUTE_AGE_UNDER + str(age_under)) def with_anchors(self, anchors): + """ + Set the anchors for the age verification + + :param list[SandboxAnchor] anchors: + :return: the updated builder + """ self.__anchors = anchors return self diff --git a/yoti_python_sdk/sandbox/anchor.py b/yoti_python_sdk/sandbox/anchor.py index 8f544dc5..c03a62f9 100644 --- a/yoti_python_sdk/sandbox/anchor.py +++ b/yoti_python_sdk/sandbox/anchor.py @@ -22,18 +22,38 @@ def __init__(self, anchor_type=None, sub_type=None, value=None, timestamp=None): @property def anchor_type(self): + """ + Returns the anchor type + + :return: the type + """ return self.__anchor_type @property def sub_type(self): + """ + Returns the anchor sub-type + + :return: the sub-type + """ return self.__sub_type @property def value(self): + """ + Returns the anchor value + + :return: the value + """ return self.__value @property def timestamp(self): + """ + Returns the anchor timestamp + + :return: the timestamp + """ return self.__timestamp def __dict__(self): @@ -46,6 +66,11 @@ def __dict__(self): @staticmethod def builder(): + """ + Creates an instance of the sandbox anchor builder + + :return: instance of SandboxAnchorBuilder + """ return SandboxAnchorBuilder() @@ -57,22 +82,51 @@ def __init__(self): self.__timestamp = None def with_type(self, type): + """ + Sets the type of the anchor on the builder + + :param str type: the anchor type + :return: the updated builder + """ self.__type = type return self def with_value(self, value): + """ + Sets the value of the anchor on the builder + + :param str value: the anchor value + :return: the updated builder + """ self.__value = value return self def with_sub_type(self, sub_type): + """ + Sets the sub type of the anchor on the builder + + :param str sub_type: the anchor sub type + :return: the updated builder + """ self.__sub_type = sub_type return self def with_timestamp(self, timestamp): + """ + Sets the timestamp of the anchor on the builder + + :param int timestamp: the anchor timestamp + :return: the updated builder + """ self.__timestamp = timestamp return self def build(self): + """ + Creates a SandboxAnchor using values supplied to the builder + + :return: the sandbox anchor + """ return SandboxAnchor( self.__type, self.__sub_type, self.__value, self.__timestamp ) diff --git a/yoti_python_sdk/sandbox/attribute.py b/yoti_python_sdk/sandbox/attribute.py index e4d359ad..fb6eb505 100644 --- a/yoti_python_sdk/sandbox/attribute.py +++ b/yoti_python_sdk/sandbox/attribute.py @@ -22,30 +22,60 @@ def __init__(self, name=None, value=None, anchors=None, derivation=None): @property def name(self): + """ + Returns the name of the attribute + + :return: the name + """ return self.__name @property def value(self): + """ + Returns the value of the attribute + + :return: the value + """ return self.__value @property def anchors(self): + """ + Returns the anchors associated with the attribute + + :return: the anchors + """ return self.__anchors @property def sources(self): + """ + Returns a filtered list of the associated anchors, only returning source anchors + + :return: list of filtered source anchors + """ return list( filter(lambda a: a.anchor_type == config.ANCHOR_SOURCE, self.__anchors) ) @property def verifiers(self): + """ + Returns a filtered list of the associated anchors, only returning verifier anchors + + :return: list of filtered verifier anchors + """ return list( filter(lambda a: a.anchor_type == config.ANCHOR_VERIFIER, self.__anchors) ) @property def derivation(self): + """ + Returns the derivation of the attribute + + :return: the derivation + """ return self.__derivation def __dict__(self): @@ -58,6 +88,11 @@ def __dict__(self): @staticmethod def builder(): + """ + Creates an instance of the sandbox attribute builder + + :return: the sandbox attribute builder + """ return SandboxAttributeBuilder() @@ -69,22 +104,51 @@ def __init__(self): self.__derivation = None def with_name(self, name): + """ + Sets the name of the attribute on the builder + + :param str name: the name of the attribute + :return: the updated builder + """ self.__name = name return self def with_value(self, value): + """ + Sets the value of the attribute on the builder + + :param value: the value of the attribute + :return: the updated builder + """ self.__value = value return self def with_anchors(self, anchors): + """ + Sets the list of anchors associated with the attribute + + :param list[SandboxAnchor] anchors: the associated anchors + :return: + """ self.__anchors = anchors return self def with_derivation(self, derivation): + """ + Sets the derivation of the attribute on the builder + + :param str derivation: the derivation + :return: the updated builder + """ self.__derivation = derivation return self def build(self): + """ + Create an instance of SandboxAttribute using values supplied to the builder + + :return: instance of SandboxAttribute + """ return SandboxAttribute( self.__name, self.__value, self.__anchors, self.__derivation ) diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py index 16dd5399..4a24ce5f 100644 --- a/yoti_python_sdk/sandbox/client.py +++ b/yoti_python_sdk/sandbox/client.py @@ -49,6 +49,15 @@ def __init__(self, sdk_id, pem_file, sandbox_url): self.__crypto = Crypto(pem_data) def setup_sharing_profile(self, request_token): + """ + Using the supplied YotiTokenRequest, this function will make a request + to the defined sandbox environment to create a profile with the supplied values. + The returned token can be used against the sandbox environment to retrieve the profile + using the standard YotiClient. + + :param YotiTokenRequest request_token: + :return: the token for accessing a profile + """ request_path = self.__endpoint.get_sandbox_path() response = SandboxClient.post( self.__sandbox_url, request_path, self.__crypto, request_token @@ -58,6 +67,11 @@ def setup_sharing_profile(self, request_token): @staticmethod def builder(): + """ + Creates an instance of the sandbox client builder + + :return: instance of SandboxClientBuilder + """ return SandboxClientBuilder() @staticmethod @@ -124,18 +138,42 @@ def __init__(self): self.__sandbox_url = None def for_application(self, sdk_id): + """ + Sets the application ID on the builder + + :param str sdk_id: the SDK ID supplied from Yoti Hub + :return: the updated builder + """ self.__sdk_id = sdk_id return self def with_pem_file(self, pem_file): + """ + Sets the pem file to be used on the builder + + :param str pem_file: path to the PEM file + :return: the updated builder + """ self.__pem_file = pem_file return self def with_sandbox_url(self, sandbox_url): + """ + Sets the URL of the sandbox environment on the builder + + :param str sandbox_url: the sandbox environment URL + :return: the updated builder + """ self.__sandbox_url = sandbox_url return self def build(self): + """ + Using all supplied values, create an instance of the SandboxClient. + + :raises ValueError: one or more of the values is None + :return: instance of SandboxClient + """ if ( self.__sdk_id is None or self.__pem_file is None diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py index 60d41b96..a11f5d46 100644 --- a/yoti_python_sdk/sandbox/token.py +++ b/yoti_python_sdk/sandbox/token.py @@ -9,12 +9,16 @@ def __init__(self, token): @property def token(self): + """ + The token to be used by the Client + + :return: the token + """ return self.__token class YotiTokenRequest(object): def __init__(self, remember_me_id=None, sandbox_attributes=None): - if remember_me_id is None: remember_me_id = "" @@ -32,6 +36,11 @@ def __dict__(self): @staticmethod def builder(): + """ + Creates an instance of the yoti token request builder + + :return: instance of YotiTokenRequestBuilder + """ return YotiTokenRequestBuilder() @@ -41,87 +50,201 @@ def __init__(self): self.attributes = [] def with_remember_me_id(self, remember_me_id): + """ + Sets the remember me id on the builder + + :param remember_me_id: the remember me id + :return: the updated builder + """ self.remember_me_id = remember_me_id return self def with_attribute(self, sandbox_attribute): + """ + Appends a SandboxAttribute to the list of attributes on the builder + + :param SandboxAttribute sandbox_attribute: + :return: the updated builder + """ self.attributes.append(sandbox_attribute) return self def with_given_names(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for given names + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_GIVEN_NAMES, value, anchors ) return self.with_attribute(attribute) def with_family_name(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for family name + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_FAMILY_NAME, value, anchors ) return self.with_attribute(attribute) def with_full_name(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for full name + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute(config.ATTRIBUTE_FULL_NAME, value, anchors) return self.with_attribute(attribute) def with_date_of_birth(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for date of birth + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_DATE_OF_BIRTH, value, anchors ) return self.with_attribute(attribute) def with_age_verification(self, age_verification): + """ + Creates and appends a SandboxAttribute with a given age verification + + :param SandboxAgeVerification age_verification: the age verification + :return: + """ return self.with_attribute(age_verification.to_attribute()) def with_gender(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for gender + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute(config.ATTRIBUTE_GENDER, value, anchors) return self.with_attribute(attribute) def with_phone_number(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for phone number + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_PHONE_NUMBER, value, anchors ) return self.with_attribute(attribute) def with_nationality(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for nationality + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_NATIONALITY, value, anchors ) return self.with_attribute(attribute) def with_postal_address(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for postal address + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_POSTAL_ADDRESS, value, anchors ) return self.with_attribute(attribute) def with_structured_postal_address(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for structured postal address + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS, value, anchors ) return self.with_attribute(attribute) def with_selfie(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for a selfie + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ base64_selfie = base64.b64encode(value).decode("utf-8") return self.with_base64_selfie(base64_selfie, anchors) def with_base64_selfie(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for given names + + :param str value: base64 encoded value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute(config.ATTRIBUTE_SELFIE, value, anchors) return self.with_attribute(attribute) def with_email_address(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for email address + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_EMAIL_ADDRESS, value, anchors ) return self.with_attribute(attribute) def with_document_details(self, value, anchors=None): + """ + Creates and appends a SandboxAttribute for document details + + :param str value: the value + :param list[SandboxAnchor] anchors: optional list of anchors + :return: + """ attribute = self.__create_attribute( config.ATTRIBUTE_DOCUMENT_DETAILS, value, anchors ) return self.with_attribute(attribute) def build(self): + """ + Creates an instance of YotiTokenRequest using the supplied values + + :return: instance of YotiTokenRequest + """ return YotiTokenRequest(self.remember_me_id, self.attributes) def __create_attribute(self, name, value, anchors=None): From 2b76efd6f8360c24fa1fdbc5b26cc14c742e9225 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 5 Sep 2019 14:56:02 +0100 Subject: [PATCH 52/74] SDK-602: Remove duplication of constants from sandbox modules --- yoti_python_sdk/sandbox/anchor.py | 7 +------ yoti_python_sdk/sandbox/client.py | 8 +++----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/yoti_python_sdk/sandbox/anchor.py b/yoti_python_sdk/sandbox/anchor.py index c03a62f9..4d85a2aa 100644 --- a/yoti_python_sdk/sandbox/anchor.py +++ b/yoti_python_sdk/sandbox/anchor.py @@ -1,9 +1,4 @@ -UNKNOWN_EXTENSION = "" -SOURCE_EXTENSION = "1.3.6.1.4.1.47127.1.1.1" -VERIFIER_EXTENSION = "1.3.6.1.4.1.47127.1.1.2" - -UNKNOWN_ANCHOR_TYPE = "Unknown" -UNKNOWN_ANCHOR_VALUE = "" +from yoti_python_sdk.anchor import UNKNOWN_ANCHOR_TYPE class SandboxAnchor(object): diff --git a/yoti_python_sdk/sandbox/client.py b/yoti_python_sdk/sandbox/client.py index 4a24ce5f..fa0a819f 100644 --- a/yoti_python_sdk/sandbox/client.py +++ b/yoti_python_sdk/sandbox/client.py @@ -14,6 +14,7 @@ X_YOTI_SDK_VERSION, JSON_CONTENT_TYPE, ) +from yoti_python_sdk.client import HTTP_SUPPORTED_METHODS from yoti_python_sdk.sandbox.token import YotiTokenRequest, YotiTokenResponse from yoti_python_sdk.sandbox.attribute import SandboxAttribute from yoti_python_sdk.sandbox.anchor import SandboxAnchor @@ -34,9 +35,6 @@ def default(self, o): class SandboxClient(object): - - HTTP_SUPPORTED_METHODS = ["POST", "PUT", "PATCH", "GET", "DELETE"] - def __init__(self, sdk_id, pem_file, sandbox_url): self.sdk_id = sdk_id self.__endpoint = SandboxEndpoint(sdk_id) @@ -114,10 +112,10 @@ def __read_pem_file(key_file_path, error_source): @staticmethod def __create_request(http_method, path, content): - if http_method not in SandboxClient.HTTP_SUPPORTED_METHODS: + if http_method not in HTTP_SUPPORTED_METHODS: raise ValueError( "{} is not in the list of supported methods: {}".format( - http_method, SandboxClient.HTTP_SUPPORTED_METHODS + http_method, HTTP_SUPPORTED_METHODS ) ) From 32523310618d0e782cd03ee29ebaac269e466ab5 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 14 Aug 2019 18:30:39 +0100 Subject: [PATCH 53/74] SDK-1132: Add code coverage and testing for Sonaqube --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index a8c26a58..d1e324ae 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,4 +4,4 @@ sonar.projectVersion = 2.8.2 sonar.exclusions=yoti_python_sdk/tests/**,examples/** sonar.python.pylint.reportPath=coverage.out -sonar.verbose = true \ No newline at end of file +sonar.verbose = true From ec32204e368300303e6562a2972af3c8245289c6 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 09:31:59 +0100 Subject: [PATCH 54/74] SDK-1132: Update pytest version and add pytest-cov and pylint as dependencies --- requirements.in | 4 +++- requirements.txt | 27 ++++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/requirements.in b/requirements.in index 6ec178ff..fcf4a3e5 100644 --- a/requirements.in +++ b/requirements.in @@ -7,7 +7,9 @@ mock==2.0.0 pbr==1.10.0 protobuf==3.7.0 pyopenssl==18.0.0 -pytest==3.3.2 +pytest==5.0.1 +pytest-cov==2.7.1 +pylint==2.3.1 pytz==2018.9 requests>=2.20.0 urllib3>=1.24.2 diff --git a/requirements.txt b/requirements.txt index f4c72ff6..a9ef5c9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,40 +7,53 @@ asn1==2.2.0 asn1crypto==0.24.0 # via cryptography aspy.yaml==1.3.0 # via pre-commit -attrs==18.1.0 # via pytest +astroid==2.2.5 # via pylint +atomicwrites==1.3.0 # via pytest +attrs==18.1.0 # via packaging, 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 +coverage==4.5.4 # via pytest-cov 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-metadata==0.18 # via pluggy, pre-commit, pytest importlib-resources==1.0.2 # via pre-commit +isort==4.3.21 # via pylint itsdangerous==0.24 +lazy-object-proxy==1.4.1 # via astroid +mccabe==0.6.1 # via pylint mock==2.0.0 +more-itertools==7.2.0 # via pytest nodeenv==1.3.3 # via pre-commit +packaging==19.1 # via pytest pbr==1.10.0 -pluggy==0.6.0 # via pytest +pluggy==0.12.0 # via pytest pre-commit==1.17.0 protobuf==3.7.0 py==1.5.3 # via pytest pycparser==2.18 # via cffi +pylint==2.3.1 pyopenssl==18.0.0 -pytest==3.3.2 +pyparsing==2.4.2 # via packaging +pytest-cov==2.7.1 +pytest==5.0.1 pytz==2018.9 pyyaml==5.1.1 # via aspy.yaml, pre-commit requests==2.21.0 -six==1.10.0 # via cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest +six==1.10.0 # via astroid, cfgv, cryptography, mock, packaging, pre-commit, protobuf, pyopenssl toml==0.10.0 # via pre-commit +typed-ast==1.4.0 # via astroid urllib3==1.24.2 virtualenv==15.2.0 +wcwidth==0.1.7 # via pytest wheel==0.24.0 -wrapt==1.11.2 # via deprecated +wrapt==1.11.2 # via astroid, deprecated zipp==0.5.2 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -# setuptools==41.1.0 # via protobuf, pytest +# setuptools==41.1.0 # via protobuf From ff373a571acb1f62a1460c2ae293271d82ec37fc Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 09:40:09 +0100 Subject: [PATCH 55/74] SDK-1132: Update pytest version to try and make it compatible with older python versions --- requirements.in | 2 +- requirements.txt | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/requirements.in b/requirements.in index fcf4a3e5..a33531fe 100644 --- a/requirements.in +++ b/requirements.in @@ -7,7 +7,7 @@ mock==2.0.0 pbr==1.10.0 protobuf==3.7.0 pyopenssl==18.0.0 -pytest==5.0.1 +pytest==3.6 pytest-cov==2.7.1 pylint==2.3.1 pytz==2018.9 diff --git a/requirements.txt b/requirements.txt index a9ef5c9e..f5c0291b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ asn1crypto==0.24.0 # via cryptography aspy.yaml==1.3.0 # via pre-commit astroid==2.2.5 # via pylint atomicwrites==1.3.0 # via pytest -attrs==18.1.0 # via packaging, pytest +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 @@ -21,7 +21,7 @@ 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 pluggy, pre-commit, pytest +importlib-metadata==0.18 # via pre-commit importlib-resources==1.0.2 # via pre-commit isort==4.3.21 # via pylint itsdangerous==0.24 @@ -30,30 +30,27 @@ mccabe==0.6.1 # via pylint mock==2.0.0 more-itertools==7.2.0 # via pytest nodeenv==1.3.3 # via pre-commit -packaging==19.1 # via pytest pbr==1.10.0 -pluggy==0.12.0 # via pytest +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 pylint==2.3.1 pyopenssl==18.0.0 -pyparsing==2.4.2 # via packaging pytest-cov==2.7.1 -pytest==5.0.1 +pytest==3.6.0 pytz==2018.9 pyyaml==5.1.1 # via aspy.yaml, pre-commit requests==2.21.0 -six==1.10.0 # via astroid, cfgv, cryptography, mock, packaging, pre-commit, protobuf, pyopenssl +six==1.10.0 # via astroid, cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest toml==0.10.0 # via pre-commit typed-ast==1.4.0 # via astroid urllib3==1.24.2 virtualenv==15.2.0 -wcwidth==0.1.7 # via pytest wheel==0.24.0 wrapt==1.11.2 # via astroid, deprecated zipp==0.5.2 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -# setuptools==41.1.0 # via protobuf +# setuptools==41.1.0 # via protobuf, pytest From bf614a034f6b16cbf69331de6a466a880a16f454 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 15 Aug 2019 09:53:11 +0100 Subject: [PATCH 56/74] SDK-826: Remove pytest-cov and pylint due to python 2.7 incompatability --- requirements.in | 4 +--- requirements.txt | 16 +++------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/requirements.in b/requirements.in index a33531fe..6ec178ff 100644 --- a/requirements.in +++ b/requirements.in @@ -7,9 +7,7 @@ mock==2.0.0 pbr==1.10.0 protobuf==3.7.0 pyopenssl==18.0.0 -pytest==3.6 -pytest-cov==2.7.1 -pylint==2.3.1 +pytest==3.3.2 pytz==2018.9 requests>=2.20.0 urllib3>=1.24.2 diff --git a/requirements.txt b/requirements.txt index f5c0291b..f4c72ff6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,15 +7,12 @@ asn1==2.2.0 asn1crypto==0.24.0 # via cryptography aspy.yaml==1.3.0 # via pre-commit -astroid==2.2.5 # via pylint -atomicwrites==1.3.0 # via pytest 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 -coverage==4.5.4 # via pytest-cov cryptography==2.4.1 deprecated==1.2.6 future==0.15.2 @@ -23,12 +20,8 @@ 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 -isort==4.3.21 # via pylint itsdangerous==0.24 -lazy-object-proxy==1.4.1 # via astroid -mccabe==0.6.1 # via pylint mock==2.0.0 -more-itertools==7.2.0 # via pytest nodeenv==1.3.3 # via pre-commit pbr==1.10.0 pluggy==0.6.0 # via pytest @@ -36,20 +29,17 @@ pre-commit==1.17.0 protobuf==3.7.0 py==1.5.3 # via pytest pycparser==2.18 # via cffi -pylint==2.3.1 pyopenssl==18.0.0 -pytest-cov==2.7.1 -pytest==3.6.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 astroid, cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest +six==1.10.0 # via cfgv, cryptography, mock, pre-commit, protobuf, pyopenssl, pytest toml==0.10.0 # via pre-commit -typed-ast==1.4.0 # via astroid urllib3==1.24.2 virtualenv==15.2.0 wheel==0.24.0 -wrapt==1.11.2 # via astroid, deprecated +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: From d8589030bf3685a88c5b2ea8bd9451ca4c92c9a0 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 20 Aug 2019 16:14:52 +0100 Subject: [PATCH 57/74] SDK-602: Initial commit of Sandbox integration helper modules --- yoti_python_sdk/config.py | 6 ++++++ yoti_python_sdk/sandbox/anchor.py | 1 + yoti_python_sdk/sandbox/token.py | 1 + 3 files changed, 8 insertions(+) diff --git a/yoti_python_sdk/config.py b/yoti_python_sdk/config.py index 8b3b88c0..8b093a6a 100644 --- a/yoti_python_sdk/config.py +++ b/yoti_python_sdk/config.py @@ -36,3 +36,9 @@ ANCHOR_VALUE_PASS_CARD = "PASS_CARD" ANCHOR_VALUE_DRIVING_LICENCE = "DRIVING_LICENCE" +UNKNOWN_EXTENSION = "" +SOURCE_EXTENSION = "1.3.6.1.4.1.47127.1.1.1" +VERIFIER_EXTENSION = "1.3.6.1.4.1.47127.1.1.2" + +UNKNOWN_ANCHOR_TYPE = "Unknown" +UNKNOWN_ANCHOR_VALUE = "" diff --git a/yoti_python_sdk/sandbox/anchor.py b/yoti_python_sdk/sandbox/anchor.py index 4d85a2aa..6b340f19 100644 --- a/yoti_python_sdk/sandbox/anchor.py +++ b/yoti_python_sdk/sandbox/anchor.py @@ -5,6 +5,7 @@ class SandboxAnchor(object): def __init__(self, anchor_type=None, sub_type=None, value=None, timestamp=None): if anchor_type is None: anchor_type = UNKNOWN_ANCHOR_TYPE + if sub_type is None: sub_type = "" if value is None: diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py index a11f5d46..03cfaf0f 100644 --- a/yoti_python_sdk/sandbox/token.py +++ b/yoti_python_sdk/sandbox/token.py @@ -247,5 +247,6 @@ def build(self): """ return YotiTokenRequest(self.remember_me_id, self.attributes) + @staticmethod def __create_attribute(self, name, value, anchors=None): return SandboxAttribute(name, value, anchors) From a38802687954360319a3e8bd1548c87bc1518a34 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 21 Aug 2019 09:45:38 +0100 Subject: [PATCH 58/74] SDK-602: Move anchor constants back out of config module until major release: --- yoti_python_sdk/config.py | 7 ------- yoti_python_sdk/sandbox/anchor.py | 1 - 2 files changed, 8 deletions(-) diff --git a/yoti_python_sdk/config.py b/yoti_python_sdk/config.py index 8b093a6a..6500f78c 100644 --- a/yoti_python_sdk/config.py +++ b/yoti_python_sdk/config.py @@ -35,10 +35,3 @@ ANCHOR_VALUE_NATIONAL_ID = "NATIONAL_ID" ANCHOR_VALUE_PASS_CARD = "PASS_CARD" ANCHOR_VALUE_DRIVING_LICENCE = "DRIVING_LICENCE" - -UNKNOWN_EXTENSION = "" -SOURCE_EXTENSION = "1.3.6.1.4.1.47127.1.1.1" -VERIFIER_EXTENSION = "1.3.6.1.4.1.47127.1.1.2" - -UNKNOWN_ANCHOR_TYPE = "Unknown" -UNKNOWN_ANCHOR_VALUE = "" diff --git a/yoti_python_sdk/sandbox/anchor.py b/yoti_python_sdk/sandbox/anchor.py index 6b340f19..4d85a2aa 100644 --- a/yoti_python_sdk/sandbox/anchor.py +++ b/yoti_python_sdk/sandbox/anchor.py @@ -5,7 +5,6 @@ class SandboxAnchor(object): def __init__(self, anchor_type=None, sub_type=None, value=None, timestamp=None): if anchor_type is None: anchor_type = UNKNOWN_ANCHOR_TYPE - if sub_type is None: sub_type = "" if value is None: From a53c06f57c0641939f1963467cb08669f02fd390 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 16:11:40 +0100 Subject: [PATCH 59/74] SDK-1076: Add initial implementation of SignedRequest and SignedRequestBuilder, used for created signed requests to Yoti API's --- yoti_python_sdk/crypto.py | 18 ++++++ yoti_python_sdk/http.py | 128 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 yoti_python_sdk/http.py diff --git a/yoti_python_sdk/crypto.py b/yoti_python_sdk/crypto.py index abaebfe9..55ebd7b1 100644 --- a/yoti_python_sdk/crypto.py +++ b/yoti_python_sdk/crypto.py @@ -5,6 +5,9 @@ from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from os.path import isfile, expanduser +from past.builtins import basestring + class Crypto: def __init__(self, pem_container): @@ -65,3 +68,18 @@ def strip_pkcs5_padding(data): if isinstance(stripped, bytearray): stripped = str(stripped) return stripped + + @staticmethod + def read_pem_file(key_file_path): + try: + key_file_path = expanduser(key_file_path) + + if not isinstance(key_file_path, basestring) or not isfile(key_file_path): + raise IOError("File not found: {0}".format(key_file_path)) + + with open(key_file_path, "rb") as pem_file: + return Crypto(pem_file.read().strip()) + except (AttributeError, IOError, TypeError, OSError) as exc: + error = "Could not read private key file: '{0}'".format(key_file_path) + exception = "{0}: {1}".format(type(exc).__name__, exc) + raise RuntimeError("{0}: {1}".format(error, exception)) diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py new file mode 100644 index 00000000..f6b7f798 --- /dev/null +++ b/yoti_python_sdk/http.py @@ -0,0 +1,128 @@ +from yoti_python_sdk.crypto import Crypto +from yoti_python_sdk.config import ( + X_YOTI_AUTH_KEY, + X_YOTI_AUTH_DIGEST, + X_YOTI_SDK, + SDK_IDENTIFIER, + X_YOTI_SDK_VERSION, + JSON_CONTENT_TYPE, +) +from cryptography.fernet import base64 +from urllib.parse import urlencode + +import yoti_python_sdk +import requests +import uuid +import time + +HTTP_POST = "POST" +HTTP_GET = "GET" +HTTP_SUPPORTED_METHODS = ["POST", "PUT", "PATCH", "GET", "DELETE"] + + +class SignedRequest(object): + def __init__(self, base_url, crypto): + self.__base_url = base_url + self.__crypto = crypto + + def do_request(self, endpoint, http_method, payload=None, query_params=None): + endpoint = endpoint + self.__append_query_params(query_params) + headers = self.__get_request_headers(endpoint, http_method, payload) + url = self.__base_url + endpoint + + return requests.request( + method=http_method, + url=url, + data=payload, + headers=headers, + verify=yoti_python_sdk.YOTI_API_VERIFY_SSL, + ) + + def post(self, endpoint, payload=None, query_params=None): + return self.do_request( + endpoint, HTTP_POST, payload=payload, query_params=query_params + ) + + def get(self, endpoint, query_params=None): + return self.do_request(endpoint, HTTP_GET, query_params=query_params) + + def __append_query_params(self, query_params=None): + required = { + "nonce": self.__create_nonce(), + "timestamp": self.__create_timestamp(), + } + + query_params = self.__merge_query_params(query_params, required) + return "?{}".format(urlencode(query_params)) + + @staticmethod + def __merge_query_params(query_params, required): + if query_params is None: + return required + + merged = query_params.copy(query_params) + merged.update(required) + return merged + + def __get_request_headers(self, path, http_method, content): + request = self.__create_request(http_method, path, content) + sdk_version = yoti_python_sdk.__version__ + + return { + X_YOTI_AUTH_KEY: self.__crypto.get_public_key(), + X_YOTI_AUTH_DIGEST: self.__crypto.sign(request), + X_YOTI_SDK: SDK_IDENTIFIER, + X_YOTI_SDK_VERSION: "{0}-{1}".format(SDK_IDENTIFIER, sdk_version), + "Content-Type": JSON_CONTENT_TYPE, + "Accept": JSON_CONTENT_TYPE, + } + + @staticmethod + def __create_request(http_method, path, content): + if http_method not in HTTP_SUPPORTED_METHODS: + raise ValueError( + "{} is not in the list of supported methods: {}".format( + http_method, HTTP_SUPPORTED_METHODS + ) + ) + + request = "{}&{}".format(http_method, path) + + if content is not None: + b64encoded = base64.b64encode(content) + b64ascii = b64encoded.decode("ascii") + request += "&" + b64ascii + + return request + + @staticmethod + def __create_nonce(): + return uuid.uuid4() + + @staticmethod + def __create_timestamp(): + return int(time.time() * 1000) + + @staticmethod + def builder(): + return SignedRequestBuilder() + + +class SignedRequestBuilder(object): + def __init__(self): + self.__crypto = None + self.__base_url = None + + def with_pem_file(self, pem_file_path): + self.__crypto = Crypto.read_pem_file(pem_file_path) + return self + + def with_base_url(self, base_url): + self.__base_url = base_url + return self + + def build(self): + if self.__crypto is None or self.__base_url is None: + raise ValueError("Crypto and base URL must not be None") + + return SignedRequest(self.__base_url, self.__crypto) From e26034834664463d3ecf8849a5f414a13301f99a Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 17:10:24 +0100 Subject: [PATCH 60/74] SDK-1076: Update client to use new signed request in get_activity_details --- yoti_python_sdk/client.py | 98 +++++++------------------------------ yoti_python_sdk/endpoint.py | 22 +++++++-- yoti_python_sdk/http.py | 8 ++- 3 files changed, 43 insertions(+), 85 deletions(-) diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index 0352ecdf..6c708377 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -3,26 +3,16 @@ import json from os import environ -from os.path import isfile, expanduser import requests -from cryptography.fernet import base64 -from past.builtins import basestring import yoti_python_sdk from yoti_python_sdk import aml from yoti_python_sdk.activity_details import ActivityDetails from yoti_python_sdk.crypto import Crypto from yoti_python_sdk.endpoint import Endpoint +from yoti_python_sdk.http import SignedRequest from yoti_python_sdk.protobuf import protobuf -from .config import ( - X_YOTI_AUTH_KEY, - X_YOTI_AUTH_DIGEST, - X_YOTI_SDK, - SDK_IDENTIFIER, - X_YOTI_SDK_VERSION, - JSON_CONTENT_TYPE, -) NO_KEY_FILE_SPECIFIED_ERROR = ( "Please specify the correct private key file " @@ -38,45 +28,26 @@ class Client(object): def __init__(self, sdk_id=None, pem_file_path=None): self.sdk_id = sdk_id or environ.get("YOTI_CLIENT_SDK_ID") - pem_file_path_env = environ.get("YOTI_KEY_FILE_PATH") - - if pem_file_path is not None: - error_source = "argument specified in Client()" - pem = self.__read_pem_file(pem_file_path, error_source) - elif pem_file_path_env is not None: - error_source = "specified by the YOTI_KEY_FILE_PATH env variable" - pem = self.__read_pem_file(pem_file_path_env, error_source) - else: - raise RuntimeError(NO_KEY_FILE_SPECIFIED_ERROR) + pem_file_path_env = environ.get("YOTI_KEY_FILE_PATH", pem_file_path) - self.__crypto = Crypto(pem) - self.__endpoint = Endpoint(sdk_id) + self.__crypto = Crypto.read_pem_file(pem_file_path_env) - @staticmethod - def __read_pem_file(key_file_path, error_source): - try: - key_file_path = expanduser(key_file_path) - - if not isinstance(key_file_path, basestring) or not isfile(key_file_path): - raise IOError("File not found: {0}".format(key_file_path)) - with open(key_file_path, "rb") as pem_file: - return pem_file.read().strip() - except (AttributeError, IOError, TypeError, OSError) as exc: - error = 'Could not read private key file: "{0}", passed as: {1} '.format( - key_file_path, error_source - ) - exception = "{0}: {1}".format(type(exc).__name__, exc) - raise RuntimeError("{0}: {1}".format(error, exception)) + self.__signed_request = ( + SignedRequest.builder() + .with_pem_file(self.__crypto) + .with_base_url(yoti_python_sdk.YOTI_API_ENDPOINT) + .build() + ) + self.__endpoint = Endpoint(sdk_id) def get_activity_details(self, encrypted_request_token): - proto = protobuf.Protobuf() - http_method = "GET" content = None response = self.__make_activity_details_request( - encrypted_request_token, http_method, content + encrypted_request_token, content ) receipt = json.loads(response.text).get("receipt") + proto = protobuf.Protobuf() encrypted_data = proto.current_user(receipt) encrypted_application_profile = proto.current_application(receipt) @@ -147,19 +118,17 @@ def http_error_handler(response, error_messages={}): else: raise RuntimeError(UNKNOWN_HTTP_ERROR.format(status_code, response.text)) - def __make_activity_details_request( - self, encrypted_request_token, http_method, content - ): + def __make_activity_details_request(self, encrypted_request_token): decrypted_token = self.__crypto.decrypt_token(encrypted_request_token).decode( "utf-8" ) - 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_python_sdk.YOTI_API_VERIFY_SSL + query_params = {"appId": self.sdk_id} + path = self.__endpoint.get_activity_details_request_path( + decrypted_token, no_params=True ) + response = self.__signed_request.get(path, query_params) + self.http_error_handler( response, {"default": "Unsuccessful Yoti API call: {} {}"} ) @@ -185,34 +154,3 @@ def __make_aml_check_request(self, http_method, aml_profile): ) return response - - def __get_request_headers(self, path, http_method, content): - request = self.__create_request(http_method, path, content) - sdk_version = yoti_python_sdk.__version__ - - return { - X_YOTI_AUTH_KEY: self.__crypto.get_public_key(), - X_YOTI_AUTH_DIGEST: self.__crypto.sign(request), - X_YOTI_SDK: SDK_IDENTIFIER, - X_YOTI_SDK_VERSION: "{0}-{1}".format(SDK_IDENTIFIER, sdk_version), - "Content-Type": JSON_CONTENT_TYPE, - "Accept": JSON_CONTENT_TYPE, - } - - @staticmethod - def __create_request(http_method, path, content): - if http_method not in HTTP_SUPPORTED_METHODS: - raise ValueError( - "{} is not in the list of supported methods: {}".format( - http_method, HTTP_SUPPORTED_METHODS - ) - ) - - request = "{}&{}".format(http_method, path) - - if content is not None: - b64encoded = base64.b64encode(content) - b64ascii = b64encoded.decode("ascii") - request += "&" + b64ascii - - return request diff --git a/yoti_python_sdk/endpoint.py b/yoti_python_sdk/endpoint.py index 0ba3f2e2..d0c2a16a 100644 --- a/yoti_python_sdk/endpoint.py +++ b/yoti_python_sdk/endpoint.py @@ -1,3 +1,4 @@ +from deprecated import deprecated import time import uuid @@ -6,7 +7,14 @@ class Endpoint(object): def __init__(self, sdk_id): self.sdk_id = sdk_id - def get_activity_details_request_path(self, decrypted_request_token): + def get_activity_details_request_path( + self, decrypted_request_token, no_params=False + ): + # TODO: Remove no_params in 3.0.0 and get rid of the query parameters in the formatted + # TODO: string + if no_params: + return "/profile/{0}".format(decrypted_request_token) + return "/profile/{0}?nonce={1}×tamp={2}&appId={3}".format( decrypted_request_token, self.__create_nonce(), @@ -14,12 +22,18 @@ def get_activity_details_request_path(self, decrypted_request_token): self.sdk_id, ) - def get_aml_request_url(self): + def get_aml_request_url(self, no_params=False): + if no_params: + return "/aml-check" + return "/aml-check?appId={0}×tamp={1}&nonce={2}".format( self.sdk_id, self.__create_timestamp(), self.__create_nonce() ) - def get_dynamic_share_request_url(self): + def get_dynamic_share_request_url(self, no_params=False): + if no_params: + return "/qrcodes/app/{appid}".format(appid=self.sdk_id) + return "/qrcodes/apps/{appid}?nonce={nonce}×tamp={timestamp}".format( appid=self.sdk_id, nonce=self.__create_nonce(), @@ -27,9 +41,11 @@ def get_dynamic_share_request_url(self): ) @staticmethod + @deprecated def __create_nonce(): return uuid.uuid4() @staticmethod + @deprecated def __create_timestamp(): return int(time.time() * 1000) diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index f6b7f798..1352d45b 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -113,8 +113,12 @@ def __init__(self): self.__crypto = None self.__base_url = None - def with_pem_file(self, pem_file_path): - self.__crypto = Crypto.read_pem_file(pem_file_path) + def with_pem_file(self, pem_file): + if isinstance(pem_file, Crypto): + self.__crypto = pem_file + else: + self.__crypto = Crypto.read_pem_file(pem_file) + return self def with_base_url(self, base_url): From 352e40a254b0a95c167c4b87593a95f079d4fc69 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 17:20:34 +0100 Subject: [PATCH 61/74] SDK-1076: Fix client passing wrong params when getting activity details and syntax error in SignedRequest object --- yoti_python_sdk/client.py | 5 +---- yoti_python_sdk/http.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index 6c708377..f78a2e9e 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -41,10 +41,7 @@ def __init__(self, sdk_id=None, pem_file_path=None): self.__endpoint = Endpoint(sdk_id) def get_activity_details(self, encrypted_request_token): - content = None - response = self.__make_activity_details_request( - encrypted_request_token, content - ) + response = self.__make_activity_details_request(encrypted_request_token) receipt = json.loads(response.text).get("receipt") proto = protobuf.Protobuf() diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index 1352d45b..7ad77050 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -60,7 +60,7 @@ def __merge_query_params(query_params, required): if query_params is None: return required - merged = query_params.copy(query_params) + merged = query_params.copy() merged.update(required) return merged From 7523485695e8f80dd112bb633624a7a39eb1bd7d Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Fri, 23 Aug 2019 17:25:58 +0100 Subject: [PATCH 62/74] SDK-1076: Update http module to try importing urllib first, then urlparse for python 2.7 support --- yoti_python_sdk/http.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index 7ad77050..4ac37017 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -8,13 +8,17 @@ JSON_CONTENT_TYPE, ) from cryptography.fernet import base64 -from urllib.parse import urlencode import yoti_python_sdk import requests import uuid import time +try: + from urllib.parse import urlencode +except ImportError: + from urlparse import urlencode + HTTP_POST = "POST" HTTP_GET = "GET" HTTP_SUPPORTED_METHODS = ["POST", "PUT", "PATCH", "GET", "DELETE"] From 2b1d4cb0ad5b1cfc16e88034689a869cbfb737c0 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 3 Sep 2019 18:02:26 +0100 Subject: [PATCH 63/74] SDK-1076: Update SignedRequest and SignedRequestBuilder to use new format of creating a signed request --- yoti_python_sdk/client.py | 20 ++-- yoti_python_sdk/http.py | 235 +++++++++++++++++++++++++++++--------- 2 files changed, 195 insertions(+), 60 deletions(-) diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index f78a2e9e..c3e80c51 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -31,13 +31,6 @@ def __init__(self, sdk_id=None, pem_file_path=None): pem_file_path_env = environ.get("YOTI_KEY_FILE_PATH", pem_file_path) self.__crypto = Crypto.read_pem_file(pem_file_path_env) - - self.__signed_request = ( - SignedRequest.builder() - .with_pem_file(self.__crypto) - .with_base_url(yoti_python_sdk.YOTI_API_ENDPOINT) - .build() - ) self.__endpoint = Endpoint(sdk_id) def get_activity_details(self, encrypted_request_token): @@ -119,12 +112,21 @@ def __make_activity_details_request(self, encrypted_request_token): decrypted_token = self.__crypto.decrypt_token(encrypted_request_token).decode( "utf-8" ) - query_params = {"appId": self.sdk_id} path = self.__endpoint.get_activity_details_request_path( decrypted_token, no_params=True ) - response = self.__signed_request.get(path, query_params) + signed_request = ( + SignedRequest.builder() + .with_get() + .with_pem_file(self.__crypto) + .with_base_url(yoti_python_sdk.YOTI_API_ENDPOINT) + .with_endpoint(path) + .with_param("appId", self.sdk_id) + .build() + ) + + response = signed_request.execute() self.http_error_handler( response, {"default": "Unsuccessful Yoti API call: {} {}"} diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index 4ac37017..cb876d3f 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -25,54 +25,175 @@ class SignedRequest(object): - def __init__(self, base_url, crypto): - self.__base_url = base_url - self.__crypto = crypto + def __init__(self, url, http_method, payload, headers): + self.__url = url + self.__http_method = http_method + self.__payload = payload + self.__headers = headers - def do_request(self, endpoint, http_method, payload=None, query_params=None): - endpoint = endpoint + self.__append_query_params(query_params) - headers = self.__get_request_headers(endpoint, http_method, payload) - url = self.__base_url + endpoint + @property + def url(self): + """ + Returns the URL for the SignedRequest + """ + return self.__url - return requests.request( - method=http_method, - url=url, - data=payload, - headers=headers, - verify=yoti_python_sdk.YOTI_API_VERIFY_SSL, - ) + @property + def method(self): + """ + Returns the HTTP method for the SignedRequest + """ + return self.__http_method + + @property + def data(self): + """ + Returns the payload data for the SignedRequest + """ + return self.__payload + + @property + def headers(self): + """ + Returns the HTTP headers for the SignedRequest + """ + return self.__headers - def post(self, endpoint, payload=None, query_params=None): - return self.do_request( - endpoint, HTTP_POST, payload=payload, query_params=query_params + def prepare(self): + """ + Creates a PreparedRequest object for use in a requests Session + """ + r = requests.Request( + method=self.method, url=self.url, headers=self.headers, data=self.data ) + return r.prepare() + + def execute(self): + """ + Creates and sends a PreparedRequest in a requests Session, returning the requests Response object + """ + prepared = self.prepare() + with requests.Session() as s: + return s.send(prepared) + + @staticmethod + def builder(): + """ + Returns an instance of SignedRequestBuilder + """ + return SignedRequestBuilder() + - def get(self, endpoint, query_params=None): - return self.do_request(endpoint, HTTP_GET, query_params=query_params) +class SignedRequestBuilder(object): + def __init__(self): + self.__crypto = None + self.__base_url = None + self.__endpoint = None + self.__http_method = None + self.__params = None + self.__headers = None + self.__payload = None + + def with_pem_file(self, pem_file): + """ + Sets the PEM file to be used for signing the request. Can be an instance of yoti_python_sdk.crypto.Crypto + or a path to a PEM file + """ + if isinstance(pem_file, Crypto): + self.__crypto = pem_file + else: + self.__crypto = Crypto.read_pem_file(pem_file) + + return self + + def with_base_url(self, base_url): + """ + Sets the base URL for the signed request + """ + self.__base_url = base_url + return self + + def with_endpoint(self, endpoint): + """ + Sets the endpoint for the signed request + """ + self.__endpoint = endpoint + return self + + def with_param(self, name, value): + """ + Sets a query param to be used with the endpoint + """ + if self.__params is None: + self.__params = {} + + self.__params[name] = value + return self + + def with_header(self, name, value): + """ + Sets a HTTP header to be used in the request + """ + if self.__headers is None: + self.__headers = {} + + self.__headers[name] = value + return self + + def with_http_method(self, http_method): + """ + Sets the HTTP method to be used in the request + """ + self.__http_method = http_method + return self + + def with_post(self): + """ + Sets the HTTP method for a POST request + """ + self.with_http_method(HTTP_POST) + return self + + def with_get(self): + """ + Sets the HTTP method for a GET request + """ + self.__http_method = HTTP_GET + return self def __append_query_params(self, query_params=None): + """ + Appends supplied query params in a dict to default query params. + Returns a url encoded query param string + """ required = { "nonce": self.__create_nonce(), "timestamp": self.__create_timestamp(), } - query_params = self.__merge_query_params(query_params, required) + query_params = self.__merge_dictionary(query_params, required) return "?{}".format(urlencode(query_params)) @staticmethod - def __merge_query_params(query_params, required): - if query_params is None: - return required + def __merge_dictionary(a, b): + """ + Merges two dictionaries a and b, with b taking precedence over a + """ + if a is None: + return b - merged = query_params.copy() - merged.update(required) + merged = a.copy() + merged.update(b) return merged def __get_request_headers(self, path, http_method, content): + """ + Returns a dictionary of request headers, also using supplied headers from builder. Default headers take precedence. + """ request = self.__create_request(http_method, path, content) sdk_version = yoti_python_sdk.__version__ - return { + default = { X_YOTI_AUTH_KEY: self.__crypto.get_public_key(), X_YOTI_AUTH_DIGEST: self.__crypto.sign(request), X_YOTI_SDK: SDK_IDENTIFIER, @@ -81,8 +202,20 @@ def __get_request_headers(self, path, http_method, content): "Accept": JSON_CONTENT_TYPE, } + if self.__headers is not None: + return self.__merge_dictionary(self.__headers, default) + + return default + @staticmethod def __create_request(http_method, path, content): + """ + Creates a concatenated string that is used in the X-YOTI-AUTH-DIGEST header + :param http_method: + :param path: + :param content: + :return: + """ if http_method not in HTTP_SUPPORTED_METHODS: raise ValueError( "{} is not in the list of supported methods: {}".format( @@ -99,6 +232,20 @@ def __create_request(http_method, path, content): return request + def __validate_request(self): + """ + Validates the request object to ensure the required values + have been supplied. + """ + if self.__base_url is None: + raise ValueError("Base URL must not be None") + if self.__endpoint is None: + raise ValueError("Endpoint must not be None") + if self.__crypto is None: + raise ValueError("PEM file must not be None") + if self.__http_method is None: + raise ValueError("HTTP method must be specified") + @staticmethod def __create_nonce(): return uuid.uuid4() @@ -107,30 +254,16 @@ def __create_nonce(): def __create_timestamp(): return int(time.time() * 1000) - @staticmethod - def builder(): - return SignedRequestBuilder() - - -class SignedRequestBuilder(object): - def __init__(self): - self.__crypto = None - self.__base_url = None - - def with_pem_file(self, pem_file): - if isinstance(pem_file, Crypto): - self.__crypto = pem_file - else: - self.__crypto = Crypto.read_pem_file(pem_file) - - return self - - def with_base_url(self, base_url): - self.__base_url = base_url - return self - def build(self): - if self.__crypto is None or self.__base_url is None: - raise ValueError("Crypto and base URL must not be None") + """ + Builds a SignedRequest object with the supplied values + """ + self.__validate_request() + + endpoint = self.__endpoint + self.__append_query_params(self.__params) + headers = self.__get_request_headers( + endpoint, self.__http_method, self.__payload + ) + url = self.__base_url + endpoint - return SignedRequest(self.__base_url, self.__crypto) + return SignedRequest(url, self.__http_method, self.__payload, headers) From 6cdbfa2dc19bc1ffeb7e47f031779b7a43521faa Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 4 Sep 2019 11:43:13 +0100 Subject: [PATCH 64/74] SDK-1076: Update SignedRequest and SignedRequestBuilder with payload function, and write unit tests --- yoti_python_sdk/http.py | 30 ++-- yoti_python_sdk/tests/test_http.py | 249 +++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 13 deletions(-) create mode 100644 yoti_python_sdk/tests/test_http.py diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index cb876d3f..54714dc0 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -14,9 +14,9 @@ import uuid import time -try: +try: # pragma: no cover from urllib.parse import urlencode -except ImportError: +except ImportError: # pragma: no cover from urlparse import urlencode HTTP_POST = "POST" @@ -70,11 +70,11 @@ def prepare(self): def execute(self): """ - Creates and sends a PreparedRequest in a requests Session, returning the requests Response object + Send the signed request, returning the requests Response object """ - prepared = self.prepare() - with requests.Session() as s: - return s.send(prepared) + return requests.request( + url=self.url, method=self.method, data=self.data, headers=self.headers + ) @staticmethod def builder(): @@ -120,6 +120,13 @@ def with_endpoint(self, endpoint): self.__endpoint = endpoint return self + def with_payload(self, payload): + """ + Sets the payload for the signed request. Must be a valid JSON string + """ + self.__payload = payload + return self + def with_param(self, name, value): """ Sets a query param to be used with the endpoint @@ -144,6 +151,9 @@ def with_http_method(self, http_method): """ Sets the HTTP method to be used in the request """ + if http_method not in HTTP_SUPPORTED_METHODS: + raise ValueError("{} is an unsupported HTTP method".format(http_method)) + self.__http_method = http_method return self @@ -158,7 +168,7 @@ def with_get(self): """ Sets the HTTP method for a GET request """ - self.__http_method = HTTP_GET + self.with_http_method(HTTP_GET) return self def __append_query_params(self, query_params=None): @@ -216,12 +226,6 @@ def __create_request(http_method, path, content): :param content: :return: """ - if http_method not in HTTP_SUPPORTED_METHODS: - raise ValueError( - "{} is not in the list of supported methods: {}".format( - http_method, HTTP_SUPPORTED_METHODS - ) - ) request = "{}&{}".format(http_method, path) diff --git a/yoti_python_sdk/tests/test_http.py b/yoti_python_sdk/tests/test_http.py new file mode 100644 index 00000000..f0457a5c --- /dev/null +++ b/yoti_python_sdk/tests/test_http.py @@ -0,0 +1,249 @@ +try: + from unittest import mock +except ImportError: + import mock + +from yoti_python_sdk.http import SignedRequest +from yoti_python_sdk.crypto import Crypto +from yoti_python_sdk.tests import conftest +from requests import PreparedRequest + +from yoti_python_sdk.tests.mocks import mocked_requests_get + +import pytest +import json + + +@pytest.fixture(scope="module") +def json_payload(): + payload = {"Hello": "World"} + return json.dumps(payload).encode() + + +@pytest.fixture(scope="module") +def valid_base_url(): + return "https://localhost:8443/api/v1" + + +@pytest.fixture(scope="module") +def valid_endpoint(): + return "/profile" + + +@pytest.fixture(scope="module") +def expected_request_headers(): + return ["X-Yoti-Auth-Key", "X-Yoti-Auth-Digest", "X-Yoti-SDK", "X-Yoti-SDK-Version"] + + +def test_create_signed_request_get_required_properties( + valid_base_url, valid_endpoint, expected_request_headers +): + http_method = "GET" + + signed_request = ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_http_method(http_method) + .build() + ) + + assert (valid_base_url + valid_endpoint) in signed_request.url + assert signed_request.method == http_method + assert signed_request.data is None + + header_keys = signed_request.headers.keys() + for header in expected_request_headers: + assert header in header_keys + + +def test_create_signed_request_missing_pem_file(valid_base_url, valid_endpoint): + http_method = "GET" + + with pytest.raises(ValueError) as ex: + ( + SignedRequest.builder() + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_http_method(http_method) + .build() + ) + + assert "PEM file" in str(ex.value) + + +def test_create_signed_request_missing_base_url(valid_endpoint): + http_method = "GET" + + with pytest.raises(ValueError) as ex: + ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_endpoint(valid_endpoint) + .with_http_method(http_method) + .build() + ) + + assert "Base URL" in str(ex.value) + + +def test_create_signed_request_missing_endpoint(valid_base_url): + http_method = "GET" + + with pytest.raises(ValueError) as ex: + ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_http_method(http_method) + .build() + ) + + assert "Endpoint" in str(ex.value) + + +def test_create_signed_request_missing_http_method(valid_base_url, valid_endpoint): + with pytest.raises(ValueError) as ex: + ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .build() + ) + + assert "HTTP method" in str(ex.value) + + +def test_create_signed_request_with_invalid_http_method(): + + with pytest.raises(ValueError) as ex: + SignedRequest.builder().with_http_method("INVALID").build() + + assert str(ex.value) == "INVALID is an unsupported HTTP method" + + +def test_create_signed_request_with_payload( + valid_base_url, valid_endpoint, json_payload +): + http_method = "POST" + + signed_request = ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_http_method(http_method) + .with_payload(json_payload) + .build() + ) + + assert signed_request.data == json_payload + + +def test_create_signed_request_with_header(valid_base_url, valid_endpoint): + http_method = "POST" + + signed_request = ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_http_method(http_method) + .with_header("My-Http-Header", "someValue") + .build() + ) + + headers = signed_request.headers + + assert "My-Http-Header" in headers + assert headers["My-Http-Header"] == "someValue" + + +def test_create_signed_request_with_query_param(valid_base_url, valid_endpoint): + http_method = "POST" + + signed_request = ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_http_method(http_method) + .with_param("sdkId", "mySdkId") + .build() + ) + + assert "sdkId=mySdkId" in signed_request.url + + +def test_create_signed_request_with_crypto_object(valid_base_url, valid_endpoint): + http_method = "GET" + crypto = Crypto.read_pem_file(conftest.PEM_FILE_PATH) + + ( + SignedRequest.builder() + .with_pem_file(crypto) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_http_method(http_method) + .build() + ) + + +def test_create_signed_request_with_get_convenience_method( + valid_base_url, valid_endpoint +): + ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_get() + .build() + ) + + +def test_create_signed_request_with_post_convenience_method( + valid_base_url, valid_endpoint, json_payload +): + ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_post() + .with_payload(json_payload) + .build() + ) + + +def test_create_prepared_request_from_signed_request(valid_base_url, valid_endpoint): + signed_request = ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_post() + .build() + ) + + prepared_request = signed_request.prepare() + assert isinstance(prepared_request, PreparedRequest) + + +@mock.patch("requests.request", side_effect=mocked_requests_get) +def test_execute_signed_request(valid_base_url, valid_endpoint): + signed_request = ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_post() + .build() + ) + + response = signed_request.execute() + + assert response.status_code == 200 + assert response.text is not None From 36b730a857c6aaa50938346225e8fc196f5210f9 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 4 Sep 2019 15:58:13 +0100 Subject: [PATCH 65/74] SDK-1076: Add error source back to crypto file read method --- yoti_python_sdk/crypto.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yoti_python_sdk/crypto.py b/yoti_python_sdk/crypto.py index 55ebd7b1..02958072 100644 --- a/yoti_python_sdk/crypto.py +++ b/yoti_python_sdk/crypto.py @@ -70,7 +70,7 @@ def strip_pkcs5_padding(data): return stripped @staticmethod - def read_pem_file(key_file_path): + def read_pem_file(key_file_path, error_source=None): try: key_file_path = expanduser(key_file_path) @@ -80,6 +80,8 @@ def read_pem_file(key_file_path): with open(key_file_path, "rb") as pem_file: return Crypto(pem_file.read().strip()) except (AttributeError, IOError, TypeError, OSError) as exc: - error = "Could not read private key file: '{0}'".format(key_file_path) + error = "Could not read private key file: '{0}', passed as: {1}".format( + key_file_path, error_source + ) exception = "{0}: {1}".format(type(exc).__name__, exc) raise RuntimeError("{0}: {1}".format(error, exception)) From 3d63d4f97c1d379e36653d6db0eb54b767b36376 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 4 Sep 2019 16:01:26 +0100 Subject: [PATCH 66/74] SDK-1076: Update tests for SignedRequest and SignedRequestBuilder, as well as updating the rest of the SDK to make use of the new builders --- yoti_python_sdk/client.py | 72 ++++++---- .../dynamic_sharing_service/share_url.py | 2 +- yoti_python_sdk/http.py | 81 +++++++++--- yoti_python_sdk/tests/conftest.py | 6 + yoti_python_sdk/tests/mocks.py | 13 ++ yoti_python_sdk/tests/test_client.py | 124 ++++++------------ yoti_python_sdk/tests/test_http.py | 61 ++++++++- 7 files changed, 223 insertions(+), 136 deletions(-) diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index c3e80c51..b783fafb 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -4,8 +4,6 @@ import json from os import environ -import requests - import yoti_python_sdk from yoti_python_sdk import aml from yoti_python_sdk.activity_details import ActivityDetails @@ -26,12 +24,36 @@ class Client(object): - def __init__(self, sdk_id=None, pem_file_path=None): + def __init__(self, sdk_id=None, pem_file_path=None, request_handler=None): self.sdk_id = sdk_id or environ.get("YOTI_CLIENT_SDK_ID") pem_file_path_env = environ.get("YOTI_KEY_FILE_PATH", pem_file_path) - self.__crypto = Crypto.read_pem_file(pem_file_path_env) + if pem_file_path is not None: + error_source = "argument specified in Client()" + self.__crypto = Crypto.read_pem_file(pem_file_path, error_source) + elif pem_file_path_env is not None: + error_source = "specified by the YOTI_KEY_FILE_PATH env variable" + self.__crypto = Crypto.read_pem_file(pem_file_path_env, error_source) + else: + raise RuntimeError(NO_KEY_FILE_SPECIFIED_ERROR) + self.__endpoint = Endpoint(sdk_id) + self.__request_handler = request_handler + + def make_request(self, http_method, endpoint, body): + signed_request = ( + SignedRequest.builder() + .with_pem_file(self.__crypto) + .with_base_url(yoti_python_sdk.YOTI_API_ENDPOINT) + .with_endpoint(endpoint) + .with_http_method(http_method) + .with_payload(body) + .with_request_handler(self.__request_handler) + .build() + ) + + response = signed_request.execute() + return response def get_activity_details(self, encrypted_request_token): response = self.__make_activity_details_request(encrypted_request_token) @@ -70,24 +92,10 @@ def perform_aml_check(self, aml_profile): if aml_profile is None: raise TypeError("aml_profile not set") - http_method = "POST" - - response = self.__make_aml_check_request(http_method, aml_profile) + response = self.__make_aml_check_request(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, - ) - return response - @property def endpoints(self): return self.__endpoint @@ -123,6 +131,7 @@ def __make_activity_details_request(self, encrypted_request_token): .with_base_url(yoti_python_sdk.YOTI_API_ENDPOINT) .with_endpoint(path) .with_param("appId", self.sdk_id) + .with_request_handler(self.__request_handler) .build() ) @@ -134,20 +143,25 @@ def __make_activity_details_request(self, encrypted_request_token): return response - def __make_aml_check_request(self, http_method, aml_profile): + def __make_aml_check_request(self, aml_profile): aml_profile_json = json.dumps(aml_profile.__dict__, sort_keys=True) aml_profile_bytes = aml_profile_json.encode() - path = self.__endpoint.get_aml_request_url() - 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_python_sdk.YOTI_API_VERIFY_SSL, + path = self.__endpoint.get_aml_request_url(no_params=True) + + signed_request = ( + SignedRequest.builder() + .with_pem_file(self.__crypto) + .with_base_url(yoti_python_sdk.YOTI_API_ENDPOINT) + .with_endpoint(path) + .with_payload(aml_profile_bytes) + .with_param("appId", self.sdk_id) + .with_post() + .with_request_handler(self.__request_handler) + .build() ) + response = signed_request.execute() + self.http_error_handler( response, {"default": "Unsuccessful Yoti API call: {} {}"} ) diff --git a/yoti_python_sdk/dynamic_sharing_service/share_url.py b/yoti_python_sdk/dynamic_sharing_service/share_url.py index 31df805a..913fedb0 100644 --- a/yoti_python_sdk/dynamic_sharing_service/share_url.py +++ b/yoti_python_sdk/dynamic_sharing_service/share_url.py @@ -19,7 +19,7 @@ def create_share_url(yoti_client, dynamic_scenario): http_method = "POST" payload = json.dumps(dynamic_scenario, sort_keys=True).encode() - endpoint = yoti_client.endpoints.get_dynamic_share_request_url() + endpoint = yoti_client.endpoints.get_dynamic_share_request_url(no_params=True) response = yoti_client.make_request(http_method, endpoint, payload) client.Client.http_error_handler(response, SHARE_URL_ERRORS) diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index 54714dc0..1d3dc9f6 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -8,6 +8,7 @@ JSON_CONTENT_TYPE, ) from cryptography.fernet import base64 +from abc import ABCMeta, abstractmethod import yoti_python_sdk import requests @@ -17,19 +18,52 @@ try: # pragma: no cover from urllib.parse import urlencode except ImportError: # pragma: no cover - from urlparse import urlencode + from urllib import urlencode HTTP_POST = "POST" HTTP_GET = "GET" HTTP_SUPPORTED_METHODS = ["POST", "PUT", "PATCH", "GET", "DELETE"] +class RequestHandler: + """ + Default request handler for signing requests using the requests library. + This type can be inherited and the execute method overridden to use any + preferred HTTP library. + """ + + __metaclass__ = ABCMeta # Python 2 compatability + + @staticmethod + @abstractmethod + def execute(request): + return NotImplemented + + +class DefaultRequestHandler(RequestHandler): + @staticmethod + def execute(request): + """ + Execute the HTTP request supplied + """ + if not isinstance(request, SignedRequest): + raise TypeError("RequestHandler expects instance of SignedRequest") + + return requests.request( + url=request.url, + method=request.method, + data=request.data, + headers=request.headers, + ) + + class SignedRequest(object): - def __init__(self, url, http_method, payload, headers): + def __init__(self, url, http_method, payload, headers, request_handler=None): self.__url = url self.__http_method = http_method self.__payload = payload self.__headers = headers + self.__request_handler = request_handler @property def url(self): @@ -59,22 +93,15 @@ def headers(self): """ return self.__headers - def prepare(self): - """ - Creates a PreparedRequest object for use in a requests Session - """ - r = requests.Request( - method=self.method, url=self.url, headers=self.headers, data=self.data - ) - return r.prepare() - def execute(self): """ - Send the signed request, returning the requests Response object + Send the signed request, using the default RequestHandler if one has not be supplied """ - return requests.request( - url=self.url, method=self.method, data=self.data, headers=self.headers - ) + if self.__request_handler is None: + print("Using default request handler") + return DefaultRequestHandler.execute(self) + + return self.__request_handler.execute(self) @staticmethod def builder(): @@ -93,6 +120,7 @@ def __init__(self): self.__params = None self.__headers = None self.__payload = None + self.__request_handler = None def with_pem_file(self, pem_file): """ @@ -157,6 +185,25 @@ def with_http_method(self, http_method): self.__http_method = http_method return self + def with_request_handler(self, handler): + # If no handler is passed, just return as the default will be used + if handler is None: + return self + + try: + if not issubclass(handler, RequestHandler): + raise TypeError( + "Handler must be instance of yoti_python_sdk.http.RequestHandler" + ) + except Exception: + # ABC + raise TypeError( + "Handler must be instance of yoti_python_sdk.http.RequestHandler" + ) + + self.__request_handler = handler + return self + def with_post(self): """ Sets the HTTP method for a POST request @@ -270,4 +317,6 @@ def build(self): ) url = self.__base_url + endpoint - return SignedRequest(url, self.__http_method, self.__payload, headers) + return SignedRequest( + url, self.__http_method, self.__payload, headers, self.__request_handler + ) diff --git a/yoti_python_sdk/tests/conftest.py b/yoti_python_sdk/tests/conftest.py index e478886b..335e201e 100644 --- a/yoti_python_sdk/tests/conftest.py +++ b/yoti_python_sdk/tests/conftest.py @@ -6,6 +6,7 @@ from yoti_python_sdk import Client from yoti_python_sdk.crypto import Crypto +from yoti_python_sdk.tests.mocks import MockRequestHandler FIXTURES_DIR = join(dirname(abspath(__file__)), "fixtures") PEM_FILE_PATH = join(FIXTURES_DIR, "sdk-test.pem") @@ -17,6 +18,11 @@ YOTI_CLIENT_SDK_ID = "737204aa-d54e-49a4-8bde-26ddbe6d880c" +@pytest.fixture(scope="module") +def mock_request_handler(): + return MockRequestHandler + + @pytest.fixture(scope="module") def client(): return Client(YOTI_CLIENT_SDK_ID, PEM_FILE_PATH) diff --git a/yoti_python_sdk/tests/mocks.py b/yoti_python_sdk/tests/mocks.py index 8d916fb3..225ec33a 100644 --- a/yoti_python_sdk/tests/mocks.py +++ b/yoti_python_sdk/tests/mocks.py @@ -1,4 +1,5 @@ from uuid import UUID +from yoti_python_sdk.http import RequestHandler, SignedRequest class MockResponse: @@ -7,6 +8,18 @@ def __init__(self, status_code, text): self.text = text +class MockRequestHandler(RequestHandler): + @staticmethod + def execute(request): + """ + Execute the HTTP request supplied + """ + if not isinstance(request, SignedRequest): + raise RuntimeError("RequestHandler expects instance of SignedRequest") + + return mocked_requests_get() + + def mocked_requests_get(*args, **kwargs): with open("yoti_python_sdk/tests/fixtures/response.txt", "r") as f: response = f.read() diff --git a/yoti_python_sdk/tests/test_client.py b/yoti_python_sdk/tests/test_client.py index 72ef7c0f..81cdff0e 100644 --- a/yoti_python_sdk/tests/test_client.py +++ b/yoti_python_sdk/tests/test_client.py @@ -1,33 +1,19 @@ # -*- 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 datetime import datetime +from os import environ +from past.builtins import basestring + +from yoti_python_sdk import config 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 @@ -41,6 +27,17 @@ mocked_timestamp, mocked_uuid4, ) +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, +) + +import pytest +import yoti_python_sdk INVALID_KEY_FILE_PATH = "/invalid/path/to/file.txt" INVALID_KEY_FILES = ( @@ -149,7 +146,9 @@ def test_creating_client_instance_with_valid_key_file_env_but_invalid_key_file_a assert str(INVALID_KEY_FILE_PATH) in str(exc.value) -@mock.patch("requests.get", side_effect=mocked_requests_get) +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", 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( @@ -163,9 +162,6 @@ 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, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL - ) assert isinstance(activity_details, ActivityDetails) assert ( @@ -199,7 +195,10 @@ def test_requesting_activity_details_with_correct_data( assert "" in [anchor.value for anchor in phone_number.anchors] -@mock.patch("requests.get", side_effect=mocked_requests_get_null_profile) +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + 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( @@ -213,9 +212,6 @@ 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, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL - ) assert ( activity_details.user_id == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" @@ -228,7 +224,10 @@ def test_requesting_activity_details_with_null_profile( assert isinstance(activity_details, ActivityDetails) -@mock.patch("requests.get", side_effect=mocked_requests_get_empty_profile) +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + 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( @@ -242,9 +241,6 @@ 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, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL - ) assert ( activity_details.user_id == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" @@ -257,7 +253,10 @@ def test_requesting_activity_details_with_empty_profile( assert isinstance(activity_details, ActivityDetails) -@mock.patch("requests.get", side_effect=mocked_requests_get_missing_profile) +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + 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( @@ -271,9 +270,6 @@ 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, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL - ) assert ( activity_details.user_id == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" @@ -286,48 +282,10 @@ def test_requesting_activity_details_with_missing_profile( 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( + "yoti_python_sdk.http.SignedRequest.execute", + 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( @@ -341,12 +299,7 @@ def test_perform_aml_check_details_with_correct_data( 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, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL - ) + mock_post.assert_called_once() assert isinstance(aml_result, aml.AmlResult) assert isinstance(aml_result.on_watch_list, bool) @@ -363,7 +316,10 @@ def test_perform_aml_check_with_null_profile(client): assert expected_error in str(exc.value) -@mock.patch("requests.post", side_effect=mocked_requests_post_aml_profile_not_found) +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + 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( diff --git a/yoti_python_sdk/tests/test_http.py b/yoti_python_sdk/tests/test_http.py index f0457a5c..2dde002a 100644 --- a/yoti_python_sdk/tests/test_http.py +++ b/yoti_python_sdk/tests/test_http.py @@ -3,10 +3,9 @@ except ImportError: import mock -from yoti_python_sdk.http import SignedRequest +from yoti_python_sdk.http import SignedRequest, DefaultRequestHandler from yoti_python_sdk.crypto import Crypto from yoti_python_sdk.tests import conftest -from requests import PreparedRequest from yoti_python_sdk.tests.mocks import mocked_requests_get @@ -218,7 +217,8 @@ def test_create_signed_request_with_post_convenience_method( ) -def test_create_prepared_request_from_signed_request(valid_base_url, valid_endpoint): +@mock.patch("requests.request", side_effect=mocked_requests_get) +def test_execute_signed_request(valid_base_url, valid_endpoint): signed_request = ( SignedRequest.builder() .with_pem_file(conftest.PEM_FILE_PATH) @@ -228,17 +228,39 @@ def test_create_prepared_request_from_signed_request(valid_base_url, valid_endpo .build() ) - prepared_request = signed_request.prepare() - assert isinstance(prepared_request, PreparedRequest) + response = signed_request.execute() + + assert response.status_code == 200 + assert response.text is not None + + +def test_signed_request_with_custom_request_handler( + valid_base_url, valid_endpoint, mock_request_handler +): + signed_request = ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_request_handler(mock_request_handler) + .with_post() + .build() + ) + + response = signed_request.execute() + + assert response.status_code == 200 + assert response.text is not None @mock.patch("requests.request", side_effect=mocked_requests_get) -def test_execute_signed_request(valid_base_url, valid_endpoint): +def test_signed_request_passing_request_handler_as_none(valid_base_url, valid_endpoint): signed_request = ( SignedRequest.builder() .with_pem_file(conftest.PEM_FILE_PATH) .with_base_url(valid_base_url) .with_endpoint(valid_endpoint) + .with_request_handler(None) .with_post() .build() ) @@ -247,3 +269,30 @@ def test_execute_signed_request(valid_base_url, valid_endpoint): assert response.status_code == 200 assert response.text is not None + + +def test_signed_request_should_throw_error_when_request_handler_wrong_type( + valid_base_url, valid_endpoint +): + + with pytest.raises(TypeError) as ex: + ( + SignedRequest.builder() + .with_pem_file(conftest.PEM_FILE_PATH) + .with_base_url(valid_base_url) + .with_endpoint(valid_endpoint) + .with_request_handler("myRequestHandler") + .with_post() + .build() + ) + + assert "yoti_python_sdk.http.RequestHandler" in str(ex.value) + + +def test_default_request_handler_should_raise_exception_when_passed_non_signed_request( + valid_base_url, valid_endpoint +): + with pytest.raises(TypeError) as ex: + DefaultRequestHandler.execute("someBadValue") + + assert "RequestHandler expects instance of SignedRequest" == str(ex.value) From f26b3318f314850b19e54c3f813399d5c5adfdf2 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 4 Sep 2019 16:10:43 +0100 Subject: [PATCH 67/74] SDK-1076: Remove debug print statement from DefaultRequestHandler --- yoti_python_sdk/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index 1d3dc9f6..d0d9ada4 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -98,7 +98,6 @@ def execute(self): Send the signed request, using the default RequestHandler if one has not be supplied """ if self.__request_handler is None: - print("Using default request handler") return DefaultRequestHandler.execute(self) return self.__request_handler.execute(self) From 725a2a416813300223a9a8bd35604a18900bfff2 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Wed, 4 Sep 2019 16:38:51 +0100 Subject: [PATCH 68/74] SDK-1076: Update test to support 3.4 and 3.5 --- yoti_python_sdk/tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti_python_sdk/tests/test_client.py b/yoti_python_sdk/tests/test_client.py index 81cdff0e..52ff9115 100644 --- a/yoti_python_sdk/tests/test_client.py +++ b/yoti_python_sdk/tests/test_client.py @@ -299,7 +299,7 @@ def test_perform_aml_check_details_with_correct_data( aml_result = client.perform_aml_check(aml_profile) - mock_post.assert_called_once() + mock_post.assert_called_once_with() assert isinstance(aml_result, aml.AmlResult) assert isinstance(aml_result.on_watch_list, bool) From de533cefa9699153ca7ceb531d803b2bb3648622 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 5 Sep 2019 10:29:15 +0100 Subject: [PATCH 69/74] SDK-1076: Implement YotiResponse to standardise the response object used in the SDK: --- yoti_python_sdk/client.py | 11 ++++++++++- yoti_python_sdk/http.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py index b783fafb..61eb0bf4 100644 --- a/yoti_python_sdk/client.py +++ b/yoti_python_sdk/client.py @@ -9,7 +9,7 @@ from yoti_python_sdk.activity_details import ActivityDetails from yoti_python_sdk.crypto import Crypto from yoti_python_sdk.endpoint import Endpoint -from yoti_python_sdk.http import SignedRequest +from yoti_python_sdk.http import SignedRequest, YotiResponse from yoti_python_sdk.protobuf import protobuf NO_KEY_FILE_SPECIFIED_ERROR = ( @@ -53,6 +53,10 @@ def make_request(self, http_method, endpoint, body): ) response = signed_request.execute() + + if not isinstance(response, YotiResponse): + raise TypeError("Response must be of type YotiResponse") + return response def get_activity_details(self, encrypted_request_token): @@ -94,6 +98,9 @@ def perform_aml_check(self, aml_profile): response = self.__make_aml_check_request(aml_profile) + if not isinstance(response, YotiResponse): + raise TypeError("Response must be of type YotiResponse") + return aml.AmlResult(response.text) @property @@ -161,6 +168,8 @@ def __make_aml_check_request(self, aml_profile): ) response = signed_request.execute() + if not isinstance(response, YotiResponse): + raise TypeError("Response must be of type YotiResponse") self.http_error_handler( response, {"default": "Unsuccessful Yoti API call: {} {}"} diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index d0d9ada4..7d883db6 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -25,11 +25,17 @@ HTTP_SUPPORTED_METHODS = ["POST", "PUT", "PATCH", "GET", "DELETE"] +class YotiResponse(object): + def __init__(self, status_code, text): + self.status_code = status_code + self.text = text + + class RequestHandler: """ Default request handler for signing requests using the requests library. This type can be inherited and the execute method overridden to use any - preferred HTTP library. + preferred HTTP library. Must return type YotiResponse for use in the SDK. """ __metaclass__ = ABCMeta # Python 2 compatability @@ -49,13 +55,15 @@ def execute(request): if not isinstance(request, SignedRequest): raise TypeError("RequestHandler expects instance of SignedRequest") - return requests.request( + response = requests.request( url=request.url, method=request.method, data=request.data, headers=request.headers, ) + return YotiResponse(status_code=response.status_code, text=response.text) + class SignedRequest(object): def __init__(self, url, http_method, payload, headers, request_handler=None): From 5753218ca135227e530920ad78bd387f9989941a Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 5 Sep 2019 10:37:21 +0100 Subject: [PATCH 70/74] SDK-1076: Update MockResponse to inherit from YotiResponse --- yoti_python_sdk/tests/mocks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yoti_python_sdk/tests/mocks.py b/yoti_python_sdk/tests/mocks.py index 225ec33a..f2373d07 100644 --- a/yoti_python_sdk/tests/mocks.py +++ b/yoti_python_sdk/tests/mocks.py @@ -1,11 +1,11 @@ from uuid import UUID from yoti_python_sdk.http import RequestHandler, SignedRequest +from yoti_python_sdk.http import YotiResponse -class MockResponse: +class MockResponse(YotiResponse): def __init__(self, status_code, text): - self.status_code = status_code - self.text = text + super(MockResponse, self).__init__(status_code, text) class MockRequestHandler(RequestHandler): From cd19dff30d0949be0dc03e1a186eaa42810de78f Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 5 Sep 2019 12:34:09 +0100 Subject: [PATCH 71/74] SDK-1076: Remove TODO comment --- yoti_python_sdk/endpoint.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/yoti_python_sdk/endpoint.py b/yoti_python_sdk/endpoint.py index d0c2a16a..381ba721 100644 --- a/yoti_python_sdk/endpoint.py +++ b/yoti_python_sdk/endpoint.py @@ -10,8 +10,6 @@ def __init__(self, sdk_id): def get_activity_details_request_path( self, decrypted_request_token, no_params=False ): - # TODO: Remove no_params in 3.0.0 and get rid of the query parameters in the formatted - # TODO: string if no_params: return "/profile/{0}".format(decrypted_request_token) From bcea9e51452761c286cb87c812b75fe5aa362e07 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 5 Sep 2019 15:54:05 +0100 Subject: [PATCH 72/74] SDK-1076: Fix merge error --- yoti_python_sdk/sandbox/token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py index 03cfaf0f..0ec77b27 100644 --- a/yoti_python_sdk/sandbox/token.py +++ b/yoti_python_sdk/sandbox/token.py @@ -248,5 +248,5 @@ def build(self): return YotiTokenRequest(self.remember_me_id, self.attributes) @staticmethod - def __create_attribute(self, name, value, anchors=None): + def __create_attribute(name, value, anchors=None): return SandboxAttribute(name, value, anchors) From a4f00214324e36ba705addca9bedf42b3eb46795 Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Tue, 10 Sep 2019 10:30:09 +0100 Subject: [PATCH 73/74] SDK-1076: Move create_nonce and create_timestamp into a utils module, and update references in the rest of the SDK --- sonar-project.properties | 2 +- yoti_python_sdk/endpoint.py | 25 ++++--------------------- yoti_python_sdk/http.py | 16 ++-------------- yoti_python_sdk/sandbox/endpoint.py | 15 ++------------- yoti_python_sdk/utils.py | 20 ++++++++++++++++++++ 5 files changed, 29 insertions(+), 49 deletions(-) create mode 100644 yoti_python_sdk/utils.py diff --git a/sonar-project.properties b/sonar-project.properties index d1e324ae..20048c32 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ sonar.projectKey = yoti-web-sdk:python sonar.projectName = python-sdk -sonar.projectVersion = 2.8.2 +sonar.projectVersion = 2.9.0 sonar.exclusions=yoti_python_sdk/tests/**,examples/** sonar.python.pylint.reportPath=coverage.out diff --git a/yoti_python_sdk/endpoint.py b/yoti_python_sdk/endpoint.py index 381ba721..0244d128 100644 --- a/yoti_python_sdk/endpoint.py +++ b/yoti_python_sdk/endpoint.py @@ -1,6 +1,4 @@ -from deprecated import deprecated -import time -import uuid +from yoti_python_sdk.utils import create_timestamp, create_nonce class Endpoint(object): @@ -14,10 +12,7 @@ def get_activity_details_request_path( return "/profile/{0}".format(decrypted_request_token) return "/profile/{0}?nonce={1}×tamp={2}&appId={3}".format( - decrypted_request_token, - self.__create_nonce(), - self.__create_timestamp(), - self.sdk_id, + decrypted_request_token, create_nonce(), create_timestamp(), self.sdk_id ) def get_aml_request_url(self, no_params=False): @@ -25,7 +20,7 @@ def get_aml_request_url(self, no_params=False): return "/aml-check" return "/aml-check?appId={0}×tamp={1}&nonce={2}".format( - self.sdk_id, self.__create_timestamp(), self.__create_nonce() + self.sdk_id, create_timestamp(), create_nonce() ) def get_dynamic_share_request_url(self, no_params=False): @@ -33,17 +28,5 @@ def get_dynamic_share_request_url(self, no_params=False): return "/qrcodes/app/{appid}".format(appid=self.sdk_id) return "/qrcodes/apps/{appid}?nonce={nonce}×tamp={timestamp}".format( - appid=self.sdk_id, - nonce=self.__create_nonce(), - timestamp=self.__create_timestamp(), + appid=self.sdk_id, nonce=create_nonce(), timestamp=create_timestamp() ) - - @staticmethod - @deprecated - def __create_nonce(): - return uuid.uuid4() - - @staticmethod - @deprecated - def __create_timestamp(): - return int(time.time() * 1000) diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index 7d883db6..9db9ad4a 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -1,4 +1,5 @@ from yoti_python_sdk.crypto import Crypto +from yoti_python_sdk.utils import create_nonce, create_timestamp from yoti_python_sdk.config import ( X_YOTI_AUTH_KEY, X_YOTI_AUTH_DIGEST, @@ -12,8 +13,6 @@ import yoti_python_sdk import requests -import uuid -import time try: # pragma: no cover from urllib.parse import urlencode @@ -230,10 +229,7 @@ def __append_query_params(self, query_params=None): Appends supplied query params in a dict to default query params. Returns a url encoded query param string """ - required = { - "nonce": self.__create_nonce(), - "timestamp": self.__create_timestamp(), - } + required = {"nonce": create_nonce(), "timestamp": create_timestamp()} query_params = self.__merge_dictionary(query_params, required) return "?{}".format(urlencode(query_params)) @@ -304,14 +300,6 @@ def __validate_request(self): if self.__http_method is None: raise ValueError("HTTP method must be specified") - @staticmethod - def __create_nonce(): - return uuid.uuid4() - - @staticmethod - def __create_timestamp(): - return int(time.time() * 1000) - def build(self): """ Builds a SignedRequest object with the supplied values diff --git a/yoti_python_sdk/sandbox/endpoint.py b/yoti_python_sdk/sandbox/endpoint.py index e671777a..194dd812 100644 --- a/yoti_python_sdk/sandbox/endpoint.py +++ b/yoti_python_sdk/sandbox/endpoint.py @@ -1,6 +1,5 @@ from yoti_python_sdk.endpoint import Endpoint -import time -import uuid +from yoti_python_sdk.utils import create_timestamp, create_nonce class SandboxEndpoint(Endpoint): @@ -9,15 +8,5 @@ def __init__(self, sdk_id): def get_sandbox_path(self): return "/apps/{sdk_id}/tokens?timestamp={timestamp}&nonce={nonce}".format( - sdk_id=self.sdk_id, - nonce=self.__create_nonce(), - timestamp=self.__create_timestamp(), + sdk_id=self.sdk_id, nonce=create_nonce(), timestamp=create_timestamp() ) - - @staticmethod - def __create_nonce(): - return uuid.uuid4() - - @staticmethod - def __create_timestamp(): - return int(time.time() * 1000) diff --git a/yoti_python_sdk/utils.py b/yoti_python_sdk/utils.py new file mode 100644 index 00000000..773b4112 --- /dev/null +++ b/yoti_python_sdk/utils.py @@ -0,0 +1,20 @@ +import uuid +import time + + +def create_nonce(): + """ + Create and return a nonce + \ + :return: the nonce + """ + return uuid.uuid4() + + +def create_timestamp(): + """ + Create and return a timestamp + + :return: the timestamp as a int + """ + return int(time.time() * 1000) From 149f6be351084ac7bc6723a1f6dd61c0a7e9337a Mon Sep 17 00:00:00 2001 From: Alex Burt Date: Thu, 12 Sep 2019 11:15:26 +0100 Subject: [PATCH 74/74] Release-2.9.0: Bump version number --- 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 8b02bf07..06ae705b 100644 --- a/yoti_python_sdk/version.py +++ b/yoti_python_sdk/version.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "2.8.2" +__version__ = "2.9.0"