diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..c598a531 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners +* @Dovchik @650elx @krogers0607 @asein-sinch @JPPortier \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2f2c4d54..fe687583 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -21,7 +21,13 @@ env: APP_ID: ${{ secrets.APP_ID }} EMPTY_PROJECT_ID: ${{ secrets.EMPTY_PROJECT_ID }} TEMPLATES_ORIGIN: ${{ secrets.TEMPLATES_ORIGIN }} - + APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} + APPLICATION_KEY: ${{ secrets.APPLICATION_KEY }} + VERIFICATION_ID: ${{ secrets.VERIFICATION_ID }} + VERIFICATION_ORIGIN: ${{ secrets.VERIFICATION_ORIGIN}} + VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP}} + VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE}} + VERIFICATION_REQUEST_SIGNATURE: ${{ secrets.VERIFICATION_REQUEST_SIGNATURE}} jobs: build: @@ -29,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index f21a3945..e79cdc6f 100644 --- a/.gitignore +++ b/.gitignore @@ -126,4 +126,7 @@ dmypy.json cython_debug/ # PyCharm -.idea/ \ No newline at end of file +.idea/ + +# Poetry +poetry.lock \ No newline at end of file diff --git a/README.md b/README.md index 55a01d9e..43bcd3fb 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/sinch/sinch-sdk-python/blob/main/LICENSE) - [![Python 3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/) [![Python 3.9](https://img.shields.io/badge/python-3.9-blue.svg)](https://www.python.org/downloads/release/python-390/) [![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/) [![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110/) + [![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/) + @@ -93,12 +94,9 @@ Sinch client provides access to the following Sinch product domains: Usage example of the `numbers` domain: ```python -from sinch.domains.numbers.enums import NumberType - available_numbers = sinch_client.numbers.available.list( region_code="US", - number_type=NumberType.LOCAL.value, - project_id="Shrubbery" + number_type="LOCAL" ) ``` Returned values are represented as Python `dataclasses`: @@ -128,8 +126,7 @@ from sinch.domains.numbers.exceptions import NumbersException try: nums = sinch_client.numbers.available.list( region_code="US", - number_type="LOCAL", - project_id="project" + number_type="LOCAL" ) except NumbersException as err: pass diff --git a/examples/flask_example.py b/examples/flask_example.py index f199381a..f8a68ac2 100644 --- a/examples/flask_example.py +++ b/examples/flask_example.py @@ -16,7 +16,7 @@ ) -@app.route("/create_app") +@app.route("/create_app", methods=['POST']) def project(): conversation_api_app = sinch_client.conversation.app.create( display_name="Shrubbery", @@ -27,6 +27,10 @@ def project(): "token": "herring" } } - ] + ], + retention_policy={ + "ttl_days": 20, + "retention_type": "MESSAGE_EXPIRE_POLICY" + } ) return {"sinch_app_id": conversation_api_app.id} diff --git a/examples/logging_example.py b/examples/logging_example.py index a296abc1..ab619f09 100644 --- a/examples/logging_example.py +++ b/examples/logging_example.py @@ -8,12 +8,19 @@ logger = logging.getLogger("myapp.sinch") logger.setLevel(logging.DEBUG) -sinch_log_file_handler = logging.FileHandler("/tmp/spam.log") -sinch_log_file_handler.setLevel(logging.DEBUG) +file_handler = logging.FileHandler("/tmp/test_python_logging.log") +file_handler.setLevel(logging.DEBUG) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -sinch_log_file_handler.setFormatter(formatter) +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +file_handler.setFormatter(formatter) + +logger.addHandler(file_handler) +logger.addHandler(console_handler) sinch_client = Client( key_id=os.getenv("KEY_ID"), @@ -21,3 +28,15 @@ project_id=os.getenv("PROJECT_ID"), logger=logger ) + + +def main(): + available_numbers_response = sinch_client.numbers.available.list( + region_code="US", + number_type="LOCAL" + ) + print(available_numbers_response) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..e9d5f351 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[tool.poetry] +name = "sinch" +description = "Sinch SDK for Python programming language" +version = "0.3.1" +license = "Apache 2.0" +readme = "README.md" +authors = [ + "Sinch Developer Experience Team ", +] +repository = "https://github.com/sinch/sinch-sdk-python" +documentation = "https://developers.sinch.com" +classifiers = [ + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Communications :: Telephony", + "Intended Audience :: Developers" +] +keywords = ["sinch", "sdk"] + +[tool.poetry.dependencies] +python = ">=3.9" +requests = "*" +aiohttp = "*" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements-dev.txt b/requirements-dev.txt index a47ae0ed..609c1c94 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,11 @@ flake8 bandit requests aiohttp +flask +fastapi +httpx pytest-asyncio coverage mypy types-requests +coverage diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index cd3beb65..00000000 --- a/setup.cfg +++ /dev/null @@ -1,35 +0,0 @@ -[metadata] -name = sinch -version = 0.2.0 -description = Sinch SDK for Python programming language -long_description = file: README.md -long_description_content_type = text/markdown -author = Sinch Developer Experience Team -author_email = dev@sinch.com -license = Apache 2.0 -license_file = LICENSE -include_package_data = true -keywords = sinch, sdk -classifiers = - License :: OSI Approved :: Apache Software License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: Implementation :: CPython - Topic :: Software Development :: Libraries :: Python Modules - Topic :: Communications :: Telephony - Intended Audience :: Developers - -project_urls = - homepage = https://github.com/sinch/sinch-sdk-python - documentation = https://developers.sinch.com - -[options] -packages = find: -install_requires = - requests - aiohttp -python_requires = >=3.8 diff --git a/setup.py b/setup.py deleted file mode 100644 index b024da80..00000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -from setuptools import setup - - -setup() diff --git a/sinch/__init__.py b/sinch/__init__.py index 2aa45eda..8fb62224 100644 --- a/sinch/__init__.py +++ b/sinch/__init__.py @@ -1,6 +1,7 @@ """ Sinch Python SDK To access Sinch resources, use the Sync or Async version of the Sinch Client. """ +__version__ = "0.3.1" from sinch.core.clients.sinch_client_sync import Client from sinch.core.clients.sinch_client_async import ClientAsync diff --git a/sinch/core/adapters/asyncio_http_adapter.py b/sinch/core/adapters/asyncio_http_adapter.py index 458b1c55..1a0374b8 100644 --- a/sinch/core/adapters/asyncio_http_adapter.py +++ b/sinch/core/adapters/asyncio_http_adapter.py @@ -3,52 +3,52 @@ from sinch.core.ports.http_transport import AsyncHTTPTransport, HttpRequest from sinch.core.endpoint import HTTPEndpoint from sinch.core.models.http_response import HTTPResponse -from sinch.core.models.base_model import SinchBaseModel class HTTPTransportAioHTTP(AsyncHTTPTransport): - async def request(self, endpoint: HTTPEndpoint) -> SinchBaseModel: + def __init__(self, sinch): + super().__init__(sinch) + self.http_session = None + + async def request(self, endpoint: HTTPEndpoint) -> HTTPResponse: request_data: HttpRequest = self.prepare_request(endpoint) request_data_with_auth: HttpRequest = await self.authenticate(endpoint, request_data) + if not self.http_session: + self.http_session = aiohttp.ClientSession() + self.sinch.configuration.logger.debug( f"Async HTTP {request_data_with_auth.http_method} call with headers:" f" {request_data_with_auth.headers} and body: {request_data_with_auth.request_body}" f" to URL: {request_data_with_auth.url}" ) - if request_data_with_auth.auth: - auth_data = aiohttp.BasicAuth(request_data_with_auth.auth[0], request_data_with_auth.auth[1]) - else: - auth_data = None - - async with aiohttp.ClientSession() as session: - async with session.request( - method=request_data_with_auth.http_method, - headers=request_data_with_auth.headers, - url=request_data_with_auth.url, - data=request_data_with_auth.request_body, - auth=auth_data, - params=request_data_with_auth.query_params, - timeout=aiohttp.ClientTimeout( - total=self.sinch.configuration.connection_timeout - ) - ) as response: - response_body = {} - raw_response_body = await response.read() - if raw_response_body: - response_body = json.loads(raw_response_body) - - self.sinch.configuration.logger.debug( - f"Async HTTP {response.status} response with headers: {response.headers}" - f"and body: {response_body} from URL: {request_data_with_auth.url}" - ) + async with self.http_session.request( + method=request_data.http_method, + headers=request_data.headers, + url=request_data.url, + data=request_data.request_body, + auth=request_data.auth, + params=request_data.query_params, + timeout=aiohttp.ClientTimeout( + total=self.sinch.configuration.connection_timeout + ) + ) as response: + + response_body = await response.read() + if response_body: + response_body = json.loads(response_body) + + self.sinch.configuration.logger.debug( + f"Async HTTP {response.status} response with headers: {response.headers}" + f"and body: {response_body} from URL: {request_data.url}" + ) - return await self.handle_response( - endpoint=endpoint, - http_response=HTTPResponse( - status_code=response.status, - body=response_body, - headers=dict(response.headers) - ) + return await self.handle_response( + endpoint=endpoint, + http_response=HTTPResponse( + status_code=response.status, + body=response_body, + headers=response.headers ) + ) diff --git a/sinch/core/adapters/requests_http_transport.py b/sinch/core/adapters/requests_http_transport.py index 3b389a60..4d91c8c0 100644 --- a/sinch/core/adapters/requests_http_transport.py +++ b/sinch/core/adapters/requests_http_transport.py @@ -11,7 +11,7 @@ class HTTPTransportRequests(HTTPTransport): def __init__(self, sinch: ClientBase): super().__init__(sinch) - self.session = requests.Session() + self.http_session = requests.Session() def request(self, endpoint: HTTPEndpoint) -> SinchBaseModel: request_data: HttpRequest = self.prepare_request(endpoint) @@ -23,12 +23,12 @@ def request(self, endpoint: HTTPEndpoint) -> SinchBaseModel: f"to URL: {request_data_with_auth.url}" ) - response = self.session.request( - method=request_data_with_auth.http_method, - url=request_data_with_auth.url, - data=request_data_with_auth.request_body, - auth=request_data_with_auth.auth, - headers=request_data_with_auth.headers, + response = self.http_session.request( + method=request_data.http_method, + url=request_data.url, + data=request_data.request_body, + auth=request_data.auth, + headers=request_data.headers, timeout=self.sinch.configuration.connection_timeout, params=request_data_with_auth.query_params ) diff --git a/sinch/core/clients/sinch_client_async.py b/sinch/core/clients/sinch_client_async.py index 1db9422d..33b4039c 100644 --- a/sinch/core/clients/sinch_client_async.py +++ b/sinch/core/clients/sinch_client_async.py @@ -8,6 +8,7 @@ from sinch.domains.numbers import NumbersAsync from sinch.domains.conversation import ConversationAsync from sinch.domains.sms import SMSAsync +from sinch.domains.verification import Verification as VerificationAsync class ClientAsync(ClientBase): @@ -18,19 +19,14 @@ class ClientAsync(ClientBase): """ def __init__( self, - key_id: str, - key_secret: str, - project_id: str, + key_id, + key_secret, + project_id, logger_name: Optional[str] = None, - logger: Optional[Logger] = None + logger: Optional[Logger] = None, + application_key: Optional[str] = None, + application_secret: Optional[str] = None ): - super().__init__( - key_id=key_id, - key_secret=key_secret, - project_id=project_id, - logger_name=logger_name, - logger=logger - ) self.configuration = Configuration( key_id=key_id, key_secret=key_secret, @@ -38,9 +34,13 @@ def __init__( logger_name=logger_name, logger=logger, transport=HTTPTransportAioHTTP(self), - token_manager=TokenManagerAsync(self) + token_manager=TokenManagerAsync(self), + application_secret=application_secret, + application_key=application_key ) + self.authentication = AuthenticationAsync(self) self.numbers = NumbersAsync(self) self.conversation = ConversationAsync(self) self.sms = SMSAsync(self) + self.verification = VerificationAsync(self) diff --git a/sinch/core/clients/sinch_client_base.py b/sinch/core/clients/sinch_client_base.py index 1c22a68f..ccefe3e2 100644 --- a/sinch/core/clients/sinch_client_base.py +++ b/sinch/core/clients/sinch_client_base.py @@ -1,5 +1,4 @@ -from abc import ABC -from sinch.core.exceptions import ValidationException +from abc import ABC, abstractmethod from sinch.core.clients.sinch_client_configuration import Configuration from sinch.domains.authentication import AuthenticationBase from sinch.domains.numbers import NumbersBase @@ -12,7 +11,7 @@ class ClientBase(ABC): """ Sinch abstract base class for concrete Sinch Client implementations. - By default this SDK provides two implementations - sync and async. + By default, this SDK provides two implementations - sync and async. Feel free to utilize any of them for you custom implementation. """ configuration: Configuration @@ -21,23 +20,18 @@ class ClientBase(ABC): conversation: ConversationBase sms: SMSBase + @abstractmethod def __init__( self, - key_id: str, - key_secret: str, - project_id: str, + key_id, + key_secret, + project_id, logger_name: Optional[str] = None, - logger: Optional[Logger] = None + logger: Optional[Logger] = None, + application_key: str = None, + application_secret: str = None ): - if not key_id or not key_secret or not project_id: - raise ValidationException( - message=( - "key_id, key_secret and project_id are required by the Sinch Client. " - "Those credentials can be obtained from Sinch portal." - ), - is_from_server=False, - response=None - ) + pass - def __repr__(self) -> str: + def __repr__(self): return f"Sinch SDK client for project_id: {self.configuration.project_id}" diff --git a/sinch/core/clients/sinch_client_configuration.py b/sinch/core/clients/sinch_client_configuration.py index 64b3b347..4ff689af 100644 --- a/sinch/core/clients/sinch_client_configuration.py +++ b/sinch/core/clients/sinch_client_configuration.py @@ -19,14 +19,19 @@ def __init__( logger: Optional[Logger] = None, logger_name: Optional[str] = None, disable_https: bool = False, - connection_timeout: int = 10 + connection_timeout: int = 10, + application_key: str = None, + application_secret: str = None ): self.key_id = key_id self.key_secret = key_secret self.project_id = project_id + self.application_key = application_key + self.application_secret = application_secret self.connection_timeout = connection_timeout self.auth_origin = "auth.sinch.com" self.numbers_origin = "numbers.api.sinch.com" + self.verification_origin = "verification.api.sinch.com" self._conversation_region = "eu" self._conversation_domain = ".conversation.api.sinch.com" self._sms_region = "us" diff --git a/sinch/core/clients/sinch_client_sync.py b/sinch/core/clients/sinch_client_sync.py index cb3985e3..9a963d76 100644 --- a/sinch/core/clients/sinch_client_sync.py +++ b/sinch/core/clients/sinch_client_sync.py @@ -8,6 +8,7 @@ from sinch.domains.numbers import Numbers from sinch.domains.conversation import Conversation from sinch.domains.sms import SMS +from sinch.domains.verification import Verification class Client(ClientBase): @@ -18,19 +19,14 @@ class Client(ClientBase): """ def __init__( self, - key_id: str, - key_secret: str, - project_id: str, + key_id, + key_secret, + project_id, logger_name: Optional[str] = None, - logger: Optional[Logger] = None + logger: Optional[Logger] = None, + application_key: str = None, + application_secret: str = None ): - super().__init__( - key_id=key_id, - key_secret=key_secret, - project_id=project_id, - logger_name=logger_name, - logger=logger - ) self.configuration = Configuration( key_id=key_id, key_secret=key_secret, @@ -38,9 +34,13 @@ def __init__( logger_name=logger_name, logger=logger, transport=HTTPTransportRequests(self), - token_manager=TokenManager(self) + token_manager=TokenManager(self), + application_key=application_key, + application_secret=application_secret ) + self.authentication = Authentication(self) self.numbers = Numbers(self) self.conversation = Conversation(self) self.sms = SMS(self) + self.verification = Verification(self) diff --git a/sinch/core/endpoint.py b/sinch/core/endpoint.py index 9415bcfb..75724f66 100644 --- a/sinch/core/endpoint.py +++ b/sinch/core/endpoint.py @@ -18,8 +18,11 @@ class HTTPEndpoint(ABC): def __init__(self, project_id: str, request_data: SinchRequestBaseModel): pass - def build_url(self, sinch: 'ClientBase') -> str: - return '' + def get_url_without_origin(self, sinch): + return '/' + '/'.join(self.build_url(sinch).split('/')[1:]) + + def build_url(self, sinch): + return def build_query_params(self) -> dict: return {} diff --git a/sinch/core/enums.py b/sinch/core/enums.py index 3d59aa5f..ad18db4b 100644 --- a/sinch/core/enums.py +++ b/sinch/core/enums.py @@ -12,3 +12,4 @@ class HTTPMethod(Enum): class HTTPAuthentication(Enum): BASIC = "BASIC" OAUTH = "OAUTH" + SIGNED = "SIGNED" diff --git a/sinch/core/ports/http_transport.py b/sinch/core/ports/http_transport.py index 6766e666..a3080fc4 100644 --- a/sinch/core/ports/http_transport.py +++ b/sinch/core/ports/http_transport.py @@ -1,29 +1,45 @@ +from typing import TYPE_CHECKING, Any, Coroutine, cast +import aiohttp from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Union, Any, Coroutine, cast +from platform import python_version from sinch.core.endpoint import HTTPEndpoint +from sinch.core.signature import Signature from sinch.core.models.http_request import HttpRequest from sinch.core.models.http_response import HTTPResponse +from sinch.core.exceptions import ValidationException from sinch.core.enums import HTTPAuthentication from sinch.core.token_manager import TokenState from sinch.core.models.base_model import SinchBaseModel +from sinch import __version__ as sdk_version if TYPE_CHECKING: from sinch.core.clients.sinch_client_base import ClientBase + class HTTPTransport(ABC): def __init__(self, sinch: 'ClientBase'): self.sinch = sinch @abstractmethod - def request(self, endpoint: HTTPEndpoint) -> Union[SinchBaseModel, Coroutine[Any, Any, SinchBaseModel]]: + def request(self, endpoint: HTTPEndpoint) -> HTTPResponse: pass - def authenticate( - self, - endpoint: HTTPEndpoint, - request_data: HttpRequest - ) -> Union[HttpRequest, Coroutine[Any, Any, HttpRequest]]: + def authenticate(self, endpoint, request_data): + if endpoint.HTTP_AUTHENTICATION in (HTTPAuthentication.BASIC.value, HTTPAuthentication.OAUTH.value): + if ( + not self.sinch.configuration.key_id + or not self.sinch.configuration.key_secret + or not self.sinch.configuration.project_id + ): + raise ValidationException( + message=( + "key_id, key_secret and project_id are required by this API. " + "Those credentials can be obtained from Sinch portal." + ), + is_from_server=False, + response=None + ) if endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.BASIC.value: request_data.auth = (self.sinch.configuration.key_id, self.sinch.configuration.key_secret) @@ -32,10 +48,27 @@ def authenticate( if endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.OAUTH.value: token = self.sinch.authentication.get_auth_token().access_token - request_data.headers = { + request_data.headers.update({ "Authorization": f"Bearer {token}", "Content-Type": "application/json" - } + }) + elif endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.SIGNED.value: + if not self.sinch.configuration.application_key or not self.sinch.configuration.application_secret: + raise ValidationException( + message=( + "application key and application secret are required by this API. " + "Those credentials can be obtained from Sinch portal." + ), + is_from_server=False, + response=None + ) + signature = Signature( + self.sinch, + endpoint.HTTP_METHOD, + request_data.request_body, + endpoint.get_url_without_origin(self.sinch) + ) + request_data.headers = signature.get_http_headers_with_signature() return request_data @@ -44,7 +77,10 @@ def prepare_request(self, endpoint: HTTPEndpoint) -> HttpRequest: url_query_params = endpoint.build_query_params() return HttpRequest( - headers={}, + headers={ + "User-Agent": f"sinch-sdk/{sdk_version} (Python/{python_version()};" + f" {self.__class__.__name__};)" + }, protocol=protocol, url=protocol + endpoint.build_url(self.sinch), http_method=endpoint.HTTP_METHOD.value, @@ -53,12 +89,8 @@ def prepare_request(self, endpoint: HTTPEndpoint) -> HttpRequest: auth=None ) - def handle_response( - self, - endpoint: HTTPEndpoint, - http_response: HTTPResponse - ) -> Union[SinchBaseModel, Coroutine[Any, Any, SinchBaseModel]]: - if http_response.status_code == 401: + def handle_response(self, endpoint: HTTPEndpoint, http_response: HTTPResponse): + if http_response.status_code == 401 and endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.OAUTH.value: self.sinch.configuration.token_manager.handle_invalid_token(http_response) if self.sinch.configuration.token_manager.token_state == TokenState.EXPIRED: return self.request(endpoint=endpoint) # type: ignore @@ -67,9 +99,12 @@ def handle_response( class AsyncHTTPTransport(HTTPTransport): - async def authenticate(self, endpoint: HTTPEndpoint, request_data: HttpRequest) -> HttpRequest: - if endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.BASIC: - request_data.auth = (self.sinch.configuration.key_id, self.sinch.configuration.key_secret) + async def authenticate(self, endpoint, request_data): + if endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.BASIC.value: + request_data.auth = aiohttp.BasicAuth( + self.sinch.configuration.key_id, + self.sinch.configuration.key_secret + ) else: request_data.auth = None @@ -82,8 +117,8 @@ async def authenticate(self, endpoint: HTTPEndpoint, request_data: HttpRequest) return request_data - async def handle_response(self, endpoint: HTTPEndpoint, http_response: HTTPResponse) -> SinchBaseModel: - if http_response.status_code == 401: + async def handle_response(self, endpoint: HTTPEndpoint, http_response: HTTPResponse): + if http_response.status_code == 401 and endpoint.HTTP_AUTHENTICATION == HTTPAuthentication.OAUTH.value: self.sinch.configuration.token_manager.handle_invalid_token(http_response) if self.sinch.configuration.token_manager.token_state == TokenState.EXPIRED: return await cast(Coroutine[Any, Any, SinchBaseModel], self.request(endpoint=endpoint)) diff --git a/sinch/core/signature.py b/sinch/core/signature.py new file mode 100644 index 00000000..5e456266 --- /dev/null +++ b/sinch/core/signature.py @@ -0,0 +1,58 @@ +import hashlib +import hmac +import base64 +from datetime import datetime, timezone + + +class Signature: + def __init__( + self, + sinch, + http_method, + request_data, + request_uri, + content_type=None, + signature_timestamp=None + ): + self.sinch = sinch + self.http_method = http_method + self.content_type = content_type or 'application/json; charset=UTF-8' + self.request_data = request_data + self.signature_timestamp = signature_timestamp or datetime.now(timezone.utc).isoformat() + self.request_uri = request_uri + self.authorization_signature = None + + def get_http_headers_with_signature(self): + if not self.authorization_signature: + self.calculate() + + return { + "Content-Type": self.content_type, + "Authorization": ( + f"Application {self.sinch.configuration.application_key}:{self.authorization_signature}" + ), + "x-timestamp": self.signature_timestamp + } + + def calculate(self): + b64_decoded_application_secret = base64.b64decode(self.sinch.configuration.application_secret) + if self.request_data: + encoded_verification_request = hashlib.md5(self.request_data.encode()) + encoded_verification_request = base64.b64encode(encoded_verification_request.digest()) + + else: + encoded_verification_request = ''.encode() + + request_timestamp = "x-timestamp:" + self.signature_timestamp + + string_to_sign = ( + self.http_method + '\n' + + encoded_verification_request.decode() + '\n' + + self.content_type + '\n' + + request_timestamp + '\n' + + self.request_uri + ) + + self.authorization_signature = base64.b64encode( + hmac.new(b64_decoded_application_secret, string_to_sign.encode(), hashlib.sha256).digest() + ).decode() diff --git a/sinch/domains/numbers/__init__.py b/sinch/domains/numbers/__init__.py index 5a950ae4..ea7e1e8e 100644 --- a/sinch/domains/numbers/__init__.py +++ b/sinch/domains/numbers/__init__.py @@ -4,6 +4,8 @@ from sinch.domains.numbers.endpoints.available.list_available_numbers import AvailableNumbersEndpoint from sinch.domains.numbers.endpoints.available.activate_number import ActivateNumberEndpoint from sinch.domains.numbers.endpoints.available.rent_any_number import RentAnyNumberEndpoint +from sinch.domains.numbers.endpoints.callbacks.get_configuration import GetNumbersCallbackConfigurationEndpoint +from sinch.domains.numbers.endpoints.callbacks.update_configuration import UpdateNumbersCallbackConfigurationEndpoint from sinch.domains.numbers.endpoints.active.list_active_numbers_for_project import ListActiveNumbersEndpoint from sinch.domains.numbers.endpoints.active.update_number_configuration import UpdateNumberConfigurationEndpoint @@ -20,7 +22,6 @@ ListAvailableNumbersRequest, ActivateNumberRequest, CheckNumberAvailabilityRequest, RentAnyNumberRequest ) - from sinch.domains.numbers.models.regions.responses import ListAvailableRegionsResponse from sinch.domains.numbers.models.available.responses import ( ListAvailableNumbersResponse, ActivateNumberResponse, @@ -30,6 +31,13 @@ ListActiveNumbersResponse, UpdateNumberConfigurationResponse, GetNumberConfigurationResponse, ReleaseNumberFromProjectResponse ) +from sinch.domains.numbers.models.callbacks.responses import ( + GetNumbersCallbackConfigurationResponse, + UpdateNumbersCallbackConfigurationResponse +) +from sinch.domains.numbers.models.callbacks.requests import ( + UpdateNumbersCallbackConfigurationRequest +) class AvailableNumbers: @@ -267,6 +275,28 @@ def list( ) +class Callbacks: + def __init__(self, sinch): + self._sinch = sinch + + def get_configuration(self) -> GetNumbersCallbackConfigurationResponse: + return self._sinch.configuration.transport.request( + GetNumbersCallbackConfigurationEndpoint( + project_id=self._sinch.configuration.project_id + ) + ) + + def update_configuration(self, hmac_secret) -> UpdateNumbersCallbackConfigurationResponse: + return self._sinch.configuration.transport.request( + UpdateNumbersCallbackConfigurationEndpoint( + project_id=self._sinch.configuration.project_id, + request_data=UpdateNumbersCallbackConfigurationRequest( + hmac_secret=hmac_secret + ) + ) + ) + + class NumbersBase: """ Documentation for Sinch virtual Numbers is found at https://developers.sinch.com/docs/numbers/. @@ -286,6 +316,7 @@ def __init__(self, sinch): self.available = AvailableNumbers(self._sinch) self.regions = AvailableRegions(self._sinch) self.active = ActiveNumbers(self._sinch) + self.callbacks = Callbacks(self._sinch) class NumbersAsync(NumbersBase): @@ -299,3 +330,4 @@ def __init__(self, sinch): self.available = AvailableNumbers(self._sinch) self.regions = AvailableRegions(self._sinch) self.active = ActiveNumbersWithAsyncPagination(self._sinch) + self.callbacks = Callbacks(self._sinch) diff --git a/sinch/domains/numbers/endpoints/callbacks/__init__.py b/sinch/domains/numbers/endpoints/callbacks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sinch/domains/numbers/endpoints/callbacks/get_configuration.py b/sinch/domains/numbers/endpoints/callbacks/get_configuration.py new file mode 100644 index 00000000..78bea393 --- /dev/null +++ b/sinch/domains/numbers/endpoints/callbacks/get_configuration.py @@ -0,0 +1,27 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.numbers.endpoints.numbers_endpoint import NumbersEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethod +from sinch.domains.numbers.models.callbacks.responses import GetNumbersCallbackConfigurationResponse + + +class GetNumbersCallbackConfigurationEndpoint(NumbersEndpoint): + ENDPOINT_URL = "{origin}/v1/projects/{project_id}/callbackConfiguration" + HTTP_METHOD = HTTPMethod.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.OAUTH.value + + def __init__(self, project_id: str): + super().__init__(project_id, None) + self.project_id = project_id + + def build_url(self, sinch): + return self.ENDPOINT_URL.format( + origin=sinch.configuration.numbers_origin, + project_id=self.project_id + ) + + def handle_response(self, response: HTTPResponse) -> GetNumbersCallbackConfigurationResponse: + super().handle_response(response) + return GetNumbersCallbackConfigurationResponse( + project_id=response.body['projectId'], + hmac_secret=response.body['hmacSecret'] + ) diff --git a/sinch/domains/numbers/endpoints/callbacks/update_configuration.py b/sinch/domains/numbers/endpoints/callbacks/update_configuration.py new file mode 100644 index 00000000..f40de79d --- /dev/null +++ b/sinch/domains/numbers/endpoints/callbacks/update_configuration.py @@ -0,0 +1,32 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.numbers.endpoints.numbers_endpoint import NumbersEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethod +from sinch.domains.numbers.models.callbacks.responses import UpdateNumbersCallbackConfigurationResponse +from sinch.domains.numbers.models.callbacks.requests import UpdateNumbersCallbackConfigurationRequest + + +class UpdateNumbersCallbackConfigurationEndpoint(NumbersEndpoint): + ENDPOINT_URL = "{origin}/v1/projects/{project_id}/callbackConfiguration" + HTTP_METHOD = HTTPMethod.PATCH.value + HTTP_AUTHENTICATION = HTTPAuthentication.OAUTH.value + + def __init__(self, project_id: str, request_data: UpdateNumbersCallbackConfigurationRequest): + super().__init__(project_id, request_data) + self.project_id = project_id + self.request_data = request_data + + def build_url(self, sinch): + return self.ENDPOINT_URL.format( + origin=sinch.configuration.numbers_origin, + project_id=self.project_id + ) + + def request_body(self): + return self.request_data.as_json() + + def handle_response(self, response: HTTPResponse) -> UpdateNumbersCallbackConfigurationResponse: + super().handle_response(response) + return UpdateNumbersCallbackConfigurationResponse( + project_id=response.body['projectId'], + hmac_secret=response.body['hmacSecret'] + ) diff --git a/sinch/domains/numbers/models/callbacks/__init__.py b/sinch/domains/numbers/models/callbacks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sinch/domains/numbers/models/callbacks/requests.py b/sinch/domains/numbers/models/callbacks/requests.py new file mode 100644 index 00000000..621fbad7 --- /dev/null +++ b/sinch/domains/numbers/models/callbacks/requests.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from sinch.core.models.base_model import SinchRequestBaseModel + + +@dataclass +class UpdateNumbersCallbackConfigurationRequest(SinchRequestBaseModel): + hmac_secret: str diff --git a/sinch/domains/numbers/models/callbacks/responses.py b/sinch/domains/numbers/models/callbacks/responses.py new file mode 100644 index 00000000..73fe758b --- /dev/null +++ b/sinch/domains/numbers/models/callbacks/responses.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +from sinch.core.models.base_model import SinchBaseModel + + +@dataclass +class NumbersCallbackConfigurationResponse(SinchBaseModel): + project_id: str + hmac_secret: str + + +@dataclass +class GetNumbersCallbackConfigurationResponse(NumbersCallbackConfigurationResponse): + pass + + +@dataclass +class UpdateNumbersCallbackConfigurationResponse(NumbersCallbackConfigurationResponse): + pass diff --git a/sinch/domains/verification/__init__.py b/sinch/domains/verification/__init__.py new file mode 100644 index 00000000..e32a8b2a --- /dev/null +++ b/sinch/domains/verification/__init__.py @@ -0,0 +1,152 @@ +from sinch.domains.verification.endpoints.start_verification import StartVerificationEndpoint +from sinch.domains.verification.endpoints.report_verification_using_identity import ( + ReportVerificationByIdentityEndpoint +) +from sinch.domains.verification.endpoints.report_verification_using_id import ( + ReportVerificationByIdEndpoint +) +from sinch.domains.verification.endpoints.get_verification_by_id import ( + GetVerificationStatusByIdEndpoint +) +from sinch.domains.verification.endpoints.get_verification_by_identity import ( + GetVerificationStatusByIdentityEndpoint +) +from sinch.domains.verification.endpoints.get_verification_by_reference import ( + GetVerificationStatusByReferenceEndpoint +) +from sinch.domains.verification.models.responses import ( + StartVerificationResponse, + ReportVerificationByIdentityResponse, + ReportVerificationByIdResponse, + GetVerificationStatusByIdentityResponse, + GetVerificationStatusByIdResponse, + GetVerificationStatusByReferenceResponse +) +from sinch.domains.verification.models.requests import ( + StartVerificationRequest, + ReportVerificationByIdentityRequest, + ReportVerificationByIdRequest, + GetVerificationStatusByIdRequest, + GetVerificationStatusByIdentityRequest, + GetVerificationStatusByReferenceRequest +) + +from sinch.domains.verification.enums import VerificationMethod + + +class Verifications: + def __init__(self, sinch): + self._sinch = sinch + + def start( + self, + identity: dict, + method: VerificationMethod, + reference: str = None, + custom: str = None, + flash_call_options: dict = None + ) -> StartVerificationResponse: + return self._sinch.configuration.transport.request( + StartVerificationEndpoint( + request_data=StartVerificationRequest( + identity=identity, + method=method, + reference=reference, + custom=custom, + flash_call_options=flash_call_options + ) + ) + ) + + def report_by_id( + self, + id: str, + verification_report_request: dict + ) -> ReportVerificationByIdResponse: + return self._sinch.configuration.transport.request( + ReportVerificationByIdEndpoint( + request_data=ReportVerificationByIdRequest( + id, + verification_report_request + ) + ) + ) + + def report_by_identity( + self, + endpoint, + verification_report_request + ) -> ReportVerificationByIdentityResponse: + return self._sinch.configuration.transport.request( + ReportVerificationByIdentityEndpoint( + request_data=ReportVerificationByIdentityRequest( + endpoint, + verification_report_request + ) + ) + ) + + +class VerificationStatus: + def __init__(self, sinch): + self._sinch = sinch + + def get_by_reference(self, reference) -> GetVerificationStatusByReferenceResponse: + return self._sinch.configuration.transport.request( + GetVerificationStatusByReferenceEndpoint( + request_data=GetVerificationStatusByReferenceRequest( + reference=reference + ) + ) + ) + + def get_by_id(self, id) -> GetVerificationStatusByIdResponse: + return self._sinch.configuration.transport.request( + GetVerificationStatusByIdEndpoint( + request_data=GetVerificationStatusByIdRequest( + id=id + ) + ) + ) + + def get_by_identity(self, endpoint, method) -> GetVerificationStatusByIdentityResponse: + return self._sinch.configuration.transport.request( + GetVerificationStatusByIdentityEndpoint( + request_data=GetVerificationStatusByIdentityRequest( + endpoint=endpoint, + method=method + ) + ) + ) + + +class VerificationBase: + """ + Documentation for the Verification API: https://developers.sinch.com/docs/verification/ + """ + def __init__(self, sinch): + self._sinch = sinch + + +class Verification(VerificationBase): + """ + Synchronous version of the Verification Domain + """ + __doc__ += VerificationBase.__doc__ + + def __init__(self, sinch): + super(Verification, self).__init__(sinch) + self.verifications = Verifications(self._sinch) + self.verification_status = VerificationStatus(self._sinch) + + +class VerificationAsync(VerificationBase): + """ + Asynchronous version of the Verification Domain + """ + __doc__ += VerificationBase.__doc__ + + def __init__(self, sinch): + super(VerificationAsync, self).__init__(sinch) + self.verifications = Verifications(self._sinch) + self.verification_status = VerificationStatus(self._sinch) diff --git a/sinch/domains/verification/endpoints/__init__.py b/sinch/domains/verification/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sinch/domains/verification/endpoints/get_verification_by_id.py b/sinch/domains/verification/endpoints/get_verification_by_id.py new file mode 100644 index 00000000..4a64f6a8 --- /dev/null +++ b/sinch/domains/verification/endpoints/get_verification_by_id.py @@ -0,0 +1,35 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.verification.endpoints.verification_endpoint import VerificationEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethod +from sinch.domains.verification.models.requests import GetVerificationStatusByIdRequest +from sinch.domains.verification.models.responses import GetVerificationStatusByIdResponse + + +class GetVerificationStatusByIdEndpoint(VerificationEndpoint): + ENDPOINT_URL = "{origin}/verification/v1/verifications/id/{id}" + HTTP_METHOD = HTTPMethod.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: GetVerificationStatusByIdRequest): + self.request_data = request_data + + def build_url(self, sinch): + return self.ENDPOINT_URL.format( + origin=sinch.configuration.verification_origin, + id=self.request_data.id + ) + + def handle_response(self, response: HTTPResponse) -> GetVerificationStatusByIdResponse: + super().handle_response(response) + return GetVerificationStatusByIdResponse( + id=response.body.get("id"), + method=response.body.get("method"), + status=response.body.get("status"), + price=response.body.get("price"), + identity=response.body.get("identity"), + country_id=response.body.get("country_id"), + verification_timestamp=response.body.get("verification_timestamp"), + reference=response.body.get("reference"), + reason=response.body.get("reason"), + call_complete=response.body.get("call_complete") + ) diff --git a/sinch/domains/verification/endpoints/get_verification_by_identity.py b/sinch/domains/verification/endpoints/get_verification_by_identity.py new file mode 100644 index 00000000..41a471e5 --- /dev/null +++ b/sinch/domains/verification/endpoints/get_verification_by_identity.py @@ -0,0 +1,36 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.verification.endpoints.verification_endpoint import VerificationEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethod +from sinch.domains.verification.models.requests import GetVerificationStatusByIdentityRequest +from sinch.domains.verification.models.responses import GetVerificationStatusByIdentityResponse + + +class GetVerificationStatusByIdentityEndpoint(VerificationEndpoint): + ENDPOINT_URL = "{origin}/verification/v1/verifications/{method}/number/{endpoint}" + HTTP_METHOD = HTTPMethod.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: GetVerificationStatusByIdentityRequest): + self.request_data = request_data + + def build_url(self, sinch): + return self.ENDPOINT_URL.format( + origin=sinch.configuration.verification_origin, + method=self.request_data.method, + endpoint=self.request_data.endpoint + ) + + def handle_response(self, response: HTTPResponse) -> GetVerificationStatusByIdentityResponse: + super().handle_response(response) + return GetVerificationStatusByIdentityResponse( + id=response.body.get("id"), + method=response.body.get("method"), + status=response.body.get("status"), + price=response.body.get("price"), + identity=response.body.get("identity"), + country_id=response.body.get("country_id"), + verification_timestamp=response.body.get("verification_timestamp"), + reference=response.body.get("reference"), + reason=response.body.get("reason"), + call_complete=response.body.get("call_complete") + ) diff --git a/sinch/domains/verification/endpoints/get_verification_by_reference.py b/sinch/domains/verification/endpoints/get_verification_by_reference.py new file mode 100644 index 00000000..52fafdd5 --- /dev/null +++ b/sinch/domains/verification/endpoints/get_verification_by_reference.py @@ -0,0 +1,35 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.verification.endpoints.verification_endpoint import VerificationEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethod +from sinch.domains.verification.models.requests import GetVerificationStatusByReferenceRequest +from sinch.domains.verification.models.responses import GetVerificationStatusByReferenceResponse + + +class GetVerificationStatusByReferenceEndpoint(VerificationEndpoint): + ENDPOINT_URL = "{origin}/verification/v1/verifications/reference/{reference}" + HTTP_METHOD = HTTPMethod.GET.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: GetVerificationStatusByReferenceRequest): + self.request_data = request_data + + def build_url(self, sinch): + return self.ENDPOINT_URL.format( + origin=sinch.configuration.verification_origin, + reference=self.request_data.reference + ) + + def handle_response(self, response: HTTPResponse) -> GetVerificationStatusByReferenceResponse: + super().handle_response(response) + return GetVerificationStatusByReferenceResponse( + id=response.body.get("id"), + method=response.body.get("method"), + status=response.body.get("status"), + price=response.body.get("price"), + identity=response.body.get("identity"), + country_id=response.body.get("country_id"), + verification_timestamp=response.body.get("verification_timestamp"), + reference=response.body.get("reference"), + reason=response.body.get("reason"), + call_complete=response.body.get("call_complete") + ) diff --git a/sinch/domains/verification/endpoints/report_verification_using_id.py b/sinch/domains/verification/endpoints/report_verification_using_id.py new file mode 100644 index 00000000..f974134d --- /dev/null +++ b/sinch/domains/verification/endpoints/report_verification_using_id.py @@ -0,0 +1,39 @@ +import json +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.verification.endpoints.verification_endpoint import VerificationEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethod +from sinch.domains.verification.models.requests import ReportVerificationByIdRequest +from sinch.domains.verification.models.responses import ReportVerificationByIdResponse + + +class ReportVerificationByIdEndpoint(VerificationEndpoint): + ENDPOINT_URL = "{origin}/verification/v1/verifications/id/{id}" + HTTP_METHOD = HTTPMethod.PUT.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: ReportVerificationByIdRequest): + self.request_data = request_data + + def build_url(self, sinch): + return self.ENDPOINT_URL.format( + origin=sinch.configuration.verification_origin, + id=self.request_data.id + ) + + def request_body(self): + return json.dumps(self.request_data.verification_report_request) + + def handle_response(self, response: HTTPResponse) -> ReportVerificationByIdResponse: + super().handle_response(response) + return ReportVerificationByIdResponse( + id=response.body.get("id"), + method=response.body.get("method"), + status=response.body.get("status"), + price=response.body.get("price"), + identity=response.body.get("identity"), + country_id=response.body.get("country_id"), + verification_timestamp=response.body.get("verification_timestamp"), + reference=response.body.get("reference"), + reason=response.body.get("reason"), + call_complete=response.body.get("call_complete") + ) diff --git a/sinch/domains/verification/endpoints/report_verification_using_identity.py b/sinch/domains/verification/endpoints/report_verification_using_identity.py new file mode 100644 index 00000000..3da9a9f1 --- /dev/null +++ b/sinch/domains/verification/endpoints/report_verification_using_identity.py @@ -0,0 +1,39 @@ +import json +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.verification.endpoints.verification_endpoint import VerificationEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethod +from sinch.domains.verification.models.requests import ReportVerificationByIdentityRequest +from sinch.domains.verification.models.responses import ReportVerificationByIdentityResponse + + +class ReportVerificationByIdentityEndpoint(VerificationEndpoint): + ENDPOINT_URL = "{origin}/verification/v1/verifications/number/{endpoint}" + HTTP_METHOD = HTTPMethod.PUT.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: ReportVerificationByIdentityRequest): + self.request_data = request_data + + def build_url(self, sinch): + return self.ENDPOINT_URL.format( + origin=sinch.configuration.verification_origin, + endpoint=self.request_data.endpoint + ) + + def request_body(self): + return json.dumps(self.request_data.verification_report_request) + + def handle_response(self, response: HTTPResponse) -> ReportVerificationByIdentityResponse: + super().handle_response(response) + return ReportVerificationByIdentityResponse( + id=response.body.get("id"), + method=response.body.get("method"), + status=response.body.get("status"), + price=response.body.get("price"), + identity=response.body.get("identity"), + country_id=response.body.get("country_id"), + verification_timestamp=response.body.get("verification_timestamp"), + reference=response.body.get("reference"), + reason=response.body.get("reason"), + call_complete=response.body.get("call_complete") + ) diff --git a/sinch/domains/verification/endpoints/start_verification.py b/sinch/domains/verification/endpoints/start_verification.py new file mode 100644 index 00000000..89858375 --- /dev/null +++ b/sinch/domains/verification/endpoints/start_verification.py @@ -0,0 +1,50 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.domains.verification.endpoints.verification_endpoint import VerificationEndpoint +from sinch.core.enums import HTTPAuthentication, HTTPMethod +from sinch.domains.verification.enums import VerificationMethod +from sinch.domains.verification.models.requests import StartVerificationRequest +from sinch.domains.verification.models.responses import ( + StartVerificationResponse, + StartSMSInitiateVerificationResponse, + StartDataInitiateVerificationResponse, + StartCalloutInitiateVerificationResponse, + StartFlashCallInitiateVerificationResponse +) + + +class StartVerificationEndpoint(VerificationEndpoint): + ENDPOINT_URL = "{origin}/verification/v1/verifications" + HTTP_METHOD = HTTPMethod.POST.value + HTTP_AUTHENTICATION = HTTPAuthentication.SIGNED.value + + def __init__(self, request_data: StartVerificationRequest): + self.request_data = request_data + + def build_url(self, sinch): + return self.ENDPOINT_URL.format( + origin=sinch.configuration.verification_origin, + ) + + def request_body(self): + return self.request_data.as_json() + + def handle_response(self, response: HTTPResponse) -> StartVerificationResponse: + if self.request_data.method == VerificationMethod.SMS.value: + return StartSMSInitiateVerificationResponse( + **response.body + ) + elif self.request_data.method == VerificationMethod.FLASHCALL.value: + return StartFlashCallInitiateVerificationResponse( + id=response.body.get("id"), + method=response.body.get("method"), + _links=response.body.get("_links"), + flashcall=response.body.get("flashCall") + ) + elif self.request_data.method == VerificationMethod.CALLOUT.value: + return StartCalloutInitiateVerificationResponse( + **response.body + ) + elif self.request_data.method == VerificationMethod.SEAMLESS.value: + return StartDataInitiateVerificationResponse( + **response.body + ) diff --git a/sinch/domains/verification/endpoints/verification_endpoint.py b/sinch/domains/verification/endpoints/verification_endpoint.py new file mode 100644 index 00000000..e7898b62 --- /dev/null +++ b/sinch/domains/verification/endpoints/verification_endpoint.py @@ -0,0 +1,13 @@ +from sinch.core.models.http_response import HTTPResponse +from sinch.core.endpoint import HTTPEndpoint +from sinch.domains.verification.exceptions import VerificationException + + +class VerificationEndpoint(HTTPEndpoint): + def handle_response(self, response: HTTPResponse): + if response.status_code >= 400: + raise VerificationException( + message=response.body["message"], + response=response, + is_from_server=True + ) diff --git a/sinch/domains/verification/enums.py b/sinch/domains/verification/enums.py new file mode 100644 index 00000000..82f6c80d --- /dev/null +++ b/sinch/domains/verification/enums.py @@ -0,0 +1,17 @@ +from enum import Enum + + +class VerificationMethod(Enum): + SMS = "sms" + FLASHCALL = "flashcall" + CALLOUT = "callout" + SEAMLESS = "seamless" + + +class VerificationStatus(Enum): + PENDING = "PENDING" + SUCCESSFUL = "SUCCESSFUL" + FAIL = "FAIL" + DENIED = "DENIED" + ABORTED = "ABORTED" + ERROR = "ERROR" diff --git a/sinch/domains/verification/exceptions.py b/sinch/domains/verification/exceptions.py new file mode 100644 index 00000000..91d913a8 --- /dev/null +++ b/sinch/domains/verification/exceptions.py @@ -0,0 +1,5 @@ +from sinch.core.exceptions import SinchException + + +class VerificationException(SinchException): + pass diff --git a/sinch/domains/verification/models/__init__.py b/sinch/domains/verification/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sinch/domains/verification/models/requests.py b/sinch/domains/verification/models/requests.py new file mode 100644 index 00000000..fddb010c --- /dev/null +++ b/sinch/domains/verification/models/requests.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass +from sinch.core.models.base_model import SinchRequestBaseModel +from sinch.domains.verification.enums import VerificationMethod + + +@dataclass +class StartVerificationRequest(SinchRequestBaseModel): + identity: dict + method: VerificationMethod + reference: str + custom: str + flash_call_options: dict + + +@dataclass +class ReportVerificationByIdentityRequest(SinchRequestBaseModel): + endpoint: str + verification_report_request: dict + + +@dataclass +class ReportVerificationByIdRequest(SinchRequestBaseModel): + id: str + verification_report_request: dict + + +@dataclass +class GetVerificationStatusByReferenceRequest(SinchRequestBaseModel): + reference: str + + +@dataclass +class GetVerificationStatusByIdRequest(SinchRequestBaseModel): + id: str + + +@dataclass +class GetVerificationStatusByIdentityRequest(SinchRequestBaseModel): + endpoint: str + method: VerificationMethod diff --git a/sinch/domains/verification/models/responses.py b/sinch/domains/verification/models/responses.py new file mode 100644 index 00000000..911c1195 --- /dev/null +++ b/sinch/domains/verification/models/responses.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from sinch.core.models.base_model import SinchBaseModel +from sinch.domains.verification.enums import VerificationMethod, VerificationStatus + + +@dataclass +class StartVerificationResponse(SinchBaseModel): + id: str + method: VerificationMethod + _links: list + + +@dataclass +class StartSMSInitiateVerificationResponse(StartVerificationResponse): + sms: dict + + +@dataclass +class StartFlashCallInitiateVerificationResponse(StartVerificationResponse): + flashcall: dict + + +@dataclass +class StartCalloutInitiateVerificationResponse(StartVerificationResponse): + callout: dict + + +@dataclass +class StartDataInitiateVerificationResponse(StartVerificationResponse): + seamless: dict + + +@dataclass +class VerificationResponse(SinchBaseModel): + id: str + method: VerificationMethod + status: VerificationStatus + price: dict + identity: dict + country_id: str + verification_timestamp: str + reference: str + reason: str + call_complete: bool + + +@dataclass +class ReportVerificationResponse(VerificationResponse): + pass + + +@dataclass +class ReportVerificationByIdentityResponse(ReportVerificationResponse): + pass + + +@dataclass +class ReportVerificationByIdResponse(ReportVerificationResponse): + pass + + +@dataclass +class GetVerificationStatusByReferenceResponse(VerificationResponse): + pass + + +@dataclass +class GetVerificationStatusByIdResponse(VerificationResponse): + pass + + +@dataclass +class GetVerificationStatusByIdentityResponse(VerificationResponse): + pass diff --git a/tests/conftest.py b/tests/conftest.py index 4bf19fab..a7cb74fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,6 +42,7 @@ def configure_origin( templates_origin, auth_origin, sms_origin, + verification_origin, disable_ssl ): if auth_origin: @@ -59,6 +60,9 @@ def configure_origin( if sms_origin: sinch_client.configuration.sms_origin = sms_origin + if verification_origin: + sinch_client.configuration.verification_origin = verification_origin + if disable_ssl: sinch_client.configuration.disable_https = True @@ -100,6 +104,11 @@ def sms_origin(): return os.getenv("SMS_ORIGIN") +@pytest.fixture +def verification_origin(): + return os.getenv("VERIFICATION_ORIGIN") + + @pytest.fixture def templates_origin(): return os.getenv("TEMPLATES_ORIGIN") @@ -120,6 +129,21 @@ def origin_phone_number(): return os.getenv("ORIGIN_PHONE_NUMBER") +@pytest.fixture +def application_key(): + return os.getenv("APPLICATION_KEY") + + +@pytest.fixture +def application_secret(): + return os.getenv("APPLICATION_SECRET") + + +@pytest.fixture +def verification_id(): + return os.getenv("VERIFICATION_ID") + + @pytest.fixture def app_id(): return os.getenv("APP_ID") @@ -135,6 +159,21 @@ def empty_project_id(): return os.getenv("EMPTY_PROJECT_ID") +@pytest.fixture +def verification_request_signature(): + return os.getenv("VERIFICATION_REQUEST_SIGNATURE") + + +@pytest.fixture +def verification_request_with_empty_body_signature(): + return os.getenv("VERIFICATION_REQUEST_WITH_EMPTY_BODY_SIGNATURE") + + +@pytest.fixture +def verification_request_signature_timestamp(): + return os.getenv("VERIFICATION_REQUEST_SIGNATURE_TIMESTAMP") + + @pytest.fixture def http_response(): return HTTPResponse( @@ -155,7 +194,7 @@ def sms_http_response(): return HTTPResponse( status_code=404, body={ - "text": "Nobody expects the Spanish Inquisition!" + "text": "Nobody expects the Spanish Inquisition!" }, headers={ "SAMPLE_HEADER": "test" @@ -237,34 +276,6 @@ def second_int_based_pagination_response(): ) -@pytest.fixture -def int_based_pagination_request_data(): - return IntBasedPaginationRequest( - page=0, - page_size=2 - ) - - -@pytest.fixture -def first_int_based_pagination_response(): - return IntBasedPaginationResponse( - count=4, - page=0, - page_size=2, - pig_dogs=["Bartosz", "Piotr"] - ) - - -@pytest.fixture -def second_int_based_pagination_response(): - return IntBasedPaginationResponse( - count=4, - page=1, - page_size=2, - pig_dogs=["Walaszek", "Połać"] - ) - - @pytest.fixture def third_int_based_pagination_response(): return IntBasedPaginationResponse( @@ -275,15 +286,26 @@ def third_int_based_pagination_response(): ) +@pytest.fixture +def int_based_pagination_request_data(): + return IntBasedPaginationRequest( + page=0, + page_size=2 + ) + + @pytest.fixture def sinch_client_sync( key_id, key_secret, + application_key, + application_secret, numbers_origin, conversation_origin, templates_origin, auth_origin, sms_origin, + verification_origin, disable_ssl, project_id ): @@ -291,13 +313,16 @@ def sinch_client_sync( Client( key_id=key_id, key_secret=key_secret, - project_id=project_id + project_id=project_id, + application_key=application_key, + application_secret=application_secret ), numbers_origin, conversation_origin, templates_origin, auth_origin, sms_origin, + verification_origin, disable_ssl ) @@ -306,11 +331,14 @@ def sinch_client_sync( def sinch_client_async( key_id, key_secret, + application_key, + application_secret, numbers_origin, conversation_origin, templates_origin, auth_origin, sms_origin, + verification_origin, disable_ssl, project_id ): @@ -318,12 +346,15 @@ def sinch_client_async( ClientAsync( key_id=key_id, key_secret=key_secret, - project_id=project_id + project_id=project_id, + application_key=application_key, + application_secret=application_secret ), numbers_origin, conversation_origin, templates_origin, auth_origin, sms_origin, + verification_origin, disable_ssl ) diff --git a/tests/e2e/numbers/callbacks/test_get_callbacks_configuration.py b/tests/e2e/numbers/callbacks/test_get_callbacks_configuration.py new file mode 100644 index 00000000..d0b36d51 --- /dev/null +++ b/tests/e2e/numbers/callbacks/test_get_callbacks_configuration.py @@ -0,0 +1,6 @@ +from sinch.domains.numbers.models.callbacks.responses import GetNumbersCallbackConfigurationResponse + + +def test_get_callback_configuration_configuration(sinch_client_sync): + get_callback_configuration_response = sinch_client_sync.numbers.callbacks.get_configuration() + assert isinstance(get_callback_configuration_response, GetNumbersCallbackConfigurationResponse) diff --git a/tests/e2e/numbers/callbacks/test_update_callback_configuration.py b/tests/e2e/numbers/callbacks/test_update_callback_configuration.py new file mode 100644 index 00000000..024d4dce --- /dev/null +++ b/tests/e2e/numbers/callbacks/test_update_callback_configuration.py @@ -0,0 +1,8 @@ +from sinch.domains.numbers.models.callbacks.responses import UpdateNumbersCallbackConfigurationResponse + + +def test_update_callback_configuration_configuration(sinch_client_sync): + update_callback_configuration_response = sinch_client_sync.numbers.callbacks.update_configuration( + hmac_secret="Secret" + ) + assert isinstance(update_callback_configuration_response, UpdateNumbersCallbackConfigurationResponse) diff --git a/tests/e2e/verification/test_get_report_using_id.py b/tests/e2e/verification/test_get_report_using_id.py new file mode 100644 index 00000000..68962909 --- /dev/null +++ b/tests/e2e/verification/test_get_report_using_id.py @@ -0,0 +1,23 @@ +from sinch.domains.verification.models.responses import GetVerificationStatusByIdResponse + + +def test_get_report_verification_using_id( + sinch_client_sync, + phone_number, + verification_id +): + verification_response = sinch_client_sync.verification.verification_status.get_by_id( + id=verification_id + ) + assert isinstance(verification_response, GetVerificationStatusByIdResponse) + + +async def test_get_report_verification_using_id_async( + sinch_client_async, + phone_number, + verification_id +): + verification_response = await sinch_client_async.verification.verification_status.get_by_id( + id=verification_id + ) + assert isinstance(verification_response, GetVerificationStatusByIdResponse) diff --git a/tests/e2e/verification/test_get_report_using_identity.py b/tests/e2e/verification/test_get_report_using_identity.py new file mode 100644 index 00000000..569369f3 --- /dev/null +++ b/tests/e2e/verification/test_get_report_using_identity.py @@ -0,0 +1,23 @@ +from sinch.domains.verification.models.responses import GetVerificationStatusByIdentityResponse + + +def test_get_report_verification_using_identity( + sinch_client_sync, + phone_number +): + verification_response = sinch_client_sync.verification.verification_status.get_by_identity( + endpoint=phone_number, + method="sms" + ) + assert isinstance(verification_response, GetVerificationStatusByIdentityResponse) + + +async def test_get_report_verification_using_identity_async( + sinch_client_async, + phone_number +): + verification_response = await sinch_client_async.verification.verification_status.get_by_identity( + endpoint=phone_number, + method="sms" + ) + assert isinstance(verification_response, GetVerificationStatusByIdentityResponse) diff --git a/tests/e2e/verification/test_get_report_using_reference.py b/tests/e2e/verification/test_get_report_using_reference.py new file mode 100644 index 00000000..b2443f1f --- /dev/null +++ b/tests/e2e/verification/test_get_report_using_reference.py @@ -0,0 +1,19 @@ +from sinch.domains.verification.models.responses import GetVerificationStatusByReferenceResponse + + +def test_get_report_verification_using_reference( + sinch_client_sync +): + verification_response = sinch_client_sync.verification.verification_status.get_by_reference( + reference="random" + ) + assert isinstance(verification_response, GetVerificationStatusByReferenceResponse) + + +async def test_get_report_verification_using_reference_async( + sinch_client_async +): + verification_response = await sinch_client_async.verification.verification_status.get_by_reference( + reference="random" + ) + assert isinstance(verification_response, GetVerificationStatusByReferenceResponse) diff --git a/tests/e2e/verification/test_report_verification_using_id.py b/tests/e2e/verification/test_report_verification_using_id.py new file mode 100644 index 00000000..67381db5 --- /dev/null +++ b/tests/e2e/verification/test_report_verification_using_id.py @@ -0,0 +1,35 @@ +from sinch.domains.verification.models.responses import ReportVerificationByIdResponse + + +def test_report_verification_using_id_and_sms( + sinch_client_sync, + phone_number, + verification_id +): + verification_response = sinch_client_sync.verification.verifications.report_by_id( + id=verification_id, + verification_report_request={ + "method": "sms", + "sms": { + "code": "2302" + } + } + ) + assert isinstance(verification_response, ReportVerificationByIdResponse) + + +async def test_report_verification_using_id_and_sms_async( + sinch_client_async, + phone_number, + verification_id +): + verification_response = await sinch_client_async.verification.verifications.report_by_id( + id=verification_id, + verification_report_request={ + "method": "sms", + "sms": { + "code": "2302" + } + } + ) + assert isinstance(verification_response, ReportVerificationByIdResponse) diff --git a/tests/e2e/verification/test_report_verification_using_identity.py b/tests/e2e/verification/test_report_verification_using_identity.py new file mode 100644 index 00000000..816c6a6b --- /dev/null +++ b/tests/e2e/verification/test_report_verification_using_identity.py @@ -0,0 +1,33 @@ +from sinch.domains.verification.models.responses import ReportVerificationByIdentityResponse + + +def test_report_verification_using_identity_and_sms( + sinch_client_sync, + phone_number +): + verification_response = sinch_client_sync.verification.verifications.report_by_identity( + endpoint=phone_number, + verification_report_request={ + "method": "sms", + "sms": { + "code": "2302" + } + } + ) + assert isinstance(verification_response, ReportVerificationByIdentityResponse) + + +async def test_report_verification_using_identity_and_sms_async( + sinch_client_async, + phone_number +): + verification_response = await sinch_client_async.verification.verifications.report_by_identity( + endpoint=phone_number, + verification_report_request={ + "method": "sms", + "sms": { + "code": "2302" + } + } + ) + assert isinstance(verification_response, ReportVerificationByIdentityResponse) diff --git a/tests/e2e/verification/test_start_verification.py b/tests/e2e/verification/test_start_verification.py new file mode 100644 index 00000000..da3dfb21 --- /dev/null +++ b/tests/e2e/verification/test_start_verification.py @@ -0,0 +1,53 @@ +from sinch.domains.verification.models.responses import ( + StartSMSInitiateVerificationResponse, + StartFlashCallInitiateVerificationResponse +) +from sinch.domains.verification.enums import VerificationMethod + + +def test_start_verification_sms( + sinch_client_sync, + phone_number +): + verification_response = sinch_client_sync.verification.verifications.start( + method="sms", + identity={ + "type": "number", + "endpoint": phone_number + }, + reference="random" + ) + + assert isinstance(verification_response, StartSMSInitiateVerificationResponse) + + +def test_start_verification_flash_call( + sinch_client_sync, + phone_number +): + verification_response = sinch_client_sync.verification.verifications.start( + method=VerificationMethod.FLASHCALL.value, + identity={ + "type": "number", + "endpoint": phone_number + }, + reference="random5" + ) + + assert isinstance(verification_response, StartFlashCallInitiateVerificationResponse) + + +async def test_start_verification_async( + sinch_client_async, + phone_number +): + verification_response = await sinch_client_async.verification.verifications.start( + method="sms", + identity={ + "type": "number", + "endpoint": phone_number + }, + reference="random" + ) + + assert isinstance(verification_response, StartSMSInitiateVerificationResponse) diff --git a/tests/examples/test_fast_api_example.py b/tests/examples/test_fast_api_example.py new file mode 100644 index 00000000..71983627 --- /dev/null +++ b/tests/examples/test_fast_api_example.py @@ -0,0 +1,16 @@ +from fastapi.testclient import TestClient +from examples.fast_api_example import app, sinch_client + + +def test_get_available_numbers_get_endpoint( + auth_origin, + numbers_origin +): + sinch_client.configuration.auth_origin = auth_origin + sinch_client.configuration.numbers_origin = numbers_origin + sinch_client.configuration.disable_https = True + + client = TestClient(app) + response = client.get("/available_numbers") + assert response.status_code == 200 + assert "available_numbers" in response.json() diff --git a/tests/examples/test_flask_example.py b/tests/examples/test_flask_example.py new file mode 100644 index 00000000..a317b60d --- /dev/null +++ b/tests/examples/test_flask_example.py @@ -0,0 +1,16 @@ +from examples.flask_example import app, sinch_client + + +def test_flask_create_app_get_endpoint( + auth_origin, + conversation_origin +): + sinch_client.configuration.auth_origin = auth_origin + sinch_client.configuration.conversation_origin = conversation_origin + sinch_client.configuration.disable_https = True + app.testing = True + flask_client = app.test_client() + + response = flask_client.post("/create_app") + assert response.status_code == 200 + assert "sinch_app_id" in response.json diff --git a/tests/examples/test_logging_example.py b/tests/examples/test_logging_example.py new file mode 100644 index 00000000..3b05df8b --- /dev/null +++ b/tests/examples/test_logging_example.py @@ -0,0 +1,19 @@ +from examples.logging_example import main, sinch_client + + +def test_sinch_client_logging_with_e2e_test( + caplog, + auth_origin, + numbers_origin +): + sinch_client.configuration.auth_origin = auth_origin + sinch_client.configuration.numbers_origin = numbers_origin + sinch_client.configuration.disable_https = True + main() + + assert len(caplog.records) + + with open("/tmp/test_python_logging.log") as fd: + log_messages = fd.read() + assert "DEBUG" in log_messages + assert "myapp.sinch" in log_messages diff --git a/tests/integration/test_logging.py b/tests/integration/test_logging.py index fc27e61a..0e62482f 100644 --- a/tests/integration/test_logging.py +++ b/tests/integration/test_logging.py @@ -4,9 +4,9 @@ def mock_http_transport(client): - client.configuration.transport.session = Mock() - client.configuration.transport.session.request.return_value.content = {} - client.configuration.transport.session.request.return_value.headers = {} + client.configuration.transport.http_session = Mock() + client.configuration.transport.http_session.request.return_value.content = None + client.configuration.transport.http_session.request.return_value.headers = None client.configuration.transport.prepare_request = Mock() client.configuration.transport.authenticate = Mock() return client @@ -17,18 +17,21 @@ def test_default_logger(sinch_client_sync, caplog): sinch_client = mock_http_transport(sinch_client_sync) http_endpoint = Mock() sinch_client.configuration.transport.request(http_endpoint) - assert len(caplog.records) == 2 + assert len(caplog.get_records("call")) == 2 assert caplog.records[0].levelname == "DEBUG" def test_changing_logger_name_within_the_client(sinch_client_sync, caplog): + logger_name = "SumOlimpijczyk" sinch_client_sync.configuration.logger.setLevel(logging.DEBUG) - sinch_client_sync.configuration.logger.name = "SumOlimpijczyk" + sinch_client_sync.configuration.logger.name = logger_name sinch_client = mock_http_transport(sinch_client_sync) + caplog.set_level(logging.DEBUG, logger=logger_name) http_endpoint = Mock() sinch_client.configuration.transport.request(http_endpoint) - assert len(caplog.records) == 2 - assert caplog.records[0].name == "SumOlimpijczyk" + + assert len([record for record in caplog.records if record.name == logger_name]) == 2 + assert caplog.records[0].name == logger_name def test_logger_with_logging_to_file(sinch_client_sync): diff --git a/tests/integration/test_request_signing.py b/tests/integration/test_request_signing.py new file mode 100644 index 00000000..46e49abc --- /dev/null +++ b/tests/integration/test_request_signing.py @@ -0,0 +1,60 @@ +import json +from sinch.core.signature import Signature + + +def test_request_signature( + sinch_client_sync, + verification_request_signature, + verification_request_signature_timestamp +): + signature = Signature( + sinch_client_sync, + http_method="GET", + request_data=json.dumps({"test": "test"}), + request_uri="/verification/v1/verifications", + signature_timestamp=verification_request_signature_timestamp + ) + signature.calculate() + + assert signature.authorization_signature + assert isinstance(signature.authorization_signature, str) + assert verification_request_signature == signature.authorization_signature + + +def test_request_signature_using_empty_body( + sinch_client_sync, + verification_request_with_empty_body_signature, + verification_request_signature_timestamp +): + signature = Signature( + sinch_client_sync, + http_method="POST", + request_data=None, + request_uri="/verification/v1/verifications", + signature_timestamp=verification_request_signature_timestamp + ) + signature.calculate() + + assert signature.authorization_signature + assert isinstance(signature.authorization_signature, str) + assert verification_request_with_empty_body_signature == signature.authorization_signature + + +def test_get_headers_with_signature_and_async_client( + sinch_client_async, + verification_request_with_empty_body_signature, + verification_request_signature_timestamp +): + signature = Signature( + sinch_client_async, + http_method="POST", + request_data=None, + request_uri="/verification/v1/verifications", + signature_timestamp=verification_request_signature_timestamp + ) + headers = signature.get_http_headers_with_signature() + + assert "x-timestamp" in headers + assert "Authorization" in headers + assert "Content-Type" in headers + assert verification_request_with_empty_body_signature in headers["Authorization"] diff --git a/tests/integration/test_token_refresh.py b/tests/integration/test_token_refresh.py index 51c55984..36b97fd3 100644 --- a/tests/integration/test_token_refresh.py +++ b/tests/integration/test_token_refresh.py @@ -3,10 +3,15 @@ from sinch.core.models.http_response import HTTPResponse from sinch.domains.authentication.endpoints.oauth import OAuthEndpoint +from sinch.domains.numbers.endpoints.available.list_available_numbers import AvailableNumbersEndpoint from sinch.domains.authentication.exceptions import AuthenticationException -def test_handling_401_without_expiration(sinch_client_sync, auth_token_as_dict): +def test_handling_401_without_expiration( + sinch_client_sync, + auth_token_as_dict, + project_id +): http_response = HTTPResponse( status_code=401, headers={ @@ -26,7 +31,8 @@ def test_handling_401_without_expiration(sinch_client_sync, auth_token_as_dict): def test_handling_401_with_expired_token_gets_invalidated_and_refreshed( sinch_client_sync, auth_token_as_dict, - expired_token_http_response + expired_token_http_response, + project_id ): sinch_client_sync.configuration.token_manager.set_auth_token(auth_token_as_dict) sinch_client_sync.configuration.transport.request = Mock() @@ -34,7 +40,7 @@ def test_handling_401_with_expired_token_gets_invalidated_and_refreshed( transport_response = sinch_client_sync.configuration.transport.handle_response( http_response=expired_token_http_response, - endpoint=OAuthEndpoint() + endpoint=AvailableNumbersEndpoint(project_id, {}) ) assert transport_response == "Token Refresh!" @@ -42,7 +48,8 @@ def test_handling_401_with_expired_token_gets_invalidated_and_refreshed( async def test_handling_401_with_expired_token_gets_refreshed_async( sinch_client_async, auth_token_as_dict, - expired_token_http_response + expired_token_http_response, + project_id ): sinch_client_async.configuration.token_manager.set_auth_token(auth_token_as_dict) sinch_client_async.configuration.transport.request = AsyncMock() @@ -50,6 +57,6 @@ async def test_handling_401_with_expired_token_gets_refreshed_async( transport_response = await sinch_client_async.configuration.transport.handle_response( http_response=expired_token_http_response, - endpoint=OAuthEndpoint() + endpoint=AvailableNumbersEndpoint(project_id, {}) ) assert transport_response == "Token Refresh!" diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 158304df..fb83355d 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -1,7 +1,6 @@ from sinch.core.clients.sinch_client_configuration import Configuration from sinch.core.adapters.requests_http_transport import HTTPTransportRequests -from sinch.core.adapters.asyncio_http_adapter import HTTPTransportAioHTTP -from sinch.core.token_manager import TokenManager, TokenManagerAsync +from sinch.core.token_manager import TokenManager def test_configuration_initialization_happy_path(sinch_client_sync): @@ -54,8 +53,8 @@ def test_if_logger_name_was_preserved_correctly(sinch_client_async): key_secret="a", project_id="Kickflip!", logger_name=clever_monty_python_quote, - transport=HTTPTransportAioHTTP(sinch_client_async), - token_manager=TokenManagerAsync(sinch_client_async) + transport=HTTPTransportRequests(sinch_client_async), + token_manager=TokenManager(sinch_client_async) ) client_configuration.logger.name = clever_monty_python_quote assert client_configuration.logger.name == clever_monty_python_quote diff --git a/tests/unit/test_user_agent_header.py b/tests/unit/test_user_agent_header.py new file mode 100644 index 00000000..159d1c17 --- /dev/null +++ b/tests/unit/test_user_agent_header.py @@ -0,0 +1,10 @@ +from sinch.domains.conversation.endpoints.app.delete_app import DeleteConversationAppEndpoint +from sinch.domains.conversation.models.app.requests import DeleteConversationAppRequest + + +def test_user_agent_header_creation(sinch_client_sync): + endpoint = DeleteConversationAppRequest(app_id="42") + http_endpoint = DeleteConversationAppEndpoint(sinch_client_sync, endpoint) + http_request = sinch_client_sync.configuration.transport.prepare_request(http_endpoint) + assert "User-Agent" in http_request.headers +