From e195168652ae247def6c23522cf061f4ca80cba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Fri, 19 Nov 2021 16:38:47 +0100 Subject: [PATCH 01/49] removed options bag, enabled and fixed tests --- .../identity/_shared/user_credential.py | 28 ++++++++------- .../identity/_shared/user_credential_async.py | 20 ++++++----- .../_shared/user_token_refresh_options.py | 36 ------------------- ...tests.py => test_user_credential_tests.py} | 29 ++++++--------- 4 files changed, 38 insertions(+), 75 deletions(-) delete mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_token_refresh_options.py rename sdk/communication/azure-communication-identity/tests/{user_credential_tests.py => test_user_credential_tests.py} (64%) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 9b5f17dcc95d..6efdf0296d9b 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -5,33 +5,35 @@ # -------------------------------------------------------------------------- from threading import Lock, Condition from datetime import timedelta -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) - +from typing import Any +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The token refresher to provide capacity to fetch fresh token + :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword refresh_proactively: Whether to refresh the token proactively or not + :keyword refresh_time_before_token_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str - **kwargs + **kwargs # type: Any ): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() + if not isinstance(token, six.string_types): + raise TypeError("token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_token_expiry = kwargs.pop('refresh_time_before_token_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._lock = Condition(Lock()) self._some_thread_refreshing = False diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 52a99e7a4b6a..741bfb967b7b 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -10,26 +10,31 @@ Tuple, Any ) - +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword refresh_proactively: Whether to refresh the token proactively or not + :keyword refresh_time_before_token_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() + if not isinstance(token, six.string_types): + raise TypeError("token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_token_expiry = kwargs.pop('refresh_time_before_token_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._lock = Condition(Lock()) self._some_thread_refreshing = False @@ -56,7 +61,6 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument self._some_thread_refreshing = True break - if should_this_thread_refresh: try: newtoken = await self._token_refresher() # pylint:disable=not-callable diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_token_refresh_options.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_token_refresh_options.py deleted file mode 100644 index 6bdc0d456026..000000000000 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_token_refresh_options.py +++ /dev/null @@ -1,36 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) -import six -from .utils import create_access_token - -class CommunicationTokenRefreshOptions(object): - """Options for refreshing CommunicationTokenCredential. - :param str token: The token used to authenticate to an Azure Communication service - :param token_refresher: The token refresher to provide capacity to fetch fresh token - :raises: TypeError - """ - - def __init__(self, - token, # type: str - token_refresher=None - ): - # type: (str) -> None - if not isinstance(token, six.string_types): - raise TypeError("token must be a string.") - self._token = token - self._token_refresher = token_refresher - - def get_token(self): - """Return the the serialized JWT token.""" - return create_access_token(self._token) - - def get_token_refresher(self): - """Return the token refresher to provide capacity to fetch fresh token.""" - return self._token_refresher diff --git a/sdk/communication/azure-communication-identity/tests/user_credential_tests.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py similarity index 64% rename from sdk/communication/azure-communication-identity/tests/user_credential_tests.py rename to sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py index ec461402d09a..45c30d1c5db3 100644 --- a/sdk/communication/azure-communication-identity/tests/user_credential_tests.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py @@ -6,42 +6,34 @@ from unittest import TestCase from unittest.mock import MagicMock from azure.communication.identity._shared.user_credential import CommunicationTokenCredential -from azure.communication.identity._shared.user_token_refresh_options import CommunicationTokenRefreshOptions from azure.communication.identity._shared.utils import create_access_token class TestCommunicationTokenCredential(TestCase): - sample_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."+\ + sample_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ "eyJleHAiOjMyNTAzNjgwMDAwfQ.9i7FNNHHJT8cOzo-yrAUJyBSfJ-tPPk2emcHavOEpWc" sample_token_expiry = 32503680000 - expired_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."+\ + expired_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ "eyJleHAiOjEwMH0.1h_scYkNp-G98-O4cW6KvfJZwiz54uJMyeDACE4nypg" - def test_communicationtokencredential_decodes_token(self): - refresh_options = CommunicationTokenRefreshOptions(self.sample_token) - credential = CommunicationTokenCredential(refresh_options) + credential = CommunicationTokenCredential(self.sample_token) access_token = credential.get_token() - self.assertEqual(access_token.token, self.sample_token) def test_communicationtokencredential_throws_if_invalid_token(self): - refresh_options = CommunicationTokenRefreshOptions("foo.bar.tar") - self.assertRaises(ValueError, lambda: CommunicationTokenCredential(refresh_options)) + self.assertRaises( + ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) def test_communicationtokencredential_throws_if_nonstring_token(self): - refresh_options = CommunicationTokenRefreshOptions(454): - self.assertRaises(TypeError, lambda: CommunicationTokenCredential(refresh_options) + self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) def test_communicationtokencredential_static_token_returns_expired_token(self): - refresh_options = CommunicationTokenRefreshOptions(self.expired_token) - credential = CommunicationTokenCredential(refresh_options) - + credential = CommunicationTokenCredential(self.expired_token) self.assertEqual(credential.get_token().token, self.expired_token) def test_communicationtokencredential_token_expired_refresh_called(self): refresher = MagicMock(return_value=self.sample_token) - refresh_options = CommunicationTokenRefreshOptions(self.sample_token, refresher) access_token = CommunicationTokenCredential( self.expired_token, token_refresher=refresher).get_token() @@ -50,9 +42,10 @@ def test_communicationtokencredential_token_expired_refresh_called(self): def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): - refresher = MagicMock(return_value=create_access_token(self.expired_token)) - refresh_options = CommunicationTokenRefreshOptions(self.expired_token, refresher) - credential = CommunicationTokenCredential(refresh_options) + refresher = MagicMock( + return_value=create_access_token(self.expired_token)) + credential = CommunicationTokenCredential( + self.expired_token, token_refresher=refresher) credential.get_token() access_token = credential.get_token() From 0cc987834ce4e64abb3f4a71533f3b01924dee5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Sun, 21 Nov 2021 19:06:50 +0100 Subject: [PATCH 02/49] fix build problems --- .../tests/test_user_credential_tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py index 45c30d1c5db3..b1885cdc32a8 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py @@ -4,7 +4,10 @@ # license information. # -------------------------------------------------------------------------- from unittest import TestCase -from unittest.mock import MagicMock +try: + from unittest.mock import MagicMock +except ImportError: # python < 3.3 + from mock import MagicMock # type: ignore from azure.communication.identity._shared.user_credential import CommunicationTokenCredential from azure.communication.identity._shared.utils import create_access_token From 0f91e804f499d260c5bc50c4f7aa28131aeafe5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Thu, 25 Nov 2021 00:11:59 +0100 Subject: [PATCH 03/49] initial implementation of configurable autorefresh --- .../identity/_shared/user_credential.py | 41 +++- .../communication/identity/_shared/utils.py | 7 +- .../tests/test_user_credential_tests.py | 180 +++++++++++++++++- 3 files changed, 211 insertions(+), 17 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 6efdf0296d9b..b476b5076ed3 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition +from threading import Lock, Condition, Timer from datetime import timedelta from typing import Any import six @@ -16,7 +16,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword refresh_proactively: Whether to refresh the token proactively or not - :keyword refresh_time_before_token_expiry: The time before the token expires to refresh the token + :keyword refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ @@ -28,14 +28,24 @@ def __init__(self, **kwargs # type: Any ): if not isinstance(token, six.string_types): - raise TypeError("token must be a string.") + raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_token_expiry = kwargs.pop('refresh_time_before_token_expiry', timedelta( + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + if(self._refresh_proactively): + self._schedule_refresh() + + def __enter__(self): + return self + + def __exit__(self, *args): + if self._timer is not None: + self._timer.cancel() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -46,8 +56,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument if not self._token_refresher or not self._token_expiring(): return self._token - should_this_thread_refresh = False + self._update_token_and_reschedule() + return self._token + def _update_token_and_reschedule(self): + should_this_thread_refresh = False with self._lock: while self._token_expiring(): if self._some_thread_refreshing: @@ -74,15 +87,31 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument self._lock.notify_all() raise + if(self._refresh_proactively): + self._schedule_refresh() return self._token + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() + def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() self._lock.acquire() def _token_expiring(self): + if(self._refresh_proactively): + interval = self._refresh_time_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + interval.total_seconds() def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index c9255a4217d7..23c39f561002 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -10,9 +10,8 @@ cast, Tuple, ) -from datetime import datetime +from datetime import datetime, timezone import calendar -from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken def _convert_datetime_to_utc_int(input_datetime): @@ -55,12 +54,12 @@ def parse_connection_str(conn_str): def get_current_utc_time(): # type: () -> str - return str(datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + return str(datetime.now(tz=timezone.utc).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" def get_current_utc_as_int(): # type: () -> int - current_utc_datetime = datetime.utcnow() + current_utc_datetime = datetime.now(tz=timezone.utc) return _convert_datetime_to_utc_int(current_utc_datetime) def create_access_token(token): diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py index b1885cdc32a8..8babf5757f28 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py @@ -5,19 +5,64 @@ # -------------------------------------------------------------------------- from unittest import TestCase try: - from unittest.mock import MagicMock + from unittest.mock import MagicMock, Mock except ImportError: # python < 3.3 - from mock import MagicMock # type: ignore + from mock import MagicMock, Mock # type: ignore +import azure.communication.identity._shared.user_credential as user_credential from azure.communication.identity._shared.user_credential import CommunicationTokenCredential from azure.communication.identity._shared.utils import create_access_token +from azure.communication.identity._shared.utils import get_current_utc_as_int +from unittest.mock import patch +from datetime import timedelta +from functools import wraps +import base64 + + +def patch_threading_timer(target_timer): + """patch_threading_timer acts similarly to unittest.mock.patch as a + function decorator, but specifically for threading.Timer. The function + passed to threading.Timer is called right away with all given arguments. + + :arg str target_timer: the target Timer (threading.Timer) to be patched + """ + + def decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + def side_effect(interval, function, args=None, kwargs=None): + args = args if args is not None else [] + kwargs = kwargs if kwargs is not None else {} + # Call the original function + if(interval <= 0): + function(*args, **kwargs) + # Return a mock object to allow function calls on the returned value + return Mock() + + with patch(target_timer, side_effect=side_effect) as timer_mock: + # Pass the mock object to the decorated function for further assertions + return f(*(*args, timer_mock), **kwargs) + + return wrapper + + return decorator class TestCommunicationTokenCredential(TestCase): - sample_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - "eyJleHAiOjMyNTAzNjgwMDAwfQ.9i7FNNHHJT8cOzo-yrAUJyBSfJ-tPPk2emcHavOEpWc" - sample_token_expiry = 32503680000 - expired_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - "eyJleHAiOjEwMH0.1h_scYkNp-G98-O4cW6KvfJZwiz54uJMyeDACE4nypg" + + @staticmethod + def get_token_with_custom_expiry(expires_on: int): + expiry_json = '{"exp": ' + str(expires_on) + '}' + base64expiry = base64.b64encode( + expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") + token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ + f"{base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + return token_template + + def __init__(self, methodName: str = ...) -> None: + super().__init__(methodName=methodName) + self.sample_token = self.get_token_with_custom_expiry( + 32503680000) # 1/1/2030 + self.expired_token = self.get_token_with_custom_expiry(100) # 1/1/1970 def test_communicationtokencredential_decodes_token(self): credential = CommunicationTokenCredential(self.sample_token) @@ -55,3 +100,124 @@ def test_communicationtokencredential_token_expired_refresh_called_as_necessary( self.assertEqual(refresher.call_count, 2) self.assertEqual(access_token.token, self.expired_token) + + def test_uses_initial_token_as_expected(self): + refresher = MagicMock( + return_value=self.expired_token) + credential = CommunicationTokenCredential( + self.sample_token, token_refresher=refresher, refresh_proactively=True) + + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 0) + self.assertEqual(access_token.token, self.sample_token) + + @patch_threading_timer(f'{user_credential.__name__}.Timer') + def test_communicationtokencredential_does_not_proactively_refresh_before_specified_time(self, timer_mock): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = create_access_token( + self.get_token_with_custom_expiry(start_timestamp + token_validity_minutes * 60)) + + refresher = MagicMock(return_value=create_access_token(self.get_token_with_custom_expiry( + skip_to_timestamp + token_validity_minutes * 60))) + + # travel in time to the point where the token should still not be refreshed + with patch(f'{user_credential.__name__}.get_current_utc_as_int', return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token.token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 0) + self.assertEqual(access_token.token, initial_token.token) + # check that next refresh is always scheduled + self.assertTrue(credential._timer is not None) + + @patch_threading_timer(f'{user_credential.__name__}.Timer') + def test_communicationtokencredential_proactively_refreshes_after_specified_time(self, timer_mock): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = create_access_token( + self.get_token_with_custom_expiry(start_timestamp + token_validity_minutes * 60)) + + refresher = MagicMock(return_value=create_access_token(self.get_token_with_custom_expiry( + skip_to_timestamp + token_validity_minutes * 60))) + + # travel in time to the point where the token should be refreshed + with patch(f'{user_credential.__name__}.get_current_utc_as_int', return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token.token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 1) + self.assertNotEqual(access_token.token, initial_token.token) + # check that next refresh is always scheduled + self.assertTrue(credential._timer is not None) + + def test_communicationtokencredential_repeats_scheduling(self): + refresh_seconds = 1 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + refresh_seconds + + refresher = MagicMock(return_value=create_access_token(self.get_token_with_custom_expiry( + skip_to_timestamp + token_validity_minutes * 60))) + + # travel in time to the point where the token should be refreshed + with patch(f'{user_credential.__name__}.get_current_utc_as_int', return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + self.expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 1) + self.assertNotEqual(access_token.token, self.expired_token) + # check that next refresh is always scheduled + self.assertTrue(credential._timer is not None) + credential._timer.cancel() + + @patch_threading_timer(f'{user_credential.__name__}.Timer') + def test_exit_cancels_timer(self, timer_mock): + refresher = MagicMock(return_value=self.sample_token) + + with CommunicationTokenCredential( + self.sample_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + self.assertTrue(credential._timer.is_alive()) + + self.assertEqual(credential._timer.is_alive.call_count, 1) + self.assertEqual(credential._timer.cancel.call_count, 1) + self.assertEqual(refresher.call_count, 0) + + @patch_threading_timer(f'{user_credential.__name__}.Timer') + def test_refresher_called_only_once(self, timer_mock): + refresher = MagicMock( + return_value=create_access_token(self.sample_token)) + + credential = CommunicationTokenCredential( + self.expired_token, + token_refresher=refresher, + refresh_proactively=True) + + for _ in range(10): + credential.get_token() + + self.assertEqual(refresher.call_count, 1) From b375e2f0137e84a7399efe5304baef8f46e37dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Thu, 25 Nov 2021 17:13:48 +0100 Subject: [PATCH 04/49] python 2.7 compat changes --- .../communication/identity/_shared/utils.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index 23c39f561002..8176499a8516 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -6,13 +6,15 @@ import base64 import json +import calendar from typing import ( # pylint: disable=unused-import cast, Tuple, ) -from datetime import datetime, timezone -import calendar +from datetime import datetime from azure.core.credentials import AccessToken +from msrest.serialization import TZ_UTC + def _convert_datetime_to_utc_int(input_datetime): """ @@ -25,6 +27,7 @@ def _convert_datetime_to_utc_int(input_datetime): """ return int(calendar.timegm(input_datetime.utctimetuple())) + def parse_connection_str(conn_str): # type: (str) -> Tuple[str, str, str, str] if conn_str is None: @@ -52,16 +55,19 @@ def parse_connection_str(conn_str): return host, str(shared_access_key) + def get_current_utc_time(): # type: () -> str - return str(datetime.now(tz=timezone.utc).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + def get_current_utc_as_int(): # type: () -> int - current_utc_datetime = datetime.now(tz=timezone.utc) + current_utc_datetime = datetime.now(tz=TZ_UTC) return _convert_datetime_to_utc_int(current_utc_datetime) + def create_access_token(token): # type: (str) -> azure.core.credentials.AccessToken """Creates an instance of azure.core.credentials.AccessToken from a @@ -83,7 +89,8 @@ def create_access_token(token): raise ValueError(token_parse_err_msg) try: - padded_base64_payload = base64.b64decode(parts[1] + "==").decode('ascii') + padded_base64_payload = base64.b64decode( + parts[1] + "==").decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) @@ -91,10 +98,10 @@ def create_access_token(token): raise ValueError(token_parse_err_msg) def get_authentication_policy( - endpoint, # type: str - credential, # type: TokenCredential or str - decode_url=False, # type: bool - is_async=False, # type: bool + endpoint, # type: str + credential, # type: TokenCredential or str + decode_url=False, # type: bool + is_async=False, # type: bool ): # type: (...) -> BearerTokenCredentialPolicy or HMACCredentialPolicy """Returns the correct authentication policy based From 2e26dfbd7bf24e86c05e1e02c41232cb4f117ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Fri, 26 Nov 2021 14:52:47 +0100 Subject: [PATCH 05/49] py27 compat changes --- .../tests/test_user_credential_tests.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py index 8babf5757f28..dc64f4d9c73f 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py @@ -5,14 +5,14 @@ # -------------------------------------------------------------------------- from unittest import TestCase try: - from unittest.mock import MagicMock, Mock + from unittest.mock import MagicMock, Mock, patch except ImportError: # python < 3.3 - from mock import MagicMock, Mock # type: ignore + from mock import MagicMock, Mock, patch # type: ignore import azure.communication.identity._shared.user_credential as user_credential from azure.communication.identity._shared.user_credential import CommunicationTokenCredential from azure.communication.identity._shared.utils import create_access_token from azure.communication.identity._shared.utils import get_current_utc_as_int -from unittest.mock import patch +#from unittest.mock import patch from datetime import timedelta from functools import wraps import base64 @@ -40,7 +40,7 @@ def side_effect(interval, function, args=None, kwargs=None): with patch(target_timer, side_effect=side_effect) as timer_mock: # Pass the mock object to the decorated function for further assertions - return f(*(*args, timer_mock), **kwargs) + return f(*(args[0], timer_mock), **kwargs) return wrapper @@ -50,19 +50,19 @@ def side_effect(interval, function, args=None, kwargs=None): class TestCommunicationTokenCredential(TestCase): @staticmethod - def get_token_with_custom_expiry(expires_on: int): + def get_token_with_custom_expiry(expires_on): expiry_json = '{"exp": ' + str(expires_on) + '}' base64expiry = base64.b64encode( expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - f"{base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" return token_template - def __init__(self, methodName: str = ...) -> None: - super().__init__(methodName=methodName) - self.sample_token = self.get_token_with_custom_expiry( + @classmethod + def setUpClass(cls): + cls.sample_token = cls.get_token_with_custom_expiry( 32503680000) # 1/1/2030 - self.expired_token = self.get_token_with_custom_expiry(100) # 1/1/1970 + cls.expired_token = cls.get_token_with_custom_expiry(100) # 1/1/1970 def test_communicationtokencredential_decodes_token(self): credential = CommunicationTokenCredential(self.sample_token) @@ -112,7 +112,7 @@ def test_uses_initial_token_as_expected(self): self.assertEqual(refresher.call_count, 0) self.assertEqual(access_token.token, self.sample_token) - @patch_threading_timer(f'{user_credential.__name__}.Timer') + @patch_threading_timer(user_credential.__name__+'.Timer') def test_communicationtokencredential_does_not_proactively_refresh_before_specified_time(self, timer_mock): refresh_minutes = 30 token_validity_minutes = 60 @@ -126,7 +126,7 @@ def test_communicationtokencredential_does_not_proactively_refresh_before_specif skip_to_timestamp + token_validity_minutes * 60))) # travel in time to the point where the token should still not be refreshed - with patch(f'{user_credential.__name__}.get_current_utc_as_int', return_value=skip_to_timestamp): + with patch(user_credential.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): credential = CommunicationTokenCredential( initial_token.token, token_refresher=refresher, @@ -140,7 +140,7 @@ def test_communicationtokencredential_does_not_proactively_refresh_before_specif # check that next refresh is always scheduled self.assertTrue(credential._timer is not None) - @patch_threading_timer(f'{user_credential.__name__}.Timer') + @patch_threading_timer(user_credential.__name__+'.Timer') def test_communicationtokencredential_proactively_refreshes_after_specified_time(self, timer_mock): refresh_minutes = 30 token_validity_minutes = 60 @@ -154,7 +154,7 @@ def test_communicationtokencredential_proactively_refreshes_after_specified_time skip_to_timestamp + token_validity_minutes * 60))) # travel in time to the point where the token should be refreshed - with patch(f'{user_credential.__name__}.get_current_utc_as_int', return_value=skip_to_timestamp): + with patch(user_credential.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): credential = CommunicationTokenCredential( initial_token.token, token_refresher=refresher, @@ -178,7 +178,7 @@ def test_communicationtokencredential_repeats_scheduling(self): skip_to_timestamp + token_validity_minutes * 60))) # travel in time to the point where the token should be refreshed - with patch(f'{user_credential.__name__}.get_current_utc_as_int', return_value=skip_to_timestamp): + with patch(user_credential.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): credential = CommunicationTokenCredential( self.expired_token, token_refresher=refresher, @@ -193,7 +193,7 @@ def test_communicationtokencredential_repeats_scheduling(self): self.assertTrue(credential._timer is not None) credential._timer.cancel() - @patch_threading_timer(f'{user_credential.__name__}.Timer') + @patch_threading_timer(user_credential.__name__+'.Timer') def test_exit_cancels_timer(self, timer_mock): refresher = MagicMock(return_value=self.sample_token) @@ -207,7 +207,7 @@ def test_exit_cancels_timer(self, timer_mock): self.assertEqual(credential._timer.cancel.call_count, 1) self.assertEqual(refresher.call_count, 0) - @patch_threading_timer(f'{user_credential.__name__}.Timer') + @patch_threading_timer(user_credential.__name__+'.Timer') def test_refresher_called_only_once(self, timer_mock): refresher = MagicMock( return_value=create_access_token(self.sample_token)) From 64ac5df4ae48cdb915bffab018d691e8583471b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Fri, 26 Nov 2021 16:15:52 +0100 Subject: [PATCH 06/49] fixed linting problems + comments --- .../identity/_shared/user_credential.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index b476b5076ed3..cb9e8a50bc10 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -14,9 +14,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token - :keyword refresh_proactively: Whether to refresh the token proactively or not - :keyword refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ @@ -37,7 +37,7 @@ def __init__(self, self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False - if(self._refresh_proactively): + if self._refresh_proactively: self._schedule_refresh() def __enter__(self): @@ -87,7 +87,7 @@ def _update_token_and_reschedule(self): self._lock.notify_all() raise - if(self._refresh_proactively): + if self._refresh_proactively: self._schedule_refresh() return self._token @@ -105,7 +105,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.acquire() def _token_expiring(self): - if(self._refresh_proactively): + if self._refresh_proactively: interval = self._refresh_time_before_expiry else: interval = timedelta( From 4c11b1600b0cddd9b82f078e1baf1ed55febb9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Fri, 26 Nov 2021 16:35:56 +0100 Subject: [PATCH 07/49] py27 fixed flaky test --- .../tests/test_user_credential_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py index dc64f4d9c73f..33a9a481de20 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py @@ -12,7 +12,6 @@ from azure.communication.identity._shared.user_credential import CommunicationTokenCredential from azure.communication.identity._shared.utils import create_access_token from azure.communication.identity._shared.utils import get_current_utc_as_int -#from unittest.mock import patch from datetime import timedelta from functools import wraps import base64 @@ -101,7 +100,8 @@ def test_communicationtokencredential_token_expired_refresh_called_as_necessary( self.assertEqual(refresher.call_count, 2) self.assertEqual(access_token.token, self.expired_token) - def test_uses_initial_token_as_expected(self): + @patch_threading_timer(user_credential.__name__+'.Timer') + def test_uses_initial_token_as_expected(self, timer_mock): refresher = MagicMock( return_value=self.expired_token) credential = CommunicationTokenCredential( From 623bb1b7e8a7422f779c216e55fcecb4440d2a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Sat, 27 Nov 2021 10:05:18 +0100 Subject: [PATCH 08/49] linting issues --- .../azure/communication/identity/_shared/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index 8176499a8516..8f1b6f0e808c 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -12,8 +12,8 @@ Tuple, ) from datetime import datetime -from azure.core.credentials import AccessToken from msrest.serialization import TZ_UTC +from azure.core.credentials import AccessToken def _convert_datetime_to_utc_int(input_datetime): From 13c4469bc86f39fd0a7d4f37720638d6024da9b6 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 1 Dec 2021 20:03:48 +0100 Subject: [PATCH 09/49] CommunicationTokenCredential async implemenation & tests are added --- .../identity/_shared/user_credential_async.py | 94 ++++++--- .../communication/identity/_shared/utils.py | 22 ++ .../tests/test_user_credential_async.py | 190 ++++++++++++++++++ 3 files changed, 274 insertions(+), 32 deletions(-) create mode 100644 sdk/communication/azure-communication-identity/tests/test_user_credential_async.py diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 741bfb967b7b..88b6dd2c183c 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -3,24 +3,23 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from asyncio import Condition, Lock from datetime import timedelta -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) +from typing import Any import six from .utils import get_current_utc_as_int from .utils import create_access_token +from .utils_async import AsyncTimer class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token - :keyword refresh_proactively: Whether to refresh the token proactively or not - :keyword refresh_time_before_token_expiry: The time before the token expires to refresh the token + :param str token: The token used to authenticate to an Azure Communication service. + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: + The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ @@ -29,33 +28,35 @@ class CommunicationTokenCredential(object): def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): - raise TypeError("token must be a string.") + raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_token_expiry = kwargs.pop('refresh_time_before_token_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) - self._lock = Condition(Lock()) + self._timer = None + self._async_mutex = Lock() + self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token + await self._update_token_and_reschedule() + return self._token + async def _update_token_and_reschedule(self): should_this_thread_refresh = False - async with self._lock: - - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - await self._wait_till_inprogress_thread_finish_refreshing() + await self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -63,31 +64,58 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument if should_this_thread_refresh: try: - newtoken = await self._token_refresher() # pylint:disable=not-callable - + new_token = await self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") async with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: async with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise - + if self._refresh_proactively: + self._schedule_refresh() return self._token - async def _wait_till_inprogress_thread_finish_refreshing(self): + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() + + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) + self._timer.start() + + async def _wait_till_lock_owner_finishes_refreshing(self): + self._lock.release() await self._lock.acquire() - def _token_expiring(self): - return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + def _is_token_expiring_soon(self, token): + if self._refresh_proactively: + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) + return token.expires_on - get_current_utc_as_int() <\ + interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + @classmethod + def _is_token_valid(cls, token): + return get_current_utc_as_int() < token.expires_on async def close(self) -> None: pass @@ -96,4 +124,6 @@ async def __aenter__(self): return self async def __aexit__(self, *args): - await self.close() + if self._timer is not None: + self._timer.cancel() + await self.close() \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index 8f1b6f0e808c..8f41aede250f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -7,6 +7,7 @@ import base64 import json import calendar +import asyncio from typing import ( # pylint: disable=unused-import cast, Tuple, @@ -132,3 +133,24 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) + +class AsyncTimer: + """A non-blocking timer, that calls a function after a specified number of seconds: + :param int interval: time interval in seconds + :param callable callback: function to be called after the interval has elapsed + """ + def __init__(self, interval, callback): + self._interval = interval + self._callback = callback + self._task = None + + def start(self): + self._task = asyncio.ensure_future(self._job()) + + async def _job(self): + await asyncio.sleep(self._interval) + await self._callback() + + def cancel(self): + if self._task is not None: + self._task.cancel() diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py new file mode 100644 index 000000000000..e126b13ca91e --- /dev/null +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -0,0 +1,190 @@ + +# coding: utf-8 +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from datetime import datetime, timedelta +import pytest +import base64 +import asyncio +try: + from unittest.mock import MagicMock, Mock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, Mock, patch +from azure.communication.identity._shared.user_credential_async import CommunicationTokenCredential +import azure.communication.identity._shared.user_credential_async as user_credential_async +from azure.communication.identity._shared.utils import create_access_token +from azure.communication.identity._shared.utils import get_current_utc_as_int + +class TestCommunicationTokenCredential: + + @staticmethod + def generate_token_with_custom_expiry(valid_for_seconds): + expires_on = datetime.now() + timedelta(seconds=valid_for_seconds) + expiry_json = '{"exp": ' + str(expires_on.timestamp()) + '}' + base64expiry = base64.b64encode( + expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") + token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ + base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + return token_template + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_nonstring_token(self): + with pytest.raises(TypeError) as err: + credential = CommunicationTokenCredential(1234) + assert str(err.value) == "token must be a string." + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_invalid_token(self): + with pytest.raises(ValueError) as err: + credential = CommunicationTokenCredential("not a token") + assert str(err.value) == "Token is not formatted correctly" + + @pytest.mark.asyncio + async def test_init_with_valid_token(self): + initial_token = self.generate_token_with_custom_expiry(5 * 60) + credential = CommunicationTokenCredential(initial_token) + access_token = await credential.get_token() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_immediately_with_expired_token(self): + refreshed_token = self.generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(refreshed_token)) + expired_token = self.generate_token_with_custom_expiry(-(5 * 60)) + + credential = CommunicationTokenCredential(expired_token, token_refresher=refresher) + async with credential: + access_token = await credential.get_token() + + refresher.assert_called_once() + assert refreshed_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_before_expiring_time(self): + initial_token = self.generate_token_with_custom_expiry(15 * 60) + refreshed_token = self.generate_token_with_custom_expiry(10*60) + refresher = MagicMock(return_value=create_access_token(refreshed_token)) + + credential = CommunicationTokenCredential(initial_token, token_refresher=refresher, refresh_proactively=True) + async with credential: + access_token = await credential.get_token() + + refresher.assert_not_called() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = self.generate_token_with_custom_expiry(15 * 60) + new_token = self.generate_token_with_custom_expiry(10*60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential(generated_token, token_refresher=refresher, refresh_proactively=False) + async with credential: + for i in range(10): + access_token = await credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_as_necessary(self): + expired_token = self.generate_token_with_custom_expiry(-(10 * 60)) + refresher = MagicMock(return_value=create_access_token(expired_token)) + + credential = CommunicationTokenCredential(expired_token, token_refresher=refresher) + async with credential: + access_token = await credential.get_token() + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert expired_token == access_token.token + + @pytest.mark.asyncio + async def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = self.generate_token_with_custom_expiry(token_validity_minutes * 60) + refreshed_token = self.generate_token_with_custom_expiry(2 * token_validity_minutes * 60) + refresher = MagicMock(return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = self.generate_token_with_custom_expiry(token_validity_minutes * 60) + refreshed_token = self.generate_token_with_custom_expiry(2 * token_validity_minutes * 60) + refresher = MagicMock(return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + + @pytest.mark.asyncio + async def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = self.generate_token_with_custom_expiry(-5 * 60) + first_refreshed_token = create_access_token(self.generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token(self.generate_token_with_custom_expiry(10*60)) + refresher = MagicMock(side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + async with credential: + await asyncio.sleep(4) + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_exit_cancels_timer(self): + refreshed_token = create_access_token(self.generate_token_with_custom_expiry(30*60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = self.generate_token_with_custom_expiry(-10*60) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) + credential._timer.cancel() + await asyncio.sleep(3) + assert refresher.call_count == 0 \ No newline at end of file From 599c47cc3564c858f1e0a9632da75c5b1b260d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Thu, 2 Dec 2021 10:18:34 +0100 Subject: [PATCH 10/49] split async code not to break py27 --- .../communication/identity/_shared/utils.py | 22 -------------- .../identity/_shared/utils_async.py | 30 +++++++++++++++++++ ...ntial_tests.py => test_user_credential.py} | 0 3 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py rename sdk/communication/azure-communication-identity/tests/{test_user_credential_tests.py => test_user_credential.py} (100%) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index 8f41aede250f..8f1b6f0e808c 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -7,7 +7,6 @@ import base64 import json import calendar -import asyncio from typing import ( # pylint: disable=unused-import cast, Tuple, @@ -133,24 +132,3 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) - -class AsyncTimer: - """A non-blocking timer, that calls a function after a specified number of seconds: - :param int interval: time interval in seconds - :param callable callback: function to be called after the interval has elapsed - """ - def __init__(self, interval, callback): - self._interval = interval - self._callback = callback - self._task = None - - def start(self): - self._task = asyncio.ensure_future(self._job()) - - async def _job(self): - await asyncio.sleep(self._interval) - await self._callback() - - def cancel(self): - if self._task is not None: - self._task.cancel() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py new file mode 100644 index 000000000000..f2472e2121af --- /dev/null +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import asyncio + + +class AsyncTimer: + """A non-blocking timer, that calls a function after a specified number of seconds: + :param int interval: time interval in seconds + :param callable callback: function to be called after the interval has elapsed + """ + + def __init__(self, interval, callback): + self._interval = interval + self._callback = callback + self._task = None + + def start(self): + self._task = asyncio.ensure_future(self._job()) + + async def _job(self): + await asyncio.sleep(self._interval) + await self._callback() + + def cancel(self): + if self._task is not None: + self._task.cancel() diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py similarity index 100% rename from sdk/communication/azure-communication-identity/tests/test_user_credential_tests.py rename to sdk/communication/azure-communication-identity/tests/test_user_credential.py From 2c7022704cedd02b7c4fdc9f9314640a6b4b0e07 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 2 Dec 2021 10:46:04 +0100 Subject: [PATCH 11/49] lock issue for python 3.10 is fixed --- .../identity/_shared/user_credential_async.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 88b6dd2c183c..e2ea5a6fa170 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -6,7 +6,16 @@ from asyncio import Condition, Lock from datetime import timedelta +<<<<<<< HEAD from typing import Any +======= +import sys +from typing import ( # pylint: disable=unused-import + cast, + Tuple, + Any +) +>>>>>>> 594cd1d95c (lock issue for python 3.10 is fixed) import six from .utils import get_current_utc_as_int from .utils import create_access_token @@ -34,6 +43,12 @@ def __init__(self, token: str, **kwargs: Any): self._refresh_proactively = kwargs.pop('refresh_proactively', False) self._timer = None self._async_mutex = Lock() +<<<<<<< HEAD +======= + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() +>>>>>>> 594cd1d95c (lock issue for python 3.10 is fixed) self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False if self._refresh_proactively: From c4ee92eaeddb1b8c84824955e5f24b1993899015 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 2 Dec 2021 14:16:45 +0100 Subject: [PATCH 12/49] asyncio.sleep in async tests are removed --- .../tests/test_user_credential_async.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index e126b13ca91e..d4e706254f4d 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -157,6 +157,7 @@ async def test_proactive_refresher_should_be_called_after_specified_time(self): async def test_proactive_refresher_keeps_scheduling_again(self): refresh_seconds = 2 expired_token = self.generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 first_refreshed_token = create_access_token(self.generate_token_with_custom_expiry(4)) last_refreshed_token = create_access_token(self.generate_token_with_custom_expiry(10*60)) refresher = MagicMock(side_effect=[first_refreshed_token, last_refreshed_token]) @@ -167,8 +168,9 @@ async def test_proactive_refresher_keeps_scheduling_again(self): refresh_proactively=True, refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) async with credential: - await asyncio.sleep(4) access_token = await credential.get_token() + with patch(user_credential_async.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): + access_token = await credential.get_token() assert refresher.call_count == 2 assert access_token.token == last_refreshed_token.token @@ -181,10 +183,9 @@ async def test_exit_cancels_timer(self): refresher = MagicMock(return_value=refreshed_token) expired_token = self.generate_token_with_custom_expiry(-10*60) - credential = CommunicationTokenCredential( + async with CommunicationTokenCredential( expired_token, token_refresher=refresher, - refresh_proactively=True) - credential._timer.cancel() - await asyncio.sleep(3) + refresh_proactively=True) as credential: + assert credential._timer is not None assert refresher.call_count == 0 \ No newline at end of file From 7589e7af2d4cd51c4ae67aa59381da9132555d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Thu, 2 Dec 2021 16:32:24 +0100 Subject: [PATCH 13/49] test refactored --- .../identity/_shared/user_credential.py | 1 - .../tests/_shared/helper.py | 26 ++- .../tests/test_user_credential.py | 197 +++++++----------- .../tests/test_user_credential_async.py | 185 ++++++++-------- 4 files changed, 195 insertions(+), 214 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index cb9e8a50bc10..eb515a39b2ca 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -85,7 +85,6 @@ def _update_token_and_reschedule(self): with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise if self._refresh_proactively: self._schedule_refresh() diff --git a/sdk/communication/azure-communication-identity/tests/_shared/helper.py b/sdk/communication/azure-communication-identity/tests/_shared/helper.py index 146d94b649b0..d4d125ba8488 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/helper.py @@ -4,18 +4,36 @@ # license information. # -------------------------------------------------------------------------- import re +import base64 from azure_devtools.scenario_tests import RecordingProcessor from urllib.parse import urlparse + +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + + +def generate_token_with_custom_expiry_epoch(expires_on_epoch): + expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + base64expiry = base64.b64encode( + expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") + token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ + base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + return token_template + + class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" + def process_request(self, request): resource = (urlparse(request.uri).netloc).split('.')[0] - request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', request.uri) request.uri = re.sub(resource, 'sanitized', request.uri) return request - + def process_response(self, response): if 'url' in response: - response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) - return response \ No newline at end of file + response['url'] = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', response['url']) + return response diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index 33a9a481de20..4833fa71ef05 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -5,63 +5,25 @@ # -------------------------------------------------------------------------- from unittest import TestCase try: - from unittest.mock import MagicMock, Mock, patch + from unittest.mock import MagicMock, patch except ImportError: # python < 3.3 - from mock import MagicMock, Mock, patch # type: ignore + from mock import MagicMock, patch # type: ignore import azure.communication.identity._shared.user_credential as user_credential from azure.communication.identity._shared.user_credential import CommunicationTokenCredential from azure.communication.identity._shared.utils import create_access_token from azure.communication.identity._shared.utils import get_current_utc_as_int from datetime import timedelta -from functools import wraps -import base64 - - -def patch_threading_timer(target_timer): - """patch_threading_timer acts similarly to unittest.mock.patch as a - function decorator, but specifically for threading.Timer. The function - passed to threading.Timer is called right away with all given arguments. - - :arg str target_timer: the target Timer (threading.Timer) to be patched - """ - - def decorator(f): - @wraps(f) - def wrapper(*args, **kwargs): - def side_effect(interval, function, args=None, kwargs=None): - args = args if args is not None else [] - kwargs = kwargs if kwargs is not None else {} - # Call the original function - if(interval <= 0): - function(*args, **kwargs) - # Return a mock object to allow function calls on the returned value - return Mock() - - with patch(target_timer, side_effect=side_effect) as timer_mock: - # Pass the mock object to the decorated function for further assertions - return f(*(args[0], timer_mock), **kwargs) - - return wrapper - - return decorator +from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry class TestCommunicationTokenCredential(TestCase): - @staticmethod - def get_token_with_custom_expiry(expires_on): - expiry_json = '{"exp": ' + str(expires_on) + '}' - base64expiry = base64.b64encode( - expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") - token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" - return token_template - @classmethod def setUpClass(cls): - cls.sample_token = cls.get_token_with_custom_expiry( + cls.sample_token = generate_token_with_custom_expiry_epoch( 32503680000) # 1/1/2030 - cls.expired_token = cls.get_token_with_custom_expiry(100) # 1/1/1970 + cls.expired_token = generate_token_with_custom_expiry_epoch( + 100) # 1/1/1970 def test_communicationtokencredential_decodes_token(self): credential = CommunicationTokenCredential(self.sample_token) @@ -100,8 +62,8 @@ def test_communicationtokencredential_token_expired_refresh_called_as_necessary( self.assertEqual(refresher.call_count, 2) self.assertEqual(access_token.token, self.expired_token) - @patch_threading_timer(user_credential.__name__+'.Timer') - def test_uses_initial_token_as_expected(self, timer_mock): + # @patch_threading_timer(user_credential.__name__+'.Timer') + def test_uses_initial_token_as_expected(self): # , timer_mock): refresher = MagicMock( return_value=self.expired_token) credential = CommunicationTokenCredential( @@ -112,112 +74,109 @@ def test_uses_initial_token_as_expected(self, timer_mock): self.assertEqual(refresher.call_count, 0) self.assertEqual(access_token.token, self.sample_token) - @patch_threading_timer(user_credential.__name__+'.Timer') - def test_communicationtokencredential_does_not_proactively_refresh_before_specified_time(self, timer_mock): + def test_proactive_refresher_should_not_be_called_before_specified_time(self): refresh_minutes = 30 token_validity_minutes = 60 start_timestamp = get_current_utc_as_int() skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - initial_token = create_access_token( - self.get_token_with_custom_expiry(start_timestamp + token_validity_minutes * 60)) - - refresher = MagicMock(return_value=create_access_token(self.get_token_with_custom_expiry( - skip_to_timestamp + token_validity_minutes * 60))) + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) - # travel in time to the point where the token should still not be refreshed - with patch(user_credential.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): credential = CommunicationTokenCredential( - initial_token.token, + initial_token, token_refresher=refresher, refresh_proactively=True, refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 0) - self.assertEqual(access_token.token, initial_token.token) + assert refresher.call_count == 0 + assert access_token.token == initial_token # check that next refresh is always scheduled - self.assertTrue(credential._timer is not None) + assert credential._timer is not None - @patch_threading_timer(user_credential.__name__+'.Timer') - def test_communicationtokencredential_proactively_refreshes_after_specified_time(self, timer_mock): + def test_proactive_refresher_should_be_called_after_specified_time(self): refresh_minutes = 30 token_validity_minutes = 60 start_timestamp = get_current_utc_as_int() skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - initial_token = create_access_token( - self.get_token_with_custom_expiry(start_timestamp + token_validity_minutes * 60)) - - refresher = MagicMock(return_value=create_access_token(self.get_token_with_custom_expiry( - skip_to_timestamp + token_validity_minutes * 60))) + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) - # travel in time to the point where the token should be refreshed - with patch(user_credential.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): credential = CommunicationTokenCredential( - initial_token.token, + initial_token, token_refresher=refresher, refresh_proactively=True, refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 1) - self.assertNotEqual(access_token.token, initial_token.token) + assert refresher.call_count == 1 + assert access_token.token == refreshed_token # check that next refresh is always scheduled - self.assertTrue(credential._timer is not None) - - def test_communicationtokencredential_repeats_scheduling(self): - refresh_seconds = 1 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + refresh_seconds - - refresher = MagicMock(return_value=create_access_token(self.get_token_with_custom_expiry( - skip_to_timestamp + token_validity_minutes * 60))) - - # travel in time to the point where the token should be refreshed - with patch(user_credential.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - self.expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - - access_token = credential.get_token() + assert credential._timer is not None + + def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) - self.assertEqual(refresher.call_count, 1) - self.assertNotEqual(access_token.token, self.expired_token) + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + with credential: + access_token = credential.get_token() + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token # check that next refresh is always scheduled - self.assertTrue(credential._timer is not None) - credential._timer.cancel() + assert credential._timer is not None - @patch_threading_timer(user_credential.__name__+'.Timer') - def test_exit_cancels_timer(self, timer_mock): - refresher = MagicMock(return_value=self.sample_token) + def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) with CommunicationTokenCredential( - self.sample_token, + expired_token, token_refresher=refresher, refresh_proactively=True) as credential: - self.assertTrue(credential._timer.is_alive()) + assert credential._timer is not None + assert credential._timer.finished._flag == True - self.assertEqual(credential._timer.is_alive.call_count, 1) - self.assertEqual(credential._timer.cancel.call_count, 1) - self.assertEqual(refresher.call_count, 0) - - @patch_threading_timer(user_credential.__name__+'.Timer') - def test_refresher_called_only_once(self, timer_mock): - refresher = MagicMock( - return_value=create_access_token(self.sample_token)) + def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) credential = CommunicationTokenCredential( - self.expired_token, - token_refresher=refresher, - refresh_proactively=True) - - for _ in range(10): - credential.get_token() + generated_token, token_refresher=refresher, refresh_proactively=False) + with credential: + for _ in range(10): + access_token = credential.get_token() - self.assertEqual(refresher.call_count, 1) + refresher.assert_not_called() + assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index d4e706254f4d..ed20b2779b8d 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -5,103 +5,99 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from datetime import datetime, timedelta +from datetime import timedelta import pytest -import base64 -import asyncio try: - from unittest.mock import MagicMock, Mock, patch + from unittest.mock import MagicMock, patch except ImportError: # python < 3.3 - from mock import MagicMock, Mock, patch + from mock import MagicMock, patch from azure.communication.identity._shared.user_credential_async import CommunicationTokenCredential import azure.communication.identity._shared.user_credential_async as user_credential_async from azure.communication.identity._shared.utils import create_access_token -from azure.communication.identity._shared.utils import get_current_utc_as_int - +from azure.communication.identity._shared.utils import get_current_utc_as_int +from _shared.helper import generate_token_with_custom_expiry + + class TestCommunicationTokenCredential: - - @staticmethod - def generate_token_with_custom_expiry(valid_for_seconds): - expires_on = datetime.now() + timedelta(seconds=valid_for_seconds) - expiry_json = '{"exp": ' + str(expires_on.timestamp()) + '}' - base64expiry = base64.b64encode( - expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") - token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" - return token_template - + @pytest.mark.asyncio async def test_raises_error_for_init_with_nonstring_token(self): with pytest.raises(TypeError) as err: - credential = CommunicationTokenCredential(1234) - assert str(err.value) == "token must be a string." - + CommunicationTokenCredential(1234) + assert str(err.value) == "Token must be a string." + @pytest.mark.asyncio async def test_raises_error_for_init_with_invalid_token(self): with pytest.raises(ValueError) as err: - credential = CommunicationTokenCredential("not a token") + CommunicationTokenCredential("not a token") assert str(err.value) == "Token is not formatted correctly" - + @pytest.mark.asyncio async def test_init_with_valid_token(self): - initial_token = self.generate_token_with_custom_expiry(5 * 60) + initial_token = generate_token_with_custom_expiry(5 * 60) credential = CommunicationTokenCredential(initial_token) access_token = await credential.get_token() assert initial_token == access_token.token - + @pytest.mark.asyncio async def test_refresher_should_be_called_immediately_with_expired_token(self): - refreshed_token = self.generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(refreshed_token)) - expired_token = self.generate_token_with_custom_expiry(-(5 * 60)) - - credential = CommunicationTokenCredential(expired_token, token_refresher=refresher) + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + expired_token = generate_token_with_custom_expiry(-(5 * 60)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) async with credential: access_token = await credential.get_token() - + refresher.assert_called_once() - assert refreshed_token == access_token.token - + assert refreshed_token == access_token.token + @pytest.mark.asyncio async def test_refresher_should_not_be_called_before_expiring_time(self): - initial_token = self.generate_token_with_custom_expiry(15 * 60) - refreshed_token = self.generate_token_with_custom_expiry(10*60) - refresher = MagicMock(return_value=create_access_token(refreshed_token)) - - credential = CommunicationTokenCredential(initial_token, token_refresher=refresher, refresh_proactively=True) + initial_token = generate_token_with_custom_expiry(15 * 60) + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + credential = CommunicationTokenCredential( + initial_token, token_refresher=refresher, refresh_proactively=True) async with credential: access_token = await credential.get_token() - + refresher.assert_not_called() - assert initial_token == access_token.token - + assert initial_token == access_token.token + @pytest.mark.asyncio async def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = self.generate_token_with_custom_expiry(15 * 60) - new_token = self.generate_token_with_custom_expiry(10*60) + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential(generated_token, token_refresher=refresher, refresh_proactively=False) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) async with credential: - for i in range(10): + for _ in range(10): access_token = await credential.get_token() - + refresher.assert_not_called() assert generated_token == access_token.token - + @pytest.mark.asyncio async def test_refresher_should_be_called_as_necessary(self): - expired_token = self.generate_token_with_custom_expiry(-(10 * 60)) + expired_token = generate_token_with_custom_expiry(-(10 * 60)) refresher = MagicMock(return_value=create_access_token(expired_token)) - - credential = CommunicationTokenCredential(expired_token, token_refresher=refresher) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) async with credential: + await credential.get_token() access_token = await credential.get_token() - access_token = await credential.get_token() - + assert refresher.call_count == 2 - assert expired_token == access_token.token - + assert expired_token == access_token.token + @pytest.mark.asyncio async def test_proactive_refresher_should_not_be_called_before_specified_time(self): refresh_minutes = 30 @@ -109,24 +105,27 @@ async def test_proactive_refresher_should_not_be_called_before_specified_time(se start_timestamp = get_current_utc_as_int() skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - initial_token = self.generate_token_with_custom_expiry(token_validity_minutes * 60) - refreshed_token = self.generate_token_with_custom_expiry(2 * token_validity_minutes * 60) - refresher = MagicMock(return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, refresh_proactively=True, refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) async with credential: - access_token = await credential.get_token() + access_token = await credential.get_token() assert refresher.call_count == 0 assert access_token.token == initial_token # check that next refresh is always scheduled - assert credential._timer is not None - + assert credential._timer is not None + @pytest.mark.asyncio async def test_proactive_refresher_should_be_called_after_specified_time(self): refresh_minutes = 30 @@ -134,58 +133,64 @@ async def test_proactive_refresher_should_be_called_after_specified_time(self): start_timestamp = get_current_utc_as_int() skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - initial_token = self.generate_token_with_custom_expiry(token_validity_minutes * 60) - refreshed_token = self.generate_token_with_custom_expiry(2 * token_validity_minutes * 60) - refresher = MagicMock(return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, refresh_proactively=True, refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) async with credential: - access_token = await credential.get_token() + access_token = await credential.get_token() assert refresher.call_count == 1 assert access_token.token == refreshed_token # check that next refresh is always scheduled - assert credential._timer is not None - - + assert credential._timer is not None + @pytest.mark.asyncio async def test_proactive_refresher_keeps_scheduling_again(self): refresh_seconds = 2 - expired_token = self.generate_token_with_custom_expiry(-5 * 60) + expired_token = generate_token_with_custom_expiry(-5 * 60) skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token(self.generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token(self.generate_token_with_custom_expiry(10*60)) - refresher = MagicMock(side_effect=[first_refreshed_token, last_refreshed_token]) - + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) async with credential: - access_token = await credential.get_token() - with patch(user_credential_async.__name__+'.get_current_utc_as_int', return_value=skip_to_timestamp): - access_token = await credential.get_token() - + access_token = await credential.get_token() + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = await credential.get_token() + assert refresher.call_count == 2 assert access_token.token == last_refreshed_token.token # check that next refresh is always scheduled assert credential._timer is not None - + @pytest.mark.asyncio async def test_exit_cancels_timer(self): - refreshed_token = create_access_token(self.generate_token_with_custom_expiry(30*60)) + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) refresher = MagicMock(return_value=refreshed_token) - expired_token = self.generate_token_with_custom_expiry(-10*60) - + expired_token = generate_token_with_custom_expiry(-10 * 60) + async with CommunicationTokenCredential( expired_token, token_refresher=refresher, refresh_proactively=True) as credential: assert credential._timer is not None - assert refresher.call_count == 0 \ No newline at end of file + assert refresher.call_count == 0 From dda3a63d34f31e65d2fec0530314bf314792146c Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 2 Dec 2021 18:11:32 +0100 Subject: [PATCH 14/49] updates in _shared duplicated in chat --- .../chat/_shared/user_credential.py | 66 ++++-- .../chat/_shared/user_credential_async.py | 86 +++++--- .../azure/communication/chat/_shared/utils.py | 21 +- .../communication/chat/_shared/utils_async.py | 30 +++ .../tests/_shared/helper.py | 44 ++++ .../azure-communication-chat/tests/helper.py | 19 -- .../tests/test_chat_client_e2e.py | 2 +- .../tests/test_chat_client_e2e_async.py | 2 +- .../tests/test_chat_thread_client_e2e.py | 2 +- .../test_chat_thread_client_e2e_async.py | 2 +- .../tests/test_user_credential.py | 181 ++++++++++++++++ .../tests/test_user_credential_async.py | 196 ++++++++++++++++++ 12 files changed, 575 insertions(+), 76 deletions(-) create mode 100644 sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py create mode 100644 sdk/communication/azure-communication-chat/tests/_shared/helper.py delete mode 100644 sdk/communication/azure-communication-chat/tests/helper.py create mode 100644 sdk/communication/azure-communication-chat/tests/test_user_credential.py create mode 100644 sdk/communication/azure-communication-chat/tests/test_user_credential_async.py diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 9b5f17dcc95d..e537fe599fe3 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -3,40 +3,58 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition +from threading import Lock, Condition, Timer from datetime import timedelta -from typing import ( # pylint: disable=unused-import + +from typing import ( # pylint: disable=unused-import cast, Tuple, + Any ) +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The token refresher to provide capacity to fetch fresh token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str - **kwargs + **kwargs # type: Any ): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() + if not isinstance(token, six.string_types): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() + + def __enter__(self): + return self + + def __exit__(self, *args): + if self._timer is not None: + self._timer.cancel() - def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument - # type (*str, **Any) -> AccessToken + def get_token(self): + # type () -> ~azure.core.credentials.AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -44,8 +62,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument if not self._token_refresher or not self._token_expiring(): return self._token - should_this_thread_refresh = False + self._update_token_and_reschedule() + return self._token + def _update_token_and_reschedule(self): + should_this_thread_refresh = False with self._lock: while self._token_expiring(): if self._some_thread_refreshing: @@ -70,17 +91,32 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise + if self._refresh_proactively: + self._schedule_refresh() return self._token + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() + def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() self._lock.acquire() def _token_expiring(self): + if self._refresh_proactively: + interval = self._refresh_time_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + interval.total_seconds() def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 52a99e7a4b6a..26c50287fcb8 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -5,62 +5,83 @@ # -------------------------------------------------------------------------- from asyncio import Condition, Lock from datetime import timedelta +import sys from typing import ( # pylint: disable=unused-import cast, Tuple, Any ) +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token +from .utils_async import AsyncTimer class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() - self._lock = Condition(Lock()) + if not isinstance(token, six.string_types): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None + self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() + self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() - async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument - # type (*str, **Any) -> AccessToken + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + if self._timer is not None: + self._timer.cancel() + + async def get_token(self): + # type () -> ~azure.core.credentials.AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ if not self._token_refresher or not self._token_expiring(): return self._token + await self._update_token_and_reschedule() + return self._token + async def _update_token_and_reschedule(self): should_this_thread_refresh = False - async with self._lock: - while self._token_expiring(): if self._some_thread_refreshing: if self._is_currenttoken_valid(): return self._token - await self._wait_till_inprogress_thread_finish_refreshing() + self._wait_till_inprogress_thread_finish_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True break - if should_this_thread_refresh: try: - newtoken = await self._token_refresher() # pylint:disable=not-callable - + newtoken = self._token_refresher() # pylint:disable=not-callable async with self._lock: self._token = newtoken self._some_thread_refreshing = False @@ -69,27 +90,32 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument async with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise - + if self._refresh_proactively: + self._schedule_refresh() return self._token - async def _wait_till_inprogress_thread_finish_refreshing(self): + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) + self._timer.start() + + def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() - await self._lock.acquire() + self._lock.acquire() def _token_expiring(self): + if self._refresh_proactively: + interval = self._refresh_time_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + interval.total_seconds() def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on - - async def close(self) -> None: - pass - - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - await self.close() diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py index c9255a4217d7..b46653fc9982 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py @@ -6,12 +6,12 @@ import base64 import json +import calendar from typing import ( # pylint: disable=unused-import cast, Tuple, ) from datetime import datetime -import calendar from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken @@ -26,6 +26,7 @@ def _convert_datetime_to_utc_int(input_datetime): """ return int(calendar.timegm(input_datetime.utctimetuple())) + def parse_connection_str(conn_str): # type: (str) -> Tuple[str, str, str, str] if conn_str is None: @@ -53,16 +54,18 @@ def parse_connection_str(conn_str): return host, str(shared_access_key) + def get_current_utc_time(): # type: () -> str - return str(datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" def get_current_utc_as_int(): # type: () -> int - current_utc_datetime = datetime.utcnow() + current_utc_datetime = datetime.now(tz=TZ_UTC) return _convert_datetime_to_utc_int(current_utc_datetime) + def create_access_token(token): # type: (str) -> azure.core.credentials.AccessToken """Creates an instance of azure.core.credentials.AccessToken from a @@ -84,18 +87,20 @@ def create_access_token(token): raise ValueError(token_parse_err_msg) try: - padded_base64_payload = base64.b64decode(parts[1] + "==").decode('ascii') + padded_base64_payload = base64.b64decode( + parts[1] + "==").decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) except ValueError: raise ValueError(token_parse_err_msg) + def get_authentication_policy( - endpoint, # type: str - credential, # type: TokenCredential or str - decode_url=False, # type: bool - is_async=False, # type: bool + endpoint, # type: str + credential, # type: TokenCredential or str + decode_url=False, # type: bool + is_async=False, # type: bool ): # type: (...) -> BearerTokenCredentialPolicy or HMACCredentialPolicy """Returns the correct authentication policy based diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py new file mode 100644 index 000000000000..f2472e2121af --- /dev/null +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import asyncio + + +class AsyncTimer: + """A non-blocking timer, that calls a function after a specified number of seconds: + :param int interval: time interval in seconds + :param callable callback: function to be called after the interval has elapsed + """ + + def __init__(self, interval, callback): + self._interval = interval + self._callback = callback + self._task = None + + def start(self): + self._task = asyncio.ensure_future(self._job()) + + async def _job(self): + await asyncio.sleep(self._interval) + await self._callback() + + def cancel(self): + if self._task is not None: + self._task.cancel() diff --git a/sdk/communication/azure-communication-chat/tests/_shared/helper.py b/sdk/communication/azure-communication-chat/tests/_shared/helper.py new file mode 100644 index 000000000000..eba467f7c913 --- /dev/null +++ b/sdk/communication/azure-communication-chat/tests/_shared/helper.py @@ -0,0 +1,44 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import re +import base64 +from azure_devtools.scenario_tests import RecordingProcessor +from datetime import datetime, timedelta +from functools import wraps +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + + +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + + +def generate_token_with_custom_expiry_epoch(expires_on_epoch): + expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + base64expiry = base64.b64encode( + expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") + token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ + base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + return token_template + + +class URIIdentityReplacer(RecordingProcessor): + """Replace the identity in request uri""" + + def process_request(self, request): + resource = (urlparse(request.uri).netloc).split('.')[0] + request.uri = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub(resource, 'sanitized', request.uri) + return request + + def process_response(self, response): + if 'url' in response: + response['url'] = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', response['url']) + return response diff --git a/sdk/communication/azure-communication-chat/tests/helper.py b/sdk/communication/azure-communication-chat/tests/helper.py deleted file mode 100644 index 83ea3cc8397a..000000000000 --- a/sdk/communication/azure-communication-chat/tests/helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from azure_devtools.scenario_tests import RecordingProcessor - -class URIIdentityReplacer(RecordingProcessor): - """Replace the identity in request uri""" - def process_request(self, request): - import re - request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) - return request - - def process_response(self, response): - import re - if 'url' in response: - response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) - return response diff --git a/sdk/communication/azure-communication-chat/tests/test_chat_client_e2e.py b/sdk/communication/azure-communication-chat/tests/test_chat_client_e2e.py index 5f93abbeb4b4..737fcd3ee48b 100644 --- a/sdk/communication/azure-communication-chat/tests/test_chat_client_e2e.py +++ b/sdk/communication/azure-communication-chat/tests/test_chat_client_e2e.py @@ -20,7 +20,7 @@ from azure.communication.chat._shared.utils import parse_connection_str from azure_devtools.scenario_tests import RecordingProcessor -from helper import URIIdentityReplacer +from _shared.helper import URIIdentityReplacer from chat_e2e_helper import ChatURIReplacer from _shared.testcase import ( CommunicationTestCase, diff --git a/sdk/communication/azure-communication-chat/tests/test_chat_client_e2e_async.py b/sdk/communication/azure-communication-chat/tests/test_chat_client_e2e_async.py index df4658693336..19627da57dd1 100644 --- a/sdk/communication/azure-communication-chat/tests/test_chat_client_e2e_async.py +++ b/sdk/communication/azure-communication-chat/tests/test_chat_client_e2e_async.py @@ -20,7 +20,7 @@ ) from azure.communication.identity._shared.utils import parse_connection_str from azure_devtools.scenario_tests import RecordingProcessor -from helper import URIIdentityReplacer +from _shared.helper import URIIdentityReplacer from chat_e2e_helper import ChatURIReplacer from _shared.asynctestcase import AsyncCommunicationTestCase from _shared.testcase import BodyReplacerProcessor, ResponseReplacerProcessor diff --git a/sdk/communication/azure-communication-chat/tests/test_chat_thread_client_e2e.py b/sdk/communication/azure-communication-chat/tests/test_chat_thread_client_e2e.py index 990a849cb4c2..55d7263cef29 100644 --- a/sdk/communication/azure-communication-chat/tests/test_chat_thread_client_e2e.py +++ b/sdk/communication/azure-communication-chat/tests/test_chat_thread_client_e2e.py @@ -20,7 +20,7 @@ from azure.communication.chat._shared.utils import parse_connection_str from azure_devtools.scenario_tests import RecordingProcessor -from helper import URIIdentityReplacer +from _shared.helper import URIIdentityReplacer from chat_e2e_helper import ChatURIReplacer from _shared.testcase import ( CommunicationTestCase, diff --git a/sdk/communication/azure-communication-chat/tests/test_chat_thread_client_e2e_async.py b/sdk/communication/azure-communication-chat/tests/test_chat_thread_client_e2e_async.py index 1a468780b8ab..26b59f1c1776 100644 --- a/sdk/communication/azure-communication-chat/tests/test_chat_thread_client_e2e_async.py +++ b/sdk/communication/azure-communication-chat/tests/test_chat_thread_client_e2e_async.py @@ -20,7 +20,7 @@ ) from azure.communication.identity._shared.utils import parse_connection_str from azure_devtools.scenario_tests import RecordingProcessor -from helper import URIIdentityReplacer +from _shared.helper import URIIdentityReplacer from chat_e2e_helper import ChatURIReplacer from _shared.asynctestcase import AsyncCommunicationTestCase from _shared.testcase import BodyReplacerProcessor, ResponseReplacerProcessor diff --git a/sdk/communication/azure-communication-chat/tests/test_user_credential.py b/sdk/communication/azure-communication-chat/tests/test_user_credential.py new file mode 100644 index 000000000000..1e47903a03ea --- /dev/null +++ b/sdk/communication/azure-communication-chat/tests/test_user_credential.py @@ -0,0 +1,181 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from unittest import TestCase +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch # type: ignore +import azure.communication.identity._shared.user_credential as user_credential +from azure.communication.identity._shared.user_credential import CommunicationTokenCredential +from azure.communication.identity._shared.utils import create_access_token +from azure.communication.identity._shared.utils import get_current_utc_as_int +from datetime import timedelta +from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential(TestCase): + + @classmethod + def setUpClass(cls): + cls.sample_token = generate_token_with_custom_expiry_epoch( + 32503680000) # 1/1/2030 + cls.expired_token = generate_token_with_custom_expiry_epoch( + 100) # 1/1/1970 + + def test_communicationtokencredential_decodes_token(self): + credential = CommunicationTokenCredential(self.sample_token) + access_token = credential.get_token() + self.assertEqual(access_token.token, self.sample_token) + + def test_communicationtokencredential_throws_if_invalid_token(self): + self.assertRaises( + ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) + + def test_communicationtokencredential_throws_if_nonstring_token(self): + self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) + + def test_communicationtokencredential_static_token_returns_expired_token(self): + credential = CommunicationTokenCredential(self.expired_token) + self.assertEqual(credential.get_token().token, self.expired_token) + + def test_communicationtokencredential_token_expired_refresh_called(self): + refresher = MagicMock(return_value=self.sample_token) + access_token = CommunicationTokenCredential( + self.expired_token, + token_refresher=refresher).get_token() + refresher.assert_called_once() + self.assertEqual(access_token, self.sample_token) + + def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): + refresher = MagicMock( + return_value=create_access_token(self.expired_token)) + credential = CommunicationTokenCredential( + self.expired_token, token_refresher=refresher) + + credential.get_token() + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 2) + self.assertEqual(access_token.token, self.expired_token) + + # @patch_threading_timer(user_credential.__name__+'.Timer') + def test_uses_initial_token_as_expected(self): # , timer_mock): + refresher = MagicMock( + return_value=self.expired_token) + credential = CommunicationTokenCredential( + self.sample_token, token_refresher=refresher, refresh_proactively=True) + + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 0) + self.assertEqual(access_token.token, self.sample_token) + + def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + with credential: + access_token = credential.get_token() + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + + with CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + assert credential._timer is not None + assert credential._timer.finished._flag == True + + def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) + with credential: + for _ in range(10): + access_token = credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py b/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py new file mode 100644 index 000000000000..ed20b2779b8d --- /dev/null +++ b/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py @@ -0,0 +1,196 @@ + +# coding: utf-8 +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from datetime import timedelta +import pytest +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch +from azure.communication.identity._shared.user_credential_async import CommunicationTokenCredential +import azure.communication.identity._shared.user_credential_async as user_credential_async +from azure.communication.identity._shared.utils import create_access_token +from azure.communication.identity._shared.utils import get_current_utc_as_int +from _shared.helper import generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential: + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_nonstring_token(self): + with pytest.raises(TypeError) as err: + CommunicationTokenCredential(1234) + assert str(err.value) == "Token must be a string." + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_invalid_token(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential("not a token") + assert str(err.value) == "Token is not formatted correctly" + + @pytest.mark.asyncio + async def test_init_with_valid_token(self): + initial_token = generate_token_with_custom_expiry(5 * 60) + credential = CommunicationTokenCredential(initial_token) + access_token = await credential.get_token() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_immediately_with_expired_token(self): + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + expired_token = generate_token_with_custom_expiry(-(5 * 60)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + access_token = await credential.get_token() + + refresher.assert_called_once() + assert refreshed_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_before_expiring_time(self): + initial_token = generate_token_with_custom_expiry(15 * 60) + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + credential = CommunicationTokenCredential( + initial_token, token_refresher=refresher, refresh_proactively=True) + async with credential: + access_token = await credential.get_token() + + refresher.assert_not_called() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) + async with credential: + for _ in range(10): + access_token = await credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_as_necessary(self): + expired_token = generate_token_with_custom_expiry(-(10 * 60)) + refresher = MagicMock(return_value=create_access_token(expired_token)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + await credential.get_token() + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert expired_token == access_token.token + + @pytest.mark.asyncio + async def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + async with credential: + access_token = await credential.get_token() + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + + async with CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + assert credential._timer is not None + assert refresher.call_count == 0 From 5f9addb35e1f62f5c6bb18c66f0c6b424aa8f57e Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 2 Dec 2021 19:19:29 +0100 Subject: [PATCH 15/49] updates in _shared duplicated in sms --- .../tests/test_user_credential.py | 8 +- .../tests/test_user_credential_async.py | 8 +- .../sms/_shared/user_credential.py | 66 ++++-- .../sms/_shared/user_credential_async.py | 86 +++++--- .../azure/communication/sms/_shared/utils.py | 19 +- .../communication/sms/_shared/utils_async.py | 30 +++ .../tests/_shared/helper.py | 44 ++++ .../tests/test_user_credential.py | 181 ++++++++++++++++ .../tests/test_user_credential_async.py | 196 ++++++++++++++++++ 9 files changed, 578 insertions(+), 60 deletions(-) create mode 100644 sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils_async.py create mode 100644 sdk/communication/azure-communication-sms/tests/_shared/helper.py create mode 100644 sdk/communication/azure-communication-sms/tests/test_user_credential.py create mode 100644 sdk/communication/azure-communication-sms/tests/test_user_credential_async.py diff --git a/sdk/communication/azure-communication-chat/tests/test_user_credential.py b/sdk/communication/azure-communication-chat/tests/test_user_credential.py index 1e47903a03ea..63dcc26bd61d 100644 --- a/sdk/communication/azure-communication-chat/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-chat/tests/test_user_credential.py @@ -8,10 +8,10 @@ from unittest.mock import MagicMock, patch except ImportError: # python < 3.3 from mock import MagicMock, patch # type: ignore -import azure.communication.identity._shared.user_credential as user_credential -from azure.communication.identity._shared.user_credential import CommunicationTokenCredential -from azure.communication.identity._shared.utils import create_access_token -from azure.communication.identity._shared.utils import get_current_utc_as_int +import azure.communication.chat._shared.user_credential as user_credential +from azure.communication.chat._shared.user_credential import CommunicationTokenCredential +from azure.communication.chat._shared.utils import create_access_token +from azure.communication.chat._shared.utils import get_current_utc_as_int from datetime import timedelta from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry diff --git a/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py b/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py index ed20b2779b8d..c53671dee341 100644 --- a/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py @@ -11,10 +11,10 @@ from unittest.mock import MagicMock, patch except ImportError: # python < 3.3 from mock import MagicMock, patch -from azure.communication.identity._shared.user_credential_async import CommunicationTokenCredential -import azure.communication.identity._shared.user_credential_async as user_credential_async -from azure.communication.identity._shared.utils import create_access_token -from azure.communication.identity._shared.utils import get_current_utc_as_int +from azure.communication.chat._shared.user_credential_async import CommunicationTokenCredential +import azure.communication.chat._shared.user_credential_async as user_credential_async +from azure.communication.chat._shared.utils import create_access_token +from azure.communication.chat._shared.utils import get_current_utc_as_int from _shared.helper import generate_token_with_custom_expiry diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 9b5f17dcc95d..e537fe599fe3 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -3,40 +3,58 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition +from threading import Lock, Condition, Timer from datetime import timedelta -from typing import ( # pylint: disable=unused-import + +from typing import ( # pylint: disable=unused-import cast, Tuple, + Any ) +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The token refresher to provide capacity to fetch fresh token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str - **kwargs + **kwargs # type: Any ): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() + if not isinstance(token, six.string_types): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() + + def __enter__(self): + return self + + def __exit__(self, *args): + if self._timer is not None: + self._timer.cancel() - def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument - # type (*str, **Any) -> AccessToken + def get_token(self): + # type () -> ~azure.core.credentials.AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -44,8 +62,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument if not self._token_refresher or not self._token_expiring(): return self._token - should_this_thread_refresh = False + self._update_token_and_reschedule() + return self._token + def _update_token_and_reschedule(self): + should_this_thread_refresh = False with self._lock: while self._token_expiring(): if self._some_thread_refreshing: @@ -70,17 +91,32 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise + if self._refresh_proactively: + self._schedule_refresh() return self._token + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() + def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() self._lock.acquire() def _token_expiring(self): + if self._refresh_proactively: + interval = self._refresh_time_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + interval.total_seconds() def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index 52a99e7a4b6a..26c50287fcb8 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -5,62 +5,83 @@ # -------------------------------------------------------------------------- from asyncio import Condition, Lock from datetime import timedelta +import sys from typing import ( # pylint: disable=unused-import cast, Tuple, Any ) +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token +from .utils_async import AsyncTimer class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() - self._lock = Condition(Lock()) + if not isinstance(token, six.string_types): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None + self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() + self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() - async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument - # type (*str, **Any) -> AccessToken + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + if self._timer is not None: + self._timer.cancel() + + async def get_token(self): + # type () -> ~azure.core.credentials.AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ if not self._token_refresher or not self._token_expiring(): return self._token + await self._update_token_and_reschedule() + return self._token + async def _update_token_and_reschedule(self): should_this_thread_refresh = False - async with self._lock: - while self._token_expiring(): if self._some_thread_refreshing: if self._is_currenttoken_valid(): return self._token - await self._wait_till_inprogress_thread_finish_refreshing() + self._wait_till_inprogress_thread_finish_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True break - if should_this_thread_refresh: try: - newtoken = await self._token_refresher() # pylint:disable=not-callable - + newtoken = self._token_refresher() # pylint:disable=not-callable async with self._lock: self._token = newtoken self._some_thread_refreshing = False @@ -69,27 +90,32 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument async with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise - + if self._refresh_proactively: + self._schedule_refresh() return self._token - async def _wait_till_inprogress_thread_finish_refreshing(self): + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) + self._timer.start() + + def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() - await self._lock.acquire() + self._lock.acquire() def _token_expiring(self): + if self._refresh_proactively: + interval = self._refresh_time_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + interval.total_seconds() def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on - - async def close(self) -> None: - pass - - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - await self.close() diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py index c9255a4217d7..18ee3092c9be 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py @@ -6,12 +6,12 @@ import base64 import json +import calendar from typing import ( # pylint: disable=unused-import cast, Tuple, ) from datetime import datetime -import calendar from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken @@ -26,6 +26,7 @@ def _convert_datetime_to_utc_int(input_datetime): """ return int(calendar.timegm(input_datetime.utctimetuple())) + def parse_connection_str(conn_str): # type: (str) -> Tuple[str, str, str, str] if conn_str is None: @@ -53,9 +54,10 @@ def parse_connection_str(conn_str): return host, str(shared_access_key) + def get_current_utc_time(): # type: () -> str - return str(datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" def get_current_utc_as_int(): @@ -63,6 +65,7 @@ def get_current_utc_as_int(): current_utc_datetime = datetime.utcnow() return _convert_datetime_to_utc_int(current_utc_datetime) + def create_access_token(token): # type: (str) -> azure.core.credentials.AccessToken """Creates an instance of azure.core.credentials.AccessToken from a @@ -84,18 +87,20 @@ def create_access_token(token): raise ValueError(token_parse_err_msg) try: - padded_base64_payload = base64.b64decode(parts[1] + "==").decode('ascii') + padded_base64_payload = base64.b64decode( + parts[1] + "==").decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) except ValueError: raise ValueError(token_parse_err_msg) + def get_authentication_policy( - endpoint, # type: str - credential, # type: TokenCredential or str - decode_url=False, # type: bool - is_async=False, # type: bool + endpoint, # type: str + credential, # type: TokenCredential or str + decode_url=False, # type: bool + is_async=False, # type: bool ): # type: (...) -> BearerTokenCredentialPolicy or HMACCredentialPolicy """Returns the correct authentication policy based diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils_async.py new file mode 100644 index 000000000000..f2472e2121af --- /dev/null +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils_async.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import asyncio + + +class AsyncTimer: + """A non-blocking timer, that calls a function after a specified number of seconds: + :param int interval: time interval in seconds + :param callable callback: function to be called after the interval has elapsed + """ + + def __init__(self, interval, callback): + self._interval = interval + self._callback = callback + self._task = None + + def start(self): + self._task = asyncio.ensure_future(self._job()) + + async def _job(self): + await asyncio.sleep(self._interval) + await self._callback() + + def cancel(self): + if self._task is not None: + self._task.cancel() diff --git a/sdk/communication/azure-communication-sms/tests/_shared/helper.py b/sdk/communication/azure-communication-sms/tests/_shared/helper.py new file mode 100644 index 000000000000..eba467f7c913 --- /dev/null +++ b/sdk/communication/azure-communication-sms/tests/_shared/helper.py @@ -0,0 +1,44 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import re +import base64 +from azure_devtools.scenario_tests import RecordingProcessor +from datetime import datetime, timedelta +from functools import wraps +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + + +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + + +def generate_token_with_custom_expiry_epoch(expires_on_epoch): + expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + base64expiry = base64.b64encode( + expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") + token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ + base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + return token_template + + +class URIIdentityReplacer(RecordingProcessor): + """Replace the identity in request uri""" + + def process_request(self, request): + resource = (urlparse(request.uri).netloc).split('.')[0] + request.uri = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub(resource, 'sanitized', request.uri) + return request + + def process_response(self, response): + if 'url' in response: + response['url'] = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', response['url']) + return response diff --git a/sdk/communication/azure-communication-sms/tests/test_user_credential.py b/sdk/communication/azure-communication-sms/tests/test_user_credential.py new file mode 100644 index 000000000000..8a234a3a67a8 --- /dev/null +++ b/sdk/communication/azure-communication-sms/tests/test_user_credential.py @@ -0,0 +1,181 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from unittest import TestCase +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch # type: ignore +import azure.communication.sms._shared.user_credential as user_credential +from azure.communication.sms._shared.user_credential import CommunicationTokenCredential +from azure.communication.sms._shared.utils import create_access_token +from azure.communication.sms._shared.utils import get_current_utc_as_int +from datetime import timedelta +from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential(TestCase): + + @classmethod + def setUpClass(cls): + cls.sample_token = generate_token_with_custom_expiry_epoch( + 32503680000) # 1/1/2030 + cls.expired_token = generate_token_with_custom_expiry_epoch( + 100) # 1/1/1970 + + def test_communicationtokencredential_decodes_token(self): + credential = CommunicationTokenCredential(self.sample_token) + access_token = credential.get_token() + self.assertEqual(access_token.token, self.sample_token) + + def test_communicationtokencredential_throws_if_invalid_token(self): + self.assertRaises( + ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) + + def test_communicationtokencredential_throws_if_nonstring_token(self): + self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) + + def test_communicationtokencredential_static_token_returns_expired_token(self): + credential = CommunicationTokenCredential(self.expired_token) + self.assertEqual(credential.get_token().token, self.expired_token) + + def test_communicationtokencredential_token_expired_refresh_called(self): + refresher = MagicMock(return_value=self.sample_token) + access_token = CommunicationTokenCredential( + self.expired_token, + token_refresher=refresher).get_token() + refresher.assert_called_once() + self.assertEqual(access_token, self.sample_token) + + def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): + refresher = MagicMock( + return_value=create_access_token(self.expired_token)) + credential = CommunicationTokenCredential( + self.expired_token, token_refresher=refresher) + + credential.get_token() + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 2) + self.assertEqual(access_token.token, self.expired_token) + + # @patch_threading_timer(user_credential.__name__+'.Timer') + def test_uses_initial_token_as_expected(self): # , timer_mock): + refresher = MagicMock( + return_value=self.expired_token) + credential = CommunicationTokenCredential( + self.sample_token, token_refresher=refresher, refresh_proactively=True) + + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 0) + self.assertEqual(access_token.token, self.sample_token) + + def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + with credential: + access_token = credential.get_token() + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + + with CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + assert credential._timer is not None + assert credential._timer.finished._flag == True + + def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) + with credential: + for _ in range(10): + access_token = credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-sms/tests/test_user_credential_async.py b/sdk/communication/azure-communication-sms/tests/test_user_credential_async.py new file mode 100644 index 000000000000..5d547266e2eb --- /dev/null +++ b/sdk/communication/azure-communication-sms/tests/test_user_credential_async.py @@ -0,0 +1,196 @@ + +# coding: utf-8 +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from datetime import timedelta +import pytest +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch +from azure.communication.sms._shared.user_credential_async import CommunicationTokenCredential +import azure.communication.sms._shared.user_credential_async as user_credential_async +from azure.communication.sms._shared.utils import create_access_token +from azure.communication.sms._shared.utils import get_current_utc_as_int +from _shared.helper import generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential: + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_nonstring_token(self): + with pytest.raises(TypeError) as err: + CommunicationTokenCredential(1234) + assert str(err.value) == "Token must be a string." + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_invalid_token(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential("not a token") + assert str(err.value) == "Token is not formatted correctly" + + @pytest.mark.asyncio + async def test_init_with_valid_token(self): + initial_token = generate_token_with_custom_expiry(5 * 60) + credential = CommunicationTokenCredential(initial_token) + access_token = await credential.get_token() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_immediately_with_expired_token(self): + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + expired_token = generate_token_with_custom_expiry(-(5 * 60)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + access_token = await credential.get_token() + + refresher.assert_called_once() + assert refreshed_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_before_expiring_time(self): + initial_token = generate_token_with_custom_expiry(15 * 60) + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + credential = CommunicationTokenCredential( + initial_token, token_refresher=refresher, refresh_proactively=True) + async with credential: + access_token = await credential.get_token() + + refresher.assert_not_called() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) + async with credential: + for _ in range(10): + access_token = await credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_as_necessary(self): + expired_token = generate_token_with_custom_expiry(-(10 * 60)) + refresher = MagicMock(return_value=create_access_token(expired_token)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + await credential.get_token() + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert expired_token == access_token.token + + @pytest.mark.asyncio + async def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + async with credential: + access_token = await credential.get_token() + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + + async with CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + assert credential._timer is not None + assert refresher.call_count == 0 From f2aa5be448a4a513862eb1a5c2743711ce1be655 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 2 Dec 2021 20:14:01 +0100 Subject: [PATCH 16/49] updates in _shared duplicated in networktraversal --- .../azure/communication/chat/_shared/utils.py | 4 + .../tests/_shared/helper.py | 13 +- .../communication/identity/_shared/utils.py | 4 + .../tests/_shared/helper.py | 13 +- .../_shared/user_credential.py | 65 ++++-- .../_shared/user_credential_async.py | 59 ++++-- .../networktraversal/_shared/utils.py | 19 +- .../networktraversal/_shared/utils_async.py | 30 +++ .../tests/_shared/helper.py | 14 ++ .../tests/test_user_credential.py | 181 ++++++++++++++++ .../tests/test_user_credential_async.py | 196 ++++++++++++++++++ .../azure/communication/sms/_shared/utils.py | 4 + .../tests/_shared/helper.py | 11 +- 13 files changed, 551 insertions(+), 62 deletions(-) create mode 100644 sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils_async.py create mode 100644 sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py create mode 100644 sdk/communication/azure-communication-networktraversal/tests/test_user_credential_async.py diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py index b46653fc9982..857256206e8a 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py @@ -131,3 +131,7 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) + +def _convert_expires_on_datetime_to_utc_int(expires_on): + epoch = time.mktime(datetime(1970, 1, 1).timetuple()) + return epoch-time.mktime(expires_on.timetuple()) \ No newline at end of file diff --git a/sdk/communication/azure-communication-chat/tests/_shared/helper.py b/sdk/communication/azure-communication-chat/tests/_shared/helper.py index eba467f7c913..ce9aa3375174 100644 --- a/sdk/communication/azure-communication-chat/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-chat/tests/_shared/helper.py @@ -29,16 +29,15 @@ def generate_token_with_custom_expiry_epoch(expires_on_epoch): class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" - def process_request(self, request): resource = (urlparse(request.uri).netloc).split('.')[0] - request.uri = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub(resource, 'sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) request.uri = re.sub(resource, 'sanitized', request.uri) return request - + def process_response(self, response): if 'url' in response: - response['url'] = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', response['url']) - return response + response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) + return response \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index 8f1b6f0e808c..4cd51188f710 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -132,3 +132,7 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) + +def _convert_expires_on_datetime_to_utc_int(expires_on): + epoch = time.mktime(datetime(1970, 1, 1).timetuple()) + return epoch-time.mktime(expires_on.timetuple()) \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/tests/_shared/helper.py b/sdk/communication/azure-communication-identity/tests/_shared/helper.py index d4d125ba8488..d88d18833059 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/helper.py @@ -24,16 +24,15 @@ def generate_token_with_custom_expiry_epoch(expires_on_epoch): class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" - def process_request(self, request): resource = (urlparse(request.uri).netloc).split('.')[0] - request.uri = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub(resource, 'sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) request.uri = re.sub(resource, 'sanitized', request.uri) return request - + def process_response(self, response): if 'url' in response: - response['url'] = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', response['url']) - return response + response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) + return response \ No newline at end of file diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 9b5f17dcc95d..d70896d19afb 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -3,37 +3,52 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition +from threading import Lock, Condition, Timer from datetime import timedelta -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) + +from typing import Any +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token + class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The token refresher to provide capacity to fetch fresh token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str - **kwargs + **kwargs # type: Any ): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() + if not isinstance(token, six.string_types): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() + + def __enter__(self): + return self + + def __exit__(self, *args): + if self._timer is not None: + self._timer.cancel() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -44,8 +59,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument if not self._token_refresher or not self._token_expiring(): return self._token - should_this_thread_refresh = False + self._update_token_and_reschedule() + return self._token + def _update_token_and_reschedule(self): + should_this_thread_refresh = False with self._lock: while self._token_expiring(): if self._some_thread_refreshing: @@ -70,17 +88,32 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise + if self._refresh_proactively: + self._schedule_refresh() return self._token + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() + def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() self._lock.acquire() def _token_expiring(self): + if self._refresh_proactively: + interval = self._refresh_time_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + interval.total_seconds() def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index 52a99e7a4b6a..b6f4ee1be23c 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -5,33 +5,45 @@ # -------------------------------------------------------------------------- from asyncio import Condition, Lock from datetime import timedelta -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) - +from typing import Any +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token +from .utils_async import AsyncTimer + class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() - self._lock = Condition(Lock()) + if not isinstance(token, six.string_types): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None + self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() + self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() + + async def __aenter__(self): + return self async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -40,11 +52,12 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """ if not self._token_refresher or not self._token_expiring(): return self._token + await self._update_token_and_reschedule() + return self._token + async def _update_token_and_reschedule(self): should_this_thread_refresh = False - async with self._lock: - while self._token_expiring(): if self._some_thread_refreshing: if self._is_currenttoken_valid(): @@ -56,7 +69,6 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument self._some_thread_refreshing = True break - if should_this_thread_refresh: try: newtoken = await self._token_refresher() # pylint:disable=not-callable @@ -69,11 +81,20 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument async with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise - + if self._refresh_proactively: + self._schedule_refresh() return self._token + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) + self._timer.start() + async def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() await self._lock.acquire() diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py index c9255a4217d7..a47b9f221eff 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py @@ -6,12 +6,12 @@ import base64 import json +import calendar from typing import ( # pylint: disable=unused-import cast, Tuple, ) from datetime import datetime -import calendar from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken @@ -26,6 +26,7 @@ def _convert_datetime_to_utc_int(input_datetime): """ return int(calendar.timegm(input_datetime.utctimetuple())) + def parse_connection_str(conn_str): # type: (str) -> Tuple[str, str, str, str] if conn_str is None: @@ -53,9 +54,11 @@ def parse_connection_str(conn_str): return host, str(shared_access_key) + def get_current_utc_time(): # type: () -> str - return str(datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + def get_current_utc_as_int(): @@ -63,6 +66,7 @@ def get_current_utc_as_int(): current_utc_datetime = datetime.utcnow() return _convert_datetime_to_utc_int(current_utc_datetime) + def create_access_token(token): # type: (str) -> azure.core.credentials.AccessToken """Creates an instance of azure.core.credentials.AccessToken from a @@ -84,7 +88,8 @@ def create_access_token(token): raise ValueError(token_parse_err_msg) try: - padded_base64_payload = base64.b64decode(parts[1] + "==").decode('ascii') + padded_base64_payload = base64.b64decode( + parts[1] + "==").decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) @@ -92,10 +97,10 @@ def create_access_token(token): raise ValueError(token_parse_err_msg) def get_authentication_policy( - endpoint, # type: str - credential, # type: TokenCredential or str - decode_url=False, # type: bool - is_async=False, # type: bool + endpoint, # type: str + credential, # type: TokenCredential or str + decode_url=False, # type: bool + is_async=False, # type: bool ): # type: (...) -> BearerTokenCredentialPolicy or HMACCredentialPolicy """Returns the correct authentication policy based diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils_async.py new file mode 100644 index 000000000000..f2472e2121af --- /dev/null +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils_async.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import asyncio + + +class AsyncTimer: + """A non-blocking timer, that calls a function after a specified number of seconds: + :param int interval: time interval in seconds + :param callable callback: function to be called after the interval has elapsed + """ + + def __init__(self, interval, callback): + self._interval = interval + self._callback = callback + self._task = None + + def start(self): + self._task = asyncio.ensure_future(self._job()) + + async def _job(self): + await asyncio.sleep(self._interval) + await self._callback() + + def cancel(self): + if self._task is not None: + self._task.cancel() diff --git a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py index 2f415d7f7f51..da39b5a7c2b3 100644 --- a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py @@ -4,9 +4,23 @@ # license information. # -------------------------------------------------------------------------- import re +import base64 from azure_devtools.scenario_tests import RecordingProcessor from urllib.parse import urlparse + +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + + +def generate_token_with_custom_expiry_epoch(expires_on_epoch): + expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + base64expiry = base64.b64encode( + expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") + token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ + base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + return token_template + class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" def process_request(self, request): diff --git a/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py b/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py new file mode 100644 index 000000000000..1e47903a03ea --- /dev/null +++ b/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py @@ -0,0 +1,181 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from unittest import TestCase +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch # type: ignore +import azure.communication.identity._shared.user_credential as user_credential +from azure.communication.identity._shared.user_credential import CommunicationTokenCredential +from azure.communication.identity._shared.utils import create_access_token +from azure.communication.identity._shared.utils import get_current_utc_as_int +from datetime import timedelta +from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential(TestCase): + + @classmethod + def setUpClass(cls): + cls.sample_token = generate_token_with_custom_expiry_epoch( + 32503680000) # 1/1/2030 + cls.expired_token = generate_token_with_custom_expiry_epoch( + 100) # 1/1/1970 + + def test_communicationtokencredential_decodes_token(self): + credential = CommunicationTokenCredential(self.sample_token) + access_token = credential.get_token() + self.assertEqual(access_token.token, self.sample_token) + + def test_communicationtokencredential_throws_if_invalid_token(self): + self.assertRaises( + ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) + + def test_communicationtokencredential_throws_if_nonstring_token(self): + self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) + + def test_communicationtokencredential_static_token_returns_expired_token(self): + credential = CommunicationTokenCredential(self.expired_token) + self.assertEqual(credential.get_token().token, self.expired_token) + + def test_communicationtokencredential_token_expired_refresh_called(self): + refresher = MagicMock(return_value=self.sample_token) + access_token = CommunicationTokenCredential( + self.expired_token, + token_refresher=refresher).get_token() + refresher.assert_called_once() + self.assertEqual(access_token, self.sample_token) + + def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): + refresher = MagicMock( + return_value=create_access_token(self.expired_token)) + credential = CommunicationTokenCredential( + self.expired_token, token_refresher=refresher) + + credential.get_token() + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 2) + self.assertEqual(access_token.token, self.expired_token) + + # @patch_threading_timer(user_credential.__name__+'.Timer') + def test_uses_initial_token_as_expected(self): # , timer_mock): + refresher = MagicMock( + return_value=self.expired_token) + credential = CommunicationTokenCredential( + self.sample_token, token_refresher=refresher, refresh_proactively=True) + + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 0) + self.assertEqual(access_token.token, self.sample_token) + + def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + with credential: + access_token = credential.get_token() + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + + with CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + assert credential._timer is not None + assert credential._timer.finished._flag == True + + def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) + with credential: + for _ in range(10): + access_token = credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-networktraversal/tests/test_user_credential_async.py b/sdk/communication/azure-communication-networktraversal/tests/test_user_credential_async.py new file mode 100644 index 000000000000..ed20b2779b8d --- /dev/null +++ b/sdk/communication/azure-communication-networktraversal/tests/test_user_credential_async.py @@ -0,0 +1,196 @@ + +# coding: utf-8 +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from datetime import timedelta +import pytest +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch +from azure.communication.identity._shared.user_credential_async import CommunicationTokenCredential +import azure.communication.identity._shared.user_credential_async as user_credential_async +from azure.communication.identity._shared.utils import create_access_token +from azure.communication.identity._shared.utils import get_current_utc_as_int +from _shared.helper import generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential: + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_nonstring_token(self): + with pytest.raises(TypeError) as err: + CommunicationTokenCredential(1234) + assert str(err.value) == "Token must be a string." + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_invalid_token(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential("not a token") + assert str(err.value) == "Token is not formatted correctly" + + @pytest.mark.asyncio + async def test_init_with_valid_token(self): + initial_token = generate_token_with_custom_expiry(5 * 60) + credential = CommunicationTokenCredential(initial_token) + access_token = await credential.get_token() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_immediately_with_expired_token(self): + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + expired_token = generate_token_with_custom_expiry(-(5 * 60)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + access_token = await credential.get_token() + + refresher.assert_called_once() + assert refreshed_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_before_expiring_time(self): + initial_token = generate_token_with_custom_expiry(15 * 60) + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + credential = CommunicationTokenCredential( + initial_token, token_refresher=refresher, refresh_proactively=True) + async with credential: + access_token = await credential.get_token() + + refresher.assert_not_called() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) + async with credential: + for _ in range(10): + access_token = await credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_as_necessary(self): + expired_token = generate_token_with_custom_expiry(-(10 * 60)) + refresher = MagicMock(return_value=create_access_token(expired_token)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + await credential.get_token() + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert expired_token == access_token.token + + @pytest.mark.asyncio + async def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + async with credential: + access_token = await credential.get_token() + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + + async with CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + assert credential._timer is not None + assert refresher.call_count == 0 diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py index 18ee3092c9be..2bc184d238cc 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py @@ -131,3 +131,7 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) + +def _convert_expires_on_datetime_to_utc_int(expires_on): + epoch = time.mktime(datetime(1970, 1, 1).timetuple()) + return epoch-time.mktime(expires_on.timetuple()) \ No newline at end of file diff --git a/sdk/communication/azure-communication-sms/tests/_shared/helper.py b/sdk/communication/azure-communication-sms/tests/_shared/helper.py index eba467f7c913..99f357c56d50 100644 --- a/sdk/communication/azure-communication-sms/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-sms/tests/_shared/helper.py @@ -29,16 +29,15 @@ def generate_token_with_custom_expiry_epoch(expires_on_epoch): class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" - def process_request(self, request): resource = (urlparse(request.uri).netloc).split('.')[0] - request.uri = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub(resource, 'sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) request.uri = re.sub(resource, 'sanitized', request.uri) return request - + def process_response(self, response): if 'url' in response: - response['url'] = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', response['url']) + response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) return response From c05185b1608a316026f4272a9df33fe6f8d55090 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 2 Dec 2021 21:07:15 +0100 Subject: [PATCH 17/49] updates in _shared duplicated in phonenumbers --- .../phonenumbers/_shared/user_credential.py | 66 ++++-- .../_shared/user_credential_async.py | 86 +++++--- .../phonenumbers/_shared/utils.py | 23 +- .../phonenumbers/_shared/utils_async.py | 30 +++ .../test/_shared/helper.py | 43 ++++ .../test/test_user_credential.py | 181 ++++++++++++++++ .../test/test_user_credential_async.py | 196 ++++++++++++++++++ 7 files changed, 573 insertions(+), 52 deletions(-) create mode 100644 sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils_async.py create mode 100644 sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py create mode 100644 sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py create mode 100644 sdk/communication/azure-communication-phonenumbers/test/test_user_credential_async.py diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 9b5f17dcc95d..e537fe599fe3 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -3,40 +3,58 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition +from threading import Lock, Condition, Timer from datetime import timedelta -from typing import ( # pylint: disable=unused-import + +from typing import ( # pylint: disable=unused-import cast, Tuple, + Any ) +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The token refresher to provide capacity to fetch fresh token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str - **kwargs + **kwargs # type: Any ): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() + if not isinstance(token, six.string_types): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() + + def __enter__(self): + return self + + def __exit__(self, *args): + if self._timer is not None: + self._timer.cancel() - def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument - # type (*str, **Any) -> AccessToken + def get_token(self): + # type () -> ~azure.core.credentials.AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -44,8 +62,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument if not self._token_refresher or not self._token_expiring(): return self._token - should_this_thread_refresh = False + self._update_token_and_reschedule() + return self._token + def _update_token_and_reschedule(self): + should_this_thread_refresh = False with self._lock: while self._token_expiring(): if self._some_thread_refreshing: @@ -70,17 +91,32 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise + if self._refresh_proactively: + self._schedule_refresh() return self._token + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() + def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() self._lock.acquire() def _token_expiring(self): + if self._refresh_proactively: + interval = self._refresh_time_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + interval.total_seconds() def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index 52a99e7a4b6a..26c50287fcb8 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -5,62 +5,83 @@ # -------------------------------------------------------------------------- from asyncio import Condition, Lock from datetime import timedelta +import sys from typing import ( # pylint: disable=unused-import cast, Tuple, Any ) +import six from .utils import get_current_utc_as_int -from .user_token_refresh_options import CommunicationTokenRefreshOptions +from .utils import create_access_token +from .utils_async import AsyncTimer class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): - token_refresher = kwargs.pop('token_refresher', None) - communication_token_refresh_options = CommunicationTokenRefreshOptions(token=token, - token_refresher=token_refresher) - self._token = communication_token_refresh_options.get_token() - self._token_refresher = communication_token_refresh_options.get_token_refresher() - self._lock = Condition(Lock()) + if not isinstance(token, six.string_types): + raise TypeError("Token must be a string.") + self._token = create_access_token(token) + self._token_refresher = kwargs.pop('token_refresher', None) + self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) + self._timer = None + self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() + self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + if self._refresh_proactively: + self._schedule_refresh() - async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument - # type (*str, **Any) -> AccessToken + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + if self._timer is not None: + self._timer.cancel() + + async def get_token(self): + # type () -> ~azure.core.credentials.AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ if not self._token_refresher or not self._token_expiring(): return self._token + await self._update_token_and_reschedule() + return self._token + async def _update_token_and_reschedule(self): should_this_thread_refresh = False - async with self._lock: - while self._token_expiring(): if self._some_thread_refreshing: if self._is_currenttoken_valid(): return self._token - await self._wait_till_inprogress_thread_finish_refreshing() + self._wait_till_inprogress_thread_finish_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True break - if should_this_thread_refresh: try: - newtoken = await self._token_refresher() # pylint:disable=not-callable - + newtoken = self._token_refresher() # pylint:disable=not-callable async with self._lock: self._token = newtoken self._some_thread_refreshing = False @@ -69,27 +90,32 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument async with self._lock: self._some_thread_refreshing = False self._lock.notify_all() - raise - + if self._refresh_proactively: + self._schedule_refresh() return self._token - async def _wait_till_inprogress_thread_finish_refreshing(self): + def _schedule_refresh(self): + if self._timer is not None: + self._timer.cancel() + + timespan = self._token.expires_on - \ + get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) + self._timer.start() + + def _wait_till_inprogress_thread_finish_refreshing(self): self._lock.release() - await self._lock.acquire() + self._lock.acquire() def _token_expiring(self): + if self._refresh_proactively: + interval = self._refresh_time_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + interval.total_seconds() def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on - - async def close(self) -> None: - pass - - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - await self.close() diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py index c9255a4217d7..2bc184d238cc 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py @@ -6,12 +6,12 @@ import base64 import json +import calendar from typing import ( # pylint: disable=unused-import cast, Tuple, ) from datetime import datetime -import calendar from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken @@ -26,6 +26,7 @@ def _convert_datetime_to_utc_int(input_datetime): """ return int(calendar.timegm(input_datetime.utctimetuple())) + def parse_connection_str(conn_str): # type: (str) -> Tuple[str, str, str, str] if conn_str is None: @@ -53,9 +54,10 @@ def parse_connection_str(conn_str): return host, str(shared_access_key) + def get_current_utc_time(): # type: () -> str - return str(datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" + return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" def get_current_utc_as_int(): @@ -63,6 +65,7 @@ def get_current_utc_as_int(): current_utc_datetime = datetime.utcnow() return _convert_datetime_to_utc_int(current_utc_datetime) + def create_access_token(token): # type: (str) -> azure.core.credentials.AccessToken """Creates an instance of azure.core.credentials.AccessToken from a @@ -84,18 +87,20 @@ def create_access_token(token): raise ValueError(token_parse_err_msg) try: - padded_base64_payload = base64.b64decode(parts[1] + "==").decode('ascii') + padded_base64_payload = base64.b64decode( + parts[1] + "==").decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) except ValueError: raise ValueError(token_parse_err_msg) + def get_authentication_policy( - endpoint, # type: str - credential, # type: TokenCredential or str - decode_url=False, # type: bool - is_async=False, # type: bool + endpoint, # type: str + credential, # type: TokenCredential or str + decode_url=False, # type: bool + is_async=False, # type: bool ): # type: (...) -> BearerTokenCredentialPolicy or HMACCredentialPolicy """Returns the correct authentication policy based @@ -126,3 +131,7 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) + +def _convert_expires_on_datetime_to_utc_int(expires_on): + epoch = time.mktime(datetime(1970, 1, 1).timetuple()) + return epoch-time.mktime(expires_on.timetuple()) \ No newline at end of file diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils_async.py new file mode 100644 index 000000000000..f2472e2121af --- /dev/null +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils_async.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import asyncio + + +class AsyncTimer: + """A non-blocking timer, that calls a function after a specified number of seconds: + :param int interval: time interval in seconds + :param callable callback: function to be called after the interval has elapsed + """ + + def __init__(self, interval, callback): + self._interval = interval + self._callback = callback + self._task = None + + def start(self): + self._task = asyncio.ensure_future(self._job()) + + async def _job(self): + await asyncio.sleep(self._interval) + await self._callback() + + def cancel(self): + if self._task is not None: + self._task.cancel() diff --git a/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py b/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py new file mode 100644 index 000000000000..ce9aa3375174 --- /dev/null +++ b/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import re +import base64 +from azure_devtools.scenario_tests import RecordingProcessor +from datetime import datetime, timedelta +from functools import wraps +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + + +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + + +def generate_token_with_custom_expiry_epoch(expires_on_epoch): + expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + base64expiry = base64.b64encode( + expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") + token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ + base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + return token_template + + +class URIIdentityReplacer(RecordingProcessor): + """Replace the identity in request uri""" + def process_request(self, request): + resource = (urlparse(request.uri).netloc).split('.')[0] + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub(resource, 'sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub(resource, 'sanitized', request.uri) + return request + + def process_response(self, response): + if 'url' in response: + response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) + return response \ No newline at end of file diff --git a/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py b/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py new file mode 100644 index 000000000000..5a9e5ee93cf6 --- /dev/null +++ b/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py @@ -0,0 +1,181 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from unittest import TestCase +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch # type: ignore +import azure.communication.phonenumbers._shared.user_credential as user_credential +from azure.communication.phonenumbers._shared.user_credential import CommunicationTokenCredential +from azure.communication.phonenumbers._shared.utils import create_access_token +from azure.communication.phonenumbers._shared.utils import get_current_utc_as_int +from datetime import timedelta +from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential(TestCase): + + @classmethod + def setUpClass(cls): + cls.sample_token = generate_token_with_custom_expiry_epoch( + 32503680000) # 1/1/2030 + cls.expired_token = generate_token_with_custom_expiry_epoch( + 100) # 1/1/1970 + + def test_communicationtokencredential_decodes_token(self): + credential = CommunicationTokenCredential(self.sample_token) + access_token = credential.get_token() + self.assertEqual(access_token.token, self.sample_token) + + def test_communicationtokencredential_throws_if_invalid_token(self): + self.assertRaises( + ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) + + def test_communicationtokencredential_throws_if_nonstring_token(self): + self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) + + def test_communicationtokencredential_static_token_returns_expired_token(self): + credential = CommunicationTokenCredential(self.expired_token) + self.assertEqual(credential.get_token().token, self.expired_token) + + def test_communicationtokencredential_token_expired_refresh_called(self): + refresher = MagicMock(return_value=self.sample_token) + access_token = CommunicationTokenCredential( + self.expired_token, + token_refresher=refresher).get_token() + refresher.assert_called_once() + self.assertEqual(access_token, self.sample_token) + + def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): + refresher = MagicMock( + return_value=create_access_token(self.expired_token)) + credential = CommunicationTokenCredential( + self.expired_token, token_refresher=refresher) + + credential.get_token() + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 2) + self.assertEqual(access_token.token, self.expired_token) + + # @patch_threading_timer(user_credential.__name__+'.Timer') + def test_uses_initial_token_as_expected(self): # , timer_mock): + refresher = MagicMock( + return_value=self.expired_token) + credential = CommunicationTokenCredential( + self.sample_token, token_refresher=refresher, refresh_proactively=True) + + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 0) + self.assertEqual(access_token.token, self.sample_token) + + def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + with credential: + access_token = credential.get_token() + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + + with CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + assert credential._timer is not None + assert credential._timer.finished._flag == True + + def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) + with credential: + for _ in range(10): + access_token = credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-phonenumbers/test/test_user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/test/test_user_credential_async.py new file mode 100644 index 000000000000..0994de6f7248 --- /dev/null +++ b/sdk/communication/azure-communication-phonenumbers/test/test_user_credential_async.py @@ -0,0 +1,196 @@ + +# coding: utf-8 +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from datetime import timedelta +import pytest +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch +from azure.communication.phonenumbers._shared.user_credential_async import CommunicationTokenCredential +import azure.communication.phonenumbers._shared.user_credential_async as user_credential_async +from azure.communication.phonenumbers._shared.utils import create_access_token +from azure.communication.phonenumbers._shared.utils import get_current_utc_as_int +from _shared.helper import generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential: + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_nonstring_token(self): + with pytest.raises(TypeError) as err: + CommunicationTokenCredential(1234) + assert str(err.value) == "Token must be a string." + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_invalid_token(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential("not a token") + assert str(err.value) == "Token is not formatted correctly" + + @pytest.mark.asyncio + async def test_init_with_valid_token(self): + initial_token = generate_token_with_custom_expiry(5 * 60) + credential = CommunicationTokenCredential(initial_token) + access_token = await credential.get_token() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_immediately_with_expired_token(self): + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + expired_token = generate_token_with_custom_expiry(-(5 * 60)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + access_token = await credential.get_token() + + refresher.assert_called_once() + assert refreshed_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_before_expiring_time(self): + initial_token = generate_token_with_custom_expiry(15 * 60) + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + credential = CommunicationTokenCredential( + initial_token, token_refresher=refresher, refresh_proactively=True) + async with credential: + access_token = await credential.get_token() + + refresher.assert_not_called() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, refresh_proactively=False) + async with credential: + for _ in range(10): + access_token = await credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_be_called_as_necessary(self): + expired_token = generate_token_with_custom_expiry(-(10 * 60)) + refresher = MagicMock(return_value=create_access_token(expired_token)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + await credential.get_token() + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert expired_token == access_token.token + + @pytest.mark.asyncio + async def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_keeps_scheduling_again(self): + refresh_seconds = 2 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(4)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(10 * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True, + refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + async with credential: + access_token = await credential.get_token() + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + + async with CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + refresh_proactively=True) as credential: + assert credential._timer is not None + assert refresher.call_count == 0 From c0003116047011fb6a4e04ec42b04af614df3d7a Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Fri, 3 Dec 2021 09:58:50 +0100 Subject: [PATCH 18/49] lint issue fix in utils --- .../azure/communication/chat/_shared/utils.py | 3 ++- .../azure/communication/identity/_shared/utils.py | 3 ++- .../azure/communication/networktraversal/_shared/utils.py | 1 + .../azure/communication/phonenumbers/_shared/utils.py | 3 ++- .../azure/communication/sms/_shared/utils.py | 3 ++- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py index 857256206e8a..649ed3b11504 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py @@ -7,6 +7,7 @@ import base64 import json import calendar +import time from typing import ( # pylint: disable=unused-import cast, Tuple, @@ -134,4 +135,4 @@ def get_authentication_policy( def _convert_expires_on_datetime_to_utc_int(expires_on): epoch = time.mktime(datetime(1970, 1, 1).timetuple()) - return epoch-time.mktime(expires_on.timetuple()) \ No newline at end of file + return epoch-time.mktime(expires_on.timetuple()) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index 4cd51188f710..d55ee1217da8 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -7,6 +7,7 @@ import base64 import json import calendar +import time from typing import ( # pylint: disable=unused-import cast, Tuple, @@ -135,4 +136,4 @@ def get_authentication_policy( def _convert_expires_on_datetime_to_utc_int(expires_on): epoch = time.mktime(datetime(1970, 1, 1).timetuple()) - return epoch-time.mktime(expires_on.timetuple()) \ No newline at end of file + return epoch-time.mktime(expires_on.timetuple()) diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py index a47b9f221eff..6e33ddb534a9 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py @@ -7,6 +7,7 @@ import base64 import json import calendar +import time from typing import ( # pylint: disable=unused-import cast, Tuple, diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py index 2bc184d238cc..2566220f3c99 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py @@ -7,6 +7,7 @@ import base64 import json import calendar +import time from typing import ( # pylint: disable=unused-import cast, Tuple, @@ -134,4 +135,4 @@ def get_authentication_policy( def _convert_expires_on_datetime_to_utc_int(expires_on): epoch = time.mktime(datetime(1970, 1, 1).timetuple()) - return epoch-time.mktime(expires_on.timetuple()) \ No newline at end of file + return epoch-time.mktime(expires_on.timetuple()) diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py index 2bc184d238cc..2566220f3c99 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py @@ -7,6 +7,7 @@ import base64 import json import calendar +import time from typing import ( # pylint: disable=unused-import cast, Tuple, @@ -134,4 +135,4 @@ def get_authentication_policy( def _convert_expires_on_datetime_to_utc_int(expires_on): epoch = time.mktime(datetime(1970, 1, 1).timetuple()) - return epoch-time.mktime(expires_on.timetuple()) \ No newline at end of file + return epoch-time.mktime(expires_on.timetuple()) From 1f5516a22cd5b6cd5e3f30ca4792752a083b24af Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Fri, 3 Dec 2021 14:37:56 +0100 Subject: [PATCH 19/49] python 2 compatibility fix for generate_token_with_custom_expiry & fixed sync tests termination --- .../tests/_shared/helper.py | 16 +++++++++++----- .../tests/test_user_credential.py | 6 +++--- .../tests/_shared/helper.py | 18 +++++++++++++----- .../tests/test_user_credential.py | 6 +++--- .../tests/_shared/helper.py | 16 ++++++++++++---- .../tests/test_user_credential.py | 6 +++--- .../test/_shared/helper.py | 14 ++++++++++---- .../test/test_user_credential.py | 6 +++--- .../tests/_shared/helper.py | 14 ++++++++++---- .../tests/test_user_credential.py | 6 +++--- 10 files changed, 71 insertions(+), 37 deletions(-) diff --git a/sdk/communication/azure-communication-chat/tests/_shared/helper.py b/sdk/communication/azure-communication-chat/tests/_shared/helper.py index ce9aa3375174..4ffe17869a90 100644 --- a/sdk/communication/azure-communication-chat/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-chat/tests/_shared/helper.py @@ -12,12 +12,18 @@ from urllib.parse import urlparse except ImportError: from urlparse import urlparse +import sys - -def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - - +if sys.version_info[0] < 3 or sys.version_info[1] < 4: + # python version < 3.3 + import time + def generate_token_with_custom_expiry(valid_for_seconds): + date = datetime.now() + timedelta(seconds=valid_for_seconds) + return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) +else: + def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' base64expiry = base64.b64encode( diff --git a/sdk/communication/azure-communication-chat/tests/test_user_credential.py b/sdk/communication/azure-communication-chat/tests/test_user_credential.py index 63dcc26bd61d..8d5256ed8fb2 100644 --- a/sdk/communication/azure-communication-chat/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-chat/tests/test_user_credential.py @@ -67,8 +67,8 @@ def test_uses_initial_token_as_expected(self): # , timer_mock): return_value=self.expired_token) credential = CommunicationTokenCredential( self.sample_token, token_refresher=refresher, refresh_proactively=True) - - access_token = credential.get_token() + with credential: + access_token = credential.get_token() self.assertEqual(refresher.call_count, 0) self.assertEqual(access_token.token, self.sample_token) @@ -164,7 +164,7 @@ def test_exit_cancels_timer(self): token_refresher=refresher, refresh_proactively=True) as credential: assert credential._timer is not None - assert credential._timer.finished._flag == True + assert credential._timer.finished.is_set() == True def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) diff --git a/sdk/communication/azure-communication-identity/tests/_shared/helper.py b/sdk/communication/azure-communication-identity/tests/_shared/helper.py index d88d18833059..031c3071d919 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/helper.py @@ -7,12 +7,20 @@ import base64 from azure_devtools.scenario_tests import RecordingProcessor from urllib.parse import urlparse +from datetime import datetime, timedelta +from functools import wraps +import sys - -def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - - +if sys.version_info[0] < 3 or sys.version_info[1] < 4: + # python version < 3.3 + import time + def generate_token_with_custom_expiry(valid_for_seconds): + date = datetime.now() + timedelta(seconds=valid_for_seconds) + return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) +else: + def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' base64expiry = base64.b64encode( diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index 4833fa71ef05..199f3ebc1a05 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -68,8 +68,8 @@ def test_uses_initial_token_as_expected(self): # , timer_mock): return_value=self.expired_token) credential = CommunicationTokenCredential( self.sample_token, token_refresher=refresher, refresh_proactively=True) - - access_token = credential.get_token() + with credential: + access_token = credential.get_token() self.assertEqual(refresher.call_count, 0) self.assertEqual(access_token.token, self.sample_token) @@ -165,7 +165,7 @@ def test_exit_cancels_timer(self): token_refresher=refresher, refresh_proactively=True) as credential: assert credential._timer is not None - assert credential._timer.finished._flag == True + assert credential._timer.finished.is_set() == True def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) diff --git a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py index da39b5a7c2b3..895ccac1c540 100644 --- a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py @@ -7,11 +7,19 @@ import base64 from azure_devtools.scenario_tests import RecordingProcessor from urllib.parse import urlparse +from datetime import datetime, timedelta +from functools import wraps +import sys - -def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - +if sys.version_info[0] < 3 or sys.version_info[1] < 4: + # python version < 3.3 + import time + def generate_token_with_custom_expiry(valid_for_seconds): + date = datetime.now() + timedelta(seconds=valid_for_seconds) + return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) +else: + def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' diff --git a/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py b/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py index 1e47903a03ea..a11fa7663cbe 100644 --- a/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py @@ -67,8 +67,8 @@ def test_uses_initial_token_as_expected(self): # , timer_mock): return_value=self.expired_token) credential = CommunicationTokenCredential( self.sample_token, token_refresher=refresher, refresh_proactively=True) - - access_token = credential.get_token() + with credential: + access_token = credential.get_token() self.assertEqual(refresher.call_count, 0) self.assertEqual(access_token.token, self.sample_token) @@ -164,7 +164,7 @@ def test_exit_cancels_timer(self): token_refresher=refresher, refresh_proactively=True) as credential: assert credential._timer is not None - assert credential._timer.finished._flag == True + assert credential._timer.finished.is_set() == True def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) diff --git a/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py b/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py index ce9aa3375174..80649d90cb2c 100644 --- a/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py +++ b/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py @@ -12,11 +12,17 @@ from urllib.parse import urlparse except ImportError: from urlparse import urlparse +import sys - -def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - +if sys.version_info[0] < 3 or sys.version_info[1] < 4: + # python version < 3.3 + import time + def generate_token_with_custom_expiry(valid_for_seconds): + date = datetime.now() + timedelta(seconds=valid_for_seconds) + return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) +else: + def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' diff --git a/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py b/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py index 5a9e5ee93cf6..c9fc16005c97 100644 --- a/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py @@ -67,8 +67,8 @@ def test_uses_initial_token_as_expected(self): # , timer_mock): return_value=self.expired_token) credential = CommunicationTokenCredential( self.sample_token, token_refresher=refresher, refresh_proactively=True) - - access_token = credential.get_token() + with credential: + access_token = credential.get_token() self.assertEqual(refresher.call_count, 0) self.assertEqual(access_token.token, self.sample_token) @@ -164,7 +164,7 @@ def test_exit_cancels_timer(self): token_refresher=refresher, refresh_proactively=True) as credential: assert credential._timer is not None - assert credential._timer.finished._flag == True + assert credential._timer.finished.is_set() == True def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) diff --git a/sdk/communication/azure-communication-sms/tests/_shared/helper.py b/sdk/communication/azure-communication-sms/tests/_shared/helper.py index 99f357c56d50..4dda289f1260 100644 --- a/sdk/communication/azure-communication-sms/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-sms/tests/_shared/helper.py @@ -12,11 +12,17 @@ from urllib.parse import urlparse except ImportError: from urlparse import urlparse +import sys - -def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - +if sys.version_info[0] < 3 or sys.version_info[1] < 4: + # python version < 3.3 + import time + def generate_token_with_custom_expiry(valid_for_seconds): + date = datetime.now() + timedelta(seconds=valid_for_seconds) + return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) +else: + def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' diff --git a/sdk/communication/azure-communication-sms/tests/test_user_credential.py b/sdk/communication/azure-communication-sms/tests/test_user_credential.py index 8a234a3a67a8..af0c7322737f 100644 --- a/sdk/communication/azure-communication-sms/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-sms/tests/test_user_credential.py @@ -67,8 +67,8 @@ def test_uses_initial_token_as_expected(self): # , timer_mock): return_value=self.expired_token) credential = CommunicationTokenCredential( self.sample_token, token_refresher=refresher, refresh_proactively=True) - - access_token = credential.get_token() + with credential: + access_token = credential.get_token() self.assertEqual(refresher.call_count, 0) self.assertEqual(access_token.token, self.sample_token) @@ -164,7 +164,7 @@ def test_exit_cancels_timer(self): token_refresher=refresher, refresh_proactively=True) as credential: assert credential._timer is not None - assert credential._timer.finished._flag == True + assert credential._timer.finished.is_set() == True def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) From b410688a467fea74ff076a6736313533948c3a71 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 15 Dec 2021 11:40:00 +0100 Subject: [PATCH 20/49] removed unneccasary user credential tests from sms,chat, networktraversal,phonenumber --- .../tests/test_user_credential.py | 181 ---------------- .../tests/test_user_credential_async.py | 196 ------------------ .../tests/test_user_credential.py | 181 ---------------- .../tests/test_user_credential_async.py | 196 ------------------ .../test/test_user_credential.py | 181 ---------------- .../test/test_user_credential_async.py | 196 ------------------ .../tests/test_user_credential.py | 181 ---------------- .../tests/test_user_credential_async.py | 196 ------------------ 8 files changed, 1508 deletions(-) delete mode 100644 sdk/communication/azure-communication-chat/tests/test_user_credential.py delete mode 100644 sdk/communication/azure-communication-chat/tests/test_user_credential_async.py delete mode 100644 sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py delete mode 100644 sdk/communication/azure-communication-networktraversal/tests/test_user_credential_async.py delete mode 100644 sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py delete mode 100644 sdk/communication/azure-communication-phonenumbers/test/test_user_credential_async.py delete mode 100644 sdk/communication/azure-communication-sms/tests/test_user_credential.py delete mode 100644 sdk/communication/azure-communication-sms/tests/test_user_credential_async.py diff --git a/sdk/communication/azure-communication-chat/tests/test_user_credential.py b/sdk/communication/azure-communication-chat/tests/test_user_credential.py deleted file mode 100644 index 8d5256ed8fb2..000000000000 --- a/sdk/communication/azure-communication-chat/tests/test_user_credential.py +++ /dev/null @@ -1,181 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from unittest import TestCase -try: - from unittest.mock import MagicMock, patch -except ImportError: # python < 3.3 - from mock import MagicMock, patch # type: ignore -import azure.communication.chat._shared.user_credential as user_credential -from azure.communication.chat._shared.user_credential import CommunicationTokenCredential -from azure.communication.chat._shared.utils import create_access_token -from azure.communication.chat._shared.utils import get_current_utc_as_int -from datetime import timedelta -from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry - - -class TestCommunicationTokenCredential(TestCase): - - @classmethod - def setUpClass(cls): - cls.sample_token = generate_token_with_custom_expiry_epoch( - 32503680000) # 1/1/2030 - cls.expired_token = generate_token_with_custom_expiry_epoch( - 100) # 1/1/1970 - - def test_communicationtokencredential_decodes_token(self): - credential = CommunicationTokenCredential(self.sample_token) - access_token = credential.get_token() - self.assertEqual(access_token.token, self.sample_token) - - def test_communicationtokencredential_throws_if_invalid_token(self): - self.assertRaises( - ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) - - def test_communicationtokencredential_throws_if_nonstring_token(self): - self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) - - def test_communicationtokencredential_static_token_returns_expired_token(self): - credential = CommunicationTokenCredential(self.expired_token) - self.assertEqual(credential.get_token().token, self.expired_token) - - def test_communicationtokencredential_token_expired_refresh_called(self): - refresher = MagicMock(return_value=self.sample_token) - access_token = CommunicationTokenCredential( - self.expired_token, - token_refresher=refresher).get_token() - refresher.assert_called_once() - self.assertEqual(access_token, self.sample_token) - - def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): - refresher = MagicMock( - return_value=create_access_token(self.expired_token)) - credential = CommunicationTokenCredential( - self.expired_token, token_refresher=refresher) - - credential.get_token() - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 2) - self.assertEqual(access_token.token, self.expired_token) - - # @patch_threading_timer(user_credential.__name__+'.Timer') - def test_uses_initial_token_as_expected(self): # , timer_mock): - refresher = MagicMock( - return_value=self.expired_token) - credential = CommunicationTokenCredential( - self.sample_token, token_refresher=refresher, refresh_proactively=True) - with credential: - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 0) - self.assertEqual(access_token.token, self.sample_token) - - def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 - expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) - refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) - - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - with credential: - access_token = credential.get_token() - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - - with CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) as credential: - assert credential._timer is not None - assert credential._timer.finished.is_set() == True - - def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = generate_token_with_custom_expiry(15 * 60) - new_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) - with credential: - for _ in range(10): - access_token = credential.get_token() - - refresher.assert_not_called() - assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py b/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py deleted file mode 100644 index c53671dee341..000000000000 --- a/sdk/communication/azure-communication-chat/tests/test_user_credential_async.py +++ /dev/null @@ -1,196 +0,0 @@ - -# coding: utf-8 -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from datetime import timedelta -import pytest -try: - from unittest.mock import MagicMock, patch -except ImportError: # python < 3.3 - from mock import MagicMock, patch -from azure.communication.chat._shared.user_credential_async import CommunicationTokenCredential -import azure.communication.chat._shared.user_credential_async as user_credential_async -from azure.communication.chat._shared.utils import create_access_token -from azure.communication.chat._shared.utils import get_current_utc_as_int -from _shared.helper import generate_token_with_custom_expiry - - -class TestCommunicationTokenCredential: - - @pytest.mark.asyncio - async def test_raises_error_for_init_with_nonstring_token(self): - with pytest.raises(TypeError) as err: - CommunicationTokenCredential(1234) - assert str(err.value) == "Token must be a string." - - @pytest.mark.asyncio - async def test_raises_error_for_init_with_invalid_token(self): - with pytest.raises(ValueError) as err: - CommunicationTokenCredential("not a token") - assert str(err.value) == "Token is not formatted correctly" - - @pytest.mark.asyncio - async def test_init_with_valid_token(self): - initial_token = generate_token_with_custom_expiry(5 * 60) - credential = CommunicationTokenCredential(initial_token) - access_token = await credential.get_token() - assert initial_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_be_called_immediately_with_expired_token(self): - refreshed_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - expired_token = generate_token_with_custom_expiry(-(5 * 60)) - - credential = CommunicationTokenCredential( - expired_token, token_refresher=refresher) - async with credential: - access_token = await credential.get_token() - - refresher.assert_called_once() - assert refreshed_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_not_be_called_before_expiring_time(self): - initial_token = generate_token_with_custom_expiry(15 * 60) - refreshed_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - credential = CommunicationTokenCredential( - initial_token, token_refresher=refresher, refresh_proactively=True) - async with credential: - access_token = await credential.get_token() - - refresher.assert_not_called() - assert initial_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = generate_token_with_custom_expiry(15 * 60) - new_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) - async with credential: - for _ in range(10): - access_token = await credential.get_token() - - refresher.assert_not_called() - assert generated_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_be_called_as_necessary(self): - expired_token = generate_token_with_custom_expiry(-(10 * 60)) - refresher = MagicMock(return_value=create_access_token(expired_token)) - - credential = CommunicationTokenCredential( - expired_token, token_refresher=refresher) - async with credential: - await credential.get_token() - access_token = await credential.get_token() - - assert refresher.call_count == 2 - assert expired_token == access_token.token - - @pytest.mark.asyncio - async def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - async with credential: - access_token = await credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - async with credential: - access_token = await credential.get_token() - - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 - expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) - refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) - - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - async with credential: - access_token = await credential.get_token() - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = await credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - - async with CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) as credential: - assert credential._timer is not None - assert refresher.call_count == 0 diff --git a/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py b/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py deleted file mode 100644 index a11fa7663cbe..000000000000 --- a/sdk/communication/azure-communication-networktraversal/tests/test_user_credential.py +++ /dev/null @@ -1,181 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from unittest import TestCase -try: - from unittest.mock import MagicMock, patch -except ImportError: # python < 3.3 - from mock import MagicMock, patch # type: ignore -import azure.communication.identity._shared.user_credential as user_credential -from azure.communication.identity._shared.user_credential import CommunicationTokenCredential -from azure.communication.identity._shared.utils import create_access_token -from azure.communication.identity._shared.utils import get_current_utc_as_int -from datetime import timedelta -from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry - - -class TestCommunicationTokenCredential(TestCase): - - @classmethod - def setUpClass(cls): - cls.sample_token = generate_token_with_custom_expiry_epoch( - 32503680000) # 1/1/2030 - cls.expired_token = generate_token_with_custom_expiry_epoch( - 100) # 1/1/1970 - - def test_communicationtokencredential_decodes_token(self): - credential = CommunicationTokenCredential(self.sample_token) - access_token = credential.get_token() - self.assertEqual(access_token.token, self.sample_token) - - def test_communicationtokencredential_throws_if_invalid_token(self): - self.assertRaises( - ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) - - def test_communicationtokencredential_throws_if_nonstring_token(self): - self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) - - def test_communicationtokencredential_static_token_returns_expired_token(self): - credential = CommunicationTokenCredential(self.expired_token) - self.assertEqual(credential.get_token().token, self.expired_token) - - def test_communicationtokencredential_token_expired_refresh_called(self): - refresher = MagicMock(return_value=self.sample_token) - access_token = CommunicationTokenCredential( - self.expired_token, - token_refresher=refresher).get_token() - refresher.assert_called_once() - self.assertEqual(access_token, self.sample_token) - - def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): - refresher = MagicMock( - return_value=create_access_token(self.expired_token)) - credential = CommunicationTokenCredential( - self.expired_token, token_refresher=refresher) - - credential.get_token() - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 2) - self.assertEqual(access_token.token, self.expired_token) - - # @patch_threading_timer(user_credential.__name__+'.Timer') - def test_uses_initial_token_as_expected(self): # , timer_mock): - refresher = MagicMock( - return_value=self.expired_token) - credential = CommunicationTokenCredential( - self.sample_token, token_refresher=refresher, refresh_proactively=True) - with credential: - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 0) - self.assertEqual(access_token.token, self.sample_token) - - def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 - expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) - refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) - - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - with credential: - access_token = credential.get_token() - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - - with CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) as credential: - assert credential._timer is not None - assert credential._timer.finished.is_set() == True - - def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = generate_token_with_custom_expiry(15 * 60) - new_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) - with credential: - for _ in range(10): - access_token = credential.get_token() - - refresher.assert_not_called() - assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-networktraversal/tests/test_user_credential_async.py b/sdk/communication/azure-communication-networktraversal/tests/test_user_credential_async.py deleted file mode 100644 index ed20b2779b8d..000000000000 --- a/sdk/communication/azure-communication-networktraversal/tests/test_user_credential_async.py +++ /dev/null @@ -1,196 +0,0 @@ - -# coding: utf-8 -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from datetime import timedelta -import pytest -try: - from unittest.mock import MagicMock, patch -except ImportError: # python < 3.3 - from mock import MagicMock, patch -from azure.communication.identity._shared.user_credential_async import CommunicationTokenCredential -import azure.communication.identity._shared.user_credential_async as user_credential_async -from azure.communication.identity._shared.utils import create_access_token -from azure.communication.identity._shared.utils import get_current_utc_as_int -from _shared.helper import generate_token_with_custom_expiry - - -class TestCommunicationTokenCredential: - - @pytest.mark.asyncio - async def test_raises_error_for_init_with_nonstring_token(self): - with pytest.raises(TypeError) as err: - CommunicationTokenCredential(1234) - assert str(err.value) == "Token must be a string." - - @pytest.mark.asyncio - async def test_raises_error_for_init_with_invalid_token(self): - with pytest.raises(ValueError) as err: - CommunicationTokenCredential("not a token") - assert str(err.value) == "Token is not formatted correctly" - - @pytest.mark.asyncio - async def test_init_with_valid_token(self): - initial_token = generate_token_with_custom_expiry(5 * 60) - credential = CommunicationTokenCredential(initial_token) - access_token = await credential.get_token() - assert initial_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_be_called_immediately_with_expired_token(self): - refreshed_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - expired_token = generate_token_with_custom_expiry(-(5 * 60)) - - credential = CommunicationTokenCredential( - expired_token, token_refresher=refresher) - async with credential: - access_token = await credential.get_token() - - refresher.assert_called_once() - assert refreshed_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_not_be_called_before_expiring_time(self): - initial_token = generate_token_with_custom_expiry(15 * 60) - refreshed_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - credential = CommunicationTokenCredential( - initial_token, token_refresher=refresher, refresh_proactively=True) - async with credential: - access_token = await credential.get_token() - - refresher.assert_not_called() - assert initial_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = generate_token_with_custom_expiry(15 * 60) - new_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) - async with credential: - for _ in range(10): - access_token = await credential.get_token() - - refresher.assert_not_called() - assert generated_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_be_called_as_necessary(self): - expired_token = generate_token_with_custom_expiry(-(10 * 60)) - refresher = MagicMock(return_value=create_access_token(expired_token)) - - credential = CommunicationTokenCredential( - expired_token, token_refresher=refresher) - async with credential: - await credential.get_token() - access_token = await credential.get_token() - - assert refresher.call_count == 2 - assert expired_token == access_token.token - - @pytest.mark.asyncio - async def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - async with credential: - access_token = await credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - async with credential: - access_token = await credential.get_token() - - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 - expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) - refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) - - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - async with credential: - access_token = await credential.get_token() - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = await credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - - async with CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) as credential: - assert credential._timer is not None - assert refresher.call_count == 0 diff --git a/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py b/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py deleted file mode 100644 index c9fc16005c97..000000000000 --- a/sdk/communication/azure-communication-phonenumbers/test/test_user_credential.py +++ /dev/null @@ -1,181 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from unittest import TestCase -try: - from unittest.mock import MagicMock, patch -except ImportError: # python < 3.3 - from mock import MagicMock, patch # type: ignore -import azure.communication.phonenumbers._shared.user_credential as user_credential -from azure.communication.phonenumbers._shared.user_credential import CommunicationTokenCredential -from azure.communication.phonenumbers._shared.utils import create_access_token -from azure.communication.phonenumbers._shared.utils import get_current_utc_as_int -from datetime import timedelta -from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry - - -class TestCommunicationTokenCredential(TestCase): - - @classmethod - def setUpClass(cls): - cls.sample_token = generate_token_with_custom_expiry_epoch( - 32503680000) # 1/1/2030 - cls.expired_token = generate_token_with_custom_expiry_epoch( - 100) # 1/1/1970 - - def test_communicationtokencredential_decodes_token(self): - credential = CommunicationTokenCredential(self.sample_token) - access_token = credential.get_token() - self.assertEqual(access_token.token, self.sample_token) - - def test_communicationtokencredential_throws_if_invalid_token(self): - self.assertRaises( - ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) - - def test_communicationtokencredential_throws_if_nonstring_token(self): - self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) - - def test_communicationtokencredential_static_token_returns_expired_token(self): - credential = CommunicationTokenCredential(self.expired_token) - self.assertEqual(credential.get_token().token, self.expired_token) - - def test_communicationtokencredential_token_expired_refresh_called(self): - refresher = MagicMock(return_value=self.sample_token) - access_token = CommunicationTokenCredential( - self.expired_token, - token_refresher=refresher).get_token() - refresher.assert_called_once() - self.assertEqual(access_token, self.sample_token) - - def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): - refresher = MagicMock( - return_value=create_access_token(self.expired_token)) - credential = CommunicationTokenCredential( - self.expired_token, token_refresher=refresher) - - credential.get_token() - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 2) - self.assertEqual(access_token.token, self.expired_token) - - # @patch_threading_timer(user_credential.__name__+'.Timer') - def test_uses_initial_token_as_expected(self): # , timer_mock): - refresher = MagicMock( - return_value=self.expired_token) - credential = CommunicationTokenCredential( - self.sample_token, token_refresher=refresher, refresh_proactively=True) - with credential: - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 0) - self.assertEqual(access_token.token, self.sample_token) - - def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 - expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) - refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) - - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - with credential: - access_token = credential.get_token() - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - - with CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) as credential: - assert credential._timer is not None - assert credential._timer.finished.is_set() == True - - def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = generate_token_with_custom_expiry(15 * 60) - new_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) - with credential: - for _ in range(10): - access_token = credential.get_token() - - refresher.assert_not_called() - assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-phonenumbers/test/test_user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/test/test_user_credential_async.py deleted file mode 100644 index 0994de6f7248..000000000000 --- a/sdk/communication/azure-communication-phonenumbers/test/test_user_credential_async.py +++ /dev/null @@ -1,196 +0,0 @@ - -# coding: utf-8 -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from datetime import timedelta -import pytest -try: - from unittest.mock import MagicMock, patch -except ImportError: # python < 3.3 - from mock import MagicMock, patch -from azure.communication.phonenumbers._shared.user_credential_async import CommunicationTokenCredential -import azure.communication.phonenumbers._shared.user_credential_async as user_credential_async -from azure.communication.phonenumbers._shared.utils import create_access_token -from azure.communication.phonenumbers._shared.utils import get_current_utc_as_int -from _shared.helper import generate_token_with_custom_expiry - - -class TestCommunicationTokenCredential: - - @pytest.mark.asyncio - async def test_raises_error_for_init_with_nonstring_token(self): - with pytest.raises(TypeError) as err: - CommunicationTokenCredential(1234) - assert str(err.value) == "Token must be a string." - - @pytest.mark.asyncio - async def test_raises_error_for_init_with_invalid_token(self): - with pytest.raises(ValueError) as err: - CommunicationTokenCredential("not a token") - assert str(err.value) == "Token is not formatted correctly" - - @pytest.mark.asyncio - async def test_init_with_valid_token(self): - initial_token = generate_token_with_custom_expiry(5 * 60) - credential = CommunicationTokenCredential(initial_token) - access_token = await credential.get_token() - assert initial_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_be_called_immediately_with_expired_token(self): - refreshed_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - expired_token = generate_token_with_custom_expiry(-(5 * 60)) - - credential = CommunicationTokenCredential( - expired_token, token_refresher=refresher) - async with credential: - access_token = await credential.get_token() - - refresher.assert_called_once() - assert refreshed_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_not_be_called_before_expiring_time(self): - initial_token = generate_token_with_custom_expiry(15 * 60) - refreshed_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - credential = CommunicationTokenCredential( - initial_token, token_refresher=refresher, refresh_proactively=True) - async with credential: - access_token = await credential.get_token() - - refresher.assert_not_called() - assert initial_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = generate_token_with_custom_expiry(15 * 60) - new_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) - async with credential: - for _ in range(10): - access_token = await credential.get_token() - - refresher.assert_not_called() - assert generated_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_be_called_as_necessary(self): - expired_token = generate_token_with_custom_expiry(-(10 * 60)) - refresher = MagicMock(return_value=create_access_token(expired_token)) - - credential = CommunicationTokenCredential( - expired_token, token_refresher=refresher) - async with credential: - await credential.get_token() - access_token = await credential.get_token() - - assert refresher.call_count == 2 - assert expired_token == access_token.token - - @pytest.mark.asyncio - async def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - async with credential: - access_token = await credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - async with credential: - access_token = await credential.get_token() - - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 - expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) - refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) - - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - async with credential: - access_token = await credential.get_token() - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = await credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - - async with CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) as credential: - assert credential._timer is not None - assert refresher.call_count == 0 diff --git a/sdk/communication/azure-communication-sms/tests/test_user_credential.py b/sdk/communication/azure-communication-sms/tests/test_user_credential.py deleted file mode 100644 index af0c7322737f..000000000000 --- a/sdk/communication/azure-communication-sms/tests/test_user_credential.py +++ /dev/null @@ -1,181 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from unittest import TestCase -try: - from unittest.mock import MagicMock, patch -except ImportError: # python < 3.3 - from mock import MagicMock, patch # type: ignore -import azure.communication.sms._shared.user_credential as user_credential -from azure.communication.sms._shared.user_credential import CommunicationTokenCredential -from azure.communication.sms._shared.utils import create_access_token -from azure.communication.sms._shared.utils import get_current_utc_as_int -from datetime import timedelta -from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry - - -class TestCommunicationTokenCredential(TestCase): - - @classmethod - def setUpClass(cls): - cls.sample_token = generate_token_with_custom_expiry_epoch( - 32503680000) # 1/1/2030 - cls.expired_token = generate_token_with_custom_expiry_epoch( - 100) # 1/1/1970 - - def test_communicationtokencredential_decodes_token(self): - credential = CommunicationTokenCredential(self.sample_token) - access_token = credential.get_token() - self.assertEqual(access_token.token, self.sample_token) - - def test_communicationtokencredential_throws_if_invalid_token(self): - self.assertRaises( - ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) - - def test_communicationtokencredential_throws_if_nonstring_token(self): - self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) - - def test_communicationtokencredential_static_token_returns_expired_token(self): - credential = CommunicationTokenCredential(self.expired_token) - self.assertEqual(credential.get_token().token, self.expired_token) - - def test_communicationtokencredential_token_expired_refresh_called(self): - refresher = MagicMock(return_value=self.sample_token) - access_token = CommunicationTokenCredential( - self.expired_token, - token_refresher=refresher).get_token() - refresher.assert_called_once() - self.assertEqual(access_token, self.sample_token) - - def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): - refresher = MagicMock( - return_value=create_access_token(self.expired_token)) - credential = CommunicationTokenCredential( - self.expired_token, token_refresher=refresher) - - credential.get_token() - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 2) - self.assertEqual(access_token.token, self.expired_token) - - # @patch_threading_timer(user_credential.__name__+'.Timer') - def test_uses_initial_token_as_expected(self): # , timer_mock): - refresher = MagicMock( - return_value=self.expired_token) - credential = CommunicationTokenCredential( - self.sample_token, token_refresher=refresher, refresh_proactively=True) - with credential: - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 0) - self.assertEqual(access_token.token, self.sample_token) - - def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 - expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) - refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) - - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - with credential: - access_token = credential.get_token() - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - - def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - - with CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) as credential: - assert credential._timer is not None - assert credential._timer.finished.is_set() == True - - def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = generate_token_with_custom_expiry(15 * 60) - new_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) - with credential: - for _ in range(10): - access_token = credential.get_token() - - refresher.assert_not_called() - assert generated_token == access_token.token diff --git a/sdk/communication/azure-communication-sms/tests/test_user_credential_async.py b/sdk/communication/azure-communication-sms/tests/test_user_credential_async.py deleted file mode 100644 index 5d547266e2eb..000000000000 --- a/sdk/communication/azure-communication-sms/tests/test_user_credential_async.py +++ /dev/null @@ -1,196 +0,0 @@ - -# coding: utf-8 -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from datetime import timedelta -import pytest -try: - from unittest.mock import MagicMock, patch -except ImportError: # python < 3.3 - from mock import MagicMock, patch -from azure.communication.sms._shared.user_credential_async import CommunicationTokenCredential -import azure.communication.sms._shared.user_credential_async as user_credential_async -from azure.communication.sms._shared.utils import create_access_token -from azure.communication.sms._shared.utils import get_current_utc_as_int -from _shared.helper import generate_token_with_custom_expiry - - -class TestCommunicationTokenCredential: - - @pytest.mark.asyncio - async def test_raises_error_for_init_with_nonstring_token(self): - with pytest.raises(TypeError) as err: - CommunicationTokenCredential(1234) - assert str(err.value) == "Token must be a string." - - @pytest.mark.asyncio - async def test_raises_error_for_init_with_invalid_token(self): - with pytest.raises(ValueError) as err: - CommunicationTokenCredential("not a token") - assert str(err.value) == "Token is not formatted correctly" - - @pytest.mark.asyncio - async def test_init_with_valid_token(self): - initial_token = generate_token_with_custom_expiry(5 * 60) - credential = CommunicationTokenCredential(initial_token) - access_token = await credential.get_token() - assert initial_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_be_called_immediately_with_expired_token(self): - refreshed_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - expired_token = generate_token_with_custom_expiry(-(5 * 60)) - - credential = CommunicationTokenCredential( - expired_token, token_refresher=refresher) - async with credential: - access_token = await credential.get_token() - - refresher.assert_called_once() - assert refreshed_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_not_be_called_before_expiring_time(self): - initial_token = generate_token_with_custom_expiry(15 * 60) - refreshed_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - credential = CommunicationTokenCredential( - initial_token, token_refresher=refresher, refresh_proactively=True) - async with credential: - access_token = await credential.get_token() - - refresher.assert_not_called() - assert initial_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_not_be_called_when_token_still_valid(self): - generated_token = generate_token_with_custom_expiry(15 * 60) - new_token = generate_token_with_custom_expiry(10 * 60) - refresher = MagicMock(return_value=create_access_token(new_token)) - - credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) - async with credential: - for _ in range(10): - access_token = await credential.get_token() - - refresher.assert_not_called() - assert generated_token == access_token.token - - @pytest.mark.asyncio - async def test_refresher_should_be_called_as_necessary(self): - expired_token = generate_token_with_custom_expiry(-(10 * 60)) - refresher = MagicMock(return_value=create_access_token(expired_token)) - - credential = CommunicationTokenCredential( - expired_token, token_refresher=refresher) - async with credential: - await credential.get_token() - access_token = await credential.get_token() - - assert refresher.call_count == 2 - assert expired_token == access_token.token - - @pytest.mark.asyncio - async def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - async with credential: - access_token = await credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 - token_validity_minutes = 60 - start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 - - initial_token = generate_token_with_custom_expiry( - token_validity_minutes * 60) - refreshed_token = generate_token_with_custom_expiry( - 2 * token_validity_minutes * 60) - refresher = MagicMock( - return_value=create_access_token(refreshed_token)) - - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - credential = CommunicationTokenCredential( - initial_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) - async with credential: - access_token = await credential.get_token() - - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 - expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 - first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) - last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) - refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) - - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) - async with credential: - access_token = await credential.get_token() - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = await credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - - @pytest.mark.asyncio - async def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - - async with CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) as credential: - assert credential._timer is not None - assert refresher.call_count == 0 From a283f244d06b0bb68d77073d48a569116078b0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Wed, 15 Dec 2021 14:13:39 +0100 Subject: [PATCH 21/49] reduced the default refresh interval (api review) --- .../azure/communication/chat/_shared/user_credential.py | 2 +- .../azure/communication/chat/_shared/user_credential_async.py | 2 +- .../azure/communication/identity/_shared/user_credential.py | 2 +- .../communication/identity/_shared/user_credential_async.py | 2 +- .../communication/networktraversal/_shared/user_credential.py | 2 +- .../networktraversal/_shared/user_credential_async.py | 2 +- .../azure/communication/phonenumbers/_shared/user_credential.py | 2 +- .../communication/phonenumbers/_shared/user_credential_async.py | 2 +- .../azure/communication/sms/_shared/user_credential.py | 2 +- .../azure/communication/sms/_shared/user_credential_async.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index e537fe599fe3..f7c602999cee 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -27,7 +27,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token, # type: str diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 26c50287fcb8..b2bd778d5a94 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -28,7 +28,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index eb515a39b2ca..a7856268a25f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -21,7 +21,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token, # type: str diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index e2ea5a6fa170..5e8157e25e09 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -33,7 +33,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index d70896d19afb..c6b879668086 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -24,7 +24,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token, # type: str diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index b6f4ee1be23c..68ab8211d9c6 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -22,7 +22,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index e537fe599fe3..f7c602999cee 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -27,7 +27,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token, # type: str diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index 26c50287fcb8..b2bd778d5a94 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -28,7 +28,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index e537fe599fe3..f7c602999cee 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -27,7 +27,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token, # type: str diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index 26c50287fcb8..b2bd778d5a94 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -28,7 +28,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): From 1a9eea3172682682d36e9c10840775aba5c14cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Wed, 15 Dec 2021 14:47:37 +0100 Subject: [PATCH 22/49] time renamed to interval (api review) --- .../communication/chat/_shared/user_credential.py | 8 ++++---- .../chat/_shared/user_credential_async.py | 8 ++++---- .../identity/_shared/user_credential.py | 8 ++++---- .../identity/_shared/user_credential_async.py | 15 --------------- .../tests/test_user_credential.py | 6 +++--- .../tests/test_user_credential_async.py | 6 +++--- .../networktraversal/_shared/user_credential.py | 8 ++++---- .../_shared/user_credential_async.py | 9 +++++++-- .../phonenumbers/_shared/user_credential.py | 8 ++++---- .../phonenumbers/_shared/user_credential_async.py | 8 ++++---- .../communication/sms/_shared/user_credential.py | 8 ++++---- .../sms/_shared/user_credential_async.py | 8 ++++---- 12 files changed, 45 insertions(+), 55 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index f7c602999cee..9d515772209b 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -22,7 +22,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ @@ -38,7 +38,7 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) @@ -101,7 +101,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -111,7 +111,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_time_before_expiry + interval = self._refresh_interval_before_expiry else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index b2bd778d5a94..4b16437da83c 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -23,7 +23,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ @@ -36,7 +36,7 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._async_mutex = Lock() @@ -100,7 +100,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -110,7 +110,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_time_before_expiry + interval = self._refresh_interval_before_expiry else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index a7856268a25f..2ba84fe14b39 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -16,7 +16,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ @@ -32,7 +32,7 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) @@ -95,7 +95,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -105,7 +105,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_time_before_expiry + interval = self._refresh_interval_before_expiry else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 5e8157e25e09..6aad6be1762f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -6,16 +6,7 @@ from asyncio import Condition, Lock from datetime import timedelta -<<<<<<< HEAD from typing import Any -======= -import sys -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) ->>>>>>> 594cd1d95c (lock issue for python 3.10 is fixed) import six from .utils import get_current_utc_as_int from .utils import create_access_token @@ -43,12 +34,6 @@ def __init__(self, token: str, **kwargs: Any): self._refresh_proactively = kwargs.pop('refresh_proactively', False) self._timer = None self._async_mutex = Lock() -<<<<<<< HEAD -======= - if sys.version_info[:3] == (3, 10, 0): - # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): - getattr(self._async_mutex, '_get_loop', lambda: None)() ->>>>>>> 594cd1d95c (lock issue for python 3.10 is fixed) self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False if self._refresh_proactively: diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index 199f3ebc1a05..b8b1884ca54d 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -92,7 +92,7 @@ def test_proactive_refresher_should_not_be_called_before_specified_time(self): initial_token, token_refresher=refresher, refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + refresh_interval_before_expiry=timedelta(minutes=refresh_minutes)) with credential: access_token = credential.get_token() @@ -119,7 +119,7 @@ def test_proactive_refresher_should_be_called_after_specified_time(self): initial_token, token_refresher=refresher, refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + refresh_interval_before_expiry=timedelta(minutes=refresh_minutes)) with credential: access_token = credential.get_token() @@ -143,7 +143,7 @@ def test_proactive_refresher_keeps_scheduling_again(self): expired_token, token_refresher=refresher, refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + refresh_interval_before_expiry=timedelta(seconds=refresh_seconds)) with credential: access_token = credential.get_token() with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index ed20b2779b8d..02aae7807dd6 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -117,7 +117,7 @@ async def test_proactive_refresher_should_not_be_called_before_specified_time(se initial_token, token_refresher=refresher, refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + refresh_interval_before_expiry=timedelta(minutes=refresh_minutes)) async with credential: access_token = await credential.get_token() @@ -145,7 +145,7 @@ async def test_proactive_refresher_should_be_called_after_specified_time(self): initial_token, token_refresher=refresher, refresh_proactively=True, - refresh_time_before_expiry=timedelta(minutes=refresh_minutes)) + refresh_interval_before_expiry=timedelta(minutes=refresh_minutes)) async with credential: access_token = await credential.get_token() @@ -170,7 +170,7 @@ async def test_proactive_refresher_keeps_scheduling_again(self): expired_token, token_refresher=refresher, refresh_proactively=True, - refresh_time_before_expiry=timedelta(seconds=refresh_seconds)) + refresh_interval_before_expiry=timedelta(seconds=refresh_seconds)) async with credential: access_token = await credential.get_token() with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index c6b879668086..5663b9c2f651 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -19,7 +19,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ @@ -35,7 +35,7 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) @@ -98,7 +98,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -108,7 +108,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_time_before_expiry + interval = self._refresh_interval_before_expiry else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index 68ab8211d9c6..bd20e6e431b3 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -30,7 +30,7 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._async_mutex = Lock() @@ -91,7 +91,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -100,6 +100,11 @@ async def _wait_till_inprogress_thread_finish_refreshing(self): await self._lock.acquire() def _token_expiring(self): + if self._refresh_proactively: + interval = self._refresh_interval_before_expiry + else: + interval = timedelta( + minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index f7c602999cee..9d515772209b 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -22,7 +22,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ @@ -38,7 +38,7 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) @@ -101,7 +101,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -111,7 +111,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_time_before_expiry + interval = self._refresh_interval_before_expiry else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index b2bd778d5a94..4b16437da83c 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -23,7 +23,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ @@ -36,7 +36,7 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._async_mutex = Lock() @@ -100,7 +100,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -110,7 +110,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_time_before_expiry + interval = self._refresh_interval_before_expiry else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index f7c602999cee..9d515772209b 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -22,7 +22,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ @@ -38,7 +38,7 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) @@ -101,7 +101,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -111,7 +111,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_time_before_expiry + interval = self._refresh_interval_before_expiry else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index b2bd778d5a94..4b16437da83c 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -23,7 +23,7 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_time_before_expiry: The time before the token expires to refresh the token + :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ @@ -36,7 +36,7 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_time_before_expiry = kwargs.pop('refresh_time_before_expiry', timedelta( + self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._async_mutex = Lock() @@ -100,7 +100,7 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_time_before_expiry.total_seconds() + get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -110,7 +110,7 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_time_before_expiry + interval = self._refresh_interval_before_expiry else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) From 70df098a7663c50b6d66a54841f35638f59892b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Mon, 17 Jan 2022 15:13:16 +0100 Subject: [PATCH 23/49] removed config for refresh time interval --- .../identity/_shared/user_credential.py | 11 ++++---- .../identity/_shared/user_credential_async.py | 2 +- .../communication/identity/_shared/utils.py | 10 +++---- .../tests/_shared/asynctestcase.py | 7 +++++ .../tests/_shared/helper.py | 18 ++++++++----- .../tests/asynctestcase.py | 1 + .../tests/test_user_credential.py | 26 +++++++++--------- .../tests/test_user_credential_async.py | 27 +++++++++---------- 8 files changed, 56 insertions(+), 46 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 2ba84fe14b39..a0b5b12380fb 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -16,12 +16,11 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str @@ -32,8 +31,6 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False @@ -95,7 +92,8 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -105,7 +103,8 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 6aad6be1762f..88b6dd2c183c 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -24,7 +24,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index d55ee1217da8..ee859291166e 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -7,11 +7,9 @@ import base64 import json import calendar -import time -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) +from typing import (cast, + Tuple, + ) from datetime import datetime from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken @@ -62,7 +60,6 @@ def get_current_utc_time(): return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" - def get_current_utc_as_int(): # type: () -> int current_utc_datetime = datetime.now(tz=TZ_UTC) @@ -98,6 +95,7 @@ def create_access_token(token): except ValueError: raise ValueError(token_parse_err_msg) + def get_authentication_policy( endpoint, # type: str credential, # type: TokenCredential or str diff --git a/sdk/communication/azure-communication-identity/tests/_shared/asynctestcase.py b/sdk/communication/azure-communication-identity/tests/_shared/asynctestcase.py index 197c48e0079b..94b2c1fe0d08 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/asynctestcase.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/asynctestcase.py @@ -10,6 +10,7 @@ from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from .testcase import CommunicationTestCase + class AsyncCommunicationTestCase(CommunicationTestCase): @staticmethod @@ -25,3 +26,9 @@ def run(test_class_instance, *args, **kwargs): return loop.run_until_complete(test_fn(test_class_instance, **kwargs)) return run + + +def get_completed_future(result=None): + future = asyncio.Future() + future.set_result(result) + return future diff --git a/sdk/communication/azure-communication-identity/tests/_shared/helper.py b/sdk/communication/azure-communication-identity/tests/_shared/helper.py index 031c3071d919..e13ee3ecb37b 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/helper.py @@ -14,13 +14,15 @@ if sys.version_info[0] < 3 or sys.version_info[1] < 4: # python version < 3.3 import time + def generate_token_with_custom_expiry(valid_for_seconds): date = datetime.now() + timedelta(seconds=valid_for_seconds) return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) else: def generate_token_with_custom_expiry(valid_for_seconds): return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - + + def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' base64expiry = base64.b64encode( @@ -32,15 +34,19 @@ def generate_token_with_custom_expiry_epoch(expires_on_epoch): class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" + def process_request(self, request): resource = (urlparse(request.uri).netloc).split('.')[0] - request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', request.uri) request.uri = re.sub(resource, 'sanitized', request.uri) - request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', request.uri) request.uri = re.sub(resource, 'sanitized', request.uri) return request - + def process_response(self, response): if 'url' in response: - response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) - return response \ No newline at end of file + response['url'] = re.sub( + '/identities/([^/?]+)', '/identities/sanitized', response['url']) + return response diff --git a/sdk/communication/azure-communication-identity/tests/asynctestcase.py b/sdk/communication/azure-communication-identity/tests/asynctestcase.py index 31d2c2ab6c75..aa08cac55e12 100644 --- a/sdk/communication/azure-communication-identity/tests/asynctestcase.py +++ b/sdk/communication/azure-communication-identity/tests/asynctestcase.py @@ -10,6 +10,7 @@ from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from testcase import CommunicationIdentityTestCase + class AsyncCommunicationIdentityTestCase(CommunicationIdentityTestCase): @staticmethod diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index b8b1884ca54d..dc9d94e3e519 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -75,7 +75,7 @@ def test_uses_initial_token_as_expected(self): # , timer_mock): self.assertEqual(access_token.token, self.sample_token) def test_proactive_refresher_should_not_be_called_before_specified_time(self): - refresh_minutes = 30 + refresh_minutes = 10 token_validity_minutes = 60 start_timestamp = get_current_utc_as_int() skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 @@ -91,8 +91,7 @@ def test_proactive_refresher_should_not_be_called_before_specified_time(self): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, - refresh_proactively=True, - refresh_interval_before_expiry=timedelta(minutes=refresh_minutes)) + refresh_proactively=True) with credential: access_token = credential.get_token() @@ -102,10 +101,11 @@ def test_proactive_refresher_should_not_be_called_before_specified_time(self): assert credential._timer is not None def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 + refresh_minutes = 10 token_validity_minutes = 60 start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + skip_to_timestamp = start_timestamp + \ + (token_validity_minutes - refresh_minutes + 5) * 60 initial_token = generate_token_with_custom_expiry( token_validity_minutes * 60) @@ -118,8 +118,7 @@ def test_proactive_refresher_should_be_called_after_specified_time(self): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, - refresh_proactively=True, - refresh_interval_before_expiry=timedelta(minutes=refresh_minutes)) + refresh_proactively=True) with credential: access_token = credential.get_token() @@ -129,21 +128,22 @@ def test_proactive_refresher_should_be_called_after_specified_time(self): assert credential._timer is not None def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 + refresh_minutes = 10 + token_validity_minutes = 60 expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + skip_to_timestamp = get_current_utc_as_int() + (token_validity_minutes - + refresh_minutes) * 60 + 1 first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) + generate_token_with_custom_expiry(token_validity_minutes * 60)) last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) + generate_token_with_custom_expiry(2 * token_validity_minutes * 60)) refresher = MagicMock( side_effect=[first_refreshed_token, last_refreshed_token]) credential = CommunicationTokenCredential( expired_token, token_refresher=refresher, - refresh_proactively=True, - refresh_interval_before_expiry=timedelta(seconds=refresh_seconds)) + refresh_proactively=True) with credential: access_token = credential.get_token() with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 02aae7807dd6..b1f377b5eade 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -5,7 +5,6 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from datetime import timedelta import pytest try: from unittest.mock import MagicMock, patch @@ -16,6 +15,7 @@ from azure.communication.identity._shared.utils import create_access_token from azure.communication.identity._shared.utils import get_current_utc_as_int from _shared.helper import generate_token_with_custom_expiry +from _shared.asynctestcase import get_completed_future class TestCommunicationTokenCredential: @@ -43,7 +43,7 @@ async def test_init_with_valid_token(self): async def test_refresher_should_be_called_immediately_with_expired_token(self): refreshed_token = generate_token_with_custom_expiry(10 * 60) refresher = MagicMock( - return_value=create_access_token(refreshed_token)) + return_value=get_completed_future(create_access_token(refreshed_token))) expired_token = generate_token_with_custom_expiry(-(5 * 60)) credential = CommunicationTokenCredential( @@ -87,7 +87,8 @@ async def test_refresher_should_not_be_called_when_token_still_valid(self): @pytest.mark.asyncio async def test_refresher_should_be_called_as_necessary(self): expired_token = generate_token_with_custom_expiry(-(10 * 60)) - refresher = MagicMock(return_value=create_access_token(expired_token)) + refresher = MagicMock(return_value=get_completed_future( + create_access_token(expired_token))) credential = CommunicationTokenCredential( expired_token, token_refresher=refresher) @@ -116,8 +117,7 @@ async def test_proactive_refresher_should_not_be_called_before_specified_time(se credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, - refresh_proactively=True, - refresh_interval_before_expiry=timedelta(minutes=refresh_minutes)) + refresh_proactively=True) async with credential: access_token = await credential.get_token() @@ -128,24 +128,24 @@ async def test_proactive_refresher_should_not_be_called_before_specified_time(se @pytest.mark.asyncio async def test_proactive_refresher_should_be_called_after_specified_time(self): - refresh_minutes = 30 + refresh_minutes = 10 token_validity_minutes = 60 start_timestamp = get_current_utc_as_int() - skip_to_timestamp = start_timestamp + (refresh_minutes + 5) * 60 + skip_to_timestamp = start_timestamp + \ + (token_validity_minutes - refresh_minutes + 5) * 60 initial_token = generate_token_with_custom_expiry( token_validity_minutes * 60) refreshed_token = generate_token_with_custom_expiry( 2 * token_validity_minutes * 60) refresher = MagicMock( - return_value=create_access_token(refreshed_token)) + return_value=get_completed_future(create_access_token(refreshed_token))) with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, - refresh_proactively=True, - refresh_interval_before_expiry=timedelta(minutes=refresh_minutes)) + refresh_proactively=True) async with credential: access_token = await credential.get_token() @@ -156,7 +156,7 @@ async def test_proactive_refresher_should_be_called_after_specified_time(self): @pytest.mark.asyncio async def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 2 + refresh_seconds = 10 * 60 expired_token = generate_token_with_custom_expiry(-5 * 60) skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 first_refreshed_token = create_access_token( @@ -164,13 +164,12 @@ async def test_proactive_refresher_keeps_scheduling_again(self): last_refreshed_token = create_access_token( generate_token_with_custom_expiry(10 * 60)) refresher = MagicMock( - side_effect=[first_refreshed_token, last_refreshed_token]) + side_effect=[get_completed_future(first_refreshed_token), get_completed_future(last_refreshed_token)]) credential = CommunicationTokenCredential( expired_token, token_refresher=refresher, - refresh_proactively=True, - refresh_interval_before_expiry=timedelta(seconds=refresh_seconds)) + refresh_proactively=True) async with credential: access_token = await credential.get_token() with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): From f1be5254be7d0f59693e4e28da0955206190d951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Mon, 17 Jan 2022 15:39:38 +0100 Subject: [PATCH 24/49] sync changes across modalities --- .../chat/_shared/user_credential.py | 24 ++++----- .../chat/_shared/user_credential_async.py | 52 +++++++++---------- .../azure/communication/chat/_shared/utils.py | 15 ++---- .../_shared/user_credential.py | 15 +++--- .../_shared/user_credential_async.py | 21 ++++---- .../networktraversal/_shared/utils.py | 10 ++-- .../phonenumbers/_shared/user_credential.py | 24 ++++----- .../_shared/user_credential_async.py | 52 +++++++++---------- .../phonenumbers/_shared/utils.py | 13 ++--- .../sms/_shared/user_credential.py | 24 ++++----- .../sms/_shared/user_credential_async.py | 52 +++++++++---------- .../azure/communication/sms/_shared/utils.py | 13 ++--- 12 files changed, 138 insertions(+), 177 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 9d515772209b..15b1e437c561 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -3,16 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from threading import Lock, Condition, Timer from datetime import timedelta - -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) +from typing import Any import six - from .utils import get_current_utc_as_int from .utils import create_access_token @@ -22,12 +17,11 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str @@ -38,8 +32,6 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False @@ -53,8 +45,8 @@ def __exit__(self, *args): if self._timer is not None: self._timer.cancel() - def get_token(self): - # type () -> ~azure.core.credentials.AccessToken + def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument + # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -101,7 +93,8 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -111,7 +104,8 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 4b16437da83c..3497a04e3cdd 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -3,16 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from asyncio import Condition, Lock from datetime import timedelta +from typing import Any import sys -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) import six - from .utils import get_current_utc_as_int from .utils import create_access_token from .utils_async import AsyncTimer @@ -23,12 +19,11 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): @@ -36,8 +31,6 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._async_mutex = Lock() if sys.version_info[:3] == (3, 10, 0): @@ -48,15 +41,8 @@ def __init__(self, token: str, **kwargs: Any): if self._refresh_proactively: self._schedule_refresh() - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - if self._timer is not None: - self._timer.cancel() - - async def get_token(self): - # type () -> ~azure.core.credentials.AccessToken + async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument + # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -73,7 +59,7 @@ async def _update_token_and_reschedule(self): if self._is_currenttoken_valid(): return self._token - self._wait_till_inprogress_thread_finish_refreshing() + await self._wait_till_inprogress_thread_finish_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -81,9 +67,9 @@ async def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = self._token_refresher() # pylint:disable=not-callable + new_token = await self._token_refresher() # pylint:disable=not-callable async with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -100,17 +86,20 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() - def _wait_till_inprogress_thread_finish_refreshing(self): + async def _wait_till_inprogress_thread_finish_refreshing(self): + self._lock.release() - self._lock.acquire() + await self._lock.acquire() def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) @@ -119,3 +108,14 @@ def _token_expiring(self): def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on + + async def close(self) -> None: + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + if self._timer is not None: + self._timer.cancel() + await self.close() diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py index 649ed3b11504..8e0b5f9b4933 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py @@ -7,15 +7,14 @@ import base64 import json import calendar -import time -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) +from typing import (cast, + Tuple, + ) from datetime import datetime from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken + def _convert_datetime_to_utc_int(input_datetime): """ Converts DateTime in local time to the Epoch in UTC in second. @@ -63,7 +62,7 @@ def get_current_utc_time(): def get_current_utc_as_int(): # type: () -> int - current_utc_datetime = datetime.now(tz=TZ_UTC) + current_utc_datetime = datetime.utcnow() return _convert_datetime_to_utc_int(current_utc_datetime) @@ -132,7 +131,3 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) - -def _convert_expires_on_datetime_to_utc_int(expires_on): - epoch = time.mktime(datetime(1970, 1, 1).timetuple()) - return epoch-time.mktime(expires_on.timetuple()) diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 5663b9c2f651..15b1e437c561 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -3,28 +3,25 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from threading import Lock, Condition, Timer from datetime import timedelta - from typing import Any import six - from .utils import get_current_utc_as_int from .utils import create_access_token - class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str @@ -35,8 +32,6 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False @@ -98,7 +93,8 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -108,7 +104,8 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index bd20e6e431b3..1625f8d66b89 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from asyncio import Condition, Lock from datetime import timedelta from typing import Any @@ -12,7 +13,6 @@ from .utils_async import AsyncTimer - class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service @@ -22,7 +22,7 @@ class CommunicationTokenCredential(object): """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): @@ -30,8 +30,6 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._async_mutex = Lock() if sys.version_info[:3] == (3, 10, 0): @@ -71,10 +69,9 @@ async def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = await self._token_refresher() # pylint:disable=not-callable - + new_token = await self._token_refresher() # pylint:disable=not-callable async with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -91,26 +88,26 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() async def _wait_till_inprogress_thread_finish_refreshing(self): + self._lock.release() await self._lock.acquire() def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) return self._token.expires_on - get_current_utc_as_int() <\ timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on - async def close(self) -> None: pass diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py index 6e33ddb534a9..703e37555886 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py @@ -7,15 +7,14 @@ import base64 import json import calendar -import time -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) +from typing import (cast, + Tuple, + ) from datetime import datetime from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken + def _convert_datetime_to_utc_int(input_datetime): """ Converts DateTime in local time to the Epoch in UTC in second. @@ -97,6 +96,7 @@ def create_access_token(token): except ValueError: raise ValueError(token_parse_err_msg) + def get_authentication_policy( endpoint, # type: str credential, # type: TokenCredential or str diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 9d515772209b..15b1e437c561 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -3,16 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from threading import Lock, Condition, Timer from datetime import timedelta - -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) +from typing import Any import six - from .utils import get_current_utc_as_int from .utils import create_access_token @@ -22,12 +17,11 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str @@ -38,8 +32,6 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False @@ -53,8 +45,8 @@ def __exit__(self, *args): if self._timer is not None: self._timer.cancel() - def get_token(self): - # type () -> ~azure.core.credentials.AccessToken + def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument + # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -101,7 +93,8 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -111,7 +104,8 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index 4b16437da83c..3497a04e3cdd 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -3,16 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from asyncio import Condition, Lock from datetime import timedelta +from typing import Any import sys -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) import six - from .utils import get_current_utc_as_int from .utils import create_access_token from .utils_async import AsyncTimer @@ -23,12 +19,11 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): @@ -36,8 +31,6 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._async_mutex = Lock() if sys.version_info[:3] == (3, 10, 0): @@ -48,15 +41,8 @@ def __init__(self, token: str, **kwargs: Any): if self._refresh_proactively: self._schedule_refresh() - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - if self._timer is not None: - self._timer.cancel() - - async def get_token(self): - # type () -> ~azure.core.credentials.AccessToken + async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument + # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -73,7 +59,7 @@ async def _update_token_and_reschedule(self): if self._is_currenttoken_valid(): return self._token - self._wait_till_inprogress_thread_finish_refreshing() + await self._wait_till_inprogress_thread_finish_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -81,9 +67,9 @@ async def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = self._token_refresher() # pylint:disable=not-callable + new_token = await self._token_refresher() # pylint:disable=not-callable async with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -100,17 +86,20 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() - def _wait_till_inprogress_thread_finish_refreshing(self): + async def _wait_till_inprogress_thread_finish_refreshing(self): + self._lock.release() - self._lock.acquire() + await self._lock.acquire() def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) @@ -119,3 +108,14 @@ def _token_expiring(self): def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on + + async def close(self) -> None: + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + if self._timer is not None: + self._timer.cancel() + await self.close() diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py index 2566220f3c99..8e0b5f9b4933 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py @@ -7,15 +7,14 @@ import base64 import json import calendar -import time -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) +from typing import (cast, + Tuple, + ) from datetime import datetime from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken + def _convert_datetime_to_utc_int(input_datetime): """ Converts DateTime in local time to the Epoch in UTC in second. @@ -132,7 +131,3 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) - -def _convert_expires_on_datetime_to_utc_int(expires_on): - epoch = time.mktime(datetime(1970, 1, 1).timetuple()) - return epoch-time.mktime(expires_on.timetuple()) diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 9d515772209b..15b1e437c561 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -3,16 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from threading import Lock, Condition, Timer from datetime import timedelta - -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) +from typing import Any import six - from .utils import get_current_utc_as_int from .utils import create_access_token @@ -22,12 +17,11 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token, # type: str @@ -38,8 +32,6 @@ def __init__(self, self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False @@ -53,8 +45,8 @@ def __exit__(self, *args): if self._timer is not None: self._timer.cancel() - def get_token(self): - # type () -> ~azure.core.credentials.AccessToken + def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument + # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -101,7 +93,8 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() @@ -111,7 +104,8 @@ def _wait_till_inprogress_thread_finish_refreshing(self): def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index 4b16437da83c..3497a04e3cdd 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -3,16 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from asyncio import Condition, Lock from datetime import timedelta +from typing import Any import sys -from typing import ( # pylint: disable=unused-import - cast, - Tuple, - Any -) import six - from .utils import get_current_utc_as_int from .utils import create_access_token from .utils_async import AsyncTimer @@ -23,12 +19,11 @@ class CommunicationTokenCredential(object): :param str token: The token used to authenticate to an Azure Communication service :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not - :keyword timedelta refresh_interval_before_expiry: The time interval before token expiry that causes the token_refresher to be called if refresh_proactively is true. :raises: TypeError """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 - _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 4.5 + _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): @@ -36,8 +31,6 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) - self._refresh_interval_before_expiry = kwargs.pop('refresh_interval_before_expiry', timedelta( - minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES)) self._timer = None self._async_mutex = Lock() if sys.version_info[:3] == (3, 10, 0): @@ -48,15 +41,8 @@ def __init__(self, token: str, **kwargs: Any): if self._refresh_proactively: self._schedule_refresh() - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - if self._timer is not None: - self._timer.cancel() - - async def get_token(self): - # type () -> ~azure.core.credentials.AccessToken + async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument + # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ @@ -73,7 +59,7 @@ async def _update_token_and_reschedule(self): if self._is_currenttoken_valid(): return self._token - self._wait_till_inprogress_thread_finish_refreshing() + await self._wait_till_inprogress_thread_finish_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -81,9 +67,9 @@ async def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = self._token_refresher() # pylint:disable=not-callable + new_token = await self._token_refresher() # pylint:disable=not-callable async with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -100,17 +86,20 @@ def _schedule_refresh(self): self._timer.cancel() timespan = self._token.expires_on - \ - get_current_utc_as_int() - self._refresh_interval_before_expiry.total_seconds() + get_current_utc_as_int() - timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() - def _wait_till_inprogress_thread_finish_refreshing(self): + async def _wait_till_inprogress_thread_finish_refreshing(self): + self._lock.release() - self._lock.acquire() + await self._lock.acquire() def _token_expiring(self): if self._refresh_proactively: - interval = self._refresh_interval_before_expiry + interval = timedelta( + minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) @@ -119,3 +108,14 @@ def _token_expiring(self): def _is_currenttoken_valid(self): return get_current_utc_as_int() < self._token.expires_on + + async def close(self) -> None: + pass + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + if self._timer is not None: + self._timer.cancel() + await self.close() diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py index 2566220f3c99..8e0b5f9b4933 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py @@ -7,15 +7,14 @@ import base64 import json import calendar -import time -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) +from typing import (cast, + Tuple, + ) from datetime import datetime from msrest.serialization import TZ_UTC from azure.core.credentials import AccessToken + def _convert_datetime_to_utc_int(input_datetime): """ Converts DateTime in local time to the Epoch in UTC in second. @@ -132,7 +131,3 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) - -def _convert_expires_on_datetime_to_utc_int(expires_on): - epoch = time.mktime(datetime(1970, 1, 1).timetuple()) - return epoch-time.mktime(expires_on.timetuple()) From 5d3653b2159f5d5784d5efc287f5dfec705e1710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Mon, 17 Jan 2022 16:41:50 +0100 Subject: [PATCH 25/49] linting issues --- .../azure/communication/chat/_shared/user_credential.py | 5 +---- .../communication/chat/_shared/user_credential_async.py | 2 +- .../azure/communication/identity/_shared/user_credential.py | 5 +---- .../networktraversal/_shared/user_credential.py | 5 +---- .../networktraversal/_shared/user_credential_async.py | 2 +- .../communication/phonenumbers/_shared/user_credential.py | 5 +---- .../phonenumbers/_shared/user_credential_async.py | 2 +- .../azure/communication/sms/_shared/user_credential.py | 5 +---- .../azure/communication/sms/_shared/user_credential_async.py | 2 +- 9 files changed, 9 insertions(+), 24 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 15b1e437c561..5732d51182a8 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -23,10 +23,7 @@ class CommunicationTokenCredential(object): _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 - def __init__(self, - token, # type: str - **kwargs # type: Any - ): + def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): raise TypeError("Token must be a string.") self._token = create_access_token(token) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 3497a04e3cdd..b230af938537 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -17,7 +17,7 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index a0b5b12380fb..5167cf66109b 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -22,10 +22,7 @@ class CommunicationTokenCredential(object): _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 - def __init__(self, - token, # type: str - **kwargs # type: Any - ): + def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): raise TypeError("Token must be a string.") self._token = create_access_token(token) diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 15b1e437c561..5732d51182a8 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -23,10 +23,7 @@ class CommunicationTokenCredential(object): _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 - def __init__(self, - token, # type: str - **kwargs # type: Any - ): + def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): raise TypeError("Token must be a string.") self._token = create_access_token(token) diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index 1625f8d66b89..662446f7e95d 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -16,7 +16,7 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 15b1e437c561..5732d51182a8 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -23,10 +23,7 @@ class CommunicationTokenCredential(object): _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 - def __init__(self, - token, # type: str - **kwargs # type: Any - ): + def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): raise TypeError("Token must be a string.") self._token = create_access_token(token) diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index 3497a04e3cdd..b230af938537 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -17,7 +17,7 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 15b1e437c561..5732d51182a8 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -23,10 +23,7 @@ class CommunicationTokenCredential(object): _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 _DEFAULT_AUTOREFRESH_INTERVAL_MINUTES = 10 - def __init__(self, - token, # type: str - **kwargs # type: Any - ): + def __init__(self, token: str, **kwargs: Any): if not isinstance(token, six.string_types): raise TypeError("Token must be a string.") self._token = create_access_token(token) diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index 3497a04e3cdd..b230af938537 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -17,7 +17,7 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ From 69655775d5232af42d65d0077b4fb1828ce71349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Mon, 17 Jan 2022 17:11:41 +0100 Subject: [PATCH 26/49] linting issues --- .../azure/communication/chat/_shared/user_credential_async.py | 3 ++- .../networktraversal/_shared/user_credential_async.py | 3 ++- .../phonenumbers/_shared/user_credential_async.py | 3 ++- .../azure/communication/sms/_shared/user_credential_async.py | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index b230af938537..c698b4909db6 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -17,7 +17,8 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: + The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index 662446f7e95d..c7cc7797c19d 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -16,7 +16,8 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: + The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index b230af938537..c698b4909db6 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -17,7 +17,8 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: + The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index b230af938537..c698b4909db6 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -17,7 +17,8 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token refresher to provide capacity to fetch fresh token + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: + The async token refresher to provide capacity to fetch fresh token :keyword bool refresh_proactively: Whether to refresh the token proactively or not :raises: TypeError """ From 47730294f4232d1d7bee72decf929f9d89da9fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Tue, 18 Jan 2022 09:19:41 +0100 Subject: [PATCH 27/49] implemented fractional backoff + fixed tests --- .../chat/_shared/user_credential.py | 37 +++++++++++-------- .../chat/_shared/user_credential_async.py | 35 +++++++++++------- .../identity/_shared/user_credential.py | 37 +++++++++++-------- .../tests/test_user_credential.py | 22 +++++------ .../tests/test_user_credential_async.py | 12 +++--- .../_shared/user_credential.py | 37 +++++++++++-------- .../_shared/user_credential_async.py | 36 ++++++++++++------ .../phonenumbers/_shared/user_credential.py | 37 +++++++++++-------- .../_shared/user_credential_async.py | 35 +++++++++++------- .../sms/_shared/user_credential.py | 37 +++++++++++-------- .../sms/_shared/user_credential_async.py | 35 +++++++++++------- 11 files changed, 216 insertions(+), 144 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 5732d51182a8..2bb9f5c35ae5 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -48,7 +48,7 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token self._update_token_and_reschedule() @@ -57,12 +57,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument def _update_token_and_reschedule(self): should_this_thread_refresh = False with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - self._wait_till_inprogress_thread_finish_refreshing() + self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -70,10 +69,12 @@ def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = self._token_refresher() # pylint:disable=not-callable - + new_token = self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -89,25 +90,31 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() - def _wait_till_inprogress_thread_finish_refreshing(self): + def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ + return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index c698b4909db6..aba1b400b7f6 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -47,7 +47,7 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() return self._token @@ -55,12 +55,11 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument async def _update_token_and_reschedule(self): should_this_thread_refresh = False async with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - await self._wait_till_inprogress_thread_finish_refreshing() + await self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -68,7 +67,10 @@ async def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - new_token = await self._token_refresher() # pylint:disable=not-callable + new_token = await self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") async with self._lock: self._token = new_token self._some_thread_refreshing = False @@ -86,29 +88,36 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() - async def _wait_till_inprogress_thread_finish_refreshing(self): + async def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() await self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ + return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on async def close(self) -> None: pass diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 5167cf66109b..b444c0ab742f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -47,7 +47,7 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token self._update_token_and_reschedule() @@ -56,12 +56,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument def _update_token_and_reschedule(self): should_this_thread_refresh = False with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - self._wait_till_inprogress_thread_finish_refreshing() + self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -69,10 +68,12 @@ def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = self._token_refresher() # pylint:disable=not-callable - + new_token = self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -88,25 +89,31 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() - def _wait_till_inprogress_thread_finish_refreshing(self): + def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ + return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index dc9d94e3e519..4db17d2de20f 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +from typing import Type from unittest import TestCase try: from unittest.mock import MagicMock, patch @@ -42,30 +43,27 @@ def test_communicationtokencredential_static_token_returns_expired_token(self): self.assertEqual(credential.get_token().token, self.expired_token) def test_communicationtokencredential_token_expired_refresh_called(self): - refresher = MagicMock(return_value=self.sample_token) + refresher = MagicMock( + return_value=create_access_token(self.sample_token)) access_token = CommunicationTokenCredential( self.expired_token, token_refresher=refresher).get_token() refresher.assert_called_once() - self.assertEqual(access_token, self.sample_token) - + self.assertEqual(access_token.token, self.sample_token) - def test_communicationtokencredential_token_expired_refresh_called_as_necessary(self): + def test_communicationtokencredential_raises_if_refresher_returns_expired_token(self): refresher = MagicMock( return_value=create_access_token(self.expired_token)) credential = CommunicationTokenCredential( self.expired_token, token_refresher=refresher) - credential.get_token() - access_token = credential.get_token() - - self.assertEqual(refresher.call_count, 2) - self.assertEqual(access_token.token, self.expired_token) + with self.assertRaises(ValueError): + credential.get_token() + self.assertEqual(refresher.call_count, 1) - # @patch_threading_timer(user_credential.__name__+'.Timer') - def test_uses_initial_token_as_expected(self): # , timer_mock): + def test_uses_initial_token_as_expected(self): refresher = MagicMock( - return_value=self.expired_token) + return_value=create_access_token(self.expired_token)) credential = CommunicationTokenCredential( self.sample_token, token_refresher=refresher, refresh_proactively=True) with credential: diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index b1f377b5eade..6e9da82d7b75 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -5,6 +5,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +from unittest import TestCase import pytest try: from unittest.mock import MagicMock, patch @@ -18,7 +19,7 @@ from _shared.asynctestcase import get_completed_future -class TestCommunicationTokenCredential: +class TestCommunicationTokenCredential(TestCase): @pytest.mark.asyncio async def test_raises_error_for_init_with_nonstring_token(self): @@ -85,7 +86,7 @@ async def test_refresher_should_not_be_called_when_token_still_valid(self): assert generated_token == access_token.token @pytest.mark.asyncio - async def test_refresher_should_be_called_as_necessary(self): + async def test_raises_if_refresher_returns_expired_token(self): expired_token = generate_token_with_custom_expiry(-(10 * 60)) refresher = MagicMock(return_value=get_completed_future( create_access_token(expired_token))) @@ -93,11 +94,10 @@ async def test_refresher_should_be_called_as_necessary(self): credential = CommunicationTokenCredential( expired_token, token_refresher=refresher) async with credential: - await credential.get_token() - access_token = await credential.get_token() + with self.assertRaises(ValueError): + await credential.get_token() - assert refresher.call_count == 2 - assert expired_token == access_token.token + assert refresher.call_count == 1 @pytest.mark.asyncio async def test_proactive_refresher_should_not_be_called_before_specified_time(self): diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 5732d51182a8..2bb9f5c35ae5 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -48,7 +48,7 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token self._update_token_and_reschedule() @@ -57,12 +57,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument def _update_token_and_reschedule(self): should_this_thread_refresh = False with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - self._wait_till_inprogress_thread_finish_refreshing() + self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -70,10 +69,12 @@ def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = self._token_refresher() # pylint:disable=not-callable - + new_token = self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -89,25 +90,31 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() - def _wait_till_inprogress_thread_finish_refreshing(self): + def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ + return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index c7cc7797c19d..765331a8a2c7 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -49,7 +49,7 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() return self._token @@ -57,12 +57,11 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument async def _update_token_and_reschedule(self): should_this_thread_refresh = False async with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - await self._wait_till_inprogress_thread_finish_refreshing() + await self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -70,7 +69,10 @@ async def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - new_token = await self._token_refresher() # pylint:disable=not-callable + new_token = await self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") async with self._lock: self._token = new_token self._some_thread_refreshing = False @@ -88,26 +90,36 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() - async def _wait_till_inprogress_thread_finish_refreshing(self): + async def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() await self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ - timedelta(minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES).total_seconds() + return token.expires_on - get_current_utc_as_int() <\ + interval.total_seconds() + + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on async def close(self) -> None: pass diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 5732d51182a8..2bb9f5c35ae5 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -48,7 +48,7 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token self._update_token_and_reschedule() @@ -57,12 +57,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument def _update_token_and_reschedule(self): should_this_thread_refresh = False with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - self._wait_till_inprogress_thread_finish_refreshing() + self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -70,10 +69,12 @@ def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = self._token_refresher() # pylint:disable=not-callable - + new_token = self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -89,25 +90,31 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() - def _wait_till_inprogress_thread_finish_refreshing(self): + def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ + return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index c698b4909db6..aba1b400b7f6 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -47,7 +47,7 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() return self._token @@ -55,12 +55,11 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument async def _update_token_and_reschedule(self): should_this_thread_refresh = False async with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - await self._wait_till_inprogress_thread_finish_refreshing() + await self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -68,7 +67,10 @@ async def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - new_token = await self._token_refresher() # pylint:disable=not-callable + new_token = await self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") async with self._lock: self._token = new_token self._some_thread_refreshing = False @@ -86,29 +88,36 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() - async def _wait_till_inprogress_thread_finish_refreshing(self): + async def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() await self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ + return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on async def close(self) -> None: pass diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 5732d51182a8..2bb9f5c35ae5 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -48,7 +48,7 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token self._update_token_and_reschedule() @@ -57,12 +57,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument def _update_token_and_reschedule(self): should_this_thread_refresh = False with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - self._wait_till_inprogress_thread_finish_refreshing() + self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -70,10 +69,12 @@ def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - newtoken = self._token_refresher() # pylint:disable=not-callable - + new_token = self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") with self._lock: - self._token = newtoken + self._token = new_token self._some_thread_refreshing = False self._lock.notify_all() except: @@ -89,25 +90,31 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() self._timer = Timer(timespan, self._update_token_and_reschedule) self._timer.start() - def _wait_till_inprogress_thread_finish_refreshing(self): + def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ + return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index c698b4909db6..aba1b400b7f6 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -47,7 +47,7 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ - if not self._token_refresher or not self._token_expiring(): + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() return self._token @@ -55,12 +55,11 @@ async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument async def _update_token_and_reschedule(self): should_this_thread_refresh = False async with self._lock: - while self._token_expiring(): + while self._is_token_expiring_soon(self._token): if self._some_thread_refreshing: - if self._is_currenttoken_valid(): + if self._is_token_valid(self._token): return self._token - - await self._wait_till_inprogress_thread_finish_refreshing() + await self._wait_till_lock_owner_finishes_refreshing() else: should_this_thread_refresh = True self._some_thread_refreshing = True @@ -68,7 +67,10 @@ async def _update_token_and_reschedule(self): if should_this_thread_refresh: try: - new_token = await self._token_refresher() # pylint:disable=not-callable + new_token = await self._token_refresher() + if not self._is_token_valid(new_token): + raise ValueError( + "The token returned from the token_refresher is expired.") async with self._lock: self._token = new_token self._some_thread_refreshing = False @@ -86,29 +88,36 @@ def _schedule_refresh(self): if self._timer is not None: self._timer.cancel() - timespan = self._token.expires_on - \ - get_current_utc_as_int() - timedelta( + token_ttl = self._token.expires_on - get_current_utc_as_int() + + if self._is_token_expiring_soon(self._token): + # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. + timespan = token_ttl / 2 + else: + # Schedule the next refresh for when it gets in to the soon-to-expire window. + timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() + self._timer = AsyncTimer(timespan, self._update_token_and_reschedule) self._timer.start() - async def _wait_till_inprogress_thread_finish_refreshing(self): + async def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() await self._lock.acquire() - def _token_expiring(self): + def _is_token_expiring_soon(self, token): if self._refresh_proactively: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return self._token.expires_on - get_current_utc_as_int() <\ + return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_currenttoken_valid(self): - return get_current_utc_as_int() < self._token.expires_on + def _is_token_valid(self, token): + return get_current_utc_as_int() < token.expires_on async def close(self) -> None: pass From 784d837e5dcffb7a9c6ff42859a870337ca5c0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Tue, 18 Jan 2022 09:28:43 +0100 Subject: [PATCH 28/49] unify test with the sync version --- .../tests/test_user_credential_async.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 6e9da82d7b75..7bf251dddc1a 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -156,13 +156,15 @@ async def test_proactive_refresher_should_be_called_after_specified_time(self): @pytest.mark.asyncio async def test_proactive_refresher_keeps_scheduling_again(self): - refresh_seconds = 10 * 60 + refresh_minutes = 10 + token_validity_minutes = 60 expired_token = generate_token_with_custom_expiry(-5 * 60) - skip_to_timestamp = get_current_utc_as_int() + refresh_seconds + 4 + skip_to_timestamp = get_current_utc_as_int() + (token_validity_minutes - + refresh_minutes) * 60 + 1 first_refreshed_token = create_access_token( - generate_token_with_custom_expiry(4)) + generate_token_with_custom_expiry(token_validity_minutes * 60)) last_refreshed_token = create_access_token( - generate_token_with_custom_expiry(10 * 60)) + generate_token_with_custom_expiry(2 * token_validity_minutes * 60)) refresher = MagicMock( side_effect=[get_completed_future(first_refreshed_token), get_completed_future(last_refreshed_token)]) From 00b1cddc4971a5113feb4f4770a30ef9015e4d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Tue, 18 Jan 2022 10:06:24 +0100 Subject: [PATCH 29/49] fractional backoff tests + linting --- .../chat/_shared/user_credential.py | 3 ++- .../chat/_shared/user_credential_async.py | 3 ++- .../identity/_shared/user_credential.py | 3 ++- .../tests/test_user_credential.py | 24 ++++++++++++++++++ .../tests/test_user_credential_async.py | 25 +++++++++++++++++++ .../_shared/user_credential.py | 3 ++- .../_shared/user_credential_async.py | 3 ++- .../phonenumbers/_shared/user_credential.py | 3 ++- .../_shared/user_credential_async.py | 3 ++- .../sms/_shared/user_credential.py | 3 ++- .../sms/_shared/user_credential_async.py | 3 ++- 11 files changed, 67 insertions(+), 9 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 2bb9f5c35ae5..ffc1a0e5bd28 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -116,5 +116,6 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index aba1b400b7f6..b769fac008c0 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -116,7 +116,8 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on async def close(self) -> None: diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index b444c0ab742f..92013810e61e 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -115,5 +115,6 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index 4db17d2de20f..7cbc2ba3daae 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -152,6 +152,30 @@ def test_proactive_refresher_keeps_scheduling_again(self): # check that next refresh is always scheduled assert credential._timer is not None + def test_fractional_backoff_applied_when_token_expiring(self): + token_validity_seconds = 5 * 60 + expiring_token = generate_token_with_custom_expiry( + token_validity_seconds) + + refresher = MagicMock( + side_effect=[create_access_token(expiring_token), create_access_token(expiring_token)]) + + credential = CommunicationTokenCredential( + expiring_token, + token_refresher=refresher, + refresh_proactively=True) + + next_milestone = token_validity_seconds / 2 + assert credential._timer.interval == next_milestone + + with credential: + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): + credential.get_token() + + assert refresher.call_count == 1 + next_milestone = next_milestone / 2 + assert credential._timer.interval == next_milestone + def test_exit_cancels_timer(self): refreshed_token = create_access_token( generate_token_with_custom_expiry(30 * 60)) diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 7bf251dddc1a..005bb91a1628 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -182,6 +182,31 @@ async def test_proactive_refresher_keeps_scheduling_again(self): # check that next refresh is always scheduled assert credential._timer is not None + @pytest.mark.asyncio + async def test_fractional_backoff_applied_when_token_expiring(self): + token_validity_seconds = 5 * 60 + expiring_token = generate_token_with_custom_expiry( + token_validity_seconds) + + refresher = MagicMock( + side_effect=[create_access_token(expiring_token), create_access_token(expiring_token)]) + + credential = CommunicationTokenCredential( + expiring_token, + token_refresher=refresher, + refresh_proactively=True) + + next_milestone = token_validity_seconds / 2 + assert credential._timer.interval == next_milestone + + async with credential: + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): + await credential.get_token() + + assert refresher.call_count == 1 + next_milestone = next_milestone / 2 + assert credential._timer.interval == next_milestone + @pytest.mark.asyncio async def test_exit_cancels_timer(self): refreshed_token = create_access_token( diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 2bb9f5c35ae5..ffc1a0e5bd28 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -116,5 +116,6 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index 765331a8a2c7..d9cc86176089 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -118,7 +118,8 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on async def close(self) -> None: diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 2bb9f5c35ae5..ffc1a0e5bd28 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -116,5 +116,6 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index aba1b400b7f6..b769fac008c0 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -116,7 +116,8 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on async def close(self) -> None: diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 2bb9f5c35ae5..ffc1a0e5bd28 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -116,5 +116,6 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index aba1b400b7f6..b769fac008c0 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -116,7 +116,8 @@ def _is_token_expiring_soon(self, token): return token.expires_on - get_current_utc_as_int() <\ interval.total_seconds() - def _is_token_valid(self, token): + @classmethod + def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on async def close(self) -> None: From 7bbf212683c48aef08a690d05a73501b4efdb1f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Wed, 19 Jan 2022 09:44:22 +0100 Subject: [PATCH 30/49] added changelog records + bumped versions --- .../azure-communication-chat/CHANGELOG.md | 19 ++++++++++++++++++- .../chat/_shared/user_credential.py | 7 ++++--- .../chat/_shared/user_credential_async.py | 7 ++++--- .../azure-communication-identity/CHANGELOG.md | 12 ++++++++++++ .../identity/_shared/user_credential.py | 7 ++++--- .../CHANGELOG.md | 8 +++++++- .../_shared/user_credential.py | 7 ++++--- .../_shared/user_credential_async.py | 7 ++++--- .../CHANGELOG.md | 19 +++++++++++++------ .../phonenumbers/_shared/user_credential.py | 7 ++++--- .../_shared/user_credential_async.py | 7 ++++--- .../azure-communication-sms/CHANGELOG.md | 15 ++++++++++++++- .../sms/_shared/user_credential.py | 7 ++++--- .../sms/_shared/user_credential_async.py | 7 ++++--- 14 files changed, 100 insertions(+), 36 deletions(-) diff --git a/sdk/communication/azure-communication-chat/CHANGELOG.md b/sdk/communication/azure-communication-chat/CHANGELOG.md index 1ac145c46cd7..b16a1272f5b7 100644 --- a/sdk/communication/azure-communication-chat/CHANGELOG.md +++ b/sdk/communication/azure-communication-chat/CHANGELOG.md @@ -2,6 +2,9 @@ ## 1.2.0 (Unreleased) +- Added support for proactive refreshing of tokens + - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. + ### Features Added ### Breaking Changes @@ -12,16 +15,20 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later. ## 1.1.0 (2021-09-15) + - Updated `azure-communication-chat` version. ## 1.1.0b1 (2021-08-16) ### Added + - Added support to add `metadata` for `message` - Added support to add `sender_display_name` for `ChatThreadClient.send_typing_notification` ## 1.0.0 (2021-03-29) + ### Breaking Changes + - Renamed `ChatThread` to `ChatThreadProperties`. - Renamed `get_chat_thread` to `get_properties`. - Moved `get_properties` under `ChatThreadClient`. @@ -37,22 +44,29 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later. - Refactored implementation of `CommunicationUserIdentifier`, `PhoneNumberIdentifier`, `MicrosoftTeamsUserIdentifier`, `UnknownIdentifier` to use a `dict` property bag. ## 1.0.0b5 (2021-03-09) + ### Breaking Changes + - Added support for communication identifiers instead of raw strings. - Changed return type of `create_chat_thread`: `ChatThreadClient -> CreateChatThreadResult` - Changed return types `add_participants`: `None -> list[(ChatThreadParticipant, CommunicationError)]` - Added check for failure in `add_participant` - Dropped support for Python 3.5 + ### Added + - Removed nullable references from method signatures. ## 1.0.0b4 (2021-02-09) + ### Breaking Changes + - Uses `CommunicationUserIdentifier` and `CommunicationIdentifier` in place of `CommunicationUser`, and `CommunicationTokenCredential` instead of `CommunicationUserCredential`. - Removed priority field (ChatMessage.Priority). - Renamed PhoneNumber to PhoneNumberIdentifier. ### Added + - Support for CreateChatThreadResult and AddChatParticipantsResult to handle partial errors in batch calls. - Added idempotency identifier parameter for chat creation calls. - Added support for readreceipts and getparticipants pagination. @@ -61,10 +75,13 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later. - Added `MicrosoftTeamsUserIdentifier`. ## 1.0.0b3 (2020-11-16) + - Updated `azure-communication-chat` version. ## 1.0.0b2 (2020-10-06) + - Updated `azure-communication-chat` version. ## 1.0.0b1 (2020-09-22) - - Add ChatClient and ChatThreadClient. + +- Add ChatClient and ChatThreadClient. diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index ffc1a0e5bd28..e6001a08e906 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -14,9 +14,10 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :param str token: The token used to authenticate to an Azure Communication service. + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index b769fac008c0..e2794e9be3be 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -16,10 +16,11 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service + :param str token: The token used to authenticate to an Azure Communication service. :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-identity/CHANGELOG.md b/sdk/communication/azure-communication-identity/CHANGELOG.md index bbc96fbd5e9b..a0ced41f4bca 100644 --- a/sdk/communication/azure-communication-identity/CHANGELOG.md +++ b/sdk/communication/azure-communication-identity/CHANGELOG.md @@ -4,6 +4,9 @@ ### Features Added +- Added support for proactive refreshing of tokens + - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. + ### Breaking Changes ### Bugs Fixed @@ -12,20 +15,26 @@ - Python 2.7 is no longer supported. Please use Python version 3.6 or later. ## 1.1.0b1 (2021-11-09) + ### Features Added + - Added support for Microsoft 365 Teams identities - `CommunicationIdentityClient` added a new method `get_token_for_teams_user` that provides the ability to exchange an AAD access token of a Teams user for a Communication Identity access token ## 1.0.1 (2021-06-08) + ### Bug Fixes + - Fixed async client to use async bearer token credential policy instead of sync policy. ## 1.0.0 (2021-03-29) + - Stable release of `azure-communication-identity`. ## 1.0.0b5 (2021-03-09) ### Breaking + - CommunicationIdentityClient's (synchronous and asynchronous) `issue_token` function is now renamed to `get_token`. - The CommunicationIdentityClient constructor uses type `TokenCredential` and `AsyncTokenCredential` for the credential parameter. - Dropped support for 3.5 @@ -33,13 +42,16 @@ ## 1.0.0b4 (2021-02-09) ### Added + - Added CommunicationIdentityClient (originally was part of the azure.communication.administration package). - Added ability to create a user and issue token for it at the same time. ### Breaking + - CommunicationIdentityClient.revoke_tokens now revoke all the currently issued tokens instead of revoking tokens issued prior to a given time. - CommunicationIdentityClient.issue_tokens returns an instance of `azure.core.credentials.AccessToken` instead of `CommunicationUserToken`. + [read_me]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/communication/azure-communication-identity/README.md [documentation]: https://docs.microsoft.com/azure/communication-services/quickstarts/access-tokens?pivots=programming-language-python diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 92013810e61e..d8a1442e55e5 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -13,9 +13,10 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :param str token: The token used to authenticate to an Azure Communication service. + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-networktraversal/CHANGELOG.md b/sdk/communication/azure-communication-networktraversal/CHANGELOG.md index 70a257b21151..dc68777c4023 100644 --- a/sdk/communication/azure-communication-networktraversal/CHANGELOG.md +++ b/sdk/communication/azure-communication-networktraversal/CHANGELOG.md @@ -1,6 +1,11 @@ # Release History -## 1.0.0 (2022-02-04) +## 1.0.0b3 (Unreleased) + +- Added support for proactive refreshing of tokens + - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. + +### Features Added ### Breaking Changes @@ -31,4 +36,5 @@ The first preview of the Azure Communication Relay Client has the following feat - Added CommunicationRelayClient.get_relay_configuration in preview. + [read_me]: https://github.com/Azure/azure-sdk-for-python/blob/master/sdk/communication/ diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index ffc1a0e5bd28..e6001a08e906 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -14,9 +14,10 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :param str token: The token used to authenticate to an Azure Communication service. + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index d9cc86176089..d913bca296d0 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -15,10 +15,11 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service + :param str token: The token used to authenticate to an Azure Communication service. :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md b/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md index d4f08250f8e2..8dddd8135759 100644 --- a/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md +++ b/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md @@ -1,6 +1,9 @@ # Release History -## 1.2.0 (Unreleased) +## 1.1.0 (Unreleased) + +- Added support for proactive refreshing of tokens + - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. ### Features Added @@ -23,30 +26,34 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later. - Updates dependency `azure-core` to `1.20.0` ## 1.0.1 (2021-06-08) + ### Bug Fixes + - Fixed async client to use async bearer token credential policy instead of sync policy. ## 1.0.0 (2021-04-26) + - Stable release of `azure-communication-phonenumbers`. ## 1.0.0b5 (2021-03-29) ### Breaking Changes + - Renamed AcquiredPhoneNumber to PurchasedPhoneNumber - Renamed PhoneNumbersClient.get_phone_number and PhoneNumbersAsyncClient.get_phone_number to PhoneNumbersClient.get_purchased_phone_number -and PhoneNumbersAsyncClient.get_purchased_phone_number + and PhoneNumbersAsyncClient.get_purchased_phone_number - Renamed PhoneNumbersClient.list_acquired_phone_numbers and PhoneNumbersAsyncClient.list_acquired_phone_numbers to PhoneNumbersClient.list_purchased_phone_numbers -and PhoneNumbersAsyncClient.list_purchased_phone_numbers + and PhoneNumbersAsyncClient.list_purchased_phone_numbers ## 1.0.0b4 (2021-03-09) + - Dropped support for Python 3.5 ### Added -- Added PhoneNumbersClient (originally was part of the azure.communication.administration package). - - +- Added PhoneNumbersClient (originally was part of the azure.communication.administration package). + [read_me]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/communication/azure-communication-phonenumbers/README.md [documentation]: https://docs.microsoft.com/azure/communication-services/quickstarts/access-tokens?pivots=programming-language-python diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index ffc1a0e5bd28..e6001a08e906 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -14,9 +14,10 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :param str token: The token used to authenticate to an Azure Communication service. + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index b769fac008c0..e2794e9be3be 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -16,10 +16,11 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service + :param str token: The token used to authenticate to an Azure Communication service. :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-sms/CHANGELOG.md b/sdk/communication/azure-communication-sms/CHANGELOG.md index c9ebba5e89d4..5e465a7ad71f 100644 --- a/sdk/communication/azure-communication-sms/CHANGELOG.md +++ b/sdk/communication/azure-communication-sms/CHANGELOG.md @@ -2,6 +2,9 @@ ## 1.1.0 (Unreleased) +- Added support for proactive refreshing of tokens + - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. + ### Features Added ### Breaking Changes @@ -12,15 +15,19 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later. ## 1.0.1 (2021-06-08) + ### Bug Fixes -- Fixed async client to use async bearer token credential policy instead of sync policy. +- Fixed async client to use async bearer token credential policy instead of sync policy. ## 1.0.0 (2021-03-29) + - Stable release of `azure-communication-sms`. ## 1.0.0b6 (2021-03-09) + ### Added + - Added support for Azure Active Directory authentication. - Added support for 1:N SMS messaging. - Added support for SMS idempotency. @@ -29,22 +36,28 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later. - The SmsClient constructor uses type `TokenCredential` and `AsyncTokenCredential` for the credential parameter. ### Breaking + - Send method takes in strings for phone numbers instead of `PhoneNumberIdentifier`. - Send method returns a list of `SmsSendResult`s instead of a `SendSmsResponse`. - Dropped support for Python 3.5 ## 1.0.0b4 (2020-11-16) + - Updated `azure-communication-sms` version. ### Breaking Changes + - Replaced CommunicationUser with CommunicationUserIdentifier. - Replaced PhoneNumber with PhoneNumberIdentifier. ## 1.0.0b3 (2020-10-07) + - Add dependency to `azure-communication-nspkg` package, to support py2 ## 1.0.0b2 (2020-10-06) + - Updated `azure-communication-sms` version. ## 1.0.0b1 (2020-09-22) + - Preview release of the package. diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index ffc1a0e5bd28..e6001a08e906 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -14,9 +14,10 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + :param str token: The token used to authenticate to an Azure Communication service. + :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index b769fac008c0..e2794e9be3be 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -16,10 +16,11 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. - :param str token: The token used to authenticate to an Azure Communication service + :param str token: The token used to authenticate to an Azure Communication service. :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch fresh token - :keyword bool refresh_proactively: Whether to refresh the token proactively or not + The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ From 3553b88cbbec40d3bfd8f1e2e81b7dcc93b760bb Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 16 Feb 2022 10:44:58 +0100 Subject: [PATCH 31/49] Removed ayncio.Lock workaround for a bug in Python 3.10 --- .../azure/communication/chat/_shared/user_credential_async.py | 3 --- .../networktraversal/_shared/user_credential_async.py | 3 --- .../phonenumbers/_shared/user_credential_async.py | 3 --- .../azure/communication/sms/_shared/user_credential_async.py | 3 --- 4 files changed, 12 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index e2794e9be3be..203cce172389 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -35,9 +35,6 @@ def __init__(self, token: str, **kwargs: Any): self._refresh_proactively = kwargs.pop('refresh_proactively', False) self._timer = None self._async_mutex = Lock() - if sys.version_info[:3] == (3, 10, 0): - # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): - getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False if self._refresh_proactively: diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index d913bca296d0..d7de7b0cec89 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -34,9 +34,6 @@ def __init__(self, token: str, **kwargs: Any): self._refresh_proactively = kwargs.pop('refresh_proactively', False) self._timer = None self._async_mutex = Lock() - if sys.version_info[:3] == (3, 10, 0): - # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): - getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False if self._refresh_proactively: diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index e2794e9be3be..203cce172389 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -35,9 +35,6 @@ def __init__(self, token: str, **kwargs: Any): self._refresh_proactively = kwargs.pop('refresh_proactively', False) self._timer = None self._async_mutex = Lock() - if sys.version_info[:3] == (3, 10, 0): - # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): - getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False if self._refresh_proactively: diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index e2794e9be3be..203cce172389 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -35,9 +35,6 @@ def __init__(self, token: str, **kwargs: Any): self._refresh_proactively = kwargs.pop('refresh_proactively', False) self._timer = None self._async_mutex = Lock() - if sys.version_info[:3] == (3, 10, 0): - # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): - getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False if self._refresh_proactively: From 385917e1c6914dc78a7078894cbfb1f51928c06d Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 16 Feb 2022 13:46:41 +0100 Subject: [PATCH 32/49] fixed linting issues --- .../communication/chat/_shared/user_credential_async.py | 1 - .../communication/identity/_shared/user_credential.py | 1 + .../identity/_shared/user_credential_async.py | 2 +- .../azure/communication/identity/_shared/utils.py | 6 +----- .../azure-communication-identity/tests/_shared/helper.py | 3 +-- .../azure-communication-networktraversal/CHANGELOG.md | 6 ++++-- .../networktraversal/_shared/user_credential_async.py | 5 ++--- .../azure/communication/networktraversal/_shared/utils.py | 1 - .../tests/_shared/helper.py | 2 +- .../azure-communication-phonenumbers/CHANGELOG.md | 7 +++---- .../phonenumbers/_shared/user_credential_async.py | 1 - .../communication/sms/_shared/user_credential_async.py | 1 - 12 files changed, 14 insertions(+), 22 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 203cce172389..4188727c9327 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -7,7 +7,6 @@ from asyncio import Condition, Lock from datetime import timedelta from typing import Any -import sys import six from .utils import get_current_utc_as_int from .utils import create_access_token diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index d8a1442e55e5..e6001a08e906 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- + from threading import Lock, Condition, Timer from datetime import timedelta from typing import Any diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 88b6dd2c183c..4188727c9327 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -126,4 +126,4 @@ async def __aenter__(self): async def __aexit__(self, *args): if self._timer is not None: self._timer.cancel() - await self.close() \ No newline at end of file + await self.close() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index ee859291166e..8e0b5f9b4933 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -62,7 +62,7 @@ def get_current_utc_time(): def get_current_utc_as_int(): # type: () -> int - current_utc_datetime = datetime.now(tz=TZ_UTC) + current_utc_datetime = datetime.utcnow() return _convert_datetime_to_utc_int(current_utc_datetime) @@ -131,7 +131,3 @@ def get_authentication_policy( raise TypeError("Unsupported credential: {}. Use an access token string to use HMACCredentialsPolicy" "or a token credential from azure.identity".format(type(credential))) - -def _convert_expires_on_datetime_to_utc_int(expires_on): - epoch = time.mktime(datetime(1970, 1, 1).timetuple()) - return epoch-time.mktime(expires_on.timetuple()) diff --git a/sdk/communication/azure-communication-identity/tests/_shared/helper.py b/sdk/communication/azure-communication-identity/tests/_shared/helper.py index e13ee3ecb37b..21b29eb382b6 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/helper.py @@ -6,9 +6,9 @@ import re import base64 from azure_devtools.scenario_tests import RecordingProcessor -from urllib.parse import urlparse from datetime import datetime, timedelta from functools import wraps +from urllib.parse import urlparse import sys if sys.version_info[0] < 3 or sys.version_info[1] < 4: @@ -31,7 +31,6 @@ def generate_token_with_custom_expiry_epoch(expires_on_epoch): base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" return token_template - class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" diff --git a/sdk/communication/azure-communication-networktraversal/CHANGELOG.md b/sdk/communication/azure-communication-networktraversal/CHANGELOG.md index dc68777c4023..84e9e6e9c134 100644 --- a/sdk/communication/azure-communication-networktraversal/CHANGELOG.md +++ b/sdk/communication/azure-communication-networktraversal/CHANGELOG.md @@ -1,11 +1,13 @@ # Release History -## 1.0.0b3 (Unreleased) +## 1.1.0 (Unreleased) + +### Features Added - Added support for proactive refreshing of tokens - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. -### Features Added +## 1.0.0 (2022-02-04) ### Breaking Changes diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index d7de7b0cec89..4188727c9327 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -39,9 +39,6 @@ def __init__(self, token: str, **kwargs: Any): if self._refresh_proactively: self._schedule_refresh() - async def __aenter__(self): - return self - async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken """The value of the configured token. @@ -127,4 +124,6 @@ async def __aenter__(self): return self async def __aexit__(self, *args): + if self._timer is not None: + self._timer.cancel() await self.close() diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py index 703e37555886..8e0b5f9b4933 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py @@ -60,7 +60,6 @@ def get_current_utc_time(): return str(datetime.now(tz=TZ_UTC).strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT" - def get_current_utc_as_int(): # type: () -> int current_utc_datetime = datetime.utcnow() diff --git a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py index 895ccac1c540..dda9b5c28454 100644 --- a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py @@ -6,9 +6,9 @@ import re import base64 from azure_devtools.scenario_tests import RecordingProcessor -from urllib.parse import urlparse from datetime import datetime, timedelta from functools import wraps +from urllib.parse import urlparse import sys if sys.version_info[0] < 3 or sys.version_info[1] < 4: diff --git a/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md b/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md index 8dddd8135759..c90c1efed5e2 100644 --- a/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md +++ b/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md @@ -1,9 +1,6 @@ # Release History -## 1.1.0 (Unreleased) - -- Added support for proactive refreshing of tokens - - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. +## 1.2.0 (Unreleased) ### Features Added @@ -18,6 +15,8 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later. - Users can now purchase United Kingdom (GB) toll free and geographic phone numbers for PSTN Calling - Users can now purchase Denmark (DK) toll free and geographic phone numbers for PSTN Calling +- Added support for proactive refreshing of tokens + - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. ### Features Added - Adds support for API verion `2022-01-11-preview2` diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index 203cce172389..4188727c9327 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -7,7 +7,6 @@ from asyncio import Condition, Lock from datetime import timedelta from typing import Any -import sys import six from .utils import get_current_utc_as_int from .utils import create_access_token diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index 203cce172389..4188727c9327 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -7,7 +7,6 @@ from asyncio import Condition, Lock from datetime import timedelta from typing import Any -import sys import six from .utils import get_current_utc_as_int from .utils import create_access_token From 859fb8dbfb666aef0cf576e832c93d33cd20d465 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Fri, 18 Feb 2022 14:55:44 +0100 Subject: [PATCH 33/49] phonenumbers changelog updated --- .../azure-communication-phonenumbers/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md b/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md index c90c1efed5e2..3a7640dc4193 100644 --- a/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md +++ b/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md @@ -2,6 +2,8 @@ ## 1.2.0 (Unreleased) +- Added support for proactive refreshing of tokens + - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. ### Features Added ### Breaking Changes @@ -15,8 +17,6 @@ Python 2.7 is no longer supported. Please use Python version 3.6 or later. - Users can now purchase United Kingdom (GB) toll free and geographic phone numbers for PSTN Calling - Users can now purchase Denmark (DK) toll free and geographic phone numbers for PSTN Calling -- Added support for proactive refreshing of tokens - - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. ### Features Added - Adds support for API verion `2022-01-11-preview2` From 026aebf1761e99e23605e83c90bcbce80e31c52f Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Fri, 18 Feb 2022 16:03:00 +0100 Subject: [PATCH 34/49] fixed PR comments --- .../azure/communication/chat/_shared/user_credential.py | 6 +++--- .../communication/chat/_shared/user_credential_async.py | 6 +++--- .../azure/communication/chat/_shared/utils.py | 2 +- .../azure-communication-chat/tests/_shared/helper.py | 6 +++--- .../azure/communication/identity/_shared/user_credential.py | 6 +++--- .../communication/identity/_shared/user_credential_async.py | 6 +++--- .../azure/communication/identity/_shared/utils.py | 2 +- .../azure-communication-identity/tests/_shared/helper.py | 6 +++--- .../networktraversal/_shared/user_credential.py | 6 +++--- .../networktraversal/_shared/user_credential_async.py | 6 +++--- .../azure/communication/networktraversal/_shared/utils.py | 2 +- .../tests/_shared/helper.py | 6 +++--- .../communication/phonenumbers/_shared/user_credential.py | 6 +++--- .../phonenumbers/_shared/user_credential_async.py | 6 +++--- .../azure/communication/phonenumbers/_shared/utils.py | 2 +- .../azure-communication-phonenumbers/test/_shared/helper.py | 6 +++--- .../azure/communication/sms/_shared/user_credential.py | 6 +++--- .../communication/sms/_shared/user_credential_async.py | 6 +++--- .../azure/communication/sms/_shared/utils.py | 2 +- .../azure-communication-sms/tests/_shared/helper.py | 6 +++--- 20 files changed, 50 insertions(+), 50 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index e6001a08e906..228d52666df4 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -95,7 +95,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -114,8 +114,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 4188727c9327..98f3df795e3f 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -89,7 +89,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -110,8 +110,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py index 8e0b5f9b4933..0eef1aa7514f 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py @@ -88,7 +88,7 @@ def create_access_token(token): try: padded_base64_payload = base64.b64decode( - parts[1] + "==").decode('ascii') + parts[1] + '==').decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) diff --git a/sdk/communication/azure-communication-chat/tests/_shared/helper.py b/sdk/communication/azure-communication-chat/tests/_shared/helper.py index 4ffe17869a90..9ad90b39c572 100644 --- a/sdk/communication/azure-communication-chat/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-chat/tests/_shared/helper.py @@ -25,11 +25,11 @@ def generate_token_with_custom_expiry(valid_for_seconds): return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) def generate_token_with_custom_expiry_epoch(expires_on_epoch): - expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") - token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + token_template = (f'''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. + {base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs''') return token_template diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index e6001a08e906..228d52666df4 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -95,7 +95,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -114,8 +114,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 4188727c9327..98f3df795e3f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -89,7 +89,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -110,8 +110,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index 8e0b5f9b4933..0eef1aa7514f 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -88,7 +88,7 @@ def create_access_token(token): try: padded_base64_payload = base64.b64decode( - parts[1] + "==").decode('ascii') + parts[1] + '==').decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) diff --git a/sdk/communication/azure-communication-identity/tests/_shared/helper.py b/sdk/communication/azure-communication-identity/tests/_shared/helper.py index 21b29eb382b6..ccc526f8dbcf 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/helper.py @@ -24,11 +24,11 @@ def generate_token_with_custom_expiry(valid_for_seconds): def generate_token_with_custom_expiry_epoch(expires_on_epoch): - expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") - token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + token_template = (f'''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. + {base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs''') return token_template class URIIdentityReplacer(RecordingProcessor): diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index e6001a08e906..228d52666df4 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -95,7 +95,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -114,8 +114,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index 4188727c9327..98f3df795e3f 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -89,7 +89,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -110,8 +110,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py index 8e0b5f9b4933..0eef1aa7514f 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils.py @@ -88,7 +88,7 @@ def create_access_token(token): try: padded_base64_payload = base64.b64decode( - parts[1] + "==").decode('ascii') + parts[1] + '==').decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) diff --git a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py index dda9b5c28454..e3eaf9ee90bc 100644 --- a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py @@ -22,11 +22,11 @@ def generate_token_with_custom_expiry(valid_for_seconds): return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) def generate_token_with_custom_expiry_epoch(expires_on_epoch): - expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") - token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + token_template = (f'''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. + {base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs''') return token_template class URIIdentityReplacer(RecordingProcessor): diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index e6001a08e906..228d52666df4 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -95,7 +95,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -114,8 +114,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index 4188727c9327..98f3df795e3f 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -89,7 +89,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -110,8 +110,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py index 8e0b5f9b4933..0eef1aa7514f 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py @@ -88,7 +88,7 @@ def create_access_token(token): try: padded_base64_payload = base64.b64decode( - parts[1] + "==").decode('ascii') + parts[1] + '==').decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) diff --git a/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py b/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py index 80649d90cb2c..de3adc588fc2 100644 --- a/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py +++ b/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py @@ -25,11 +25,11 @@ def generate_token_with_custom_expiry(valid_for_seconds): return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) def generate_token_with_custom_expiry_epoch(expires_on_epoch): - expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") - token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + token_template = (f'''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. + {base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs''') return token_template diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index e6001a08e906..228d52666df4 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -95,7 +95,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -114,8 +114,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index 4188727c9327..98f3df795e3f 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -89,7 +89,7 @@ def _schedule_refresh(self): if self._is_token_expiring_soon(self._token): # Schedule the next refresh for when it reaches a certain percentage of the remaining lifetime. - timespan = token_ttl / 2 + timespan = token_ttl // 2 else: # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( @@ -110,8 +110,8 @@ def _is_token_expiring_soon(self, token): else: interval = timedelta( minutes=self._ON_DEMAND_REFRESHING_INTERVAL_MINUTES) - return token.expires_on - get_current_utc_as_int() <\ - interval.total_seconds() + return ((token.expires_on - get_current_utc_as_int()) + < interval.total_seconds()) @classmethod def _is_token_valid(cls, token): diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py index 8e0b5f9b4933..0eef1aa7514f 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py @@ -88,7 +88,7 @@ def create_access_token(token): try: padded_base64_payload = base64.b64decode( - parts[1] + "==").decode('ascii') + parts[1] + '==').decode('ascii') payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) diff --git a/sdk/communication/azure-communication-sms/tests/_shared/helper.py b/sdk/communication/azure-communication-sms/tests/_shared/helper.py index 4dda289f1260..fbad3bd34454 100644 --- a/sdk/communication/azure-communication-sms/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-sms/tests/_shared/helper.py @@ -25,11 +25,11 @@ def generate_token_with_custom_expiry(valid_for_seconds): return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) def generate_token_with_custom_expiry_epoch(expires_on_epoch): - expiry_json = '{"exp": ' + str(expires_on_epoch) + '}' + expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( expiry_json.encode('utf-8')).decode('utf-8').rstrip("=") - token_template = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +\ - base64expiry + ".adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs" + token_template = (f'''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. + {base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs''') return token_template From bd9647f8197c0bb32aa113b79801a2973af5767d Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Mon, 21 Feb 2022 13:57:26 +0100 Subject: [PATCH 35/49] removed user_token_refresh_options from communication SDKs --- .../chat/_shared/user_credential.py | 5 +-- .../chat/_shared/user_credential_async.py | 6 ++-- .../_shared/user_token_refresh_options.py | 36 ------------------- .../identity/_shared/user_credential.py | 5 +-- .../identity/_shared/user_credential_async.py | 6 ++-- .../_shared/user_credential.py | 5 +-- .../_shared/user_credential_async.py | 6 ++-- .../_shared/user_token_refresh_options.py | 36 ------------------- .../phonenumbers/_shared/user_credential.py | 5 +-- .../_shared/user_credential_async.py | 6 ++-- .../_shared/user_token_refresh_options.py | 36 ------------------- .../sms/_shared/user_credential.py | 5 +-- .../sms/_shared/user_credential_async.py | 6 ++-- .../sms/_shared/user_token_refresh_options.py | 36 ------------------- 14 files changed, 30 insertions(+), 169 deletions(-) delete mode 100644 sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_token_refresh_options.py delete mode 100644 sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_token_refresh_options.py delete mode 100644 sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_token_refresh_options.py delete mode 100644 sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_token_refresh_options.py diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 228d52666df4..2817ed7f0083 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -15,8 +15,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 98f3df795e3f..cf31012bd4e0 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -16,9 +16,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch a fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_token_refresh_options.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_token_refresh_options.py deleted file mode 100644 index 6bdc0d456026..000000000000 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_token_refresh_options.py +++ /dev/null @@ -1,36 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) -import six -from .utils import create_access_token - -class CommunicationTokenRefreshOptions(object): - """Options for refreshing CommunicationTokenCredential. - :param str token: The token used to authenticate to an Azure Communication service - :param token_refresher: The token refresher to provide capacity to fetch fresh token - :raises: TypeError - """ - - def __init__(self, - token, # type: str - token_refresher=None - ): - # type: (str) -> None - if not isinstance(token, six.string_types): - raise TypeError("token must be a string.") - self._token = token - self._token_refresher = token_refresher - - def get_token(self): - """Return the the serialized JWT token.""" - return create_access_token(self._token) - - def get_token_refresher(self): - """Return the token refresher to provide capacity to fetch fresh token.""" - return self._token_refresher diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 228d52666df4..2817ed7f0083 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -15,8 +15,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 98f3df795e3f..cf31012bd4e0 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -16,9 +16,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch a fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 228d52666df4..2817ed7f0083 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -15,8 +15,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index 98f3df795e3f..cf31012bd4e0 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -16,9 +16,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch a fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_token_refresh_options.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_token_refresh_options.py deleted file mode 100644 index 6bdc0d456026..000000000000 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_token_refresh_options.py +++ /dev/null @@ -1,36 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) -import six -from .utils import create_access_token - -class CommunicationTokenRefreshOptions(object): - """Options for refreshing CommunicationTokenCredential. - :param str token: The token used to authenticate to an Azure Communication service - :param token_refresher: The token refresher to provide capacity to fetch fresh token - :raises: TypeError - """ - - def __init__(self, - token, # type: str - token_refresher=None - ): - # type: (str) -> None - if not isinstance(token, six.string_types): - raise TypeError("token must be a string.") - self._token = token - self._token_refresher = token_refresher - - def get_token(self): - """Return the the serialized JWT token.""" - return create_access_token(self._token) - - def get_token_refresher(self): - """Return the token refresher to provide capacity to fetch fresh token.""" - return self._token_refresher diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 228d52666df4..2817ed7f0083 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -15,8 +15,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index 98f3df795e3f..cf31012bd4e0 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -16,9 +16,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch a fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_token_refresh_options.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_token_refresh_options.py deleted file mode 100644 index 6bdc0d456026..000000000000 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_token_refresh_options.py +++ /dev/null @@ -1,36 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) -import six -from .utils import create_access_token - -class CommunicationTokenRefreshOptions(object): - """Options for refreshing CommunicationTokenCredential. - :param str token: The token used to authenticate to an Azure Communication service - :param token_refresher: The token refresher to provide capacity to fetch fresh token - :raises: TypeError - """ - - def __init__(self, - token, # type: str - token_refresher=None - ): - # type: (str) -> None - if not isinstance(token, six.string_types): - raise TypeError("token must be a string.") - self._token = token - self._token_refresher = token_refresher - - def get_token(self): - """Return the the serialized JWT token.""" - return create_access_token(self._token) - - def get_token_refresher(self): - """Return the token refresher to provide capacity to fetch fresh token.""" - return self._token_refresher diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 228d52666df4..2817ed7f0083 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -15,8 +15,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The async token refresher to provide capacity to fetch fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index 98f3df795e3f..cf31012bd4e0 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -16,9 +16,9 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: - The async token refresher to provide capacity to fetch a fresh token. - The returned token must be valid (expiration date must be in the future). + :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration + date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. :raises: TypeError """ diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_token_refresh_options.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_token_refresh_options.py deleted file mode 100644 index 6bdc0d456026..000000000000 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_token_refresh_options.py +++ /dev/null @@ -1,36 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from typing import ( # pylint: disable=unused-import - cast, - Tuple, -) -import six -from .utils import create_access_token - -class CommunicationTokenRefreshOptions(object): - """Options for refreshing CommunicationTokenCredential. - :param str token: The token used to authenticate to an Azure Communication service - :param token_refresher: The token refresher to provide capacity to fetch fresh token - :raises: TypeError - """ - - def __init__(self, - token, # type: str - token_refresher=None - ): - # type: (str) -> None - if not isinstance(token, six.string_types): - raise TypeError("token must be a string.") - self._token = token - self._token_refresher = token_refresher - - def get_token(self): - """Return the the serialized JWT token.""" - return create_access_token(self._token) - - def get_token_refresher(self): - """Return the token refresher to provide capacity to fetch fresh token.""" - return self._token_refresher From 8dc633625444cb8d5919dc7c5c2a33d5b69755e1 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Mon, 21 Feb 2022 15:44:02 +0100 Subject: [PATCH 36/49] fix cspell issues --- .vscode/cspell.json | 6 ++++-- .../azure/communication/chat/_shared/user_credential.py | 2 +- .../communication/chat/_shared/user_credential_async.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 8228c0b94d4e..726c3e1a4f36 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -187,6 +187,7 @@ "msrest", "msrestazure", "MSSQL", + "mutex", "nazsdk", "noarch", "northcentralus", @@ -329,13 +330,14 @@ ] }, { - "filename": "sdk/communication/azure-communication-identity/tests/*.py", + "filename": "sdk/communication/azure-communication-identity/tests/**", "words": [ "XVCJ", "Njgw", "FNNHHJT", "Zwiz", - "nypg" + "nypg", + "PBOF" ] } ], diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 2817ed7f0083..af80cbf63e07 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -15,7 +15,7 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index cf31012bd4e0..07436b3d2cb9 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -16,7 +16,7 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. From b9dec62ab82d71a4fb5688d82577b353c1c08e9b Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 3 Mar 2022 14:39:47 +0100 Subject: [PATCH 37/49] type hinting fix --- .../azure/communication/chat/_shared/user_credential_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 07436b3d2cb9..bf741df812a6 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -16,7 +16,7 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The async token + :keyword Optional[Callable[[], Awaitable[azure.core.credentials.AccessToken]]] token_refresher: The async token refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. From fd69d2b70b478b159ac807b441a3417e99ed77c5 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 3 Mar 2022 15:42:51 +0100 Subject: [PATCH 38/49] reverted back type hint fix --- .../azure/communication/chat/_shared/user_credential_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index bf741df812a6..07436b3d2cb9 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -16,7 +16,7 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Optional[Callable[[], Awaitable[azure.core.credentials.AccessToken]]] token_refresher: The async token + :keyword callable token_refresher: The async token refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. From f9324b96ba08c5daa184b24105478cdd88485cb6 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Fri, 4 Mar 2022 14:37:28 +0100 Subject: [PATCH 39/49] PR comment fix --- .../chat/_shared/user_credential.py | 30 +++++++++++-------- .../chat/_shared/user_credential_async.py | 23 +++++++------- .../communication/chat/_shared/utils_async.py | 3 +- .../tests/_shared/helper.py | 11 ++----- .../azure-communication-identity/CHANGELOG.md | 3 -- .../CHANGELOG.md | 4 --- .../CHANGELOG.md | 2 -- .../azure-communication-sms/CHANGELOG.md | 3 -- 8 files changed, 32 insertions(+), 47 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index af80cbf63e07..485594629c6f 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -15,11 +15,11 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). + :keyword callable token_refresher: The sync token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'refresh_proactively' is enabled without providing the 'token_refresher' callable. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -31,18 +31,11 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) + if(self._refresh_proactively and self._token_refresher is None): + raise ValueError("'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() - - def __enter__(self): - return self - - def __exit__(self, *args): - if self._timer is not None: - self._timer.cancel() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -121,3 +114,14 @@ def _is_token_expiring_soon(self, token): @classmethod def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on + + def __enter__(self): + if self._refresh_proactively: + self._schedule_refresh() + return self + + def __exit__(self, *args): + self.close() + + def close(self) -> None: + self._timer = None diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index 07436b3d2cb9..b1ed5811f3ad 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -16,11 +16,12 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). + :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'refresh_proactively' is enabled without providing the 'token_refresher' function. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -32,12 +33,12 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) + if(self._refresh_proactively and self._token_refresher is None): + raise ValueError("'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -117,13 +118,13 @@ def _is_token_expiring_soon(self, token): def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on - async def close(self) -> None: - pass - async def __aenter__(self): + if self._refresh_proactively: + self._schedule_refresh() return self async def __aexit__(self, *args): - if self._timer is not None: - self._timer.cancel() await self.close() + + async def close(self) -> None: + self._timer = None diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py index f2472e2121af..2b6695be02f5 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py @@ -26,5 +26,4 @@ async def _job(self): await self._callback() def cancel(self): - if self._task is not None: - self._task.cancel() + self._task = None diff --git a/sdk/communication/azure-communication-chat/tests/_shared/helper.py b/sdk/communication/azure-communication-chat/tests/_shared/helper.py index 9ad90b39c572..4d3585695f5a 100644 --- a/sdk/communication/azure-communication-chat/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-chat/tests/_shared/helper.py @@ -14,15 +14,8 @@ from urlparse import urlparse import sys -if sys.version_info[0] < 3 or sys.version_info[1] < 4: - # python version < 3.3 - import time - def generate_token_with_custom_expiry(valid_for_seconds): - date = datetime.now() + timedelta(seconds=valid_for_seconds) - return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) -else: - def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' diff --git a/sdk/communication/azure-communication-identity/CHANGELOG.md b/sdk/communication/azure-communication-identity/CHANGELOG.md index a0ced41f4bca..39e9859b6450 100644 --- a/sdk/communication/azure-communication-identity/CHANGELOG.md +++ b/sdk/communication/azure-communication-identity/CHANGELOG.md @@ -4,9 +4,6 @@ ### Features Added -- Added support for proactive refreshing of tokens - - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. - ### Breaking Changes ### Bugs Fixed diff --git a/sdk/communication/azure-communication-networktraversal/CHANGELOG.md b/sdk/communication/azure-communication-networktraversal/CHANGELOG.md index 0a944d2d70fd..4d8b5e314d34 100644 --- a/sdk/communication/azure-communication-networktraversal/CHANGELOG.md +++ b/sdk/communication/azure-communication-networktraversal/CHANGELOG.md @@ -4,10 +4,6 @@ ### Features Added -- Added support for proactive refreshing of tokens - - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. - - ## 1.0.0 (2022-02-04) (Deprecated) ### Breaking Changes diff --git a/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md b/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md index 3a7640dc4193..236ade382cc6 100644 --- a/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md +++ b/sdk/communication/azure-communication-phonenumbers/CHANGELOG.md @@ -2,8 +2,6 @@ ## 1.2.0 (Unreleased) -- Added support for proactive refreshing of tokens - - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. ### Features Added ### Breaking Changes diff --git a/sdk/communication/azure-communication-sms/CHANGELOG.md b/sdk/communication/azure-communication-sms/CHANGELOG.md index 5e465a7ad71f..9c42e376d729 100644 --- a/sdk/communication/azure-communication-sms/CHANGELOG.md +++ b/sdk/communication/azure-communication-sms/CHANGELOG.md @@ -2,9 +2,6 @@ ## 1.1.0 (Unreleased) -- Added support for proactive refreshing of tokens - - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. - ### Features Added ### Breaking Changes From 030273ac4647398594d16ae09a76326d5744e838 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Mon, 7 Mar 2022 11:28:55 +0100 Subject: [PATCH 40/49] reflected changes to the identity package & updated tests --- .../chat/_shared/user_credential.py | 7 ++- .../identity/_shared/user_credential.py | 37 ++++++----- .../identity/_shared/user_credential_async.py | 23 +++---- .../identity/_shared/utils_async.py | 3 +- .../tests/_shared/helper.py | 35 ++++------- .../tests/test_user_credential.py | 61 +++++++++++-------- .../tests/test_user_credential_async.py | 20 +++++- 7 files changed, 105 insertions(+), 81 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 485594629c6f..5d0365044574 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer +from threading import Lock, Condition, Timer, TIMEOUT_MAX from datetime import timedelta from typing import Any import six @@ -94,8 +94,9 @@ def _schedule_refresh(self): # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() - self._timer = Timer(timespan, self._update_token_and_reschedule) - self._timer.start() + if timespan <= TIMEOUT_MAX: + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 2817ed7f0083..5d0365044574 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer +from threading import Lock, Condition, Timer, TIMEOUT_MAX from datetime import timedelta from typing import Any import six @@ -15,11 +15,11 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). + :keyword callable token_refresher: The sync token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'refresh_proactively' is enabled without providing the 'token_refresher' callable. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -31,18 +31,11 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) + if(self._refresh_proactively and self._token_refresher is None): + raise ValueError("'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() - - def __enter__(self): - return self - - def __exit__(self, *args): - if self._timer is not None: - self._timer.cancel() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -101,8 +94,9 @@ def _schedule_refresh(self): # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() - self._timer = Timer(timespan, self._update_token_and_reschedule) - self._timer.start() + if timespan <= TIMEOUT_MAX: + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() @@ -121,3 +115,14 @@ def _is_token_expiring_soon(self, token): @classmethod def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on + + def __enter__(self): + if self._refresh_proactively: + self._schedule_refresh() + return self + + def __exit__(self, *args): + self.close() + + def close(self) -> None: + self._timer = None diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index cf31012bd4e0..b1ed5811f3ad 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -16,11 +16,12 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). + :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'refresh_proactively' is enabled without providing the 'token_refresher' function. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -32,12 +33,12 @@ def __init__(self, token: str, **kwargs: Any): self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) self._refresh_proactively = kwargs.pop('refresh_proactively', False) + if(self._refresh_proactively and self._token_refresher is None): + raise ValueError("'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -117,13 +118,13 @@ def _is_token_expiring_soon(self, token): def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on - async def close(self) -> None: - pass - async def __aenter__(self): + if self._refresh_proactively: + self._schedule_refresh() return self async def __aexit__(self, *args): - if self._timer is not None: - self._timer.cancel() await self.close() + + async def close(self) -> None: + self._timer = None diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py index f2472e2121af..2b6695be02f5 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py @@ -26,5 +26,4 @@ async def _job(self): await self._callback() def cancel(self): - if self._task is not None: - self._task.cancel() + self._task = None diff --git a/sdk/communication/azure-communication-identity/tests/_shared/helper.py b/sdk/communication/azure-communication-identity/tests/_shared/helper.py index ccc526f8dbcf..4d3585695f5a 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/helper.py @@ -8,21 +8,15 @@ from azure_devtools.scenario_tests import RecordingProcessor from datetime import datetime, timedelta from functools import wraps -from urllib.parse import urlparse +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse import sys -if sys.version_info[0] < 3 or sys.version_info[1] < 4: - # python version < 3.3 - import time - - def generate_token_with_custom_expiry(valid_for_seconds): - date = datetime.now() + timedelta(seconds=valid_for_seconds) - return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) -else: - def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - - +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( @@ -31,21 +25,18 @@ def generate_token_with_custom_expiry_epoch(expires_on_epoch): {base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs''') return token_template + class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" - def process_request(self, request): resource = (urlparse(request.uri).netloc).split('.')[0] - request.uri = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) request.uri = re.sub(resource, 'sanitized', request.uri) - request.uri = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', request.uri) + request.uri = re.sub('/identities/([^/?]+)', '/identities/sanitized', request.uri) request.uri = re.sub(resource, 'sanitized', request.uri) return request - + def process_response(self, response): if 'url' in response: - response['url'] = re.sub( - '/identities/([^/?]+)', '/identities/sanitized', response['url']) - return response + response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) + return response \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index 7cbc2ba3daae..fded2018bb48 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -4,6 +4,7 @@ # license information. # -------------------------------------------------------------------------- from typing import Type +import pytest from unittest import TestCase try: from unittest.mock import MagicMock, patch @@ -37,6 +38,17 @@ def test_communicationtokencredential_throws_if_invalid_token(self): def test_communicationtokencredential_throws_if_nonstring_token(self): self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) + + def test_communicationtokencredential_throws_if_refresh_proactively_enabled_without_token_refresher(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential(self.sample_token, refresh_proactively=True) + assert str(err.value) == "'token_refresher' must not be None." + with pytest.raises(ValueError) as err: + CommunicationTokenCredential( + self.sample_token, + refresh_proactively=True, + token_refresher=None) + assert str(err.value) == "'token_refresher' must not be None." def test_communicationtokencredential_static_token_returns_expired_token(self): credential = CommunicationTokenCredential(self.expired_token) @@ -92,11 +104,11 @@ def test_proactive_refresher_should_not_be_called_before_specified_time(self): refresh_proactively=True) with credential: access_token = credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None def test_proactive_refresher_should_be_called_after_specified_time(self): refresh_minutes = 10 @@ -120,10 +132,10 @@ def test_proactive_refresher_should_be_called_after_specified_time(self): with credential: access_token = credential.get_token() - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None def test_proactive_refresher_keeps_scheduling_again(self): refresh_minutes = 10 @@ -146,12 +158,12 @@ def test_proactive_refresher_keeps_scheduling_again(self): access_token = credential.get_token() with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): access_token = credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None - + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + def test_fractional_backoff_applied_when_token_expiring(self): token_validity_seconds = 5 * 60 expiring_token = generate_token_with_custom_expiry( @@ -166,28 +178,29 @@ def test_fractional_backoff_applied_when_token_expiring(self): refresh_proactively=True) next_milestone = token_validity_seconds / 2 - assert credential._timer.interval == next_milestone with credential: + assert credential._timer.interval == next_milestone with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): credential.get_token() - - assert refresher.call_count == 1 - next_milestone = next_milestone / 2 - assert credential._timer.interval == next_milestone + assert refresher.call_count == 1 + next_milestone = next_milestone / 2 + assert credential._timer.interval == next_milestone def test_exit_cancels_timer(self): refreshed_token = create_access_token( generate_token_with_custom_expiry(30 * 60)) refresher = MagicMock(return_value=refreshed_token) expired_token = generate_token_with_custom_expiry(-10 * 60) - - with CommunicationTokenCredential( + credential = CommunicationTokenCredential( expired_token, token_refresher=refresher, - refresh_proactively=True) as credential: + refresh_proactively=True) + with credential: + assert credential._timer is not None + assert credential._timer is None + with credential: assert credential._timer is not None - assert credential._timer.finished.is_set() == True def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 005bb91a1628..05cddec9f3c3 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -39,6 +39,18 @@ async def test_init_with_valid_token(self): credential = CommunicationTokenCredential(initial_token) access_token = await credential.get_token() assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_communicationtokencredential_throws_if_refresh_proactively_enabled_without_token_refresher(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential(self.sample_token, refresh_proactively=True) + assert str(err.value) == "'token_refresher' must not be None." + with pytest.raises(ValueError) as err: + CommunicationTokenCredential( + self.sample_token, + refresh_proactively=True, + token_refresher=None) + assert str(err.value) == "'token_refresher' must not be None." @pytest.mark.asyncio async def test_refresher_should_be_called_immediately_with_expired_token(self): @@ -213,10 +225,12 @@ async def test_exit_cancels_timer(self): generate_token_with_custom_expiry(30 * 60)) refresher = MagicMock(return_value=refreshed_token) expired_token = generate_token_with_custom_expiry(-10 * 60) - - async with CommunicationTokenCredential( + credential = CommunicationTokenCredential( expired_token, token_refresher=refresher, - refresh_proactively=True) as credential: + refresh_proactively=True) + async with credential: assert credential._timer is not None assert refresher.call_count == 0 + async with credential: + assert credential._timer is not None From 730d895913ff57223a1c821a995b879fa67d89fe Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Mon, 7 Mar 2022 18:43:28 +0100 Subject: [PATCH 41/49] added samples for CommunicationTokenCredential --- .../chat/_shared/user_credential.py | 2 + .../chat/_shared/user_credential_async.py | 6 ++ .../samples/user_credential_sample.py | 72 ++++++++++++++++++ .../samples/user_credential_sample_async.py | 76 +++++++++++++++++++ .../identity/_shared/user_credential.py | 2 + .../identity/_shared/user_credential_async.py | 2 + .../tests/test_user_credential.py | 25 +++--- 7 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 sdk/communication/azure-communication-chat/samples/user_credential_sample.py create mode 100644 sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 5d0365044574..44bbcc6e46e3 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -125,4 +125,6 @@ def __exit__(self, *args): self.close() def close(self) -> None: + if self._timer is not None: + self._timer.cancel() self._timer = None diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index b1ed5811f3ad..c6b23909a02c 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -8,6 +8,7 @@ from datetime import timedelta from typing import Any import six +import sys from .utils import get_current_utc_as_int from .utils import create_access_token from .utils_async import AsyncTimer @@ -37,6 +38,9 @@ def __init__(self, token: str, **kwargs: Any): raise ValueError("'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False @@ -127,4 +131,6 @@ async def __aexit__(self, *args): await self.close() async def close(self) -> None: + if self._timer is not None: + self._timer.cancel() self._timer = None diff --git a/sdk/communication/azure-communication-chat/samples/user_credential_sample.py b/sdk/communication/azure-communication-chat/samples/user_credential_sample.py new file mode 100644 index 000000000000..dac6b71deac3 --- /dev/null +++ b/sdk/communication/azure-communication-chat/samples/user_credential_sample.py @@ -0,0 +1,72 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +""" +FILE: user_credential_sample.py +DESCRIPTION: + These samples demonstrate create a CommunicationTokenCredential object. + The `CommunicationTokenCredential` object is used to authenticate a user with Communication Services, + such as Chat or Calling. It optionally provides an auto-refresh mechanism to ensure a continuously + stable authentication state during communications. + +USAGE: + python user_credential_sample.py + Set the environment variables with your own values before running the sample: + 1) COMMUNICATION_SAMPLES_CONNECTION_STRING - the connection string in your Communication Services resource +""" + + +import os +from azure.communication.chat import CommunicationTokenCredential +from azure.communication.identity import CommunicationIdentityClient + +class CommunicationTokenCredentialSamples(object): + + connection_string = os.environ.get("COMMUNICATION_SAMPLES_CONNECTION_STRING", None) + if not connection_string: + raise ValueError("Set COMMUNICATION_SAMPLES_CONNECTION_STRING env before run this sample.") + + identity_client = CommunicationIdentityClient.from_connection_string(connection_string) + user = identity_client.create_user() + tokenresponse = identity_client.get_token(user, scopes=["chat"]) + token = tokenresponse.token + + def create_credential_with_static_token(self): + # For short-lived clients, refreshing the token upon expiry is not necessary + # and `CommunicationTokenCredential` may be instantiated with a static token. + with CommunicationTokenCredential(self.token) as credential: + tokenresponse = credential.get_token() + print("Token issued with value: " + tokenresponse.token) + + def create_credential_with_refreshing_callback(self): + # Alternatively, for long-lived clients, you can create a `CommunicationTokenCredential` with a callback to renew tokens if expired. + # Here we assume that we have a function `fetch_token_from_server` that makes a network request to retrieve a token string for a user. + # It's necessary that the `fetch_token_from_server` function returns a valid token (with an expiration date set in the future) at all times. + fetch_token_from_server = lambda: None + with CommunicationTokenCredential( + self.token, token_refresher=fetch_token_from_server) as credential: + tokenresponse = credential.get_token() + print("Token issued with value: " + tokenresponse.token) + + def create_credential_with_proactive_refreshing_callback(self): + # Optionally, you can enable proactive token refreshing where a fresh token will be acquired as soon as the + # previous token approaches expiry. Using this method, your requests are less likely to be blocked to acquire a fresh token + fetch_token_from_server = lambda: None + with CommunicationTokenCredential( + self.token, token_refresher=fetch_token_from_server, refresh_proactively=True) as credential: + tokenresponse = credential.get_token() + print("Token issued with value: " + tokenresponse.token) + + def clean_up(self): + print("cleaning up: deleting created user.") + self.identity_client.delete_user(self.user) + +if __name__ == '__main__': + sample = CommunicationTokenCredentialSamples() + sample.create_credential_with_static_token() + sample.create_credential_with_refreshing_callback() + sample.create_credential_with_proactive_refreshing_callback() + sample.clean_up() diff --git a/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py b/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py new file mode 100644 index 000000000000..9919312eff25 --- /dev/null +++ b/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py @@ -0,0 +1,76 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +""" +FILE: user_credential_sample_async.py +DESCRIPTION: + These samples demonstrate create a CommunicationTokenCredential object. + The `CommunicationTokenCredential` object is used to authenticate a user with Communication Services, + such as Chat or Calling. It optionally provides an auto-refresh mechanism to ensure a continuously + stable authentication state during communications. + +USAGE: + python user_credential_sample_async.py + Set the environment variables with your own values before running the sample: + 1) COMMUNICATION_SAMPLES_CONNECTION_STRING - the connection string in your Communication Services resource +""" + + +import os +import asyncio +from azure.communication.chat.aio import CommunicationTokenCredential +from azure.communication.identity import CommunicationIdentityClient + +class CommunicationTokenCredentialSamples(object): + + connection_string = os.environ.get("COMMUNICATION_SAMPLES_CONNECTION_STRING", None) + if not connection_string: + raise ValueError("Set COMMUNICATION_SAMPLES_CONNECTION_STRING env before run this sample.") + + identity_client = CommunicationIdentityClient.from_connection_string(connection_string) + user = identity_client.create_user() + tokenresponse = identity_client.get_token(user, scopes=["chat"]) + token = tokenresponse.token + + async def create_credential_with_static_token(self): + # For short-lived clients, refreshing the token upon expiry is not necessary + # and `CommunicationTokenCredential` may be instantiated with a static token. + async with CommunicationTokenCredential(self.token) as credential: + tokenresponse = await credential.get_token() + print("Token issued with value: " + tokenresponse.token) + + async def create_credential_with_refreshing_callback(self): + # Alternatively, for long-lived clients, you can create a `CommunicationTokenCredential` with a callback to renew tokens if expired. + # Here we assume that we have a function `fetch_token_from_server` that makes a network request to retrieve a token string for a user. + # It's necessary that the `fetch_token_from_server` function returns a valid token (with an expiration date set in the future) at all times. + fetch_token_from_server = lambda: None + async with CommunicationTokenCredential( + self.token, token_refresher=fetch_token_from_server) as credential: + tokenresponse = await credential.get_token() + print("Token issued with value: " + tokenresponse.token) + + async def create_credential_with_proactive_refreshing_callback(self): + # Optionally, you can enable proactive token refreshing where a fresh token will be acquired as soon as the + # previous token approaches expiry. Using this method, your requests are less likely to be blocked to acquire a fresh token + fetch_token_from_server = lambda: None + async with CommunicationTokenCredential( + self.token, token_refresher=fetch_token_from_server, refresh_proactively=True) as credential: + tokenresponse = await credential.get_token() + print("Token issued with value: " + tokenresponse.token) + + def clean_up(self): + print("cleaning up: deleting created user.") + self.identity_client.delete_user(self.user) + +async def main(): + sample = CommunicationTokenCredentialSamples() + await sample.create_credential_with_static_token() + await sample.create_credential_with_refreshing_callback() + await sample.create_credential_with_proactive_refreshing_callback() + sample.clean_up() + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 5d0365044574..44bbcc6e46e3 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -125,4 +125,6 @@ def __exit__(self, *args): self.close() def close(self) -> None: + if self._timer is not None: + self._timer.cancel() self._timer = None diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index b1ed5811f3ad..7df9493939c1 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -127,4 +127,6 @@ async def __aexit__(self, *args): await self.close() async def close(self) -> None: + if self._timer is not None: + self._timer.cancel() self._timer = None diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index fded2018bb48..5aeb67774b48 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -28,9 +28,9 @@ def setUpClass(cls): 100) # 1/1/1970 def test_communicationtokencredential_decodes_token(self): - credential = CommunicationTokenCredential(self.sample_token) - access_token = credential.get_token() - self.assertEqual(access_token.token, self.sample_token) + with CommunicationTokenCredential(self.sample_token) as credential: + access_token = credential.get_token() + self.assertEqual(access_token.token, self.sample_token) def test_communicationtokencredential_throws_if_invalid_token(self): self.assertRaises( @@ -51,27 +51,24 @@ def test_communicationtokencredential_throws_if_refresh_proactively_enabled_with assert str(err.value) == "'token_refresher' must not be None." def test_communicationtokencredential_static_token_returns_expired_token(self): - credential = CommunicationTokenCredential(self.expired_token) - self.assertEqual(credential.get_token().token, self.expired_token) + with CommunicationTokenCredential(self.expired_token) as credential: + self.assertEqual(credential.get_token().token, self.expired_token) def test_communicationtokencredential_token_expired_refresh_called(self): refresher = MagicMock( return_value=create_access_token(self.sample_token)) - access_token = CommunicationTokenCredential( - self.expired_token, - token_refresher=refresher).get_token() + with CommunicationTokenCredential(self.expired_token, token_refresher=refresher) as credential: + access_token = credential.get_token() refresher.assert_called_once() self.assertEqual(access_token.token, self.sample_token) def test_communicationtokencredential_raises_if_refresher_returns_expired_token(self): refresher = MagicMock( return_value=create_access_token(self.expired_token)) - credential = CommunicationTokenCredential( - self.expired_token, token_refresher=refresher) - - with self.assertRaises(ValueError): - credential.get_token() - self.assertEqual(refresher.call_count, 1) + with CommunicationTokenCredential(self.expired_token, token_refresher=refresher) as credential: + with self.assertRaises(ValueError): + credential.get_token() + self.assertEqual(refresher.call_count, 1) def test_uses_initial_token_as_expected(self): refresher = MagicMock( From 2c44b1af53e84fd8e3e1d9f91a1ff73f12d452a2 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Tue, 8 Mar 2022 14:57:44 +0100 Subject: [PATCH 42/49] renaming proactive refresh flag --- .../azure-communication-chat/CHANGELOG.md | 3 +- .../chat/_shared/user_credential.py | 17 +++---- .../chat/_shared/user_credential_async.py | 16 +++---- .../samples/user_credential_sample.py | 2 +- .../samples/user_credential_sample_async.py | 2 +- .../identity/_shared/user_credential.py | 17 +++---- .../identity/_shared/user_credential_async.py | 14 +++--- .../tests/test_user_credential.py | 45 +++++++++---------- .../tests/test_user_credential_async.py | 20 ++++----- 9 files changed, 68 insertions(+), 68 deletions(-) diff --git a/sdk/communication/azure-communication-chat/CHANGELOG.md b/sdk/communication/azure-communication-chat/CHANGELOG.md index b16a1272f5b7..df8c5ce833be 100644 --- a/sdk/communication/azure-communication-chat/CHANGELOG.md +++ b/sdk/communication/azure-communication-chat/CHANGELOG.md @@ -3,7 +3,8 @@ ## 1.2.0 (Unreleased) - Added support for proactive refreshing of tokens - - `CommunicationTokenCredential` exposes a new boolean keyword argument `refresh_proactively` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. + - `CommunicationTokenCredential` exposes a new boolean keyword argument `proactive_refresh` that defaults to `False`. If set to `True`, the refreshing of the token will be scheduled in the background ensuring continuous authentication state. + - Added disposal function `close` for `CommunicationTokenCredential`. ### Features Added diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 44bbcc6e46e3..ea12fc28b3c1 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -15,11 +15,12 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The sync token refresher to provide capacity to fetch a fresh token. + :keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration date must be in the future). - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. + :paramtype token_refresher: Callable[[], AccessToken] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. :raises: TypeError if paramater 'token' is not a string - :raises: ValueError if the 'refresh_proactively' is enabled without providing the 'token_refresher' callable. + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -30,8 +31,8 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) - if(self._refresh_proactively and self._token_refresher is None): + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): raise ValueError("'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) @@ -77,7 +78,7 @@ def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -103,7 +104,7 @@ def _wait_till_lock_owner_finishes_refreshing(self): self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -117,7 +118,7 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index c6b23909a02c..c0b09ac81315 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -7,8 +7,8 @@ from asyncio import Condition, Lock from datetime import timedelta from typing import Any -import six import sys +import six from .utils import get_current_utc_as_int from .utils import create_access_token from .utils_async import AsyncTimer @@ -20,9 +20,9 @@ class CommunicationTokenCredential(object): :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration date must be in the future). :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. :raises: TypeError if paramater 'token' is not a string - :raises: ValueError if the 'refresh_proactively' is enabled without providing the 'token_refresher' function. + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -33,8 +33,8 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) - if(self._refresh_proactively and self._token_refresher is None): + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): raise ValueError("'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() @@ -82,7 +82,7 @@ async def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -109,7 +109,7 @@ async def _wait_till_lock_owner_finishes_refreshing(self): await self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -123,7 +123,7 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on async def __aenter__(self): - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-chat/samples/user_credential_sample.py b/sdk/communication/azure-communication-chat/samples/user_credential_sample.py index dac6b71deac3..131b78154813 100644 --- a/sdk/communication/azure-communication-chat/samples/user_credential_sample.py +++ b/sdk/communication/azure-communication-chat/samples/user_credential_sample.py @@ -56,7 +56,7 @@ def create_credential_with_proactive_refreshing_callback(self): # previous token approaches expiry. Using this method, your requests are less likely to be blocked to acquire a fresh token fetch_token_from_server = lambda: None with CommunicationTokenCredential( - self.token, token_refresher=fetch_token_from_server, refresh_proactively=True) as credential: + self.token, token_refresher=fetch_token_from_server, proactive_refresh=True) as credential: tokenresponse = credential.get_token() print("Token issued with value: " + tokenresponse.token) diff --git a/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py b/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py index 9919312eff25..62416301f0ca 100644 --- a/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py +++ b/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py @@ -57,7 +57,7 @@ async def create_credential_with_proactive_refreshing_callback(self): # previous token approaches expiry. Using this method, your requests are less likely to be blocked to acquire a fresh token fetch_token_from_server = lambda: None async with CommunicationTokenCredential( - self.token, token_refresher=fetch_token_from_server, refresh_proactively=True) as credential: + self.token, token_refresher=fetch_token_from_server, proactive_refresh=True) as credential: tokenresponse = await credential.get_token() print("Token issued with value: " + tokenresponse.token) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 44bbcc6e46e3..ea12fc28b3c1 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -15,11 +15,12 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword callable token_refresher: The sync token refresher to provide capacity to fetch a fresh token. + :keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration date must be in the future). - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. + :paramtype token_refresher: Callable[[], AccessToken] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. :raises: TypeError if paramater 'token' is not a string - :raises: ValueError if the 'refresh_proactively' is enabled without providing the 'token_refresher' callable. + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -30,8 +31,8 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) - if(self._refresh_proactively and self._token_refresher is None): + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): raise ValueError("'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) @@ -77,7 +78,7 @@ def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -103,7 +104,7 @@ def _wait_till_lock_owner_finishes_refreshing(self): self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -117,7 +118,7 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 7df9493939c1..af64c4c997b7 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -19,9 +19,9 @@ class CommunicationTokenCredential(object): :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration date must be in the future). :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. :raises: TypeError if paramater 'token' is not a string - :raises: ValueError if the 'refresh_proactively' is enabled without providing the 'token_refresher' function. + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -32,8 +32,8 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) - if(self._refresh_proactively and self._token_refresher is None): + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): raise ValueError("'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() @@ -78,7 +78,7 @@ async def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -105,7 +105,7 @@ async def _wait_till_lock_owner_finishes_refreshing(self): await self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -119,7 +119,7 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on async def __aenter__(self): - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index 5aeb67774b48..fbedbc48aba4 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -39,14 +39,14 @@ def test_communicationtokencredential_throws_if_invalid_token(self): def test_communicationtokencredential_throws_if_nonstring_token(self): self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) - def test_communicationtokencredential_throws_if_refresh_proactively_enabled_without_token_refresher(self): + def test_communicationtokencredential_throws_if_proactive_refresh_enabled_without_token_refresher(self): with pytest.raises(ValueError) as err: - CommunicationTokenCredential(self.sample_token, refresh_proactively=True) + CommunicationTokenCredential(self.sample_token, proactive_refresh=True) assert str(err.value) == "'token_refresher' must not be None." with pytest.raises(ValueError) as err: CommunicationTokenCredential( self.sample_token, - refresh_proactively=True, + proactive_refresh=True, token_refresher=None) assert str(err.value) == "'token_refresher' must not be None." @@ -74,7 +74,7 @@ def test_uses_initial_token_as_expected(self): refresher = MagicMock( return_value=create_access_token(self.expired_token)) credential = CommunicationTokenCredential( - self.sample_token, token_refresher=refresher, refresh_proactively=True) + self.sample_token, token_refresher=refresher, proactive_refresh=True) with credential: access_token = credential.get_token() @@ -98,7 +98,7 @@ def test_proactive_refresher_should_not_be_called_before_specified_time(self): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) with credential: access_token = credential.get_token() @@ -125,7 +125,7 @@ def test_proactive_refresher_should_be_called_after_specified_time(self): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) with credential: access_token = credential.get_token() @@ -150,7 +150,7 @@ def test_proactive_refresher_keeps_scheduling_again(self): credential = CommunicationTokenCredential( expired_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) with credential: access_token = credential.get_token() with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): @@ -172,7 +172,7 @@ def test_fractional_backoff_applied_when_token_expiring(self): credential = CommunicationTokenCredential( expiring_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) next_milestone = token_validity_seconds / 2 @@ -184,31 +184,28 @@ def test_fractional_backoff_applied_when_token_expiring(self): next_milestone = next_milestone / 2 assert credential._timer.interval == next_milestone - def test_exit_cancels_timer(self): - refreshed_token = create_access_token( - generate_token_with_custom_expiry(30 * 60)) - refresher = MagicMock(return_value=refreshed_token) - expired_token = generate_token_with_custom_expiry(-10 * 60) - credential = CommunicationTokenCredential( - expired_token, - token_refresher=refresher, - refresh_proactively=True) - with credential: - assert credential._timer is not None - assert credential._timer is None - with credential: - assert credential._timer is not None - def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) new_token = generate_token_with_custom_expiry(10 * 60) refresher = MagicMock(return_value=create_access_token(new_token)) credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) + generated_token, token_refresher=refresher, proactive_refresh=False) with credential: for _ in range(10): access_token = credential.get_token() refresher.assert_not_called() assert generated_token == access_token.token + + def test_exit_cancels_timer(self): + refresher = MagicMock(return_value=self.sample_token) + credential = CommunicationTokenCredential( + self.expired_token,token_refresher=refresher, proactive_refresh=True) + with credential: + assert credential._timer is not None + assert credential._timer is None + with credential: + assert credential._timer is not None + credential.close() + assert credential._timer is None diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 05cddec9f3c3..5fc6c209df1c 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -41,14 +41,14 @@ async def test_init_with_valid_token(self): assert initial_token == access_token.token @pytest.mark.asyncio - async def test_communicationtokencredential_throws_if_refresh_proactively_enabled_without_token_refresher(self): + async def test_communicationtokencredential_throws_if_proactive_refresh_enabled_without_token_refresher(self): with pytest.raises(ValueError) as err: - CommunicationTokenCredential(self.sample_token, refresh_proactively=True) + CommunicationTokenCredential(self.sample_token, proactive_refresh=True) assert str(err.value) == "'token_refresher' must not be None." with pytest.raises(ValueError) as err: CommunicationTokenCredential( self.sample_token, - refresh_proactively=True, + proactive_refresh=True, token_refresher=None) assert str(err.value) == "'token_refresher' must not be None." @@ -75,7 +75,7 @@ async def test_refresher_should_not_be_called_before_expiring_time(self): return_value=create_access_token(refreshed_token)) credential = CommunicationTokenCredential( - initial_token, token_refresher=refresher, refresh_proactively=True) + initial_token, token_refresher=refresher, proactive_refresh=True) async with credential: access_token = await credential.get_token() @@ -89,7 +89,7 @@ async def test_refresher_should_not_be_called_when_token_still_valid(self): refresher = MagicMock(return_value=create_access_token(new_token)) credential = CommunicationTokenCredential( - generated_token, token_refresher=refresher, refresh_proactively=False) + generated_token, token_refresher=refresher, proactive_refresh=False) async with credential: for _ in range(10): access_token = await credential.get_token() @@ -129,7 +129,7 @@ async def test_proactive_refresher_should_not_be_called_before_specified_time(se credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) async with credential: access_token = await credential.get_token() @@ -157,7 +157,7 @@ async def test_proactive_refresher_should_be_called_after_specified_time(self): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) async with credential: access_token = await credential.get_token() @@ -183,7 +183,7 @@ async def test_proactive_refresher_keeps_scheduling_again(self): credential = CommunicationTokenCredential( expired_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) async with credential: access_token = await credential.get_token() with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): @@ -206,7 +206,7 @@ async def test_fractional_backoff_applied_when_token_expiring(self): credential = CommunicationTokenCredential( expiring_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) next_milestone = token_validity_seconds / 2 assert credential._timer.interval == next_milestone @@ -228,7 +228,7 @@ async def test_exit_cancels_timer(self): credential = CommunicationTokenCredential( expired_token, token_refresher=refresher, - refresh_proactively=True) + proactive_refresh=True) async with credential: assert credential._timer is not None assert refresher.call_count == 0 From 22bf69f29b6c85b3b41acbd48ace515da44af3d4 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 10 Mar 2022 10:19:57 +0100 Subject: [PATCH 43/49] latest PR comments fix --- .../azure/communication/chat/_shared/user_credential.py | 5 ++++- .../communication/chat/_shared/user_credential_async.py | 5 ++++- .../azure/communication/chat/_shared/utils_async.py | 2 ++ .../communication/identity/_shared/user_credential.py | 5 ++++- .../identity/_shared/user_credential_async.py | 5 ++++- .../azure/communication/identity/_shared/utils_async.py | 2 ++ .../tests/test_user_credential.py | 9 +++++++-- .../tests/test_user_credential_async.py | 4 ++-- 8 files changed, 29 insertions(+), 8 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index ea12fc28b3c1..900446dd1f04 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -19,6 +19,9 @@ class CommunicationTokenCredential(object): The returned token must be valid (expiration date must be in the future). :paramtype token_refresher: Callable[[], AccessToken] :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ @@ -33,7 +36,7 @@ def __init__(self, token: str, **kwargs: Any): self._token_refresher = kwargs.pop('token_refresher', None) self._proactive_refresh = kwargs.pop('proactive_refresh', False) if(self._proactive_refresh and self._token_refresher is None): - raise ValueError("'token_refresher' must not be None.") + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index c0b09ac81315..e9c1a67cecda 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -21,6 +21,9 @@ class CommunicationTokenCredential(object): The returned token must be valid (expiration date must be in the future). :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ @@ -35,7 +38,7 @@ def __init__(self, token: str, **kwargs: Any): self._token_refresher = kwargs.pop('token_refresher', None) self._proactive_refresh = kwargs.pop('proactive_refresh', False) if(self._proactive_refresh and self._token_refresher is None): - raise ValueError("'token_refresher' must not be None.") + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() if sys.version_info[:3] == (3, 10, 0): diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py index 2b6695be02f5..86e0e04d273c 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils_async.py @@ -26,4 +26,6 @@ async def _job(self): await self._callback() def cancel(self): + if self._task is not None: + self._task.cancel() self._task = None diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index ea12fc28b3c1..900446dd1f04 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -19,6 +19,9 @@ class CommunicationTokenCredential(object): The returned token must be valid (expiration date must be in the future). :paramtype token_refresher: Callable[[], AccessToken] :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ @@ -33,7 +36,7 @@ def __init__(self, token: str, **kwargs: Any): self._token_refresher = kwargs.pop('token_refresher', None) self._proactive_refresh = kwargs.pop('proactive_refresh', False) if(self._proactive_refresh and self._token_refresher is None): - raise ValueError("'token_refresher' must not be None.") + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index af64c4c997b7..85e5d09eb5d3 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -20,6 +20,9 @@ class CommunicationTokenCredential(object): The returned token must be valid (expiration date must be in the future). :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ @@ -34,7 +37,7 @@ def __init__(self, token: str, **kwargs: Any): self._token_refresher = kwargs.pop('token_refresher', None) self._proactive_refresh = kwargs.pop('proactive_refresh', False) if(self._proactive_refresh and self._token_refresher is None): - raise ValueError("'token_refresher' must not be None.") + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() self._lock = Condition(self._async_mutex) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py index 2b6695be02f5..86e0e04d273c 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils_async.py @@ -26,4 +26,6 @@ async def _job(self): await self._callback() def cancel(self): + if self._task is not None: + self._task.cancel() self._task = None diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index fbedbc48aba4..b1039cc2925c 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -4,6 +4,7 @@ # license information. # -------------------------------------------------------------------------- from typing import Type +import platform import pytest from unittest import TestCase try: @@ -42,13 +43,13 @@ def test_communicationtokencredential_throws_if_nonstring_token(self): def test_communicationtokencredential_throws_if_proactive_refresh_enabled_without_token_refresher(self): with pytest.raises(ValueError) as err: CommunicationTokenCredential(self.sample_token, proactive_refresh=True) - assert str(err.value) == "'token_refresher' must not be None." + assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." with pytest.raises(ValueError) as err: CommunicationTokenCredential( self.sample_token, proactive_refresh=True, token_refresher=None) - assert str(err.value) == "'token_refresher' must not be None." + assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." def test_communicationtokencredential_static_token_returns_expired_token(self): with CommunicationTokenCredential(self.expired_token) as credential: @@ -134,6 +135,7 @@ def test_proactive_refresher_should_be_called_after_specified_time(self): # check that next refresh is always scheduled assert credential._timer is not None + @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason="This test takes too long for pypy") def test_proactive_refresher_keeps_scheduling_again(self): refresh_minutes = 10 token_validity_minutes = 60 @@ -161,6 +163,7 @@ def test_proactive_refresher_keeps_scheduling_again(self): # check that next refresh is always scheduled assert credential._timer is not None + @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason="This test takes too long for pypy") def test_fractional_backoff_applied_when_token_expiring(self): token_validity_seconds = 5 * 60 expiring_token = generate_token_with_custom_expiry( @@ -184,6 +187,7 @@ def test_fractional_backoff_applied_when_token_expiring(self): next_milestone = next_milestone / 2 assert credential._timer.interval == next_milestone + @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason="This test takes too long for pypy") def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) new_token = generate_token_with_custom_expiry(10 * 60) @@ -198,6 +202,7 @@ def test_refresher_should_not_be_called_when_token_still_valid(self): refresher.assert_not_called() assert generated_token == access_token.token + @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason="This test takes too long for pypy") def test_exit_cancels_timer(self): refresher = MagicMock(return_value=self.sample_token) credential = CommunicationTokenCredential( diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 5fc6c209df1c..08e06712399f 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -44,13 +44,13 @@ async def test_init_with_valid_token(self): async def test_communicationtokencredential_throws_if_proactive_refresh_enabled_without_token_refresher(self): with pytest.raises(ValueError) as err: CommunicationTokenCredential(self.sample_token, proactive_refresh=True) - assert str(err.value) == "'token_refresher' must not be None." + assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." with pytest.raises(ValueError) as err: CommunicationTokenCredential( self.sample_token, proactive_refresh=True, token_refresher=None) - assert str(err.value) == "'token_refresher' must not be None." + assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." @pytest.mark.asyncio async def test_refresher_should_be_called_immediately_with_expired_token(self): From 4ec2b1de1ff1aece1ddd83cca22a59fb57bd8072 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Fri, 11 Mar 2022 10:00:14 +0100 Subject: [PATCH 44/49] samples are refactored --- .../samples/user_credential_sample.py | 20 +++++++++---------- .../samples/user_credential_sample_async.py | 20 +++++++++---------- .../tests/test_user_credential.py | 3 --- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/sdk/communication/azure-communication-chat/samples/user_credential_sample.py b/sdk/communication/azure-communication-chat/samples/user_credential_sample.py index 131b78154813..055fd9e4d96a 100644 --- a/sdk/communication/azure-communication-chat/samples/user_credential_sample.py +++ b/sdk/communication/azure-communication-chat/samples/user_credential_sample.py @@ -7,7 +7,7 @@ """ FILE: user_credential_sample.py DESCRIPTION: - These samples demonstrate create a CommunicationTokenCredential object. + These samples demonstrate creating a `CommunicationTokenCredential` object. The `CommunicationTokenCredential` object is used to authenticate a user with Communication Services, such as Chat or Calling. It optionally provides an auto-refresh mechanism to ensure a continuously stable authentication state during communications. @@ -27,19 +27,19 @@ class CommunicationTokenCredentialSamples(object): connection_string = os.environ.get("COMMUNICATION_SAMPLES_CONNECTION_STRING", None) if not connection_string: - raise ValueError("Set COMMUNICATION_SAMPLES_CONNECTION_STRING env before run this sample.") + raise ValueError("Set COMMUNICATION_SAMPLES_CONNECTION_STRING env before running this sample.") identity_client = CommunicationIdentityClient.from_connection_string(connection_string) user = identity_client.create_user() - tokenresponse = identity_client.get_token(user, scopes=["chat"]) - token = tokenresponse.token + token_response = identity_client.get_token(user, scopes=["chat"]) + token = token_response.token def create_credential_with_static_token(self): # For short-lived clients, refreshing the token upon expiry is not necessary # and `CommunicationTokenCredential` may be instantiated with a static token. with CommunicationTokenCredential(self.token) as credential: - tokenresponse = credential.get_token() - print("Token issued with value: " + tokenresponse.token) + token_response = credential.get_token() + print("Token issued with value: " + token_response.token) def create_credential_with_refreshing_callback(self): # Alternatively, for long-lived clients, you can create a `CommunicationTokenCredential` with a callback to renew tokens if expired. @@ -48,8 +48,8 @@ def create_credential_with_refreshing_callback(self): fetch_token_from_server = lambda: None with CommunicationTokenCredential( self.token, token_refresher=fetch_token_from_server) as credential: - tokenresponse = credential.get_token() - print("Token issued with value: " + tokenresponse.token) + token_response = credential.get_token() + print("Token issued with value: " + token_response.token) def create_credential_with_proactive_refreshing_callback(self): # Optionally, you can enable proactive token refreshing where a fresh token will be acquired as soon as the @@ -57,8 +57,8 @@ def create_credential_with_proactive_refreshing_callback(self): fetch_token_from_server = lambda: None with CommunicationTokenCredential( self.token, token_refresher=fetch_token_from_server, proactive_refresh=True) as credential: - tokenresponse = credential.get_token() - print("Token issued with value: " + tokenresponse.token) + token_response = credential.get_token() + print("Token issued with value: " + token_response.token) def clean_up(self): print("cleaning up: deleting created user.") diff --git a/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py b/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py index 62416301f0ca..60791cee875c 100644 --- a/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py +++ b/sdk/communication/azure-communication-chat/samples/user_credential_sample_async.py @@ -7,7 +7,7 @@ """ FILE: user_credential_sample_async.py DESCRIPTION: - These samples demonstrate create a CommunicationTokenCredential object. + These samples demonstrate creating a `CommunicationTokenCredential` object. The `CommunicationTokenCredential` object is used to authenticate a user with Communication Services, such as Chat or Calling. It optionally provides an auto-refresh mechanism to ensure a continuously stable authentication state during communications. @@ -28,19 +28,19 @@ class CommunicationTokenCredentialSamples(object): connection_string = os.environ.get("COMMUNICATION_SAMPLES_CONNECTION_STRING", None) if not connection_string: - raise ValueError("Set COMMUNICATION_SAMPLES_CONNECTION_STRING env before run this sample.") + raise ValueError("Set COMMUNICATION_SAMPLES_CONNECTION_STRING env before running this sample.") identity_client = CommunicationIdentityClient.from_connection_string(connection_string) user = identity_client.create_user() - tokenresponse = identity_client.get_token(user, scopes=["chat"]) - token = tokenresponse.token + token_response = identity_client.get_token(user, scopes=["chat"]) + token = token_response.token async def create_credential_with_static_token(self): # For short-lived clients, refreshing the token upon expiry is not necessary # and `CommunicationTokenCredential` may be instantiated with a static token. async with CommunicationTokenCredential(self.token) as credential: - tokenresponse = await credential.get_token() - print("Token issued with value: " + tokenresponse.token) + token_response = await credential.get_token() + print("Token issued with value: " + token_response.token) async def create_credential_with_refreshing_callback(self): # Alternatively, for long-lived clients, you can create a `CommunicationTokenCredential` with a callback to renew tokens if expired. @@ -49,8 +49,8 @@ async def create_credential_with_refreshing_callback(self): fetch_token_from_server = lambda: None async with CommunicationTokenCredential( self.token, token_refresher=fetch_token_from_server) as credential: - tokenresponse = await credential.get_token() - print("Token issued with value: " + tokenresponse.token) + token_response = await credential.get_token() + print("Token issued with value: " + token_response.token) async def create_credential_with_proactive_refreshing_callback(self): # Optionally, you can enable proactive token refreshing where a fresh token will be acquired as soon as the @@ -58,8 +58,8 @@ async def create_credential_with_proactive_refreshing_callback(self): fetch_token_from_server = lambda: None async with CommunicationTokenCredential( self.token, token_refresher=fetch_token_from_server, proactive_refresh=True) as credential: - tokenresponse = await credential.get_token() - print("Token issued with value: " + tokenresponse.token) + token_response = await credential.get_token() + print("Token issued with value: " + token_response.token) def clean_up(self): print("cleaning up: deleting created user.") diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index b1039cc2925c..bcf05bf3dbbc 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -163,7 +163,6 @@ def test_proactive_refresher_keeps_scheduling_again(self): # check that next refresh is always scheduled assert credential._timer is not None - @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason="This test takes too long for pypy") def test_fractional_backoff_applied_when_token_expiring(self): token_validity_seconds = 5 * 60 expiring_token = generate_token_with_custom_expiry( @@ -187,7 +186,6 @@ def test_fractional_backoff_applied_when_token_expiring(self): next_milestone = next_milestone / 2 assert credential._timer.interval == next_milestone - @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason="This test takes too long for pypy") def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) new_token = generate_token_with_custom_expiry(10 * 60) @@ -202,7 +200,6 @@ def test_refresher_should_not_be_called_when_token_still_valid(self): refresher.assert_not_called() assert generated_token == access_token.token - @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason="This test takes too long for pypy") def test_exit_cancels_timer(self): refresher = MagicMock(return_value=self.sample_token) credential = CommunicationTokenCredential( From d412335d7627d9f2d20c66b140c0371cc7c39c82 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Fri, 11 Mar 2022 10:46:21 +0100 Subject: [PATCH 45/49] reflecting shared folder changes to other modalitites --- .../identity/_shared/user_credential_async.py | 4 ++ .../_shared/user_credential.py | 51 +++++++++++-------- .../_shared/user_credential_async.py | 38 +++++++++----- .../networktraversal/_shared/utils_async.py | 1 + .../tests/_shared/helper.py | 19 +++---- .../phonenumbers/_shared/user_credential.py | 51 +++++++++++-------- .../_shared/user_credential_async.py | 38 +++++++++----- .../phonenumbers/_shared/utils_async.py | 1 + .../test/_shared/helper.py | 13 ++--- .../sms/_shared/user_credential.py | 51 +++++++++++-------- .../sms/_shared/user_credential_async.py | 38 +++++++++----- .../communication/sms/_shared/utils_async.py | 1 + .../tests/_shared/helper.py | 15 ++---- 13 files changed, 187 insertions(+), 134 deletions(-) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index 85e5d09eb5d3..e9c1a67cecda 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -7,6 +7,7 @@ from asyncio import Condition, Lock from datetime import timedelta from typing import Any +import sys import six from .utils import get_current_utc_as_int from .utils import create_access_token @@ -40,6 +41,9 @@ def __init__(self, token: str, **kwargs: Any): raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 2817ed7f0083..900446dd1f04 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer +from threading import Lock, Condition, Timer, TIMEOUT_MAX from datetime import timedelta from typing import Any import six @@ -15,11 +15,15 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], AccessToken] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -30,19 +34,12 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() - - def __enter__(self): - return self - - def __exit__(self, *args): - if self._timer is not None: - self._timer.cancel() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -84,7 +81,7 @@ def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -101,15 +98,16 @@ def _schedule_refresh(self): # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() - self._timer = Timer(timespan, self._update_token_and_reschedule) - self._timer.start() + if timespan <= TIMEOUT_MAX: + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -121,3 +119,16 @@ def _is_token_expiring_soon(self, token): @classmethod def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on + + def __enter__(self): + if self._proactive_refresh: + self._schedule_refresh() + return self + + def __exit__(self, *args): + self.close() + + def close(self) -> None: + if self._timer is not None: + self._timer.cancel() + self._timer = None diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index cf31012bd4e0..e9c1a67cecda 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -7,6 +7,7 @@ from asyncio import Condition, Lock from datetime import timedelta from typing import Any +import sys import six from .utils import get_current_utc_as_int from .utils import create_access_token @@ -16,11 +17,15 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -31,13 +36,16 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -77,7 +85,7 @@ async def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -104,7 +112,7 @@ async def _wait_till_lock_owner_finishes_refreshing(self): await self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -117,13 +125,15 @@ def _is_token_expiring_soon(self, token): def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on - async def close(self) -> None: - pass - async def __aenter__(self): + if self._proactive_refresh: + self._schedule_refresh() return self async def __aexit__(self, *args): + await self.close() + + async def close(self) -> None: if self._timer is not None: self._timer.cancel() - await self.close() + self._timer = None diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils_async.py index f2472e2121af..86e0e04d273c 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/utils_async.py @@ -28,3 +28,4 @@ async def _job(self): def cancel(self): if self._task is not None: self._task.cancel() + self._task = None diff --git a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py index e3eaf9ee90bc..4d3585695f5a 100644 --- a/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-networktraversal/tests/_shared/helper.py @@ -8,19 +8,15 @@ from azure_devtools.scenario_tests import RecordingProcessor from datetime import datetime, timedelta from functools import wraps -from urllib.parse import urlparse +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse import sys -if sys.version_info[0] < 3 or sys.version_info[1] < 4: - # python version < 3.3 - import time - def generate_token_with_custom_expiry(valid_for_seconds): - date = datetime.now() + timedelta(seconds=valid_for_seconds) - return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) -else: - def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( @@ -29,6 +25,7 @@ def generate_token_with_custom_expiry_epoch(expires_on_epoch): {base64expiry}.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs''') return token_template + class URIIdentityReplacer(RecordingProcessor): """Replace the identity in request uri""" def process_request(self, request): diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 2817ed7f0083..900446dd1f04 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer +from threading import Lock, Condition, Timer, TIMEOUT_MAX from datetime import timedelta from typing import Any import six @@ -15,11 +15,15 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], AccessToken] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -30,19 +34,12 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() - - def __enter__(self): - return self - - def __exit__(self, *args): - if self._timer is not None: - self._timer.cancel() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -84,7 +81,7 @@ def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -101,15 +98,16 @@ def _schedule_refresh(self): # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() - self._timer = Timer(timespan, self._update_token_and_reschedule) - self._timer.start() + if timespan <= TIMEOUT_MAX: + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -121,3 +119,16 @@ def _is_token_expiring_soon(self, token): @classmethod def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on + + def __enter__(self): + if self._proactive_refresh: + self._schedule_refresh() + return self + + def __exit__(self, *args): + self.close() + + def close(self) -> None: + if self._timer is not None: + self._timer.cancel() + self._timer = None diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index cf31012bd4e0..e9c1a67cecda 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -7,6 +7,7 @@ from asyncio import Condition, Lock from datetime import timedelta from typing import Any +import sys import six from .utils import get_current_utc_as_int from .utils import create_access_token @@ -16,11 +17,15 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -31,13 +36,16 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -77,7 +85,7 @@ async def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -104,7 +112,7 @@ async def _wait_till_lock_owner_finishes_refreshing(self): await self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -117,13 +125,15 @@ def _is_token_expiring_soon(self, token): def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on - async def close(self) -> None: - pass - async def __aenter__(self): + if self._proactive_refresh: + self._schedule_refresh() return self async def __aexit__(self, *args): + await self.close() + + async def close(self) -> None: if self._timer is not None: self._timer.cancel() - await self.close() + self._timer = None diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils_async.py index f2472e2121af..86e0e04d273c 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils_async.py @@ -28,3 +28,4 @@ async def _job(self): def cancel(self): if self._task is not None: self._task.cancel() + self._task = None diff --git a/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py b/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py index de3adc588fc2..4d3585695f5a 100644 --- a/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py +++ b/sdk/communication/azure-communication-phonenumbers/test/_shared/helper.py @@ -14,16 +14,9 @@ from urlparse import urlparse import sys -if sys.version_info[0] < 3 or sys.version_info[1] < 4: - # python version < 3.3 - import time - def generate_token_with_custom_expiry(valid_for_seconds): - date = datetime.now() + timedelta(seconds=valid_for_seconds) - return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) -else: - def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 2817ed7f0083..900446dd1f04 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer +from threading import Lock, Condition, Timer, TIMEOUT_MAX from datetime import timedelta from typing import Any import six @@ -15,11 +15,15 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :keyword token_refresher: The sync token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], AccessToken] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -30,19 +34,12 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() - - def __enter__(self): - return self - - def __exit__(self, *args): - if self._timer is not None: - self._timer.cancel() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -84,7 +81,7 @@ def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -101,15 +98,16 @@ def _schedule_refresh(self): # Schedule the next refresh for when it gets in to the soon-to-expire window. timespan = token_ttl - timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() - self._timer = Timer(timespan, self._update_token_and_reschedule) - self._timer.start() + if timespan <= TIMEOUT_MAX: + self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): self._lock.release() self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -121,3 +119,16 @@ def _is_token_expiring_soon(self, token): @classmethod def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on + + def __enter__(self): + if self._proactive_refresh: + self._schedule_refresh() + return self + + def __exit__(self, *args): + self.close() + + def close(self) -> None: + if self._timer is not None: + self._timer.cancel() + self._timer = None diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index cf31012bd4e0..e9c1a67cecda 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -7,6 +7,7 @@ from asyncio import Condition, Lock from datetime import timedelta from typing import Any +import sys import six from .utils import get_current_utc_as_int from .utils import create_access_token @@ -16,11 +17,15 @@ class CommunicationTokenCredential(object): """Credential type used for authenticating to an Azure Communication service. :param str token: The token used to authenticate to an Azure Communication service. - :keyword Callable[[], Awaitable[azure.core.credentials.AccessToken]] token_refresher: The async token - refresher to provide capacity to fetch a fresh token. The returned token must be valid (expiration - date must be in the future). - :keyword bool refresh_proactively: Whether to refresh the token proactively or not. - :raises: TypeError + :keyword token_refresher: The async token refresher to provide capacity to fetch a fresh token. + The returned token must be valid (expiration date must be in the future). + :paramtype token_refresher: Callable[[], Awaitable[AccessToken]] + :keyword bool proactive_refresh: Whether to refresh the token proactively or not. + If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use + a background thread to attempt to refresh the token within 10 minutes before the cached token expires, + the proactive refresh will request a new token by calling the 'token_refresher' callback. + :raises: TypeError if paramater 'token' is not a string + :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ _ON_DEMAND_REFRESHING_INTERVAL_MINUTES = 2 @@ -31,13 +36,16 @@ def __init__(self, token: str, **kwargs: Any): raise TypeError("Token must be a string.") self._token = create_access_token(token) self._token_refresher = kwargs.pop('token_refresher', None) - self._refresh_proactively = kwargs.pop('refresh_proactively', False) + self._proactive_refresh = kwargs.pop('proactive_refresh', False) + if(self._proactive_refresh and self._token_refresher is None): + raise ValueError("When 'proactive_refresh' is True, 'token_refresher' must not be None.") self._timer = None self._async_mutex = Lock() + if sys.version_info[:3] == (3, 10, 0): + # Workaround for Python 3.10 bug(https://bugs.python.org/issue45416): + getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False - if self._refresh_proactively: - self._schedule_refresh() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -77,7 +85,7 @@ async def _update_token_and_reschedule(self): self._some_thread_refreshing = False self._lock.notify_all() raise - if self._refresh_proactively: + if self._proactive_refresh: self._schedule_refresh() return self._token @@ -104,7 +112,7 @@ async def _wait_till_lock_owner_finishes_refreshing(self): await self._lock.acquire() def _is_token_expiring_soon(self, token): - if self._refresh_proactively: + if self._proactive_refresh: interval = timedelta( minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES) else: @@ -117,13 +125,15 @@ def _is_token_expiring_soon(self, token): def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on - async def close(self) -> None: - pass - async def __aenter__(self): + if self._proactive_refresh: + self._schedule_refresh() return self async def __aexit__(self, *args): + await self.close() + + async def close(self) -> None: if self._timer is not None: self._timer.cancel() - await self.close() + self._timer = None diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils_async.py index f2472e2121af..86e0e04d273c 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils_async.py @@ -28,3 +28,4 @@ async def _job(self): def cancel(self): if self._task is not None: self._task.cancel() + self._task = None diff --git a/sdk/communication/azure-communication-sms/tests/_shared/helper.py b/sdk/communication/azure-communication-sms/tests/_shared/helper.py index fbad3bd34454..4d3585695f5a 100644 --- a/sdk/communication/azure-communication-sms/tests/_shared/helper.py +++ b/sdk/communication/azure-communication-sms/tests/_shared/helper.py @@ -14,16 +14,9 @@ from urlparse import urlparse import sys -if sys.version_info[0] < 3 or sys.version_info[1] < 4: - # python version < 3.3 - import time - def generate_token_with_custom_expiry(valid_for_seconds): - date = datetime.now() + timedelta(seconds=valid_for_seconds) - return generate_token_with_custom_expiry_epoch(time.mktime(date.timetuple())) -else: - def generate_token_with_custom_expiry(valid_for_seconds): - return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) - +def generate_token_with_custom_expiry(valid_for_seconds): + return generate_token_with_custom_expiry_epoch((datetime.now() + timedelta(seconds=valid_for_seconds)).timestamp()) + def generate_token_with_custom_expiry_epoch(expires_on_epoch): expiry_json = f'{{"exp": {str(expires_on_epoch)} }}' base64expiry = base64.b64encode( @@ -46,4 +39,4 @@ def process_request(self, request): def process_response(self, response): if 'url' in response: response['url'] = re.sub('/identities/([^/?]+)', '/identities/sanitized', response['url']) - return response + return response \ No newline at end of file From 8b374d15cb6090f13108c5e56c70a403badee3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0vihl=C3=ADk?= Date: Wed, 11 May 2022 22:01:26 +0200 Subject: [PATCH 46/49] fixed a typo --- sdk/communication/azure-communication-identity/README.md | 2 +- .../identity/_communication_identity_client.py | 8 ++++---- .../identity/aio/_communication_identity_client_async.py | 8 ++++---- .../tests/test_communication_identity_client.py | 8 ++++---- .../tests/test_communication_identity_client_async.py | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/sdk/communication/azure-communication-identity/README.md b/sdk/communication/azure-communication-identity/README.md index d023b1c3cb65..94b694ef4cfa 100644 --- a/sdk/communication/azure-communication-identity/README.md +++ b/sdk/communication/azure-communication-identity/README.md @@ -103,7 +103,7 @@ identity_client.delete_user(user) Use the `get_token_for_teams_user` method to exchange an AAD access token of a Teams User for a new Communication Identity access token. ```python -identity_client.get_token_for_teams_user(add_token) +identity_client.get_token_for_teams_user(aad_token) ``` # Troubleshooting diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py index 7c10ba66718e..ed9c6eff0249 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_communication_identity_client.py @@ -188,20 +188,20 @@ def revoke_tokens( @distributed_trace def get_token_for_teams_user( self, - add_token, # type: str + aad_token, # type: str **kwargs ): # type: (...) -> AccessToken """Exchanges an AAD access token of a Teams User for a new Communication Identity access token. - :param add_token: an AAD access token of a Teams User - :type add_token: str + :param aad_token: an AAD access token of a Teams User + :type aad_token: str :return: AccessToken :rtype: ~azure.core.credentials.AccessToken """ api_version = kwargs.pop("api_version", self._api_version) return self._identity_service_client.communication_identity.exchange_teams_user_access_token( - token=add_token, + token=aad_token, api_version=api_version, cls=lambda pr, u, e: AccessToken(u.token, u.expires_on), **kwargs) diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py index eab3e188a8d4..55d5b1dc6bac 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/aio/_communication_identity_client_async.py @@ -183,20 +183,20 @@ async def revoke_tokens( @distributed_trace_async async def get_token_for_teams_user( self, - add_token, # type: str + aad_token, # type: str **kwargs ) -> AccessToken: # type: (...) -> AccessToken """Exchanges an AAD access token of a Teams User for a new Communication Identity access token. - :param add_token: an AAD access token of a Teams User - :type add_token: str + :param aad_token: an AAD access token of a Teams User + :type aad_token: str :return: AccessToken :rtype: ~azure.core.credentials.AccessToken """ api_version = kwargs.pop("api_version", self._api_version) return await self._identity_service_client.communication_identity.exchange_teams_user_access_token( - token=add_token, + token=aad_token, api_version=api_version, cls=lambda pr, u, e: AccessToken(u.token, u.expires_on), **kwargs) diff --git a/sdk/communication/azure-communication-identity/tests/test_communication_identity_client.py b/sdk/communication/azure-communication-identity/tests/test_communication_identity_client.py index a88aee6e5b35..fe3251034b01 100644 --- a/sdk/communication/azure-communication-identity/tests/test_communication_identity_client.py +++ b/sdk/communication/azure-communication-identity/tests/test_communication_identity_client.py @@ -233,8 +233,8 @@ def test_get_token_for_teams_user_from_managed_identity(self, communication_live credential, http_logging_policy=get_http_logging_policy() ) - add_token = self.generate_teams_user_aad_token() - token_response = identity_client.get_token_for_teams_user(add_token) + aad_token = self.generate_teams_user_aad_token() + token_response = identity_client.get_token_for_teams_user(aad_token) assert token_response.token is not None @CommunicationPreparer() @@ -245,8 +245,8 @@ def test_get_token_for_teams_user_with_valid_token(self, communication_livetest_ communication_livetest_dynamic_connection_string, http_logging_policy=get_http_logging_policy() ) - add_token = self.generate_teams_user_aad_token() - token_response = identity_client.get_token_for_teams_user(add_token) + aad_token = self.generate_teams_user_aad_token() + token_response = identity_client.get_token_for_teams_user(aad_token) assert token_response.token is not None @CommunicationPreparer() diff --git a/sdk/communication/azure-communication-identity/tests/test_communication_identity_client_async.py b/sdk/communication/azure-communication-identity/tests/test_communication_identity_client_async.py index b4cde9c67227..7dc9a6027acd 100644 --- a/sdk/communication/azure-communication-identity/tests/test_communication_identity_client_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_communication_identity_client_async.py @@ -244,8 +244,8 @@ async def test_get_token_for_teams_user_from_managed_identity(self, communicatio http_logging_policy=get_http_logging_policy() ) async with identity_client: - add_token = self.generate_teams_user_aad_token() - token_response = await identity_client.get_token_for_teams_user(add_token) + aad_token = self.generate_teams_user_aad_token() + token_response = await identity_client.get_token_for_teams_user(aad_token) assert token_response.token is not None @@ -258,8 +258,8 @@ async def test_get_token_for_teams_user_with_valid_token(self, communication_liv http_logging_policy=get_http_logging_policy() ) async with identity_client: - add_token = self.generate_teams_user_aad_token() - token_response = await identity_client.get_token_for_teams_user(add_token) + aad_token = self.generate_teams_user_aad_token() + token_response = await identity_client.get_token_for_teams_user(aad_token) assert token_response.token is not None From 3fec4f94a1bbaa960c89d8326375e077d1b0f75b Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 25 May 2022 16:01:50 +0200 Subject: [PATCH 47/49] fix for pypy threading issue --- .../chat/_shared/user_credential.py | 9 +++++- .../azure/communication/chat/_shared/utils.py | 4 +-- .../identity/_shared/user_credential.py | 9 +++++- .../communication/identity/_shared/utils.py | 4 +-- .../tests/test_user_credential.py | 28 ++++++++++++++++--- .../networktraversal/_shared/policy.py | 2 +- .../_shared/user_credential.py | 9 +++++- .../phonenumbers/_shared/user_credential.py | 9 +++++- .../phonenumbers/_shared/utils.py | 4 +-- .../sms/_shared/user_credential.py | 9 +++++- .../azure/communication/sms/_shared/utils.py | 4 +-- 11 files changed, 73 insertions(+), 18 deletions(-) diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 900446dd1f04..64e04e58ceda 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer, TIMEOUT_MAX +from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event from datetime import timedelta from typing import Any import six @@ -40,6 +40,7 @@ def __init__(self, token: str, **kwargs: Any): self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + self._is_closed = Event() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -86,6 +87,8 @@ def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -100,6 +103,7 @@ def _schedule_refresh(self): minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() if timespan <= TIMEOUT_MAX: self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.daemon = True self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): @@ -121,6 +125,8 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): + if self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: self._schedule_refresh() return self @@ -132,3 +138,4 @@ def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py index 0eef1aa7514f..0b3556bbaa44 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/utils.py @@ -92,8 +92,8 @@ def create_access_token(token): payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) - except ValueError: - raise ValueError(token_parse_err_msg) + except ValueError as val_error: + raise ValueError(token_parse_err_msg) from val_error def get_authentication_policy( diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 900446dd1f04..64e04e58ceda 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer, TIMEOUT_MAX +from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event from datetime import timedelta from typing import Any import six @@ -40,6 +40,7 @@ def __init__(self, token: str, **kwargs: Any): self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + self._is_closed = Event() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -86,6 +87,8 @@ def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -100,6 +103,7 @@ def _schedule_refresh(self): minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() if timespan <= TIMEOUT_MAX: self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.daemon = True self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): @@ -121,6 +125,8 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): + if self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: self._schedule_refresh() return self @@ -132,3 +138,4 @@ def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py index 0eef1aa7514f..0b3556bbaa44 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/utils.py @@ -92,8 +92,8 @@ def create_access_token(token): payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) - except ValueError: - raise ValueError(token_parse_err_msg) + except ValueError as val_error: + raise ValueError(token_parse_err_msg) from val_error def get_authentication_policy( diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index bcf05bf3dbbc..ac076c3ad269 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -135,7 +135,6 @@ def test_proactive_refresher_should_be_called_after_specified_time(self): # check that next refresh is always scheduled assert credential._timer is not None - @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason="This test takes too long for pypy") def test_proactive_refresher_keeps_scheduling_again(self): refresh_minutes = 10 token_validity_minutes = 60 @@ -201,13 +200,34 @@ def test_refresher_should_not_be_called_when_token_still_valid(self): assert generated_token == access_token.token def test_exit_cancels_timer(self): - refresher = MagicMock(return_value=self.sample_token) + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) credential = CommunicationTokenCredential( self.expired_token,token_refresher=refresher, proactive_refresh=True) with credential: assert credential._timer is not None assert credential._timer is None + + credential = CommunicationTokenCredential( + self.expired_token,token_refresher=refresher, proactive_refresh=True) + credential.close() + assert credential._timer is None + + def test_exit_enter_scenario_throws_exception(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + credential = CommunicationTokenCredential( + self.expired_token,token_refresher=refresher, proactive_refresh=True) with credential: assert credential._timer is not None - credential.close() - assert credential._timer is None + assert credential._timer is None + + with pytest.raises(RuntimeError) as err: + with credential: + assert credential._timer is not None + assert str(err.value) == "An instance of CommunicationTokenCredential cannot be reused once it has been closed." + + + \ No newline at end of file diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/policy.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/policy.py index 301bfb545028..d4197ede0e38 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/policy.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/policy.py @@ -21,7 +21,7 @@ def __init__(self, decode_url=False # type: bool ): # type: (...) -> None - super().__init__() + super(HMACCredentialsPolicy, self).__init__() if host.startswith("https://"): self._host = host.replace("https://", "") diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 900446dd1f04..64e04e58ceda 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer, TIMEOUT_MAX +from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event from datetime import timedelta from typing import Any import six @@ -40,6 +40,7 @@ def __init__(self, token: str, **kwargs: Any): self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + self._is_closed = Event() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -86,6 +87,8 @@ def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -100,6 +103,7 @@ def _schedule_refresh(self): minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() if timespan <= TIMEOUT_MAX: self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.daemon = True self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): @@ -121,6 +125,8 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): + if self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: self._schedule_refresh() return self @@ -132,3 +138,4 @@ def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 900446dd1f04..64e04e58ceda 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer, TIMEOUT_MAX +from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event from datetime import timedelta from typing import Any import six @@ -40,6 +40,7 @@ def __init__(self, token: str, **kwargs: Any): self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + self._is_closed = Event() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -86,6 +87,8 @@ def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -100,6 +103,7 @@ def _schedule_refresh(self): minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() if timespan <= TIMEOUT_MAX: self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.daemon = True self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): @@ -121,6 +125,8 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): + if self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: self._schedule_refresh() return self @@ -132,3 +138,4 @@ def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py index 0eef1aa7514f..0b3556bbaa44 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/utils.py @@ -92,8 +92,8 @@ def create_access_token(token): payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) - except ValueError: - raise ValueError(token_parse_err_msg) + except ValueError as val_error: + raise ValueError(token_parse_err_msg) from val_error def get_authentication_policy( diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 900446dd1f04..64e04e58ceda 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from threading import Lock, Condition, Timer, TIMEOUT_MAX +from threading import Lock, Condition, Timer, TIMEOUT_MAX, Event from datetime import timedelta from typing import Any import six @@ -40,6 +40,7 @@ def __init__(self, token: str, **kwargs: Any): self._timer = None self._lock = Condition(Lock()) self._some_thread_refreshing = False + self._is_closed = Event() def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken @@ -86,6 +87,8 @@ def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -100,6 +103,7 @@ def _schedule_refresh(self): minutes=self._DEFAULT_AUTOREFRESH_INTERVAL_MINUTES).total_seconds() if timespan <= TIMEOUT_MAX: self._timer = Timer(timespan, self._update_token_and_reschedule) + self._timer.daemon = True self._timer.start() def _wait_till_lock_owner_finishes_refreshing(self): @@ -121,6 +125,8 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): + if self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: self._schedule_refresh() return self @@ -132,3 +138,4 @@ def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py index 0eef1aa7514f..0b3556bbaa44 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/utils.py @@ -92,8 +92,8 @@ def create_access_token(token): payload = json.loads(padded_base64_payload) return AccessToken(token, _convert_datetime_to_utc_int(datetime.fromtimestamp(payload['exp'], TZ_UTC))) - except ValueError: - raise ValueError(token_parse_err_msg) + except ValueError as val_error: + raise ValueError(token_parse_err_msg) from val_error def get_authentication_policy( From dcf0e826cc676c02b2e5ad09ca8cfa3e68f68347 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 25 May 2022 19:51:44 +0200 Subject: [PATCH 48/49] fixed test files --- .../tests/_shared/__init__.py | 2 +- .../tests/_shared/asynctestcase.py | 10 +--------- .../tests/test_user_credential_async.py | 15 ++++++++++----- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/sdk/communication/azure-communication-chat/tests/_shared/__init__.py b/sdk/communication/azure-communication-chat/tests/_shared/__init__.py index 3b0cfe17e031..841b812e10ba 100644 --- a/sdk/communication/azure-communication-chat/tests/_shared/__init__.py +++ b/sdk/communication/azure-communication-chat/tests/_shared/__init__.py @@ -3,4 +3,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for # license information. -# -------------------------------------------------------------------------- +# -------------------------------------------------------------------------- \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/tests/_shared/asynctestcase.py b/sdk/communication/azure-communication-identity/tests/_shared/asynctestcase.py index 94b2c1fe0d08..4c331bb79598 100644 --- a/sdk/communication/azure-communication-identity/tests/_shared/asynctestcase.py +++ b/sdk/communication/azure-communication-identity/tests/_shared/asynctestcase.py @@ -1,4 +1,3 @@ - # coding: utf-8 # ------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. @@ -10,7 +9,6 @@ from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from .testcase import CommunicationTestCase - class AsyncCommunicationTestCase(CommunicationTestCase): @staticmethod @@ -25,10 +23,4 @@ def run(test_class_instance, *args, **kwargs): loop = asyncio.get_event_loop() return loop.run_until_complete(test_fn(test_class_instance, **kwargs)) - return run - - -def get_completed_future(result=None): - future = asyncio.Future() - future.set_result(result) - return future + return run \ No newline at end of file diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 08e06712399f..1f5cf3489651 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -7,6 +7,7 @@ # -------------------------------------------------------------------------- from unittest import TestCase import pytest +from asyncio import Future try: from unittest.mock import MagicMock, patch except ImportError: # python < 3.3 @@ -16,11 +17,15 @@ from azure.communication.identity._shared.utils import create_access_token from azure.communication.identity._shared.utils import get_current_utc_as_int from _shared.helper import generate_token_with_custom_expiry -from _shared.asynctestcase import get_completed_future class TestCommunicationTokenCredential(TestCase): + def get_completed_future(self, result=None): + future = Future() + future.set_result(result) + return future + @pytest.mark.asyncio async def test_raises_error_for_init_with_nonstring_token(self): with pytest.raises(TypeError) as err: @@ -56,7 +61,7 @@ async def test_communicationtokencredential_throws_if_proactive_refresh_enabled_ async def test_refresher_should_be_called_immediately_with_expired_token(self): refreshed_token = generate_token_with_custom_expiry(10 * 60) refresher = MagicMock( - return_value=get_completed_future(create_access_token(refreshed_token))) + return_value=self.get_completed_future(create_access_token(refreshed_token))) expired_token = generate_token_with_custom_expiry(-(5 * 60)) credential = CommunicationTokenCredential( @@ -100,7 +105,7 @@ async def test_refresher_should_not_be_called_when_token_still_valid(self): @pytest.mark.asyncio async def test_raises_if_refresher_returns_expired_token(self): expired_token = generate_token_with_custom_expiry(-(10 * 60)) - refresher = MagicMock(return_value=get_completed_future( + refresher = MagicMock(return_value=self.get_completed_future( create_access_token(expired_token))) credential = CommunicationTokenCredential( @@ -151,7 +156,7 @@ async def test_proactive_refresher_should_be_called_after_specified_time(self): refreshed_token = generate_token_with_custom_expiry( 2 * token_validity_minutes * 60) refresher = MagicMock( - return_value=get_completed_future(create_access_token(refreshed_token))) + return_value=self.get_completed_future(create_access_token(refreshed_token))) with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): credential = CommunicationTokenCredential( @@ -178,7 +183,7 @@ async def test_proactive_refresher_keeps_scheduling_again(self): last_refreshed_token = create_access_token( generate_token_with_custom_expiry(2 * token_validity_minutes * 60)) refresher = MagicMock( - side_effect=[get_completed_future(first_refreshed_token), get_completed_future(last_refreshed_token)]) + side_effect=[self.get_completed_future(first_refreshed_token), self.get_completed_future(last_refreshed_token)]) credential = CommunicationTokenCredential( expired_token, From be1aa9f9e6e232bf443706daac6ce17d6c974674 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Thu, 2 Jun 2022 12:38:30 +0200 Subject: [PATCH 49/49] fixed latest PR comments --- .../chat/_shared/user_credential.py | 10 +- .../chat/_shared/user_credential_async.py | 14 +- .../identity/_shared/user_credential.py | 10 +- .../identity/_shared/user_credential_async.py | 14 +- .../tests/test_user_credential.py | 95 +++---- .../tests/test_user_credential_async.py | 58 ++-- ...r_credential_async_with_context_manager.py | 259 ++++++++++++++++++ ...st_user_credential_with_context_manager.py | 228 +++++++++++++++ .../_shared/user_credential.py | 10 +- .../_shared/user_credential_async.py | 14 +- .../phonenumbers/_shared/user_credential.py | 10 +- .../_shared/user_credential_async.py | 14 +- .../sms/_shared/user_credential.py | 10 +- .../sms/_shared/user_credential_async.py | 14 +- 14 files changed, 662 insertions(+), 98 deletions(-) create mode 100644 sdk/communication/azure-communication-identity/tests/test_user_credential_async_with_context_manager.py create mode 100644 sdk/communication/azure-communication-identity/tests/test_user_credential_with_context_manager.py diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py index 64e04e58ceda..f4a89336ad58 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential.py @@ -22,6 +22,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ @@ -47,10 +49,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token - self._update_token_and_reschedule() return self._token @@ -125,9 +128,10 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): - if self._is_closed.is_set(): - raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py index e9c1a67cecda..c41dc363c3e4 100644 --- a/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-chat/azure/communication/chat/_shared/user_credential_async.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from asyncio import Condition, Lock +from asyncio import Condition, Lock, Event from datetime import timedelta from typing import Any import sys @@ -24,6 +24,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh is enabled', the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ @@ -46,12 +48,16 @@ def __init__(self, token: str, **kwargs: Any): getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + self._is_closed = Event() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() @@ -90,6 +96,8 @@ async def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -127,6 +135,9 @@ def _is_token_valid(cls, token): async def __aenter__(self): if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self @@ -137,3 +148,4 @@ async def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py index 64e04e58ceda..f4a89336ad58 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential.py @@ -22,6 +22,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ @@ -47,10 +49,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token - self._update_token_and_reschedule() return self._token @@ -125,9 +128,10 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): - if self._is_closed.is_set(): - raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py index e9c1a67cecda..c41dc363c3e4 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_shared/user_credential_async.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from asyncio import Condition, Lock +from asyncio import Condition, Lock, Event from datetime import timedelta from typing import Any import sys @@ -24,6 +24,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh is enabled', the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ @@ -46,12 +48,16 @@ def __init__(self, token: str, **kwargs: Any): getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + self._is_closed = Event() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() @@ -90,6 +96,8 @@ async def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -127,6 +135,9 @@ def _is_token_valid(cls, token): async def __aenter__(self): if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self @@ -137,3 +148,4 @@ async def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential.py b/sdk/communication/azure-communication-identity/tests/test_user_credential.py index ac076c3ad269..1fd0ed6f4ca2 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential.py @@ -29,9 +29,9 @@ def setUpClass(cls): 100) # 1/1/1970 def test_communicationtokencredential_decodes_token(self): - with CommunicationTokenCredential(self.sample_token) as credential: - access_token = credential.get_token() - self.assertEqual(access_token.token, self.sample_token) + credential = CommunicationTokenCredential(self.sample_token) + access_token = credential.get_token() + self.assertEqual(access_token.token, self.sample_token) def test_communicationtokencredential_throws_if_invalid_token(self): self.assertRaises( @@ -52,32 +52,31 @@ def test_communicationtokencredential_throws_if_proactive_refresh_enabled_withou assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." def test_communicationtokencredential_static_token_returns_expired_token(self): - with CommunicationTokenCredential(self.expired_token) as credential: - self.assertEqual(credential.get_token().token, self.expired_token) + credential = CommunicationTokenCredential(self.expired_token) + self.assertEqual(credential.get_token().token, self.expired_token) def test_communicationtokencredential_token_expired_refresh_called(self): refresher = MagicMock( return_value=create_access_token(self.sample_token)) - with CommunicationTokenCredential(self.expired_token, token_refresher=refresher) as credential: - access_token = credential.get_token() + credential = CommunicationTokenCredential(self.expired_token, token_refresher=refresher) + access_token = credential.get_token() refresher.assert_called_once() self.assertEqual(access_token.token, self.sample_token) def test_communicationtokencredential_raises_if_refresher_returns_expired_token(self): refresher = MagicMock( return_value=create_access_token(self.expired_token)) - with CommunicationTokenCredential(self.expired_token, token_refresher=refresher) as credential: - with self.assertRaises(ValueError): - credential.get_token() - self.assertEqual(refresher.call_count, 1) + credential = CommunicationTokenCredential(self.expired_token, token_refresher=refresher) + with self.assertRaises(ValueError): + credential.get_token() + self.assertEqual(refresher.call_count, 1) def test_uses_initial_token_as_expected(self): refresher = MagicMock( return_value=create_access_token(self.expired_token)) credential = CommunicationTokenCredential( self.sample_token, token_refresher=refresher, proactive_refresh=True) - with credential: - access_token = credential.get_token() + access_token = credential.get_token() self.assertEqual(refresher.call_count, 0) self.assertEqual(access_token.token, self.sample_token) @@ -100,13 +99,12 @@ def test_proactive_refresher_should_not_be_called_before_specified_time(self): initial_token, token_refresher=refresher, proactive_refresh=True) - with credential: - access_token = credential.get_token() - - assert refresher.call_count == 0 - assert access_token.token == initial_token - # check that next refresh is always scheduled - assert credential._timer is not None + access_token = credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is None def test_proactive_refresher_should_be_called_after_specified_time(self): refresh_minutes = 10 @@ -127,13 +125,12 @@ def test_proactive_refresher_should_be_called_after_specified_time(self): initial_token, token_refresher=refresher, proactive_refresh=True) - with credential: - access_token = credential.get_token() + access_token = credential.get_token() - assert refresher.call_count == 1 - assert access_token.token == refreshed_token - # check that next refresh is always scheduled - assert credential._timer is not None + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None def test_proactive_refresher_keeps_scheduling_again(self): refresh_minutes = 10 @@ -152,15 +149,14 @@ def test_proactive_refresher_keeps_scheduling_again(self): expired_token, token_refresher=refresher, proactive_refresh=True) - with credential: + access_token = credential.get_token() + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): access_token = credential.get_token() - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = credential.get_token() - - assert refresher.call_count == 2 - assert access_token.token == last_refreshed_token.token - # check that next refresh is always scheduled - assert credential._timer is not None + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None def test_fractional_backoff_applied_when_token_expiring(self): token_validity_seconds = 5 * 60 @@ -177,13 +173,11 @@ def test_fractional_backoff_applied_when_token_expiring(self): next_milestone = token_validity_seconds / 2 - with credential: - assert credential._timer.interval == next_milestone - with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): - credential.get_token() - assert refresher.call_count == 1 - next_milestone = next_milestone / 2 - assert credential._timer.interval == next_milestone + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): + credential.get_token() + assert refresher.call_count == 1 + next_milestone = next_milestone / 2 + assert credential._timer.interval == next_milestone def test_refresher_should_not_be_called_when_token_still_valid(self): generated_token = generate_token_with_custom_expiry(15 * 60) @@ -192,9 +186,8 @@ def test_refresher_should_not_be_called_when_token_still_valid(self): credential = CommunicationTokenCredential( generated_token, token_refresher=refresher, proactive_refresh=False) - with credential: - for _ in range(10): - access_token = credential.get_token() + for _ in range(10): + access_token = credential.get_token() refresher.assert_not_called() assert generated_token == access_token.token @@ -205,12 +198,7 @@ def test_exit_cancels_timer(self): refresher = MagicMock(return_value=refreshed_token) credential = CommunicationTokenCredential( self.expired_token,token_refresher=refresher, proactive_refresh=True) - with credential: - assert credential._timer is not None - assert credential._timer is None - - credential = CommunicationTokenCredential( - self.expired_token,token_refresher=refresher, proactive_refresh=True) + credential.get_token() credential.close() assert credential._timer is None @@ -220,13 +208,12 @@ def test_exit_enter_scenario_throws_exception(self): refresher = MagicMock(return_value=refreshed_token) credential = CommunicationTokenCredential( self.expired_token,token_refresher=refresher, proactive_refresh=True) - with credential: - assert credential._timer is not None + credential.get_token() + credential.close() assert credential._timer is None with pytest.raises(RuntimeError) as err: - with credential: - assert credential._timer is not None + credential.get_token() assert str(err.value) == "An instance of CommunicationTokenCredential cannot be reused once it has been closed." diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py index 1f5cf3489651..b60569f7147a 100644 --- a/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async.py @@ -66,8 +66,7 @@ async def test_refresher_should_be_called_immediately_with_expired_token(self): credential = CommunicationTokenCredential( expired_token, token_refresher=refresher) - async with credential: - access_token = await credential.get_token() + access_token = await credential.get_token() refresher.assert_called_once() assert refreshed_token == access_token.token @@ -81,8 +80,7 @@ async def test_refresher_should_not_be_called_before_expiring_time(self): credential = CommunicationTokenCredential( initial_token, token_refresher=refresher, proactive_refresh=True) - async with credential: - access_token = await credential.get_token() + access_token = await credential.get_token() refresher.assert_not_called() assert initial_token == access_token.token @@ -95,9 +93,8 @@ async def test_refresher_should_not_be_called_when_token_still_valid(self): credential = CommunicationTokenCredential( generated_token, token_refresher=refresher, proactive_refresh=False) - async with credential: - for _ in range(10): - access_token = await credential.get_token() + for _ in range(10): + access_token = await credential.get_token() refresher.assert_not_called() assert generated_token == access_token.token @@ -110,9 +107,8 @@ async def test_raises_if_refresher_returns_expired_token(self): credential = CommunicationTokenCredential( expired_token, token_refresher=refresher) - async with credential: - with self.assertRaises(ValueError): - await credential.get_token() + with self.assertRaises(ValueError): + await credential.get_token() assert refresher.call_count == 1 @@ -135,8 +131,7 @@ async def test_proactive_refresher_should_not_be_called_before_specified_time(se initial_token, token_refresher=refresher, proactive_refresh=True) - async with credential: - access_token = await credential.get_token() + access_token = await credential.get_token() assert refresher.call_count == 0 assert access_token.token == initial_token @@ -163,8 +158,7 @@ async def test_proactive_refresher_should_be_called_after_specified_time(self): initial_token, token_refresher=refresher, proactive_refresh=True) - async with credential: - access_token = await credential.get_token() + access_token = await credential.get_token() assert refresher.call_count == 1 assert access_token.token == refreshed_token @@ -189,10 +183,9 @@ async def test_proactive_refresher_keeps_scheduling_again(self): expired_token, token_refresher=refresher, proactive_refresh=True) - async with credential: + access_token = await credential.get_token() + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): access_token = await credential.get_token() - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): - access_token = await credential.get_token() assert refresher.call_count == 2 assert access_token.token == last_refreshed_token.token @@ -216,9 +209,8 @@ async def test_fractional_backoff_applied_when_token_expiring(self): next_milestone = token_validity_seconds / 2 assert credential._timer.interval == next_milestone - async with credential: - with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): - await credential.get_token() + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): + await credential.get_token() assert refresher.call_count == 1 next_milestone = next_milestone / 2 @@ -234,8 +226,26 @@ async def test_exit_cancels_timer(self): expired_token, token_refresher=refresher, proactive_refresh=True) - async with credential: - assert credential._timer is not None + credential.get_token() + credential.close() + assert credential._timer is not None assert refresher.call_count == 0 - async with credential: - assert credential._timer is not None + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_exit_enter_scenario_throws_exception(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + proactive_refresh=True) + credential.get_token() + credential.close() + assert credential._timer is not None + + with pytest.raises(RuntimeError) as err: + credential.get_token() + assert str(err.value) == "An instance of CommunicationTokenCredential cannot be reused once it has been closed." diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_async_with_context_manager.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_async_with_context_manager.py new file mode 100644 index 000000000000..baebb6e6d35b --- /dev/null +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_async_with_context_manager.py @@ -0,0 +1,259 @@ + +# coding: utf-8 +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from unittest import TestCase +import pytest +from asyncio import Future +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch +from azure.communication.identity._shared.user_credential_async import CommunicationTokenCredential +import azure.communication.identity._shared.user_credential_async as user_credential_async +from azure.communication.identity._shared.utils import create_access_token +from azure.communication.identity._shared.utils import get_current_utc_as_int +from _shared.helper import generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential(TestCase): + + def get_completed_future(self, result=None): + future = Future() + future.set_result(result) + return future + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_nonstring_token(self): + with pytest.raises(TypeError) as err: + CommunicationTokenCredential(1234) + assert str(err.value) == "Token must be a string." + + @pytest.mark.asyncio + async def test_raises_error_for_init_with_invalid_token(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential("not a token") + assert str(err.value) == "Token is not formatted correctly" + + @pytest.mark.asyncio + async def test_init_with_valid_token(self): + initial_token = generate_token_with_custom_expiry(5 * 60) + credential = CommunicationTokenCredential(initial_token) + access_token = await credential.get_token() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_communicationtokencredential_throws_if_proactive_refresh_enabled_without_token_refresher(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential(self.sample_token, proactive_refresh=True) + assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." + with pytest.raises(ValueError) as err: + CommunicationTokenCredential( + self.sample_token, + proactive_refresh=True, + token_refresher=None) + assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." + + @pytest.mark.asyncio + async def test_refresher_should_be_called_immediately_with_expired_token(self): + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=self.get_completed_future(create_access_token(refreshed_token))) + expired_token = generate_token_with_custom_expiry(-(5 * 60)) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + access_token = await credential.get_token() + + refresher.assert_called_once() + assert refreshed_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_before_expiring_time(self): + initial_token = generate_token_with_custom_expiry(15 * 60) + refreshed_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + credential = CommunicationTokenCredential( + initial_token, token_refresher=refresher, proactive_refresh=True) + async with credential: + access_token = await credential.get_token() + + refresher.assert_not_called() + assert initial_token == access_token.token + + @pytest.mark.asyncio + async def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, proactive_refresh=False) + async with credential: + for _ in range(10): + access_token = await credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token + + @pytest.mark.asyncio + async def test_raises_if_refresher_returns_expired_token(self): + expired_token = generate_token_with_custom_expiry(-(10 * 60)) + refresher = MagicMock(return_value=self.get_completed_future( + create_access_token(expired_token))) + + credential = CommunicationTokenCredential( + expired_token, token_refresher=refresher) + async with credential: + with self.assertRaises(ValueError): + await credential.get_token() + + assert refresher.call_count == 1 + + @pytest.mark.asyncio + async def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 30 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + proactive_refresh=True) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 10 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + \ + (token_validity_minutes - refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=self.get_completed_future(create_access_token(refreshed_token))) + + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + proactive_refresh=True) + async with credential: + access_token = await credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_proactive_refresher_keeps_scheduling_again(self): + refresh_minutes = 10 + token_validity_minutes = 60 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + (token_validity_minutes - + refresh_minutes) * 60 + 1 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(token_validity_minutes * 60)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(2 * token_validity_minutes * 60)) + refresher = MagicMock( + side_effect=[self.get_completed_future(first_refreshed_token), self.get_completed_future(last_refreshed_token)]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + proactive_refresh=True) + async with credential: + access_token = await credential.get_token() + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = await credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_fractional_backoff_applied_when_token_expiring(self): + token_validity_seconds = 5 * 60 + expiring_token = generate_token_with_custom_expiry( + token_validity_seconds) + + refresher = MagicMock( + side_effect=[create_access_token(expiring_token), create_access_token(expiring_token)]) + + credential = CommunicationTokenCredential( + expiring_token, + token_refresher=refresher, + proactive_refresh=True) + + next_milestone = token_validity_seconds / 2 + assert credential._timer.interval == next_milestone + + async with credential: + with patch(user_credential_async.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): + await credential.get_token() + + assert refresher.call_count == 1 + next_milestone = next_milestone / 2 + assert credential._timer.interval == next_milestone + + @pytest.mark.asyncio + async def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + proactive_refresh=True) + async with credential: + assert credential._timer is not None + assert refresher.call_count == 0 + assert credential._timer is not None + + @pytest.mark.asyncio + async def test_exit_enter_scenario_throws_exception(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + expired_token = generate_token_with_custom_expiry(-10 * 60) + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + proactive_refresh=True) + async with credential: + assert credential._timer is not None + assert credential._timer is not None + + with pytest.raises(RuntimeError) as err: + with credential: + assert credential._timer is not None + assert str(err.value) == "An instance of CommunicationTokenCredential cannot be reused once it has been closed." diff --git a/sdk/communication/azure-communication-identity/tests/test_user_credential_with_context_manager.py b/sdk/communication/azure-communication-identity/tests/test_user_credential_with_context_manager.py new file mode 100644 index 000000000000..73a6eee38caa --- /dev/null +++ b/sdk/communication/azure-communication-identity/tests/test_user_credential_with_context_manager.py @@ -0,0 +1,228 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from typing import Type +import platform +import pytest +from unittest import TestCase +try: + from unittest.mock import MagicMock, patch +except ImportError: # python < 3.3 + from mock import MagicMock, patch # type: ignore +import azure.communication.identity._shared.user_credential as user_credential +from azure.communication.identity._shared.user_credential import CommunicationTokenCredential +from azure.communication.identity._shared.utils import create_access_token +from azure.communication.identity._shared.utils import get_current_utc_as_int +from datetime import timedelta +from _shared.helper import generate_token_with_custom_expiry_epoch, generate_token_with_custom_expiry + + +class TestCommunicationTokenCredential(TestCase): + + @classmethod + def setUpClass(cls): + cls.sample_token = generate_token_with_custom_expiry_epoch( + 32503680000) # 1/1/2030 + cls.expired_token = generate_token_with_custom_expiry_epoch( + 100) # 1/1/1970 + + def test_communicationtokencredential_decodes_token(self): + with CommunicationTokenCredential(self.sample_token) as credential: + access_token = credential.get_token() + self.assertEqual(access_token.token, self.sample_token) + + def test_communicationtokencredential_throws_if_invalid_token(self): + self.assertRaises( + ValueError, lambda: CommunicationTokenCredential("foo.bar.tar")) + + def test_communicationtokencredential_throws_if_nonstring_token(self): + self.assertRaises(TypeError, lambda: CommunicationTokenCredential(454)) + + def test_communicationtokencredential_throws_if_proactive_refresh_enabled_without_token_refresher(self): + with pytest.raises(ValueError) as err: + CommunicationTokenCredential(self.sample_token, proactive_refresh=True) + assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." + with pytest.raises(ValueError) as err: + CommunicationTokenCredential( + self.sample_token, + proactive_refresh=True, + token_refresher=None) + assert str(err.value) == "When 'proactive_refresh' is True, 'token_refresher' must not be None." + + def test_communicationtokencredential_static_token_returns_expired_token(self): + with CommunicationTokenCredential(self.expired_token) as credential: + self.assertEqual(credential.get_token().token, self.expired_token) + + def test_communicationtokencredential_token_expired_refresh_called(self): + refresher = MagicMock( + return_value=create_access_token(self.sample_token)) + with CommunicationTokenCredential(self.expired_token, token_refresher=refresher) as credential: + access_token = credential.get_token() + refresher.assert_called_once() + self.assertEqual(access_token.token, self.sample_token) + + def test_communicationtokencredential_raises_if_refresher_returns_expired_token(self): + refresher = MagicMock( + return_value=create_access_token(self.expired_token)) + with CommunicationTokenCredential(self.expired_token, token_refresher=refresher) as credential: + with self.assertRaises(ValueError): + credential.get_token() + self.assertEqual(refresher.call_count, 1) + + def test_uses_initial_token_as_expected(self): + refresher = MagicMock( + return_value=create_access_token(self.expired_token)) + credential = CommunicationTokenCredential( + self.sample_token, token_refresher=refresher, proactive_refresh=True) + with credential: + access_token = credential.get_token() + + self.assertEqual(refresher.call_count, 0) + self.assertEqual(access_token.token, self.sample_token) + + def test_proactive_refresher_should_not_be_called_before_specified_time(self): + refresh_minutes = 10 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + (refresh_minutes - 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + proactive_refresh=True) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 0 + assert access_token.token == initial_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_should_be_called_after_specified_time(self): + refresh_minutes = 10 + token_validity_minutes = 60 + start_timestamp = get_current_utc_as_int() + skip_to_timestamp = start_timestamp + \ + (token_validity_minutes - refresh_minutes + 5) * 60 + + initial_token = generate_token_with_custom_expiry( + token_validity_minutes * 60) + refreshed_token = generate_token_with_custom_expiry( + 2 * token_validity_minutes * 60) + refresher = MagicMock( + return_value=create_access_token(refreshed_token)) + + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + credential = CommunicationTokenCredential( + initial_token, + token_refresher=refresher, + proactive_refresh=True) + with credential: + access_token = credential.get_token() + + assert refresher.call_count == 1 + assert access_token.token == refreshed_token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_proactive_refresher_keeps_scheduling_again(self): + refresh_minutes = 10 + token_validity_minutes = 60 + expired_token = generate_token_with_custom_expiry(-5 * 60) + skip_to_timestamp = get_current_utc_as_int() + (token_validity_minutes - + refresh_minutes) * 60 + 1 + first_refreshed_token = create_access_token( + generate_token_with_custom_expiry(token_validity_minutes * 60)) + last_refreshed_token = create_access_token( + generate_token_with_custom_expiry(2 * token_validity_minutes * 60)) + refresher = MagicMock( + side_effect=[first_refreshed_token, last_refreshed_token]) + + credential = CommunicationTokenCredential( + expired_token, + token_refresher=refresher, + proactive_refresh=True) + with credential: + access_token = credential.get_token() + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=skip_to_timestamp): + access_token = credential.get_token() + + assert refresher.call_count == 2 + assert access_token.token == last_refreshed_token.token + # check that next refresh is always scheduled + assert credential._timer is not None + + def test_fractional_backoff_applied_when_token_expiring(self): + token_validity_seconds = 5 * 60 + expiring_token = generate_token_with_custom_expiry( + token_validity_seconds) + + refresher = MagicMock( + side_effect=[create_access_token(expiring_token), create_access_token(expiring_token)]) + + credential = CommunicationTokenCredential( + expiring_token, + token_refresher=refresher, + proactive_refresh=True) + + next_milestone = token_validity_seconds / 2 + + with credential: + assert credential._timer.interval == next_milestone + with patch(user_credential.__name__+'.'+get_current_utc_as_int.__name__, return_value=(get_current_utc_as_int() + next_milestone)): + credential.get_token() + assert refresher.call_count == 1 + next_milestone = next_milestone / 2 + assert credential._timer.interval == next_milestone + + def test_refresher_should_not_be_called_when_token_still_valid(self): + generated_token = generate_token_with_custom_expiry(15 * 60) + new_token = generate_token_with_custom_expiry(10 * 60) + refresher = MagicMock(return_value=create_access_token(new_token)) + + credential = CommunicationTokenCredential( + generated_token, token_refresher=refresher, proactive_refresh=False) + with credential: + for _ in range(10): + access_token = credential.get_token() + + refresher.assert_not_called() + assert generated_token == access_token.token + + def test_exit_cancels_timer(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + credential = CommunicationTokenCredential( + self.expired_token,token_refresher=refresher, proactive_refresh=True) + with credential: + assert credential._timer is not None + assert credential._timer is None + + def test_exit_enter_scenario_throws_exception(self): + refreshed_token = create_access_token( + generate_token_with_custom_expiry(30 * 60)) + refresher = MagicMock(return_value=refreshed_token) + credential = CommunicationTokenCredential( + self.expired_token,token_refresher=refresher, proactive_refresh=True) + with credential: + assert credential._timer is not None + assert credential._timer is None + + with pytest.raises(RuntimeError) as err: + with credential: + assert credential._timer is not None + assert str(err.value) == "An instance of CommunicationTokenCredential cannot be reused once it has been closed." + + + \ No newline at end of file diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py index 64e04e58ceda..f4a89336ad58 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential.py @@ -22,6 +22,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ @@ -47,10 +49,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token - self._update_token_and_reschedule() return self._token @@ -125,9 +128,10 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): - if self._is_closed.is_set(): - raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py index e9c1a67cecda..c41dc363c3e4 100644 --- a/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-networktraversal/azure/communication/networktraversal/_shared/user_credential_async.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from asyncio import Condition, Lock +from asyncio import Condition, Lock, Event from datetime import timedelta from typing import Any import sys @@ -24,6 +24,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh is enabled', the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ @@ -46,12 +48,16 @@ def __init__(self, token: str, **kwargs: Any): getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + self._is_closed = Event() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() @@ -90,6 +96,8 @@ async def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -127,6 +135,9 @@ def _is_token_valid(cls, token): async def __aenter__(self): if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self @@ -137,3 +148,4 @@ async def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py index 64e04e58ceda..f4a89336ad58 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential.py @@ -22,6 +22,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ @@ -47,10 +49,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token - self._update_token_and_reschedule() return self._token @@ -125,9 +128,10 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): - if self._is_closed.is_set(): - raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py index e9c1a67cecda..c41dc363c3e4 100644 --- a/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-phonenumbers/azure/communication/phonenumbers/_shared/user_credential_async.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from asyncio import Condition, Lock +from asyncio import Condition, Lock, Event from datetime import timedelta from typing import Any import sys @@ -24,6 +24,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh is enabled', the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ @@ -46,12 +48,16 @@ def __init__(self, token: str, **kwargs: Any): getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + self._is_closed = Event() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() @@ -90,6 +96,8 @@ async def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -127,6 +135,9 @@ def _is_token_valid(cls, token): async def __aenter__(self): if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self @@ -137,3 +148,4 @@ async def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set() diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py index 64e04e58ceda..f4a89336ad58 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential.py @@ -22,6 +22,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh' is enabled, the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' callable. """ @@ -47,10 +49,11 @@ def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token - self._update_token_and_reschedule() return self._token @@ -125,9 +128,10 @@ def _is_token_valid(cls, token): return get_current_utc_as_int() < token.expires_on def __enter__(self): - if self._is_closed.is_set(): - raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self diff --git a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py index e9c1a67cecda..c41dc363c3e4 100644 --- a/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py +++ b/sdk/communication/azure-communication-sms/azure/communication/sms/_shared/user_credential_async.py @@ -4,7 +4,7 @@ # license information. # -------------------------------------------------------------------------- -from asyncio import Condition, Lock +from asyncio import Condition, Lock, Event from datetime import timedelta from typing import Any import sys @@ -24,6 +24,8 @@ class CommunicationTokenCredential(object): If the proactive refreshing is enabled ('proactive_refresh' is true), the credential will use a background thread to attempt to refresh the token within 10 minutes before the cached token expires, the proactive refresh will request a new token by calling the 'token_refresher' callback. + When 'proactive_refresh is enabled', the Credential object must be either run within a context manager + or the 'close' method must be called once the object usage has been finished. :raises: TypeError if paramater 'token' is not a string :raises: ValueError if the 'proactive_refresh' is enabled without providing the 'token_refresher' function. """ @@ -46,12 +48,16 @@ def __init__(self, token: str, **kwargs: Any): getattr(self._async_mutex, '_get_loop', lambda: None)() self._lock = Condition(self._async_mutex) self._some_thread_refreshing = False + self._is_closed = Event() async def get_token(self, *scopes, **kwargs): # pylint: disable=unused-argument # type (*str, **Any) -> AccessToken """The value of the configured token. :rtype: ~azure.core.credentials.AccessToken """ + if self._proactive_refresh and self._is_closed.is_set(): + raise RuntimeError("An instance of CommunicationTokenCredential cannot be reused once it has been closed.") + if not self._token_refresher or not self._is_token_expiring_soon(self._token): return self._token await self._update_token_and_reschedule() @@ -90,6 +96,8 @@ async def _update_token_and_reschedule(self): return self._token def _schedule_refresh(self): + if self._is_closed.is_set(): + return if self._timer is not None: self._timer.cancel() @@ -127,6 +135,9 @@ def _is_token_valid(cls, token): async def __aenter__(self): if self._proactive_refresh: + if self._is_closed.is_set(): + raise RuntimeError( + "An instance of CommunicationTokenCredential cannot be reused once it has been closed.") self._schedule_refresh() return self @@ -137,3 +148,4 @@ async def close(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None + self._is_closed.set()