From 5dc767b12eb26731ff0de793fbfbbbb74047a0c4 Mon Sep 17 00:00:00 2001 From: vedi Date: Tue, 24 Dec 2019 15:39:39 +0300 Subject: [PATCH 1/3] Fix tests --- Makefile | 4 ++-- README.md | 2 +- tests/conftest.py | 2 +- tests/test_client.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 129473b..6510b76 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ SHELL := bash PATH := ./venv/bin:${PATH} -PYTHON = python3.7 +PYTHON = python3 PROJECT = mati isort = isort -rc -ac $(PROJECT) tests setup.py -black = black -S -l 79 --target-version py37 $(PROJECT) tests setup.py +black = black -S -l 79 --target-version py36 $(PROJECT) tests setup.py all: test diff --git a/README.md b/README.md index 9b9e214..7f4568e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ pip install mati ``` make venv -source venv/bin/active +source venv/bin/activate make test ``` diff --git a/tests/conftest.py b/tests/conftest.py index f6bb24a..419a6dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,7 +157,7 @@ def vcr_config() -> dict: @pytest.fixture def client() -> Generator: # using credentials from env - yield Client() + yield Client('api_key', 'secret_key') @pytest.fixture diff --git a/tests/test_client.py b/tests/test_client.py index dfd35e7..e81e5ea 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,7 +7,7 @@ @pytest.mark.vcr @pytest.mark.parametrize('score', [None, 'identity']) def test_client_renew_access_token(score): - client = Client() + client = Client('api_key', 'secret_key') assert client.bearer_tokens.get(score) is None client.get_valid_bearer_token(score) assert not client.bearer_tokens[score].expired From a54572ec60826cc25eed52c33eb038f176db7341 Mon Sep 17 00:00:00 2001 From: vedi Date: Thu, 26 Dec 2019 18:24:49 +0300 Subject: [PATCH 2/3] Implement basic api services --- mati/__init__.py | 7 +- mati/api_service.py | 110 ++++++++++++++++++ mati/api_service_v1.py | 110 ++++++++++++++++++ mati/call_http.py | 26 +++++ mati/resources/identities.py | 4 +- mati/types.py | 27 +++++ .../test_api_service_fetch_resource.yaml | 77 ++++++++++++ ...service_fetch_resource_with_401_error.yaml | 77 ++++++++++++ .../test_api_service_v1_fetch_resource.yaml | 90 ++++++++++++++ ...vice_v1_fetch_resource_with_401_error.yaml | 90 ++++++++++++++ tests/test_api_service.py | 80 +++++++++++++ tests/test_api_service_v1.py | 80 +++++++++++++ 12 files changed, 775 insertions(+), 3 deletions(-) create mode 100644 mati/api_service.py create mode 100644 mati/api_service_v1.py create mode 100644 mati/call_http.py create mode 100644 tests/cassettes/test_api_service_fetch_resource.yaml create mode 100644 tests/cassettes/test_api_service_fetch_resource_with_401_error.yaml create mode 100644 tests/cassettes/test_api_service_v1_fetch_resource.yaml create mode 100644 tests/cassettes/test_api_service_v1_fetch_resource_with_401_error.yaml create mode 100644 tests/test_api_service.py create mode 100644 tests/test_api_service_v1.py diff --git a/mati/__init__.py b/mati/__init__.py index c17778b..6ed7eb8 100644 --- a/mati/__init__.py +++ b/mati/__init__.py @@ -1,4 +1,9 @@ -__all__ = ['Client', '__version__'] +__all__ = ['ApiService', 'ApiServiceV1', 'api_service', 'api_service_v1', 'Client', '__version__'] from .client import Client +from .api_service import ApiService +from .api_service_v1 import ApiServiceV1 from .version import __version__ + +api_service = ApiService() +api_service_v1 = ApiServiceV1() diff --git a/mati/api_service.py b/mati/api_service.py new file mode 100644 index 0000000..7d56c85 --- /dev/null +++ b/mati/api_service.py @@ -0,0 +1,110 @@ +import hashlib +import hmac +from base64 import b64encode +from typing import Optional + +from mati.call_http import RequestOptions, call_http, ErrorResponse +from mati.types import AuthType + +API_HOST = 'https://api.getmati.com' + + +class ApiService: + bearer_auth_header: str = None + client_auth_header: str = None + host: str + webhook_secret: Optional[str] + + def init(self, client_id: str, client_secret: str, webhook_secret: str = None, host: str = None): + """ + Initializes the service. Call this method before using api calls. + --------- + :param str client_id: + :param str client_secret: + :param str host: + :param str webhook_secret: + :return: None + """ + self.host = host or API_HOST + self.webhook_secret = webhook_secret or None + self._set_client_auth(client_id, client_secret) + + def validate_signature(self, signature: str, body_str: str): + """ + Validates signature of requests. + We use webhookSecret to sign data. You put this value in the Dashboard, when define webhooks. + And provide the same value, when you initialize the service. Please, use a strong secret value. + Draw your attention. The order of the fields in the body is important. Keep the original one. + --------- + :param str signature: signature from x-signature header of request calculated on Mati side + :param str body_str: data came in request body + :raise: Exception if `webhook_secret` is not provided + :return: bool `true` if the signature is valid, `false` - otherwise + """ + if self.webhook_secret is None: + raise Exception('No webhookSecret provided') + dig = hmac.new(self.webhook_secret.encode(), msg=body_str.encode(), digestmod=hashlib.sha256) + return signature == dig.hexdigest() + + def fetch_resource(self, url: str): + """ + Fetches resource by its absolute URL using your client credentials you provide, + when you initialize the service. Usually you do not need to build url by yourself, + its values come in webhooks or in other resources. + + :param url: + :return: resource + :raise ErrorResponse if we get http error + """ + return self._call_http(url=url) + + def _set_client_auth(self, client_id, client_secret): + auth = b64encode(f'{client_id}:{client_secret}'.encode('utf-8')) + self.client_auth_header = 'Basic ' + auth.decode('ascii') + + def _set_bearer_auth(self, access_token): + self.bearer_auth_header = f'Bearer {access_token}' + + def auth(self): + auth_response = self._call_http( + path='oauth', + auth_type=AuthType.basic, + request_options=RequestOptions( + method='post', + body={'grant_type': 'client_credentials'}, + headers={'content-type': 'application/x-www-form-urlencoded'}, + ) + ) + self._set_bearer_auth(auth_response['access_token']) + return auth_response + + def _call_http( + self, + path: str = None, + url: str = None, + request_options: RequestOptions = RequestOptions(), + auth_type: AuthType = AuthType.bearer, + ): + tried_auth = False + if auth_type == AuthType.bearer and self.bearer_auth_header is None: + self.auth() + tried_auth = True + if auth_type != AuthType.none: + authorization = None + if auth_type == AuthType.bearer: + authorization = self.bearer_auth_header + elif auth_type == AuthType.basic: + authorization = self.client_auth_header + if authorization is not None: + headers = request_options.headers or dict() + headers['Authorization'] = authorization + request_options.headers = headers + request_url = url or f'{self.host}/{path}' + try: + return call_http(request_url, request_options) + except ErrorResponse as err: + if not tried_auth and auth_type == AuthType.bearer and err.response.status_code == 401: + self.auth() + request_options.headers['Authorization'] = self.bearer_auth_header + return call_http(request_url, request_options) + raise err diff --git a/mati/api_service_v1.py b/mati/api_service_v1.py new file mode 100644 index 0000000..e02f606 --- /dev/null +++ b/mati/api_service_v1.py @@ -0,0 +1,110 @@ +import hashlib +import hmac +from base64 import b64encode +from typing import Optional + +from mati.call_http import RequestOptions, call_http, ErrorResponse +from mati.types import AuthType + +API_HOST = 'https://api.getmati.com' + + +class ApiServiceV1: + bearer_auth_header: str = None + client_auth_header: str = None + host: str + webhook_secret: Optional[str] + + def init(self, client_id: str, client_secret: str, webhook_secret: str = None, host: str = None): + """ + Initializes the service. Call this method before using api calls. + --------- + :param str client_id: + :param str client_secret: + :param str host: + :param str webhook_secret: + :return: None + """ + self.host = host or API_HOST + self.webhook_secret = webhook_secret or None + self._set_client_auth(client_id, client_secret) + + def validate_signature(self, signature: str, body_str: str): + """ + Validates signature of requests. + We use webhookSecret to sign data. You put this value in the Dashboard, when define webhooks. + And provide the same value, when you initialize the service. Please, use a strong secret value. + Draw your attention. The order of the fields in the body is important. Keep the original one. + --------- + :param str signature: signature from x-signature header of request calculated on Mati side + :param str body_str: data came in request body + :raise: Exception if `webhook_secret` is not provided + :return: bool `true` if the signature is valid, `false` - otherwise + """ + if self.webhook_secret is None: + raise Exception('No webhookSecret provided') + dig = hmac.new(self.webhook_secret.encode(), msg=body_str.encode(), digestmod=hashlib.sha256) + return signature == dig.hexdigest() + + def fetch_resource(self, url: str): + """ + Fetches resource by its absolute URL using your client credentials you provide, + when you initialize the service. Usually you do not need to build url by yourself, + its values come in webhooks or in other resources. + + :param url: + :return: resource + :raise ErrorResponse if we get http error + """ + return self._call_http(url=url) + + def _set_client_auth(self, client_id, client_secret): + auth = b64encode(f'{client_id}:{client_secret}'.encode('utf-8')) + self.client_auth_header = 'Basic ' + auth.decode('ascii') + + def _set_bearer_auth(self, access_token): + self.bearer_auth_header = f'Bearer {access_token}' + + def auth(self): + auth_response = self._call_http( + path='oauth/token', + auth_type=AuthType.basic, + request_options=RequestOptions( + method='post', + body={'grant_type': 'client_credentials', 'scope': 'identity' }, + headers={'content-type': 'application/x-www-form-urlencoded'}, + ) + ) + self._set_bearer_auth(auth_response['access_token']) + return auth_response + + def _call_http( + self, + path: str = None, + url: str = None, + request_options: RequestOptions = RequestOptions(), + auth_type: AuthType = AuthType.bearer, + ): + tried_auth = False + if auth_type == AuthType.bearer and self.bearer_auth_header is None: + self.auth() + tried_auth = True + if auth_type != AuthType.none: + authorization = None + if auth_type == AuthType.bearer: + authorization = self.bearer_auth_header + elif auth_type == AuthType.basic: + authorization = self.client_auth_header + if authorization is not None: + headers = request_options.headers or dict() + headers['Authorization'] = authorization + request_options.headers = headers + request_url = url or f'{self.host}/{path}' + try: + return call_http(request_url, request_options) + except ErrorResponse as err: + if not tried_auth and auth_type == AuthType.bearer and err.response.status_code == 401: + self.auth() + request_options.headers['Authorization'] = self.bearer_auth_header + return call_http(request_url, request_options) + raise err diff --git a/mati/call_http.py b/mati/call_http.py new file mode 100644 index 0000000..b5d7cef --- /dev/null +++ b/mati/call_http.py @@ -0,0 +1,26 @@ +from typing import Dict + +from attr import dataclass +from requests import Session, Response + +session = Session() + + +@dataclass +class RequestOptions: + method: str = 'get' + headers: Dict[str, str] = None + body: Dict = None + + +class ErrorResponse(Exception): + message: str + response: Response + + +def call_http(request_url: str, request_options: RequestOptions): + response = session.request(request_options.method, request_url, headers=request_options.headers) + if not response.ok: + print(f'response.text: {response.text}') + raise ErrorResponse(response.text, response) + return response.json() diff --git a/mati/resources/identities.py b/mati/resources/identities.py index cae1231..3535c5d 100644 --- a/mati/resources/identities.py +++ b/mati/resources/identities.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from typing import ClassVar, List, Optional, Union -from ..types import UserValidationFile +from ..types import UserValidationFile, IdentityMetadata from .base import Resource from .user_verification_data import UserValidationData @@ -22,7 +22,7 @@ class Identity(Resource): status: str annotatedStatus: Optional[str] = None user: Optional[str] = None - metadata: Union[dict, List[str]] = field(default_factory=dict) + metadata: IdentityMetadata = field(default_factory=dict) fullName: Optional[str] = None facematchScore: Optional[float] = None diff --git a/mati/types.py b/mati/types.py index 6df0fde..44983e5 100644 --- a/mati/types.py +++ b/mati/types.py @@ -54,3 +54,30 @@ class UserValidationFile: region: str = '' # 2-digit US State code (if applicable) group: int = 0 page: Union[str, PageType] = PageType.front + + +########## + +IdentityMetadata = Union[dict, List[str]] + +@dataclass +class EventNameTypes(SerializableEnum): + step_completed = 'step_completed', + verification_completed = 'verification_completed', + verification_expired = 'verification_expired', + verification_inputs_completed = 'verification_inputs_completed', + verification_started = 'verification_started', + verification_updated = 'verification_updated', + + +@dataclass +class WebhookResource: + eventName: EventNameTypes + metadata: IdentityMetadata + resource: str + + +class AuthType(SerializableEnum): + bearer = 'bearer' + basic = 'basic' + none = 'none' diff --git a/tests/cassettes/test_api_service_fetch_resource.yaml b/tests/cassettes/test_api_service_fetch_resource.yaml new file mode 100644 index 0000000..24678b2 --- /dev/null +++ b/tests/cassettes/test_api_service_fetch_resource.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.22.0 + content-type: + - application/x-www-form-urlencoded + method: POST + uri: https://api.getmati.com/oauth + response: + body: + string: '{"access_token": "ACCESS_TOKEN", "expiresIn": 3600, "payload": {"user": + {"_id": "ID", "firstName": "FIRST_NAME", "lastName": "LAST_NAME"}}}' + headers: + Connection: + - keep-alive + Content-Length: + - '452' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 13:57:28 GMT + X-Request-Id: + - b4663380-a927-4564-ade7-df32249d9ac4 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.22.0 + method: GET + uri: http://resourceUrl/ + response: + body: + string: '{"name":"resourceName"}' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '23' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 14:09:44 GMT + Etag: + - W/"8f-ejjPa5NRLKui634I0HWy1uDz4OE" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_api_service_fetch_resource_with_401_error.yaml b/tests/cassettes/test_api_service_fetch_resource_with_401_error.yaml new file mode 100644 index 0000000..2fc5c12 --- /dev/null +++ b/tests/cassettes/test_api_service_fetch_resource_with_401_error.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.22.0 + content-type: + - application/x-www-form-urlencoded + method: POST + uri: https://api.getmati.com/oauth + response: + body: + string: '{"access_token": "ACCESS_TOKEN", "expiresIn": 3600, "payload": {"user": + {"_id": "ID", "firstName": "FIRST_NAME", "lastName": "LAST_NAME"}}}' + headers: + Connection: + - keep-alive + Content-Length: + - '112' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 13:57:28 GMT + X-Request-Id: + - b4663380-a927-4564-ade7-df32249d9ac4 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.22.0 + method: GET + uri: http://resourceUrl/ + response: + body: + string: '{"code":401,"message":"Invalid client: client is invalid","name":"invalid_client","status":401,"statusCode":401}' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '23' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 14:09:44 GMT + Etag: + - W/"8f-ejjPa5NRLKui634I0HWy1uDz4OE" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Xss-Protection: + - 1; mode=block + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/cassettes/test_api_service_v1_fetch_resource.yaml b/tests/cassettes/test_api_service_v1_fetch_resource.yaml new file mode 100644 index 0000000..4d29c3e --- /dev/null +++ b/tests/cassettes/test_api_service_v1_fetch_resource.yaml @@ -0,0 +1,90 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.22.0 + content-type: + - application/x-www-form-urlencoded + method: POST + uri: https://api.getmati.com/oauth/token + response: + body: + string: '{"access_token": "ACCESS_TOKEN", "expires_in": 3600, "token_type": "Bearer"}' + headers: + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '76' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 14:52:21 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - X-HTTP-Method-Override + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.22.0 + method: GET + uri: http://resourceUrl/ + response: + body: + string: '{"name":"resourceName"}' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '23' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 14:09:44 GMT + Etag: + - W/"8f-ejjPa5NRLKui634I0HWy1uDz4OE" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_api_service_v1_fetch_resource_with_401_error.yaml b/tests/cassettes/test_api_service_v1_fetch_resource_with_401_error.yaml new file mode 100644 index 0000000..f067569 --- /dev/null +++ b/tests/cassettes/test_api_service_v1_fetch_resource_with_401_error.yaml @@ -0,0 +1,90 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.22.0 + content-type: + - application/x-www-form-urlencoded + method: POST + uri: https://api.getmati.com/oauth/token + response: + body: + string: '{"access_token": "ACCESS_TOKEN", "expires_in": 3600, "token_type": "Bearer"}' + headers: + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '76' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 14:52:21 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - X-HTTP-Method-Override + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.22.0 + method: GET + uri: http://resourceUrl/ + response: + body: + string: '{"code":401,"message":"Invalid client: client is invalid","name":"invalid_client","status":401,"statusCode":401}' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '23' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 14:09:44 GMT + Etag: + - W/"8f-ejjPa5NRLKui634I0HWy1uDz4OE" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Xss-Protection: + - 1; mode=block + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/tests/test_api_service.py b/tests/test_api_service.py new file mode 100644 index 0000000..6a3710b --- /dev/null +++ b/tests/test_api_service.py @@ -0,0 +1,80 @@ +from contextlib import contextmanager + +import pytest + +from mati import ApiService +from mati.call_http import ErrorResponse + +client_id: str = 'clientId' +client_secret: str = 'clientSecret' +webhook_secret: str = 'webhookSecret' + +webhook_resource_str = '{' \ + '"eventName":"verification_completed",' \ + '"metadata":{"email":"john@gmail.com"},' \ + '"resource":"https://api.getmati.com/api/v1/verifications/db8d24783"' \ + '}' + +resource_url = 'http://resourceUrl' + + +def fetch_resource(): + api_service = ApiService() + api_service.init(client_id, client_secret) + return api_service.fetch_resource(resource_url) + + +@contextmanager +def not_raises(ExpectedException): + try: + yield + + except ExpectedException as error: + raise AssertionError(f"Raised exception {error} when it should not!") + + except Exception as error: + raise AssertionError(f"An unexpected exception {error} raised.") + + +def test_api_service_init(): + api_service = ApiService() + with not_raises(None): + api_service.init(client_id, client_secret, webhook_secret) + + +def test_api_service_validate_signature(): + api_service = ApiService() + api_service.init(client_id, client_secret, webhook_secret) + result = api_service.validate_signature( + '0c5ed2cad914fd2a1571b47bb087953af574a353ff9d96f8603f8c0d7955340c', + webhook_resource_str, + ) + assert result is True + + +def test_api_service_validate_signature_no_secret(): + api_service = ApiService() + api_service.init(client_id, client_secret) + with pytest.raises(Exception): + api_service.validate_signature('', '') + + +def test_api_service_validate_signature_wrong_signature(): + api_service = ApiService() + api_service.init(client_id, client_secret, webhook_secret) + result = api_service.validate_signature( + 'wrong sig', + webhook_resource_str, + ) + assert result is False + + +@pytest.mark.vcr +def test_api_service_fetch_resource(): + assert fetch_resource() + + +@pytest.mark.vcr +def test_api_service_fetch_resource_with_401_error(): + with pytest.raises(ErrorResponse): + fetch_resource() diff --git a/tests/test_api_service_v1.py b/tests/test_api_service_v1.py new file mode 100644 index 0000000..f8d108b --- /dev/null +++ b/tests/test_api_service_v1.py @@ -0,0 +1,80 @@ +from contextlib import contextmanager + +import pytest + +from mati import ApiServiceV1 +from mati.call_http import ErrorResponse + +client_id: str = 'clientId' +client_secret: str = 'clientSecret' +webhook_secret: str = 'webhookSecret' + +webhook_resource_str = '{' \ + '"eventName":"verification_completed",' \ + '"metadata":{"email":"john@gmail.com"},' \ + '"resource":"https://api.getmati.com/api/v1/verifications/db8d24783"' \ + '}' + +resource_url = 'http://resourceUrl' + + +def fetch_resource(): + api_service = ApiServiceV1() + api_service.init(client_id, client_secret) + return api_service.fetch_resource(resource_url) + + +@contextmanager +def not_raises(ExpectedException): + try: + yield + + except ExpectedException as error: + raise AssertionError(f"Raised exception {error} when it should not!") + + except Exception as error: + raise AssertionError(f"An unexpected exception {error} raised.") + + +def test_api_service_v1_init(): + api_service = ApiServiceV1() + with not_raises(None): + api_service.init(client_id, client_secret, webhook_secret) + + +def test_api_service_v1_validate_signature(): + api_service = ApiServiceV1() + api_service.init(client_id, client_secret, webhook_secret) + result = api_service.validate_signature( + '0c5ed2cad914fd2a1571b47bb087953af574a353ff9d96f8603f8c0d7955340c', + webhook_resource_str, + ) + assert result is True + + +def test_api_service_v1_validate_signature_no_secret(): + api_service = ApiServiceV1() + api_service.init(client_id, client_secret) + with pytest.raises(Exception): + api_service.validate_signature('', '') + + +def test_api_service_v1_validate_signature_wrong_signature(): + api_service = ApiServiceV1() + api_service.init(client_id, client_secret, webhook_secret) + result = api_service.validate_signature( + 'wrong sig', + webhook_resource_str, + ) + assert result is False + + +@pytest.mark.vcr +def test_api_service_v1_fetch_resource(): + assert fetch_resource() + + +@pytest.mark.vcr +def test_api_service_v1_fetch_resource_with_401_error(): + with pytest.raises(ErrorResponse): + fetch_resource() From c68956917f0fecb9d81af03b7890323e0178551d Mon Sep 17 00:00:00 2001 From: vedi Date: Wed, 8 Jan 2020 14:34:23 +0300 Subject: [PATCH 3/3] Implement create identity --- mati/api_service.py | 21 ++++- mati/types.py | 16 ++++ .../test_api_service_create_identity.yaml | 79 +++++++++++++++++++ .../test_api_service_fetch_resource.yaml | 2 +- ...service_fetch_resource_with_401_error.yaml | 2 +- tests/test_api_service.py | 8 +- 6 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 tests/cassettes/test_api_service_create_identity.yaml diff --git a/mati/api_service.py b/mati/api_service.py index 7d56c85..280103b 100644 --- a/mati/api_service.py +++ b/mati/api_service.py @@ -4,7 +4,7 @@ from typing import Optional from mati.call_http import RequestOptions, call_http, ErrorResponse -from mati.types import AuthType +from mati.types import AuthType, IdentityMetadata, IdentityResource API_HOST = 'https://api.getmati.com' @@ -52,12 +52,29 @@ def fetch_resource(self, url: str): when you initialize the service. Usually you do not need to build url by yourself, its values come in webhooks or in other resources. - :param url: + :param url: absolute url of the resource :return: resource :raise ErrorResponse if we get http error """ return self._call_http(url=url) + def create_identity(self, metadata: IdentityMetadata) -> IdentityResource: + """ + Starts new verification flow and creates identity. You should use result of this method + in order to get id for further `#sendInput` calls. + + :param {IdentityMetadata} metadata: payload you want to pass to the identity + :return: resource of identity created. + :raise ErrorResponse if we get http error + """ + return self._call_http( + path='v2/identities', + request_options=RequestOptions( + method='post', + body={'metadata': metadata} + ) + ) + def _set_client_auth(self, client_id, client_secret): auth = b64encode(f'{client_id}:{client_secret}'.encode('utf-8')) self.client_auth_header = 'Basic ' + auth.decode('ascii') diff --git a/mati/types.py b/mati/types.py index 44983e5..7a4836a 100644 --- a/mati/types.py +++ b/mati/types.py @@ -60,6 +60,22 @@ class UserValidationFile: IdentityMetadata = Union[dict, List[str]] +@dataclass +class IdentityStatusTypes(SerializableEnum): + deleted = 'deleted', + pending = 'pending', + rejected = 'rejected', + review_needed = 'reviewNeeded', + running = 'running', + verified = 'verified', + + +@dataclass +class IdentityResource: + id: str + status: IdentityStatusTypes + + @dataclass class EventNameTypes(SerializableEnum): step_completed = 'step_completed', diff --git a/tests/cassettes/test_api_service_create_identity.yaml b/tests/cassettes/test_api_service_create_identity.yaml new file mode 100644 index 0000000..596bc48 --- /dev/null +++ b/tests/cassettes/test_api_service_create_identity.yaml @@ -0,0 +1,79 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.22.0 + content-type: + - application/x-www-form-urlencoded + method: POST + uri: https://api.getmati.com/oauth + response: + body: + string: '{"access_token": "ACCESS_TOKEN", "expiresIn": 3600, "payload": {"user": + {"_id": "ID"}}}' + headers: + Connection: + - keep-alive + Content-Length: + - '452' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 13:57:28 GMT + X-Request-Id: + - b4663380-a927-4564-ade7-df32249d9ac4 + status: + code: 200 + message: OK +- request: + body: + string: '{"metadata": {"email": "john@gmail.com"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.22.0 + method: POST + uri: https://api.getmati.com/v2/identities + response: + body: + string: '{"id":"identityId"}' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '23' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 14:09:44 GMT + Etag: + - W/"8f-ejjPa5NRLKui634I0HWy1uDz4OE" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: OK + +version: 1 diff --git a/tests/cassettes/test_api_service_fetch_resource.yaml b/tests/cassettes/test_api_service_fetch_resource.yaml index 24678b2..07487a7 100644 --- a/tests/cassettes/test_api_service_fetch_resource.yaml +++ b/tests/cassettes/test_api_service_fetch_resource.yaml @@ -19,7 +19,7 @@ interactions: response: body: string: '{"access_token": "ACCESS_TOKEN", "expiresIn": 3600, "payload": {"user": - {"_id": "ID", "firstName": "FIRST_NAME", "lastName": "LAST_NAME"}}}' + {"_id": "ID"}}}' headers: Connection: - keep-alive diff --git a/tests/cassettes/test_api_service_fetch_resource_with_401_error.yaml b/tests/cassettes/test_api_service_fetch_resource_with_401_error.yaml index 2fc5c12..492bdd9 100644 --- a/tests/cassettes/test_api_service_fetch_resource_with_401_error.yaml +++ b/tests/cassettes/test_api_service_fetch_resource_with_401_error.yaml @@ -19,7 +19,7 @@ interactions: response: body: string: '{"access_token": "ACCESS_TOKEN", "expiresIn": 3600, "payload": {"user": - {"_id": "ID", "firstName": "FIRST_NAME", "lastName": "LAST_NAME"}}}' + {"_id": "ID"}}}' headers: Connection: - keep-alive diff --git a/tests/test_api_service.py b/tests/test_api_service.py index 6a3710b..c8f83fe 100644 --- a/tests/test_api_service.py +++ b/tests/test_api_service.py @@ -17,7 +17,6 @@ resource_url = 'http://resourceUrl' - def fetch_resource(): api_service = ApiService() api_service.init(client_id, client_secret) @@ -78,3 +77,10 @@ def test_api_service_fetch_resource(): def test_api_service_fetch_resource_with_401_error(): with pytest.raises(ErrorResponse): fetch_resource() + + +@pytest.mark.vcr +def test_api_service_create_identity(): + api_service = ApiService() + api_service.init(client_id, client_secret) + assert api_service.create_identity({'email': 'john@gmail.com'})