From f3cc54b904baff4b12c58fd8d74ac16dfdce3e61 Mon Sep 17 00:00:00 2001 From: Jay Tilala Date: Fri, 29 Jul 2022 18:10:20 +0530 Subject: [PATCH 1/6] add request timeout and backoff --- tap_braintree/__init__.py | 13 ++++++++++++- tap_braintree/streams.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/tap_braintree/__init__.py b/tap_braintree/__init__.py index 8ed6264..fb20953 100644 --- a/tap_braintree/__init__.py +++ b/tap_braintree/__init__.py @@ -2,11 +2,14 @@ import sys import json import braintree +from requests import request import singer from singer import utils from tap_braintree.discover import discover from tap_braintree.sync import sync as _sync +REQUEST_TIME_OUT = 300 + REQUIRED_CONFIG_KEYS = [ "merchant_id", "public_key", @@ -40,12 +43,20 @@ def main(): environment = getattr(braintree.Environment, config.pop("environment", "Production")) + request_timeout = config.get("request_timeout") + if request_timeout and float(request_timeout): + request_timeout = float(request_timeout) + else: + # If value in config is 0 or of type string then set default to 300 seconds. + request_timeout = REQUEST_TIME_OUT + gateway = braintree.BraintreeGateway( braintree.Configuration( environment, merchant_id = config['merchant_id'], public_key= config["public_key"], - private_key=config["private_key"] + private_key=config["private_key"], + timeout = request_timeout ) ) try: diff --git a/tap_braintree/streams.py b/tap_braintree/streams.py index b843786..5a35e49 100644 --- a/tap_braintree/streams.py +++ b/tap_braintree/streams.py @@ -1,10 +1,17 @@ import pytz import singer import braintree +import backoff from singer import utils from datetime import datetime, timedelta from .transform import transform_row +from braintree.exceptions.too_many_requests_error import TooManyRequestsError +from braintree.exceptions.server_error import ServerError +from braintree.exceptions.service_unavailable_error import ServiceUnavailableError +from braintree.exceptions.gateway_timeout_error import GatewayTimeoutError + + TRAILING_DAYS = timedelta(days=30) DEFAULT_TIMESTAMP = "1970-01-01T00:00:00Z" LOGGER = singer.get_logger() @@ -86,6 +93,14 @@ class SyncWithoutWindow(Stream): replication_keys = "updated_at" replication_method = "INCREMENTAL" + # Backoff the request for 5 times when ConnectionError, TooManyRequestsError (status code = 429), + # ServerError(status code = 500) , ServiceUnavailableError (status code = 503) , + # or GatewayTimeoutError (status code = 504) occurs + @backoff.on_exception( + backoff.expo, + (ConnectionError, TooManyRequestsError, ServerError, ServiceUnavailableError, GatewayTimeoutError), + max_tries=5, + factor=2) def sync(self, gateway, config, schema, state, selected_streams): """ Sync function for incremental stream without window logic @@ -133,6 +148,14 @@ class SyncWithWindow(Stream): replication_keys = "created_at" replication_method = "INCREMENTAL" + # Backoff the request for 5 times when ConnectionError, TooManyRequestsError (status code = 429), + # ServerError(status code = 500) , ServiceUnavailableError (status code = 503) , + # or GatewayTimeoutError (status code = 504) occurs + @backoff.on_exception( + backoff.expo, + (ConnectionError, TooManyRequestsError, ServerError, ServiceUnavailableError, GatewayTimeoutError), + max_tries=5, + factor=2) def sync(self, gateway, config, schema, state, selected_streams): """ Sync function for incremental stream with window logic @@ -247,6 +270,14 @@ class FullTableSync(Stream): sdk_call = None key_properties = ["id"] + # Backoff the request for 5 times when ConnectionError, TooManyRequestsError (status code = 429), + # ServerError(status code = 500) , ServiceUnavailableError (status code = 503) , + # or GatewayTimeoutError (status code = 504) occurs + @backoff.on_exception( + backoff.expo, + (ConnectionError, TooManyRequestsError, ServerError, ServiceUnavailableError, GatewayTimeoutError), + max_tries=5, + factor=2) def sync(self, gateway, config, schema, state, selected_streams): """ Sync function for full_table stream From a9ac5604bb139a48470c062d3a05cfb0c3cae07a Mon Sep 17 00:00:00 2001 From: Jay Tilala Date: Mon, 1 Aug 2022 15:45:50 +0530 Subject: [PATCH 2/6] Raise exception for 0 value --- tap_braintree/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tap_braintree/__init__.py b/tap_braintree/__init__.py index fb20953..4310fe6 100644 --- a/tap_braintree/__init__.py +++ b/tap_braintree/__init__.py @@ -7,6 +7,7 @@ from singer import utils from tap_braintree.discover import discover from tap_braintree.sync import sync as _sync +import datetime REQUEST_TIME_OUT = 300 @@ -43,12 +44,15 @@ def main(): environment = getattr(braintree.Environment, config.pop("environment", "Production")) - request_timeout = config.get("request_timeout") - if request_timeout and float(request_timeout): - request_timeout = float(request_timeout) - else: - # If value in config is 0 or of type string then set default to 300 seconds. - request_timeout = REQUEST_TIME_OUT + try: + # Take value of request_timeout if provided in config else take default value + request_timeout = float(config.get("request_timeout", REQUEST_TIME_OUT)) + + if request_timeout == 0: + # Raise error when request_timeout is given as 0 in config + raise ValueError() + except ValueError: + raise ValueError('Please provide a value greater than 0 for the request_timeout parameter in config') gateway = braintree.BraintreeGateway( braintree.Configuration( From f7801006d0dded992e25640384ecd78470175ce8 Mon Sep 17 00:00:00 2001 From: Jay Tilala Date: Tue, 2 Aug 2022 16:11:35 +0530 Subject: [PATCH 3/6] address given comments --- tap_braintree/streams.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tap_braintree/streams.py b/tap_braintree/streams.py index 5a35e49..4971766 100644 --- a/tap_braintree/streams.py +++ b/tap_braintree/streams.py @@ -156,6 +156,10 @@ class SyncWithWindow(Stream): (ConnectionError, TooManyRequestsError, ServerError, ServiceUnavailableError, GatewayTimeoutError), max_tries=5, factor=2) + def GetRecords(self, gateway, start, end): + """Return records between given date window""" + return self.sdk_call(gateway, start, end) + def sync(self, gateway, config, schema, state, selected_streams): """ Sync function for incremental stream with window logic @@ -186,7 +190,7 @@ def sync(self, gateway, config, schema, state, selected_streams): for start, end in self.daterange(period_start, period_end): end = min(end, period_end) - data = self.sdk_call(gateway, start, end) + data = self.GetRecords(gateway, start, end) time_extracted = utils.now() row_written_count = 0 From 288b51364819b426058b615a033aaa3168db45b4 Mon Sep 17 00:00:00 2001 From: Jay Tilala Date: Tue, 2 Aug 2022 17:32:41 +0530 Subject: [PATCH 4/6] add unitest --- tests/unittests/test_discover.py | 13 -- tests/unittests/test_exception_handling.py | 36 ----- tests/unittests/test_main.py | 43 ------ tests/unittests/test_schema.py | 24 ---- tests/unittests/test_sync.py | 150 --------------------- tests/unittests/test_timeout.py | 63 +++++++++ 6 files changed, 63 insertions(+), 266 deletions(-) delete mode 100644 tests/unittests/test_discover.py delete mode 100644 tests/unittests/test_exception_handling.py delete mode 100644 tests/unittests/test_main.py delete mode 100644 tests/unittests/test_schema.py delete mode 100644 tests/unittests/test_sync.py create mode 100644 tests/unittests/test_timeout.py diff --git a/tests/unittests/test_discover.py b/tests/unittests/test_discover.py deleted file mode 100644 index 5da2d5b..0000000 --- a/tests/unittests/test_discover.py +++ /dev/null @@ -1,13 +0,0 @@ -import unittest -from tap_braintree import discover - -class TestDiscover(unittest.TestCase): - - def test_discover(self): - ''' - This test is used to verify that proper catalog is returned by discover function - ''' - - return_catalog = discover() - self.assertTrue(type(return_catalog), dict) - \ No newline at end of file diff --git a/tests/unittests/test_exception_handling.py b/tests/unittests/test_exception_handling.py deleted file mode 100644 index 5b3abb1..0000000 --- a/tests/unittests/test_exception_handling.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest -from unittest import mock -from parameterized import parameterized -from braintree.exceptions.upgrade_required_error import UpgradeRequiredError -from braintree.exceptions.too_many_requests_error import TooManyRequestsError -from braintree.exceptions.server_error import ServerError -from braintree.exceptions.down_for_maintenance_error import DownForMaintenanceError -from tap_braintree.streams import MerchantAccount - - - -class TestBraintreeAPIResponseException(unittest.TestCase): - '''Test case to verify if errors are getting raised properly or not''' - - @parameterized.expand([ - ['upgrade_required_error', [{'error':'upgrade required error'}, UpgradeRequiredError, 426]], - ['too_many_requests_error', [{'error':'signature cannot be blank'}, TooManyRequestsError, 429]], - ['server_error', [{'error':'signature cannot be blank'}, ServerError, 500]], - ['service_unavailable_error', [{'error':'signature cannot be blank'}, DownForMaintenanceError, 503]], - ]) - @mock.patch("tap_braintree.streams.MerchantAccount.sdk_call") - def test_raised_error(self, name, actual, mocked_sdk_call): - '''Test function to test whether correct errors are getting raised or not''' - - mocked_sdk_call.side_effect = actual[1] - stream_obj = MerchantAccount() - with self.assertRaises(actual[1]) as e: - stream_obj.sync( - gateway = "test", - config = {"start_date": ""}, - schema = {}, - state = {}, - selected_streams = ["merchant_accounts"] - ) - - self.assertEqual(type(e.exception), actual[1]) diff --git a/tests/unittests/test_main.py b/tests/unittests/test_main.py deleted file mode 100644 index f9e3622..0000000 --- a/tests/unittests/test_main.py +++ /dev/null @@ -1,43 +0,0 @@ -import unittest -from unittest import mock -from parameterized import parameterized -from braintree.exceptions.authentication_error import AuthenticationError -from tap_braintree import main - -class Mocked(): - config = {"merchant_id": "test", "public_key": "test", "private_key": "test"} - state = {} - catalog = {} - - def __init__(self, *args): - pass - - -class TestMain(unittest.TestCase): - @parameterized.expand([ - ["Main function for discover", [True, Mocked], None], - ["Main function for sync mode", [False, Mocked], None], - ["Main function for authentication error in sync", [False, AuthenticationError], None] - ]) - @mock.patch("tap_braintree.json.dump") - @mock.patch("tap_braintree._sync") - @mock.patch("tap_braintree.discover") - @mock.patch("tap_braintree.braintree.BraintreeGateway") - @mock.patch("tap_braintree.utils.parse_args" ) - def test_main(self, test_name, test_value, expected_value, mocked_parse_args, mocked_gateway, mocked_discover, mocked_sync, mocked_json_dumps): - """ - Test to verify that main function execute properly - """ - Mocked.discover = test_value[0] - mocked_parse_args.return_value = Mocked - mocked_sync.side_effect = test_value[1] - - main() - - if test_value[0]: - call_count = mocked_discover.call_count - else: - call_count = mocked_sync.call_count - - self.assertEqual(call_count, 1) - \ No newline at end of file diff --git a/tests/unittests/test_schema.py b/tests/unittests/test_schema.py deleted file mode 100644 index 992245e..0000000 --- a/tests/unittests/test_schema.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest -from tap_braintree import schema -from unittest import mock - -class TestSchema(unittest.TestCase): - - def test_get_schemas(self): - ''' - This test function verifies that proper schemas and metadata are returned by the get_schemas function - ''' - - return_schemas, return_metadata = schema.get_schemas() - self.assertTrue(type(return_schemas), dict) - self.assertTrue(type(return_metadata), dict) - - - @mock.patch("os.path.join") - def test_get_abs_path(self, mock_os_path): - ''' - This test function is used to verify that we get proper path for given file - ''' - - mock_os_path.return_value = "abc/xyz" - assert schema.get_abs_path("ab") == "abc/xyz" #here "ab" is the name of the file \ No newline at end of file diff --git a/tests/unittests/test_sync.py b/tests/unittests/test_sync.py deleted file mode 100644 index 39b61e8..0000000 --- a/tests/unittests/test_sync.py +++ /dev/null @@ -1,150 +0,0 @@ -import pytz -import unittest -from datetime import datetime -from unittest.mock import patch, call -from tap_braintree import _sync -from tap_braintree.streams import MerchantAccount, AddOn, SettlementBatchSummary, Transaction, Subscription -from parameterized import parameterized - -class Mocked: - - def __init__(self, stream=None, created_at=None, updated_at=None, disbursement_date=None): - self.stream = stream - self.schema = Mocked - self.created_at = created_at - self.updated_at = updated_at - self.disbursement_details = self if disbursement_date else None - self.disbursement_date = disbursement_date - - def get_selected_streams(state): - return (Mocked(i) for i in ["add_ons", "customers", "merchant_accounts"]) - - def get_stream(stream_name): - return Mocked() - - def to_dict(): - return {} - -class TestSyncMode(unittest.TestCase): - @patch("tap_braintree.LOGGER.info") - @patch("tap_braintree.streams.FullTableSync.sync", return_value=10) - @patch("tap_braintree.streams.SyncWithWindow.sync", return_value=10) - @patch("tap_braintree.streams.SyncWithoutWindow.sync", return_value=10) - def test_sync(self, mocked_sync_without_window, mocked_sync_with_window, mocked_sync_full_table, mocked_LOGGER): - """ - Test to verify that sync function run properly based on its logger calling - """ - - _sync("test_gateway", {}, Mocked, {}) - expected = [ - call('Starting Sync'), - call("selected_streams: ['add_ons', 'customers', 'merchant_accounts']"), - call('stream: add_ons, parent: None'), - call('stream: customers, parent: None'), - call('stream: merchant_accounts, parent: None'), - call("Sync Streams: ['add_ons', 'customers', 'merchant_accounts']"), - call('START Syncing: add_ons'), - call('FINISHED Syncing: add_ons, total_records: 10'), - call('START Syncing: customers'), - call('FINISHED Syncing: customers, total_records: 10'), - call('discounts: Skipping - not selected'), - call('disputes: Skipping - not selected'), - call('START Syncing: merchant_accounts'), - call('FINISHED Syncing: merchant_accounts, total_records: 10'), - call('plans: Skipping - not selected'), - call('settlement_batch_summary: Skipping - not selected'), - call('subscriptions: Skipping - not selected'), - call('transactions: Skipping - not selected'), - call('Finished sync') - ] - self.assertEqual(mocked_LOGGER.mock_calls, expected, "Logger calls are not as expected") - - @patch("tap_braintree.streams.MerchantAccount.sdk_call", return_value=["test", "test", "test"]) - @patch("tap_braintree.streams.transform_row", return_value={'currency_iso_code': 'USD', 'default': True, 'id': 'cds', 'status': 'active'}) - def test_full_table_sync(self, mocked_transform_row, mocked_sdk_call): - """ - Test to verify that syncing return expected number of records for FULL_TABLE streams - """ - - stream_obj = MerchantAccount() - record_counts = stream_obj.sync( - gateway = "test", - config = {"start_date": ""}, - schema = {}, - state = {}, - selected_streams = ["merchant_accounts"] - ) - - self.assertEqual(mocked_transform_row.call_count, 3, "Not getting expected number of calls") - self.assertEqual(record_counts, 3, "Not getting expected number of records") - - @parameterized.expand([ - ['with_state', {"bookmarks": {"add_ons": {"updated_at": "2022-06-28T00:00:00.000000Z"}}}, None], - ['without_state', {}, None], - ]) - @patch("tap_braintree.streams.AddOn.sdk_call", return_value=[Mocked(None, datetime(2022, 5, 29, 11, 46, 12)), Mocked(None, datetime(2022, 6, 29, 11, 46, 12)), Mocked(None, datetime(2022, 6, 29, 11, 46, 12), datetime(2022, 6, 29, 11, 46, 12))]) - @patch("tap_braintree.streams.transform_row", return_value="test_data") - def test_sync_without_window(self, name, test_data, expected_data, mocked_transform_row, mocked_sdk_call): - """ - Test to verify that syncing without date window return expected number of records for INCREMENTAL streams - """ - - stream_obj = AddOn() - record_counts = stream_obj.sync( - gateway = "test", - config = {"start_date": "2022-06-25T00:00:00Z"}, - schema = {}, - state = test_data, - selected_streams = ["add_ons"] - ) - - self.assertEqual(record_counts, 2, "Not getting expected number of the records") - self.assertEqual(mocked_transform_row.call_count, 2, "Not getting expected number of calls") - - @patch("tap_braintree.streams.utils.now", return_value=datetime(2022,6,26,00,00,00).replace(tzinfo=pytz.UTC)) - @patch("tap_braintree.streams.transform_row", return_value="test_data") - @patch("tap_braintree.streams.SettlementBatchSummary.sdk_call", return_value=[{"settlement_date": datetime(2022, 6, 29, 11, 46, 12)}]) - def test_sync_with_window_for_settlement_batch_summary(self, mocked_sdk_call, mocked_transform_row, mocked_utils_now): - """ - Test to verify that syncing with date window return expected number of records for INCREMENTAL stream settlement_batch_summary - """ - - stream_obj = SettlementBatchSummary() - record_counts = stream_obj.sync( - gateway = "test", - config = {"start_date": "2022-06-25T00:00:00Z"}, - schema = {}, - state = {}, - selected_streams = ["settlement_batch_summary"] - ) - - self.assertEqual(record_counts, 1, "Not getting expected number of the records") - self.assertEqual(mocked_transform_row.call_count, 32, "Not getting expected number of calls") - - @patch("tap_braintree.streams.utils.now", return_value=datetime(2022,6,30,00,00,00).replace(tzinfo=pytz.UTC)) - @patch("tap_braintree.streams.transform_row", return_value="test_data") - @patch("tap_braintree.streams.Transaction.sdk_call", return_value=[Mocked(None, datetime(2022, 5, 29, 11, 46, 12)), Mocked(None, datetime(2022, 6, 29, 11, 46, 12)), Mocked(None, datetime(2022, 6, 29, 11, 46, 12), datetime(2022, 6, 29, 11, 46, 12), datetime(2022, 6, 29, 11, 46, 12))]) - def test_sync_with_window_for_transactions(self, mocked_sdk_call, mocked_transform_row, mocked_utils_now): - """ - Test to verify that syncing with date window return expected number of records for INCREMENTAL streams - """ - - stream_obj = Transaction() - record_counts = stream_obj.sync( - gateway = "test", - config = {"start_date": "2022-06-25T00:00:00Z"}, - schema = {}, - state = { - "bookmarks": { - "transactions": { - "latest_updated_at": "2022-06-18T07:13:21.000000Z", - "latest_disbursement_date": "2022-06-18T00:00:00.000000Z", - "created_at": "2022-05-28T11:58:25.385256Z" - } - }, - }, - selected_streams = ["transactions"] - ) - - self.assertEqual(record_counts, 2, "Not getting expected number of the records") - self.assertEqual(mocked_transform_row.call_count, 192, "Not getting expected number of calls") diff --git a/tests/unittests/test_timeout.py b/tests/unittests/test_timeout.py new file mode 100644 index 0000000..40516b6 --- /dev/null +++ b/tests/unittests/test_timeout.py @@ -0,0 +1,63 @@ +import unittest +from unittest import mock +from parameterized import parameterized +from tap_braintree import main + + +class Mocked(): + ''' Class to initialize required variables''' + def __init__(self): + self.config = {"merchant_id": "test", "public_key": "test", "private_key": "test"} + self.state = {} + self.catalog = {} + self.discover = True + self.environment = "Sandbox" + +class TestTimeout(unittest.TestCase): + ''' + Test class to validate fetched timeout value + ''' + @parameterized.expand([ + ['invalid_string_value_in_config', {"request_timeout" : "abc", "environment" : "Sandbox"}, None], + ['integer_type_zero_value_in_config', {"request_timeout" : 0, "environment" : "Sandbox"}, None], + ['string_type_zero_value_in_config', {"request_timeout" : "0", "environment" : "Sandbox"}, None] + ]) + @mock.patch("tap_braintree.braintree.Environment") + @mock.patch("tap_braintree.do_discover") + @mock.patch("tap_braintree.braintree.Configuration") + @mock.patch("tap_braintree.utils.parse_args") + def test_request_timeout_invalid_value(self, name, test_value, expected_value, mocked_parse_args,mocked_configure, mocked_discover, mocked_environment): + ''' + Test function to verify that when invalid value is provide as timeout value + in config then error is raised + ''' + mocked_obj = Mocked() + mocked_obj.config = mocked_obj.config | test_value + mocked_parse_args.return_value = mocked_obj + mocked_environment.return_value = mocked_obj + + with self.assertRaises(ValueError) as e: + main() + self.assertEqual(str(e.exception), "Please provide a value greater than 0 for the request_timeout parameter in config") + + @parameterized.expand([ + ['timeout_value_not_in_config', {"environment" : "Sandbox"}, 300.0], + ['string_type_timeout_value_in_config', {"request_timeout" : "200", "environment" : "Sandbox"}, 200.0], + ['integer_type_timeout_value_in_config', {"request_timeout" : 200, "environment" : "Sandbox"}, 200.0] + ]) + @mock.patch("tap_braintree.braintree.Environment") + @mock.patch("tap_braintree.do_discover") + @mock.patch("tap_braintree.braintree.Configuration") + @mock.patch("tap_braintree.utils.parse_args" ) + def test_request_timeout_valid_value(self,name, test_value, expected_value, mocked_parse_args,mocked_configure, mocked_discover, mocked_environment): + """ + Test function to verify when valid timeout value is provided in config and when + no value is provided in config + """ + mocked_obj = Mocked() + mocked_obj.config = mocked_obj.config | test_value + mocked_parse_args.return_value = mocked_obj + mocked_environment.return_value = mocked_obj + + main() + mocked_configure.assert_called_with(mocked_environment.Sandbox, merchant_id='test', public_key='test', private_key='test',timeout=expected_value) From ffee9a453c40d17e86dad3dfc13e0fbab1b15de1 Mon Sep 17 00:00:00 2001 From: Jay Tilala Date: Tue, 2 Aug 2022 18:58:31 +0530 Subject: [PATCH 5/6] updated unittest --- tests/unittests/test_timeout.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/unittests/test_timeout.py b/tests/unittests/test_timeout.py index 40516b6..c70ef94 100644 --- a/tests/unittests/test_timeout.py +++ b/tests/unittests/test_timeout.py @@ -13,6 +13,11 @@ def __init__(self): self.discover = True self.environment = "Sandbox" + +@mock.patch("tap_braintree.braintree.Environment") +@mock.patch("tap_braintree.do_discover") +@mock.patch("tap_braintree.braintree.Configuration") +@mock.patch("tap_braintree.utils.parse_args") class TestTimeout(unittest.TestCase): ''' Test class to validate fetched timeout value @@ -22,11 +27,7 @@ class TestTimeout(unittest.TestCase): ['integer_type_zero_value_in_config', {"request_timeout" : 0, "environment" : "Sandbox"}, None], ['string_type_zero_value_in_config', {"request_timeout" : "0", "environment" : "Sandbox"}, None] ]) - @mock.patch("tap_braintree.braintree.Environment") - @mock.patch("tap_braintree.do_discover") - @mock.patch("tap_braintree.braintree.Configuration") - @mock.patch("tap_braintree.utils.parse_args") - def test_request_timeout_invalid_value(self, name, test_value, expected_value, mocked_parse_args,mocked_configure, mocked_discover, mocked_environment): + def test_request_timeout_invalid_value(self, mocked_parse_args, mocked_configure, mocked_discover, mocked_environment, name, test_value, expected_value): ''' Test function to verify that when invalid value is provide as timeout value in config then error is raised @@ -45,11 +46,7 @@ def test_request_timeout_invalid_value(self, name, test_value, expected_value, m ['string_type_timeout_value_in_config', {"request_timeout" : "200", "environment" : "Sandbox"}, 200.0], ['integer_type_timeout_value_in_config', {"request_timeout" : 200, "environment" : "Sandbox"}, 200.0] ]) - @mock.patch("tap_braintree.braintree.Environment") - @mock.patch("tap_braintree.do_discover") - @mock.patch("tap_braintree.braintree.Configuration") - @mock.patch("tap_braintree.utils.parse_args" ) - def test_request_timeout_valid_value(self,name, test_value, expected_value, mocked_parse_args,mocked_configure, mocked_discover, mocked_environment): + def test_request_timeout_valid_value(self, mocked_parse_args, mocked_configure, mocked_discover, mocked_environment, name, test_value, expected_value, ): """ Test function to verify when valid timeout value is provided in config and when no value is provided in config From 548702d72eab3be09ff486aa8ba6a71b9c257bf6 Mon Sep 17 00:00:00 2001 From: Jay Tilala Date: Wed, 3 Aug 2022 14:45:11 +0530 Subject: [PATCH 6/6] Added unittest for backoff --- tests/unittests/test_backoff.py | 57 +++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/unittests/test_backoff.py diff --git a/tests/unittests/test_backoff.py b/tests/unittests/test_backoff.py new file mode 100644 index 0000000..4cfd607 --- /dev/null +++ b/tests/unittests/test_backoff.py @@ -0,0 +1,57 @@ +import unittest +from unittest import mock +from parameterized import parameterized +from braintree.exceptions.too_many_requests_error import TooManyRequestsError +from braintree.exceptions.server_error import ServerError +from braintree.exceptions.service_unavailable_error import ServiceUnavailableError +from braintree.exceptions.gateway_timeout_error import GatewayTimeoutError +from tap_braintree.streams import AddOn, Transaction + + +class TestBackoff(unittest.TestCase): + ''' + Test that backoff logic works properly + ''' + + gateway = "test" + config = {"start_date":"2022-08-01T00:00:00Z"} + schema = {} + state = {} + + @parameterized.expand([ + ['connection_error', ConnectionError, 5], + ['too_many_requests_error', TooManyRequestsError, 5], + ['server_error', ServerError, 5], + ['service_unavailable_error', ServiceUnavailableError, 5], + ['gateway_timeout_error', GatewayTimeoutError, 5] + ]) + @mock.patch("tap_braintree.streams.AddOn.sdk_call") + @mock.patch("time.sleep") + def test_backoff_for_sync_without_window(self, name, test_exception, expected_count, mocked_time, mocked_sdk_call): + '''Test function to verify working of backoff for sync of incremental streams without window''' + + stream_obj = AddOn() + mocked_sdk_call.side_effect = test_exception('exception') + + with self.assertRaises(test_exception): + stream_obj.sync(self.gateway, self.config, self.schema, self.state, ["add_ons"]) + self.assertEqual(mocked_sdk_call.call_count, expected_count) + + @parameterized.expand([ + ['connection_error', ConnectionError, 5], + ['too_many_requests_error', TooManyRequestsError, 5], + ['server_error', ServerError, 5], + ['service_unavailable_error', ServiceUnavailableError, 5], + ['gateway_timeout_error', GatewayTimeoutError, 5] + ]) + @mock.patch("tap_braintree.streams.Transaction.sdk_call") + @mock.patch("time.sleep") + def test_backoff_for_sync_with_window(self, name, test_exception, expected_count, mocked_time, mocked_sdk_call): + '''Test function to verify working of backoff for sync of incremental streams with window''' + + stream_obj = Transaction() + mocked_sdk_call.side_effect = test_exception('exception') + + with self.assertRaises(test_exception): + stream_obj.GetRecords(self.gateway, self.config["start_date"], self.config["start_date"]) + self.assertEqual(mocked_sdk_call.call_count, expected_count)