diff --git a/setup.py b/setup.py index 9199a744..e1214b8c 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,8 @@ install_requires=[ 'requests==2.20.0', 'singer-python==5.10.0', - 'xmltodict==0.11.0' + 'xmltodict==0.11.0', + 'nose' ], entry_points=''' [console_scripts] diff --git a/tap_salesforce/salesforce/__init__.py b/tap_salesforce/salesforce/__init__.py index 4da64320..1e5b0547 100644 --- a/tap_salesforce/salesforce/__init__.py +++ b/tap_salesforce/salesforce/__init__.py @@ -133,6 +133,18 @@ def log_backoff_attempt(details): LOGGER.info("ConnectionError detected, triggering backoff: %d try", details.get("tries")) +def log_login_backoff(details):#what is passed in for details??? + # log issue + # reset login timer + # error_message = str(e) + # if resp is None and hasattr(e, 'response') and e.response is not None: #pylint:disable=no-member + # resp = e.response #pylint:disable=no-member + # # NB: requests.models.Response is always falsy here. It is false if status code >= 400 + # if isinstance(resp, requests.models.Response): + # error_message = error_message + ", Response from Salesforce: {}".format(resp.text) + # LOGGER.info(error_message) + LOGGER.info("Error logging in to Salesforce, triggering backoff: %d try", details.get("tries")) + def field_to_property_schema(field, mdata): # pylint:disable=too-many-branches property_schema = {} @@ -228,6 +240,7 @@ def __init__(self, self.login_timer = None self.data_url = "{}/services/data/v41.0/{}" self.pk_chunking = False + self.refresh_time = None # validate start_date singer_utils.strptime(default_start_date) @@ -273,6 +286,7 @@ def check_rest_quota_usage(self, headers): factor=2, on_backoff=log_backoff_attempt) def _make_request(self, http_method, url, headers=None, body=None, stream=False, params=None): + self.ensure_fresh_credentials() if http_method == "GET": LOGGER.info("Making %s request to %s with params: %s", http_method, url, params) resp = self.session.get(url, headers=headers, stream=stream, params=params) @@ -293,6 +307,16 @@ def _make_request(self, http_method, url, headers=None, body=None, stream=False, return resp + def ensure_fresh_credentials(self): + if self.refresh_time is None or time.time() > self.refresh_time: + self.login() + + # pylint: disable=too-many-arguments + @backoff.on_exception(backoff.constant, + Exception, + max_tries=3, + interval=REFRESH_TOKEN_EXPIRATION_PERIOD, + on_backoff=log_login_backoff) def login(self): if self.is_sandbox: login_url = 'https://test.salesforce.com/services/oauth2/token' @@ -304,28 +328,19 @@ def login(self): LOGGER.info("Attempting login via OAuth2") - resp = None - try: - resp = self._make_request("POST", login_url, body=login_body, headers={"Content-Type": "application/x-www-form-urlencoded"}) - - LOGGER.info("OAuth2 login successful") - - auth = resp.json() - - self.access_token = auth['access_token'] - self.instance_url = auth['instance_url'] - except Exception as e: - error_message = str(e) - if resp is None and hasattr(e, 'response') and e.response is not None: #pylint:disable=no-member - resp = e.response #pylint:disable=no-member - # NB: requests.models.Response is always falsy here. It is false if status code >= 400 - if isinstance(resp, requests.models.Response): - error_message = error_message + ", Response from Salesforce: {}".format(resp.text) - raise Exception(error_message) from e - finally: - LOGGER.info("Starting new login timer") - self.login_timer = threading.Timer(REFRESH_TOKEN_EXPIRATION_PERIOD, self.login) - self.login_timer.start() + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + time_before_login = time.time() + + resp = self.session.post(login_url, headers=headers, data=login_body) + + LOGGER.info("OAuth2 login successful") + + auth = resp.json() + + self.access_token = auth['access_token'] + self.instance_url = auth['instance_url'] + self.refresh_time = time_before_login+REFRESH_TOKEN_EXPIRATION_PERIOD def describe(self, sobject=None): """Describes all objects or a specific object""" diff --git a/tests/unittests/test_auth_failure.py b/tests/unittests/test_auth_failure.py new file mode 100644 index 00000000..527a7b5c --- /dev/null +++ b/tests/unittests/test_auth_failure.py @@ -0,0 +1,14 @@ +import unittest +from unittest.mock import Mock +from tap_salesforce import Salesforce + +class TestAuthorizationHandling(unittest.TestCase): + def test_attempt_login_failure_with_retry(self): + """ + When we see an exception on the login, we expect the error to be raised after 3 tries + """ + salesforce_object = Salesforce(default_start_date="2021-07-15T13:25:54Z") + salesforce_object.session.post = Mock(side_effect=Exception("this is an example auth exception")) + with self.assertRaisesRegexp(Exception, "this is an example auth exception") as e: + salesforce_object.login() + self.assertEqual(3, salesforce_object.session.post.call_count)