{% 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/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..3c04a4c4 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"
@@ -71,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:
diff --git a/examples/yoti_example_flask/app.py b/examples/yoti_example_flask/app.py
index e6ade6c3..3a53249d 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)
@@ -68,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 }}
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..20048c32
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,7 @@
+sonar.projectKey = yoti-web-sdk:python
+sonar.projectName = python-sdk
+sonar.projectVersion = 2.9.0
+sonar.exclusions=yoti_python_sdk/tests/**,examples/**
+
+sonar.python.pylint.reportPath=coverage.out
+sonar.verbose = true
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(
diff --git a/yoti_python_sdk/age_verification.py b/yoti_python_sdk/age_verification.py
new file mode 100644
index 00000000..e181eb34
--- /dev/null
+++ b/yoti_python_sdk/age_verification.py
@@ -0,0 +1,44 @@
+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(":")
+ 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
+
+ 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):
+ 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/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
diff --git a/yoti_python_sdk/client.py b/yoti_python_sdk/client.py
index 3d82bed5..61eb0bf4 100644
--- a/yoti_python_sdk/client.py
+++ b/yoti_python_sdk/client.py
@@ -3,26 +3,14 @@
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, YotiResponse
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 "
@@ -36,47 +24,46 @@
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_env = environ.get("YOTI_KEY_FILE_PATH", pem_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)
+ 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"
- pem = self.__read_pem_file(pem_file_path_env, error_source)
+ self.__crypto = Crypto.read_pem_file(pem_file_path_env, error_source)
else:
raise RuntimeError(NO_KEY_FILE_SPECIFIED_ERROR)
- self.__crypto = Crypto(pem)
self.__endpoint = Endpoint(sdk_id)
+ self.__request_handler = request_handler
- @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))
+ 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()
+
+ if not isinstance(response, YotiResponse):
+ raise TypeError("Response must be of type YotiResponse")
+
+ return response
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
- )
+ response = self.__make_activity_details_request(encrypted_request_token)
receipt = json.loads(response.text).get("receipt")
+ proto = protobuf.Protobuf()
encrypted_data = proto.current_user(receipt)
encrypted_application_profile = proto.current_application(receipt)
@@ -89,32 +76,33 @@ 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):
if aml_profile is None:
raise TypeError("aml_profile not set")
- http_method = "POST"
+ response = self.__make_aml_check_request(aml_profile)
- response = self.__make_aml_check_request(http_method, aml_profile)
+ if not isinstance(response, YotiResponse):
+ raise TypeError("Response must be of type YotiResponse")
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
@@ -135,65 +123,56 @@ 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)
+ path = self.__endpoint.get_activity_details_request_path(
+ decrypted_token, no_params=True
+ )
+
+ 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)
+ .with_request_handler(self.__request_handler)
+ .build()
+ )
+
+ response = signed_request.execute()
self.http_error_handler(
- response, {"default": "Unsuccessful Yoti API call: {1}"}
+ response, {"default": "Unsuccessful Yoti API call: {} {}"}
)
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)
+ 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 = requests.post(url=url, headers=headers, data=aml_profile_bytes, verify=yoti_python_sdk.YOTI_API_VERIFY_SSL)
+ 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: {1}"}
+ response, {"default": "Unsuccessful Yoti API call: {} {}"}
)
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/config.py b/yoti_python_sdk/config.py
index 06e430ad..6500f78c 100644
--- a/yoti_python_sdk/config.py
+++ b/yoti_python_sdk/config.py
@@ -30,3 +30,8 @@
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"
+ANCHOR_VALUE_DRIVING_LICENCE = "DRIVING_LICENCE"
diff --git a/yoti_python_sdk/crypto.py b/yoti_python_sdk/crypto.py
index abaebfe9..02958072 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,20 @@ def strip_pkcs5_padding(data):
if isinstance(stripped, bytearray):
stripped = str(stripped)
return stripped
+
+ @staticmethod
+ def read_pem_file(key_file_path, error_source=None):
+ 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}', passed as: {1}".format(
+ key_file_path, error_source
+ )
+ exception = "{0}: {1}".format(type(exc).__name__, exc)
+ raise RuntimeError("{0}: {1}".format(error, exception))
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..854387da 100644
--- a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py
+++ b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py
@@ -12,33 +12,31 @@ def __init__(self):
"callback_endpoint": "",
}
- """
- @param policy A DynamicPolicy defining the attributes to be shared
- """
-
def with_policy(self, policy):
+ """
+ :param policy: A DynamicPolicy defining the attributes to be requested
+ """
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 build(self):
- return self.__scenario.copy()
+ """
+ :returns: Dictionary representation of dynamic scenario
+ """
+ scenario = self.__scenario.copy()
+ scenario["extensions"] = scenario["extensions"][:]
+ return scenario
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",
+]
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..273afbfc
--- /dev/null
+++ b/yoti_python_sdk/dynamic_sharing_service/policy/source_constraint_builder.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+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
+
+
+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.
+ It is recommended to use 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(ANCHOR_VALUE_PASSPORT, subtype)
+
+ def with_national_id(self, subtype=""):
+ """
+ :param subtype: Subtype information as a string
+ """
+ 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(ANCHOR_VALUE_PASS_CARD, subtype)
+
+ def with_driving_licence(self, subtype=""):
+ """
+ :param subtype: Subtype information as a string
+ """
+ return self.with_anchor_by_name(ANCHOR_VALUE_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..47c05c81
--- /dev/null
+++ b/yoti_python_sdk/dynamic_sharing_service/policy/wanted_anchor_builder.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+
+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..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
@@ -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,28 @@ def with_derivation(self, derivation):
self.__attribute["derivation"] = derivation
return self
+ def with_accept_self_asserted(self, value=True):
+ """
+ :param value: True if self-asserted details are allowed
+ """
+ self.__attribute["accept_self_asserted"] = 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[:]
+ return attribute
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/endpoint.py b/yoti_python_sdk/endpoint.py
index 0ba3f2e2..0244d128 100644
--- a/yoti_python_sdk/endpoint.py
+++ b/yoti_python_sdk/endpoint.py
@@ -1,35 +1,32 @@
-import time
-import uuid
+from yoti_python_sdk.utils import create_timestamp, create_nonce
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
+ ):
+ 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(),
- self.__create_timestamp(),
- self.sdk_id,
+ decrypted_request_token, create_nonce(), create_timestamp(), 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()
+ self.sdk_id, create_timestamp(), 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(),
- timestamp=self.__create_timestamp(),
+ appid=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/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/http.py b/yoti_python_sdk/http.py
new file mode 100644
index 00000000..9db9ad4a
--- /dev/null
+++ b/yoti_python_sdk/http.py
@@ -0,0 +1,317 @@
+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,
+ X_YOTI_SDK,
+ SDK_IDENTIFIER,
+ X_YOTI_SDK_VERSION,
+ JSON_CONTENT_TYPE,
+)
+from cryptography.fernet import base64
+from abc import ABCMeta, abstractmethod
+
+import yoti_python_sdk
+import requests
+
+try: # pragma: no cover
+ from urllib.parse import urlencode
+except ImportError: # pragma: no cover
+ from urllib import urlencode
+
+HTTP_POST = "POST"
+HTTP_GET = "GET"
+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. Must return type YotiResponse for use in the SDK.
+ """
+
+ __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")
+
+ 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):
+ self.__url = url
+ self.__http_method = http_method
+ self.__payload = payload
+ self.__headers = headers
+ self.__request_handler = request_handler
+
+ @property
+ def url(self):
+ """
+ Returns the URL for the SignedRequest
+ """
+ return self.__url
+
+ @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 execute(self):
+ """
+ Send the signed request, using the default RequestHandler if one has not be supplied
+ """
+ if self.__request_handler is None:
+ return DefaultRequestHandler.execute(self)
+
+ return self.__request_handler.execute(self)
+
+ @staticmethod
+ def builder():
+ """
+ Returns an instance of SignedRequestBuilder
+ """
+ return SignedRequestBuilder()
+
+
+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
+ self.__request_handler = 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_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
+ """
+ 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
+ """
+ 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
+
+ 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
+ """
+ self.with_http_method(HTTP_POST)
+ return self
+
+ def with_get(self):
+ """
+ Sets the HTTP method for a GET request
+ """
+ self.with_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": create_nonce(), "timestamp": create_timestamp()}
+
+ query_params = self.__merge_dictionary(query_params, required)
+ return "?{}".format(urlencode(query_params))
+
+ @staticmethod
+ def __merge_dictionary(a, b):
+ """
+ Merges two dictionaries a and b, with b taking precedence over a
+ """
+ if a is None:
+ return b
+
+ 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__
+
+ default = {
+ 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,
+ }
+
+ 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:
+ """
+
+ request = "{}&{}".format(http_method, path)
+
+ if content is not None:
+ b64encoded = base64.b64encode(content)
+ b64ascii = b64encoded.decode("ascii")
+ request += "&" + b64ascii
+
+ 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")
+
+ def build(self):
+ """
+ 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(
+ url, self.__http_method, self.__payload, headers, self.__request_handler
+ )
diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py
index 4a10d498..3dfb4693 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,31 @@ 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 key in self.attributes:
+ attribute = self.attributes[key]
+ 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 (
@@ -185,7 +210,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/sandbox/__init__.py b/yoti_python_sdk/sandbox/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/yoti_python_sdk/sandbox/age_verification.py b/yoti_python_sdk/sandbox/age_verification.py
new file mode 100644
index 00000000..2cb5882c
--- /dev/null
+++ b/yoti_python_sdk/sandbox/age_verification.py
@@ -0,0 +1,97 @@
+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):
+ """
+ Converts the age verification object into an Attribute
+
+ :return: Instance of SandboxAttribute
+ """
+ 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():
+ """
+ Creates a sandbox age verification builder
+
+ :return: Instance of SandboxAgeVerificationBuilder
+ """
+ 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):
+ """
+ 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
+
+ def build(self):
+ return SandboxAgeVerification(
+ self.__date_of_birth, self.__derivation, self.__anchors
+ )
diff --git a/yoti_python_sdk/sandbox/anchor.py b/yoti_python_sdk/sandbox/anchor.py
new file mode 100644
index 00000000..4d85a2aa
--- /dev/null
+++ b/yoti_python_sdk/sandbox/anchor.py
@@ -0,0 +1,127 @@
+from yoti_python_sdk.anchor import UNKNOWN_ANCHOR_TYPE
+
+
+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:
+ value = ""
+
+ self.__anchor_type = anchor_type
+ self.__sub_type = sub_type
+ self.__value = value
+ self.__timestamp = timestamp
+
+ @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):
+ return {
+ "type": self.anchor_type,
+ "value": self.value,
+ "sub_type": self.sub_type,
+ "timestamp": self.timestamp,
+ }
+
+ @staticmethod
+ def builder():
+ """
+ Creates an instance of the sandbox anchor builder
+
+ :return: instance of SandboxAnchorBuilder
+ """
+ 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):
+ """
+ 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
new file mode 100644
index 00000000..fb6eb505
--- /dev/null
+++ b/yoti_python_sdk/sandbox/attribute.py
@@ -0,0 +1,154 @@
+from yoti_python_sdk import config
+
+
+class SandboxAttribute(object):
+ def __init__(self, name=None, value=None, anchors=None, derivation=None):
+ if name is None:
+ name = ""
+
+ if value is None:
+ value = ""
+
+ 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):
+ """
+ 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):
+ return {
+ "name": self.name,
+ "value": self.value,
+ "anchors": self.anchors,
+ "derivation": self.derivation,
+ }
+
+ @staticmethod
+ def builder():
+ """
+ Creates an instance of the sandbox attribute builder
+
+ :return: the sandbox attribute builder
+ """
+ return SandboxAttributeBuilder()
+
+
+class SandboxAttributeBuilder(object):
+ def __init__(self):
+ self.__name = None
+ self.__value = None
+ self.__anchors = None
+ 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
new file mode 100644
index 00000000..fa0a819f
--- /dev/null
+++ b/yoti_python_sdk/sandbox/client.py
@@ -0,0 +1,182 @@
+from yoti_python_sdk.sandbox.endpoint import SandboxEndpoint
+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,
+)
+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
+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__()
+ if isinstance(o, SandboxAttribute):
+ return o.__dict__()
+ if isinstance(o, SandboxAnchor):
+ return o.__dict__()
+
+ return json.JSONEncoder.default(self, o)
+
+
+class SandboxClient(object):
+ 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__"
+ )
+
+ 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
+ )
+ response_payload = response.json()
+ return YotiTokenResponse(response_payload["token"])
+
+ @staticmethod
+ def builder():
+ """
+ Creates an instance of the sandbox client builder
+
+ :return: instance of SandboxClientBuilder
+ """
+ return SandboxClientBuilder()
+
+ @staticmethod
+ def post(host, path, key, content):
+ payload = json.dumps(content, cls=SandboxEncoder)
+ print(payload)
+ payload_bytes = payload.encode()
+ 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):
+ 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 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
+
+
+class SandboxClientBuilder(object):
+ def __init__(self):
+ self.__sdk_id = None
+ self.__pem_file = None
+ 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
+ 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/sandbox/endpoint.py b/yoti_python_sdk/sandbox/endpoint.py
new file mode 100644
index 00000000..194dd812
--- /dev/null
+++ b/yoti_python_sdk/sandbox/endpoint.py
@@ -0,0 +1,12 @@
+from yoti_python_sdk.endpoint import Endpoint
+from yoti_python_sdk.utils import create_timestamp, create_nonce
+
+
+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=create_nonce(), timestamp=create_timestamp()
+ )
diff --git a/yoti_python_sdk/sandbox/token.py b/yoti_python_sdk/sandbox/token.py
new file mode 100644
index 00000000..0ec77b27
--- /dev/null
+++ b/yoti_python_sdk/sandbox/token.py
@@ -0,0 +1,252 @@
+from yoti_python_sdk.sandbox.attribute import SandboxAttribute
+from yoti_python_sdk import config
+import base64
+
+
+class YotiTokenResponse(object):
+ def __init__(self, token):
+ self.__token = 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 = ""
+
+ 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():
+ """
+ Creates an instance of the yoti token request builder
+
+ :return: instance of YotiTokenRequestBuilder
+ """
+ return YotiTokenRequestBuilder()
+
+
+class YotiTokenRequestBuilder(object):
+ def __init__(self):
+ self.remember_me_id = None
+ 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)
+
+ @staticmethod
+ def __create_attribute(name, value, anchors=None):
+ return SandboxAttribute(name, value, anchors)
diff --git a/yoti_python_sdk/tests/conftest.py b/yoti_python_sdk/tests/conftest.py
index c7c87b77..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)
@@ -63,22 +69,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/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..689fc003
--- /dev/null
+++ b/yoti_python_sdk/tests/dynamic_sharing_service/policy/test_source_constraint_builder.py
@@ -0,0 +1,35 @@
+from yoti_python_sdk.dynamic_sharing_service.policy.source_constraint_builder import (
+ SourceConstraintBuilder,
+)
+from yoti_python_sdk.config import ANCHOR_VALUE_DRIVING_LICENCE, ANCHOR_VALUE_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 ANCHOR_VALUE_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 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"]
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..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
@@ -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,42 @@ 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_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_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/mocks.py b/yoti_python_sdk/tests/mocks.py
index 8d916fb3..f2373d07 100644
--- a/yoti_python_sdk/tests/mocks.py
+++ b/yoti_python_sdk/tests/mocks.py
@@ -1,10 +1,23 @@
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):
+ @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):
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..faa62c17
--- /dev/null
+++ b/yoti_python_sdk/tests/sandbox/test_sandbox_client.py
@@ -0,0 +1,49 @@
+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
+
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch
+
+import pytest
+
+
+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)
+ .with_sandbox_url("https://localhost")
+ .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"
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_age_verification.py b/yoti_python_sdk/tests/test_age_verification.py
new file mode 100644
index 00000000..020205c3
--- /dev/null
+++ b/yoti_python_sdk/tests/test_age_verification.py
@@ -0,0 +1,41 @@
+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
+
+
+@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)
+
+
+@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(MalformedAgeVerificationException):
+ 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)
+
+ 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
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..52ff9115 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 = (
@@ -119,8 +116,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 +127,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,11 +142,13 @@ 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)
+@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_with()
assert isinstance(aml_result, aml.AmlResult)
assert isinstance(aml_result.on_watch_list, bool)
@@ -360,10 +313,13 @@ 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)
+@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(
@@ -378,4 +334,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)
diff --git a/yoti_python_sdk/tests/test_http.py b/yoti_python_sdk/tests/test_http.py
new file mode 100644
index 00000000..2dde002a
--- /dev/null
+++ b/yoti_python_sdk/tests/test_http.py
@@ -0,0 +1,298 @@
+try:
+ from unittest import mock
+except ImportError:
+ import mock
+
+from yoti_python_sdk.http import SignedRequest, DefaultRequestHandler
+from yoti_python_sdk.crypto import Crypto
+from yoti_python_sdk.tests import conftest
+
+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()
+ )
+
+
+@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
+
+
+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_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()
+ )
+
+ response = signed_request.execute()
+
+ 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)
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
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)
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"