def test_context_is_reset_after_request_has_finished(self): context = {'foo': 'bar'} def responseCls(connection, response): connection.called = True self.assertEqual(connection.context, context) con = Connection() con.called = False con.connection = Mock() con.responseCls = responseCls con.set_context(context) self.assertEqual(con.context, context) con.request('/') # Context should have been reset self.assertTrue(con.called) self.assertEqual(con.context, {}) # Context should also be reset if a method inside request throws con = Connection() con.connection = Mock() con.set_context(context) self.assertEqual(con.context, context) con.connection.request = Mock(side_effect=ssl.SSLError()) try: con.request('/') except ssl.SSLError: pass self.assertEqual(con.context, {}) con.connection = Mock() con.set_context(context) self.assertEqual(con.context, context) con.responseCls = Mock(side_effect=ValueError()) try: con.request('/') except ValueError: pass self.assertEqual(con.context, {})
def test_context_is_reset_after_request_has_finished(self): context = {'foo': 'bar'} def responseCls(connection, response): connection.called = True self.assertEqual(connection.context, context) con = Connection() con.called = False con.connection = Mock() con.responseCls = responseCls con.set_context(context) self.assertEqual(con.context, context) con.request('/') # Context should have been reset self.assertTrue(con.called) self.assertEqual(con.context, {}) # Context should also be reset if a method inside request throws con = Connection() con.connection = Mock() con.set_context(context) self.assertEqual(con.context, context) con.connection.request = Mock(side_effect=ssl.SSLError()) try: con.request('/') except ssl.SSLError: pass self.assertEqual(con.context, {}) con.connection = Mock() con.set_context(context) self.assertEqual(con.context, context) con.responseCls = Mock(side_effect=ValueError()) try: con.request('/') except ValueError: pass self.assertEqual(con.context, {})
def test_context_is_reset_after_request_has_finished(self): context = {"foo": "bar"} def responseCls(connection, response) -> mock.MagicMock: connection.called = True self.assertEqual(connection.context, context) return mock.MagicMock(spec=Response) con = Connection() con.called = False con.connection = Mock() con.responseCls = responseCls con.set_context(context) self.assertEqual(con.context, context) con.request("/") # Context should have been reset self.assertTrue(con.called) self.assertEqual(con.context, {}) # Context should also be reset if a method inside request throws con = Connection(timeout=1, retry_delay=0.1) con.connection = Mock() con.set_context(context) self.assertEqual(con.context, context) con.connection.request = Mock(side_effect=ssl.SSLError()) try: con.request("/") except ssl.SSLError: pass self.assertEqual(con.context, {}) con.connection = Mock() con.set_context(context) self.assertEqual(con.context, context) con.responseCls = Mock(side_effect=ValueError()) try: con.request("/") except ValueError: pass self.assertEqual(con.context, {})
def test_cache_busting(self): params1 = {"foo1": "bar1", "foo2": "bar2"} params2 = [("foo1", "bar1"), ("foo2", "bar2")] con = Connection() con.connection = Mock() con.pre_connect_hook = Mock() con.pre_connect_hook.return_value = {}, {} con.cache_busting = False con.request(action="/path", params=params1) args, kwargs = con.pre_connect_hook.call_args self.assertFalse("cache-busting" in args[0]) self.assertEqual(args[0], params1) con.request(action="/path", params=params2) args, kwargs = con.pre_connect_hook.call_args self.assertFalse("cache-busting" in args[0]) self.assertEqual(args[0], params2) con.cache_busting = True con.request(action="/path", params=params1) args, kwargs = con.pre_connect_hook.call_args self.assertTrue("cache-busting" in args[0]) con.request(action="/path", params=params2) args, kwargs = con.pre_connect_hook.call_args self.assertTrue("cache-busting" in args[0][len(params2)])
def test_cache_busting(self): params1 = {"foo1": "bar1", "foo2": "bar2"} params2 = [("foo1", "bar1"), ("foo2", "bar2")] con = Connection() con.connection = Mock() con.pre_connect_hook = Mock() con.pre_connect_hook.return_value = {}, {} con.cache_busting = False con.request(action="/path", params=params1) args, kwargs = con.pre_connect_hook.call_args self.assertFalse("cache-busting" in args[0]) self.assertEqual(args[0], params1) con.request(action="/path", params=params2) args, kwargs = con.pre_connect_hook.call_args self.assertFalse("cache-busting" in args[0]) self.assertEqual(args[0], params2) con.cache_busting = True con.request(action="/path", params=params1) args, kwargs = con.pre_connect_hook.call_args self.assertTrue("cache-busting" in args[0]) con.request(action="/path", params=params2) args, kwargs = con.pre_connect_hook.call_args self.assertTrue("cache-busting" in args[0][len(params2)])
def test_retry_rate_limit_error_forever_with_old_retry_class( self, mock_connect): con = Connection() con.connection = Mock() self.retry_counter = 0 def mock_connect_side_effect(*args, **kwargs): self.retry_counter += 1 if self.retry_counter < 4: headers = {'retry-after': 0.1} raise RateLimitReachedError(headers=headers) return 'success' mock_connect.__name__ = 'mock_connect' headers = {'retry-after': 0.2} mock_connect.side_effect = mock_connect_side_effect retry_request = RetryForeverOnRateLimitError(timeout=0.1, retry_delay=0.1, backoff=1) retry_request(con.request)(action='/') # We have waited longer the timeout but continue to retry result = retry_request(con.request)(action='/') self.assertEqual(result, "success") self.assertEqual(mock_connect.call_count, 5, 'Retry logic failed')
def test_parse_errors_can_be_retried(self): class RetryableThrowingError(Response): parse_error_counter: int = 0 success_counter: int = 0 def __init__(self, *_, **__): super().__init__(mock.MagicMock(), mock.MagicMock()) def parse_body(self): return super().parse_body() def parse_error(self): RetryableThrowingError.parse_error_counter += 1 if RetryableThrowingError.parse_error_counter > 1: return "success" else: raise RateLimitReachedError() def success(self): RetryableThrowingError.success_counter += 1 if RetryableThrowingError.success_counter > 1: return True else: return False con = Connection() con.connection = Mock() con.responseCls = RetryableThrowingError result = con.request(action="/", retry_failed=True) self.assertEqual(result.success(), True)
def test_retry_connection(self): connection = Connection(timeout=1, retry_delay=0.2) connection.connection = Mock(request=Mock( side_effect=socket.gaierror(''))) self.assertRaises(socket.gaierror, connection.request, '/') self.assertEqual(connection.connection.request.call_count, 6)
def test_cache_busting(self): params1 = {'foo1': 'bar1', 'foo2': 'bar2'} params2 = [('foo1', 'bar1'), ('foo2', 'bar2')] con = Connection() con.connection = Mock() con.pre_connect_hook = Mock() con.pre_connect_hook.return_value = {}, {} con.cache_busting = False con.request(action='/path', params=params1) args, kwargs = con.pre_connect_hook.call_args self.assertFalse('cache-busting' in args[0]) self.assertEqual(args[0], params1) con.request(action='/path', params=params2) args, kwargs = con.pre_connect_hook.call_args self.assertFalse('cache-busting' in args[0]) self.assertEqual(args[0], params2) con.cache_busting = True con.request(action='/path', params=params1) args, kwargs = con.pre_connect_hook.call_args self.assertTrue('cache-busting' in args[0]) con.request(action='/path', params=params2) args, kwargs = con.pre_connect_hook.call_args self.assertTrue('cache-busting' in args[0][len(params2)])
def test_cache_busting(self): params1 = {'foo1': 'bar1', 'foo2': 'bar2'} params2 = [('foo1', 'bar1'), ('foo2', 'bar2')] con = Connection() con.connection = Mock() con.pre_connect_hook = Mock() con.pre_connect_hook.return_value = {}, {} con.cache_busting = False con.request(action='/path', params=params1) args, kwargs = con.pre_connect_hook.call_args self.assertFalse('cache-busting' in args[0]) self.assertEqual(args[0], params1) con.request(action='/path', params=params2) args, kwargs = con.pre_connect_hook.call_args self.assertFalse('cache-busting' in args[0]) self.assertEqual(args[0], params2) con.cache_busting = True con.request(action='/path', params=params1) args, kwargs = con.pre_connect_hook.call_args self.assertTrue('cache-busting' in args[0]) con.request(action='/path', params=params2) args, kwargs = con.pre_connect_hook.call_args self.assertTrue('cache-busting' in args[0][len(params2)])
def test_content_length(self): con = Connection() con.connection = Mock() ## GET method # No data, no content length should be present con.request('/test', method='GET', data=None) call_kwargs = con.connection.request.call_args[1] self.assertTrue('Content-Length' not in call_kwargs['headers']) # '' as data, no content length should be present con.request('/test', method='GET', data='') call_kwargs = con.connection.request.call_args[1] self.assertTrue('Content-Length' not in call_kwargs['headers']) # 'a' as data, content length should be present (data in GET is not # corect, but anyways) con.request('/test', method='GET', data='a') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '1') ## POST, PUT method # No data, content length should be present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data=None) call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '0') # '' as data, content length should be present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data='') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '0') # No data, raw request, do not touch Content-Length if present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data=None, headers={'Content-Length': '42'}, raw=True) putheader_call_list = con.connection.putheader.call_args_list self.assertIn(call('Content-Length', '42'), putheader_call_list) # '' as data, raw request, do not touch Content-Length if present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data=None, headers={'Content-Length': '42'}, raw=True) putheader_call_list = con.connection.putheader.call_args_list self.assertIn(call('Content-Length', '42'), putheader_call_list) # 'a' as data, content length should be present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data='a') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '1')
def test_retry_with_backoff(self, mock_connect): con = Connection() con.connection = Mock() mock_connect.side_effect = socket.gaierror("") retry_request = Retry(timeout=1, retry_delay=0.1, backoff=1) self.assertRaises(socket.gaierror, retry_request(con.request), action="/") self.assertGreater(mock_connect.call_count, 1, "Retry logic failed")
def test_retry_rate_limit_error_timeout(self, mock_connect): con = Connection() con.connection = Mock() mock_connect.__name__ = "mock_connect" headers = {"retry-after": 0.2} mock_connect.side_effect = RateLimitReachedError(headers=headers) retry_request = Retry(timeout=1, retry_delay=0.1, backoff=1) self.assertRaises(RateLimitReachedError, retry_request(con.request), action="/") self.assertGreater(mock_connect.call_count, 1, "Retry logic failed")
def test_retry_connection(self): con = Connection(timeout=0.2, retry_delay=0.1) con.connection = Mock() connect_method = 'libcloud.common.base.Connection.request' with patch(connect_method) as mock_connect: try: mock_connect.side_effect = socket.gaierror('') con.request('/') except socket.gaierror: pass
def test_retry_rate_limit_error_timeout(self, mock_connect): con = Connection() con.connection = Mock() mock_connect.__name__ = 'mock_connect' headers = {'retry-after': 0.2} mock_connect.side_effect = RateLimitReachedError(headers=headers) retry_request = Retry(timeout=0.4, retry_delay=0.1, backoff=1) self.assertRaises(RateLimitReachedError, retry_request(con.request), action='/') self.assertEqual(mock_connect.call_count, 2, 'Retry logic failed')
def test_content_length(self): con = Connection() con.connection = Mock() # GET method # No data, no content length should be present con.request('/test', method='GET', data=None) call_kwargs = con.connection.request.call_args[1] self.assertTrue('Content-Length' not in call_kwargs['headers']) # '' as data, no content length should be present con.request('/test', method='GET', data='') call_kwargs = con.connection.request.call_args[1] self.assertTrue('Content-Length' not in call_kwargs['headers']) # 'a' as data, content length should be present (data in GET is not # correct, but anyways) con.request('/test', method='GET', data='a') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '1') # POST, PUT method # No data, content length should be present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data=None) call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '0') # '' as data, content length should be present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data='') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '0') # No data, raw request, do not touch Content-Length if present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data=None, headers={'Content-Length': '42'}, raw=True) putheader_call_list = con.connection.putheader.call_args_list self.assertIn(call('Content-Length', '42'), putheader_call_list) # '' as data, raw request, do not touch Content-Length if present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data=None, headers={'Content-Length': '42'}, raw=True) putheader_call_list = con.connection.putheader.call_args_list self.assertIn(call('Content-Length', '42'), putheader_call_list) # 'a' as data, content length should be present for method in ['POST', 'PUT', 'post', 'put']: con.request('/test', method=method, data='a') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '1')
def test_retry_with_backoff(self): con = Connection() con.connection = Mock() connect_method = "libcloud.common.base.Connection.request" with patch(connect_method) as mock_connect: mock_connect.__name__ = "mock_connect" with self.assertRaises(socket.gaierror): mock_connect.side_effect = socket.gaierror("") retry_request = retry(timeout=2, retry_delay=0.1, backoff=1) retry_request(con.request)(action="/") self.assertGreater(mock_connect.call_count, 1, "Retry logic failed")
def test_retry_connection(self): con = Connection(timeout=1, retry_delay=0.1) con.connection = Mock() connect_method = 'libcloud.common.base.Connection.request' with patch(connect_method) as mock_connect: try: mock_connect.side_effect = socket.gaierror('') con.request('/') except socket.gaierror: pass except Exception: self.fail('Failed to raise socket exception')
def test_content_length(self): con = Connection() con.connection = Mock() # GET method # No data, no content length should be present con.request("/test", method="GET", data=None) call_kwargs = con.connection.request.call_args[1] self.assertTrue("Content-Length" not in call_kwargs["headers"]) # '' as data, no content length should be present con.request("/test", method="GET", data="") call_kwargs = con.connection.request.call_args[1] self.assertTrue("Content-Length" not in call_kwargs["headers"]) # 'a' as data, content length should be present (data in GET is not # correct, but anyways) con.request("/test", method="GET", data="a") call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs["headers"]["Content-Length"], "1") # POST, PUT method # No data, content length should be present for method in ["POST", "PUT", "post", "put"]: con.request("/test", method=method, data=None) call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs["headers"]["Content-Length"], "0") # '' as data, content length should be present for method in ["POST", "PUT", "post", "put"]: con.request("/test", method=method, data="") call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs["headers"]["Content-Length"], "0") # No data, raw request, do not touch Content-Length if present for method in ["POST", "PUT", "post", "put"]: con.request("/test", method=method, data=None, headers={"Content-Length": "42"}, raw=True) putheader_call_list = con.connection.putheader.call_args_list self.assertIn(call("Content-Length", "42"), putheader_call_list) # '' as data, raw request, do not touch Content-Length if present for method in ["POST", "PUT", "post", "put"]: con.request("/test", method=method, data=None, headers={"Content-Length": "42"}, raw=True) putheader_call_list = con.connection.putheader.call_args_list self.assertIn(call("Content-Length", "42"), putheader_call_list) # 'a' as data, content length should be present for method in ["POST", "PUT", "post", "put"]: con.request("/test", method=method, data="a") call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs["headers"]["Content-Length"], "1")
def test_retry_with_backoff(self): con = Connection() con.connection = Mock() connect_method = 'libcloud.common.base.Connection.request' with patch(connect_method) as mock_connect: mock_connect.__name__ = 'mock_connect' with self.assertRaises(socket.gaierror): mock_connect.side_effect = socket.gaierror('') retry_request = retry(timeout=0.2, retry_delay=0.1, backoff=1) retry_request(con.request)(action='/') self.assertGreater(mock_connect.call_count, 1, 'Retry logic failed')
def test_retry_with_timeout(self): con = Connection() con.connection = Mock() connect_method = 'libcloud.common.base.Connection.request' with patch(connect_method) as mock_connect: mock_connect.__name__ = 'mock_connect' with self.assertRaises(socket.gaierror): mock_connect.side_effect = socket.gaierror('') retry_request = retry(timeout=2, retry_delay=.1, backoff=1) retry_request(con.request)(action='/') self.assertGreater(mock_connect.call_count, 1, 'Retry logic failed')
def test_content_length(self): con = Connection() con.connection = Mock() ## GET method # No data, no content length should be present con.request('/test', method='GET', data=None) call_kwargs = con.connection.request.call_args[1] self.assertTrue('Content-Length' not in call_kwargs['headers']) # '' as data, no content length should be present con.request('/test', method='GET', data='') call_kwargs = con.connection.request.call_args[1] self.assertTrue('Content-Length' not in call_kwargs['headers']) # 'a' as data, content length should be present (data is GET is not # corect, but anyways) con.request('/test', method='GET', data='a') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '1') ## POST, PUT method # No data, no content length should be present con.request('/test', method='POST', data=None) call_kwargs = con.connection.request.call_args[1] self.assertTrue('Content-Length' not in call_kwargs['headers']) con.request('/test', method='PUT', data=None) call_kwargs = con.connection.request.call_args[1] self.assertTrue('Content-Length' not in call_kwargs['headers']) # '' as data, content length should be present con.request('/test', method='POST', data='') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '0') con.request('/test', method='PUT', data='') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '0') # 'a' as data, content length should be present con.request('/test', method='POST', data='a') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '1') con.request('/test', method='PUT', data='a') call_kwargs = con.connection.request.call_args[1] self.assertEqual(call_kwargs['headers']['Content-Length'], '1')
def test_retry_connection_backoff(self): connection = Connection(timeout=10, retry_delay=0.1, backoff=2) connection.connection = Mock(request=Mock( side_effect=socket.gaierror(''))) with patch('time.sleep') as sleep_fn: sleep_fn.side_effect = [ None, None, None, None, None, StopIteration ] self.assertRaises(StopIteration, connection.request, '/') self.assertEqual(connection.connection.request.call_count, 6) self.assertEqual(sleep_fn.call_count, 6) self.assertEqual( sleep_fn.call_args_list, [mock.call(i) for i in (0.1, 0.2, 0.4, 0.8, 1.6, 3.2)])
def test_retry_should_not_retry_on_non_defined_exception(self, mock_connect): con = Connection() con.connection = Mock() self.retry_counter = 0 mock_connect.__name__ = "mock_connect" mock_connect.side_effect = ValueError("should not retry this " "error") retry_request = Retry(timeout=5, retry_delay=0.1, backoff=1) self.assertRaisesRegex( ValueError, "should not retry this error", retry_request(con.request), action="/", ) self.assertEqual(mock_connect.call_count, 1, "Retry logic failed")
def test_retry_connection_timeout(self, datetime_obj): connection = Connection(timeout=65, retry_delay=20) connection.connection = Mock(request=Mock( side_effect=socket.gaierror(''))) datetime_obj.now.side_effect = [ datetime(2017, 7, 28, 0, 26, 10, 0), datetime(2017, 7, 28, 0, 26, 10, 0), datetime(2017, 7, 28, 0, 26, 30, 0), datetime(2017, 7, 28, 0, 26, 50, 0), datetime(2017, 7, 28, 0, 27, 10, 0), datetime(2017, 7, 28, 0, 27, 30, 0), ] with patch('time.sleep') as sleep_fn: self.assertRaises(socket.gaierror, connection.request, '/') self.assertEqual(sleep_fn.call_args_list, [mock.call(i) for i in (20, 20, 20, 5)])
def test_retry_connection_with_iterable_retry_delay(self): connection = Connection(timeout=20, retry_delay=(1, 1, 3, 5), backoff=1) connection.connection = Mock(request=Mock( side_effect=socket.gaierror(''))) with patch('time.sleep') as sleep_fn: sleep_fn.side_effect = [ None, None, None, None, None, StopIteration ] self.assertRaises(StopIteration, connection.request, '/') self.assertEqual(connection.connection.request.call_count, 6) self.assertEqual(sleep_fn.call_count, 6) self.assertEqual(sleep_fn.call_args_list, [mock.call(i) for i in (1, 1, 3, 5, 5, 5)])
def test_retry_should_not_retry_on_non_defined_exception( self, mock_connect): con = Connection() con.connection = Mock() connect_method = 'libcloud.common.base.Connection.request' self.retry_counter = 0 mock_connect.__name__ = 'mock_connect' headers = {'retry-after': 0.2} mock_connect.side_effect = ValueError('should not retry this ' 'error') retry_request = Retry(timeout=5, retry_delay=0.1, backoff=1) self.assertRaisesRegex(ValueError, 'should not retry this error', retry_request(con.request), action='/') self.assertEqual(mock_connect.call_count, 1, 'Retry logic failed')
def test_request_parses_errors(self): class ThrowingResponse(Response): def __init__(self, *_, **__): super().__init__(mock.MagicMock(), mock.MagicMock()) def parse_body(self): return super().parse_body() def parse_error(self): raise RateLimitReachedError() def success(self): return False con = Connection() con.connection = Mock() con.responseCls = ThrowingResponse with self.assertRaises(RateLimitReachedError): con.request(action="/")
def test_retry_rate_limit_error_success_on_second_attempt(self, mock_connect): con = Connection() con.connection = Mock() self.retry_counter = 0 def mock_connect_side_effect(*args, **kwargs): self.retry_counter += 1 if self.retry_counter < 2: headers = {"retry-after": 0.2} raise RateLimitReachedError(headers=headers) return "success" mock_connect.__name__ = "mock_connect" mock_connect.side_effect = mock_connect_side_effect retry_request = Retry(timeout=1, retry_delay=0.1, backoff=1) result = retry_request(con.request)(action="/") self.assertEqual(result, "success") self.assertEqual(mock_connect.call_count, 2, "Retry logic failed")
def test_retry_on_all_default_retry_exception_classes(self, mock_connect): con = Connection() con.connection = Mock() self.retry_counter = 0 def mock_connect_side_effect(*args, **kwargs): self.retry_counter += 1 if self.retry_counter < len(RETRY_EXCEPTIONS): raise RETRY_EXCEPTIONS[self.retry_counter] return "success" mock_connect.__name__ = "mock_connect" mock_connect.side_effect = mock_connect_side_effect retry_request = Retry(timeout=0.6, retry_delay=0.1, backoff=1) result = retry_request(con.request)(action="/") self.assertEqual(result, "success") self.assertEqual(mock_connect.call_count, len(RETRY_EXCEPTIONS), "Retry logic failed")
def test_retry_rate_limit_error_success_on_second_attempt( self, mock_connect): con = Connection() con.connection = Mock() connect_method = 'libcloud.common.base.Connection.request' self.retry_counter = 0 def mock_connect_side_effect(*args, **kwargs): self.retry_counter += 1 if self.retry_counter < 2: headers = {'retry-after': 0.2} raise RateLimitReachedError(headers=headers) return 'success' mock_connect.__name__ = 'mock_connect' mock_connect.side_effect = mock_connect_side_effect retry_request = Retry(timeout=0.6, retry_delay=0.1, backoff=1) result = retry_request(con.request)(action='/') self.assertEqual(result, "success") self.assertEqual(mock_connect.call_count, 2, 'Retry logic failed')