From 40589c752f8b476e41c3746b40750d1c873f0a7c Mon Sep 17 00:00:00 2001 From: Samuele Maci Date: Sun, 24 Jun 2018 13:54:50 +0200 Subject: [PATCH 1/6] Re-organize integration tests to expose a single Generic test class and each clients tests class extends from it --- tests/integration/conftest.py | 30 ++++++++++ tests/integration/requests_client_test.py | 73 +++++++++++++---------- 2 files changed, 70 insertions(+), 33 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 42c30d95..b684aaf6 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import socket import time +from contextlib import closing from multiprocessing import Process import bottle @@ -124,6 +126,27 @@ }, }, }, + '/sleep': { + 'get': { + 'operationId': 'sleep', + 'produces': ['application/json'], + 'parameters': [ + { + 'in': 'query', + 'name': 'sec', + 'type': 'number', + 'format': 'float', + 'required': True, + } + ], + 'responses': { + '200': { + 'description': 'HTTP/200', + 'schema': {'$ref': '#/definitions/api_response'}, + }, + }, + }, + }, }, } @@ -216,3 +239,10 @@ def wait_unit_service_starts(url, max_wait_time=10): yield server_address finally: web_service_process.terminate() + + +@pytest.fixture(scope='session') +def not_answering_http_server(): + with closing(socket.socket(socket.AF_INET)) as s: + s.bind(('', 0)) + yield 'http://localhost:{port}'.format(port=s.getsockname()[1]) diff --git a/tests/integration/requests_client_test.py b/tests/integration/requests_client_test.py index c36e3d14..639de8a4 100644 --- a/tests/integration/requests_client_test.py +++ b/tests/integration/requests_client_test.py @@ -20,10 +20,13 @@ from tests.integration.conftest import SWAGGER_SPEC_DICT -class TestServerRequestsClient: +class ServerClientGeneric: + """ + Generic class to run integration tests with the different HTTP clients definitions + """ - http_client_type = RequestsClient - http_future_adapter_type = RequestsFutureAdapter + http_client_type = None + http_future_adapter_type = None @classmethod def setup_class(cls): @@ -40,14 +43,6 @@ def encode_expected_response(cls, response): else: return str(response) - def test_fetch_specs(self, swagger_http_server): - loader = Loader( - http_client=self.http_client, - request_headers={'boolean-header': True}, - ) - spec = loader.load_spec('{server_address}/swagger.json'.format(server_address=swagger_http_server)) - assert spec == SWAGGER_SPEC_DICT - @pytest.fixture def swagger_client(self, swagger_http_server): return SwaggerClient.from_url( @@ -69,6 +64,14 @@ def _response_getter(future, timeout): raise ValueError + def test_fetch_specs(self, swagger_http_server): + loader = Loader( + http_client=self.http_client, + request_headers={'boolean-header': True}, + ) + spec = loader.load_spec('{server_address}/swagger.json'.format(server_address=swagger_http_server)) + assert spec == SWAGGER_SPEC_DICT + def test_swagger_client_json_response(self, swagger_client, result_getter): marshaled_response, raw_response = result_getter(swagger_client.json.get_json(), timeout=1) assert marshaled_response == API_RESPONSE @@ -188,8 +191,7 @@ def test_msgpack_support(self, swagger_http_server): assert unpackb(response.raw_bytes, encoding='utf-8') == API_RESPONSE def test_timeout_errors_are_thrown_as_BravadoTimeoutError(self, swagger_http_server): - timeout_errors = getattr(self.http_future_adapter_type, 'timeout_errors', []) - if not timeout_errors: + if not self.http_future_adapter_type.timeout_errors: pytest.skip('{} does NOT defines timeout_errors'.format(self.http_future_adapter_type)) with pytest.raises(BravadoTimeoutError): @@ -199,23 +201,38 @@ def test_timeout_errors_are_thrown_as_BravadoTimeoutError(self, swagger_http_ser 'params': {}, }).result(timeout=0.01) + def test_swagger_client_timeout_errors_are_thrown_as_BravadoTimeoutError( + self, swagger_client, result_getter, + ): + if not self.http_future_adapter_type.timeout_errors: + pytest.skip('{} does NOT defines timeout_errors'.format(self.http_future_adapter_type)) + + with pytest.raises(BravadoTimeoutError): + result_getter( + swagger_client.sleep.sleep(sec=0.5), + timeout=0.1, + ) + def test_timeout_errors_are_catchable_with_original_exception_types(self, swagger_http_server): - timeout_errors = getattr(self.http_future_adapter_type, 'timeout_errors', []) - if not timeout_errors: + if not self.http_future_adapter_type.timeout_errors: pytest.skip('{} does NOT defines timeout_errors'.format(self.http_future_adapter_type)) - for expected_exception in timeout_errors: - with pytest.raises(expected_exception): + for expected_exception in self.http_future_adapter_type.timeout_errors: + with pytest.raises(expected_exception) as excinfo: self.http_client.request({ 'method': 'GET', 'url': '{server_address}/sleep?sec=0.1'.format(server_address=swagger_http_server), 'params': {}, }).result(timeout=0.01) + assert isinstance(excinfo.value, BravadoTimeoutError) - def test_timeout_errors_are_catchable_as_requests_timeout(self, swagger_http_server): - if not self.http_client_type == RequestsClient: - pytest.skip('{} is not using RequestsClient'.format(self.http_future_adapter_type)) +class TestServerRequestsClient(ServerClientGeneric): + + http_client_type = RequestsClient + http_future_adapter_type = RequestsFutureAdapter + + def test_timeout_errors_are_catchable_as_requests_timeout(self, swagger_http_server): with pytest.raises(requests.exceptions.Timeout): self.http_client.request({ 'method': 'GET', @@ -234,7 +251,7 @@ def request(self, *args, **kwargs): return super(FakeRequestsClient, self).request(*args, **kwargs) -class TestServerRequestsClientFake(TestServerRequestsClient): +class TestServerRequestsClientFake(ServerClientGeneric): """ This test class uses as http client a requests client that has no timeout error specified. This is needed to ensure that the changes are back compatible @@ -250,15 +267,5 @@ def test_timeout_error_not_throws_BravadoTimeoutError_if_no_timeout_errors_speci 'url': '{server_address}/sleep?sec=0.1'.format(server_address=swagger_http_server), 'params': {}, }).result(timeout=0.01) - except BravadoTimeoutError: - pytest.fail('DID RAISE BravadoTimeoutError') - except Exception: - pass - - def test_timeout_errors_are_catchable_with_original_exception_types(self, swagger_http_server): - with pytest.raises(requests.Timeout): - self.http_client.request({ - 'method': 'GET', - 'url': '{server_address}/sleep?sec=0.1'.format(server_address=swagger_http_server), - 'params': {}, - }).result(timeout=0.01) + except Exception as e: + assert not isinstance(e, BravadoTimeoutError) From d85a50bf221c727951c3437d3ae96cf55fb1d6fb Mon Sep 17 00:00:00 2001 From: Samuele Maci Date: Sun, 24 Jun 2018 13:57:17 +0200 Subject: [PATCH 2/6] Re-organize how timeout errors are reraised (generalized to better accomodate connection errors handling) --- bravado/exception.py | 10 ++++----- bravado/fido_client.py | 2 +- bravado/http_future.py | 44 ++++++++++++++++++-------------------- bravado/requests_client.py | 2 +- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/bravado/exception.py b/bravado/exception.py index ac053882..e009853f 100644 --- a/bravado/exception.py +++ b/bravado/exception.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- +from six import with_metaclass + try: - from builtins import TimeoutError as base_exception + from builtins import TimeoutError as base_timeout_error except ImportError: # TimeoutError was introduced in python 3.3+ - base_exception = Exception + base_timeout_error = OSError -from six import with_metaclass - # Dictionary of HTTP status codes to exception classes status_map = {} @@ -332,7 +332,7 @@ class HTTPNetworkAuthenticationRequired(HTTPServerError): status_code = 511 -class BravadoTimeoutError(base_exception): +class BravadoTimeoutError(base_timeout_error): pass diff --git a/bravado/fido_client.py b/bravado/fido_client.py index cd5caa8b..5cf39767 100644 --- a/bravado/fido_client.py +++ b/bravado/fido_client.py @@ -166,7 +166,7 @@ class FidoFutureAdapter(FutureAdapter): retrieve results. """ - timeout_errors = [fido.exceptions.HTTPTimeoutError] + timeout_errors = (fido.exceptions.HTTPTimeoutError,) def __init__(self, eventual_result): self._eventual_result = eventual_result diff --git a/bravado/http_future.py b/bravado/http_future.py index 0e9e9506..787f32ab 100644 --- a/bravado/http_future.py +++ b/bravado/http_future.py @@ -43,7 +43,22 @@ class FutureAdapter(object): """ # Make sure to define the timeout errors associated with your http client - timeout_errors = [] + timeout_errors = () + + def _raise_error(self, base_exception_class, class_name_suffix, exception): + error_type = type( + '{}{}'.format(self.__class__.__name__, class_name_suffix), + (exception.__class__, base_exception_class), + dict(), + ) + six.reraise( + error_type, + error_type(*exception.args), + sys.exc_info()[2], + ) + + def _raise_timeout_error(self, exception): + self._raise_error(BravadoTimeoutError, 'Timeout', exception) def result(self, timeout=None): """ @@ -58,32 +73,15 @@ def result(self, timeout=None): def reraise_errors(func): + @wraps(func) def wrapper(self, *args, **kwargs): - timeout_errors = tuple(getattr(self.future, 'timeout_errors', None) or ()) - - # Make sure that timeout error type for a specific future adapter is generated only once - if timeout_errors and getattr(self.future, '__timeout_error_type', None) is None: - setattr( - self.future, '__timeout_error_type', - type( - '{}Timeout'.format(self.future.__class__.__name__), - tuple(list(timeout_errors) + [BravadoTimeoutError]), - dict(), - ), - ) + timeout_errors = tuple(self.future.timeout_errors or ()) - if timeout_errors: - try: - return func(self, *args, **kwargs) - except timeout_errors as exception: - six.reraise( - self.future.__timeout_error_type, - self.future.__timeout_error_type(*exception.args), - sys.exc_info()[2], - ) - else: + try: return func(self, *args, **kwargs) + except timeout_errors as exception: + self.future._raise_timeout_error(exception) return wrapper diff --git a/bravado/requests_client.py b/bravado/requests_client.py index 9e02a8e3..5d5b36e4 100644 --- a/bravado/requests_client.py +++ b/bravado/requests_client.py @@ -211,7 +211,7 @@ class RequestsFutureAdapter(FutureAdapter): HTTP calls with the Requests library in a future-y sort of way. """ - timeout_errors = [requests.exceptions.ReadTimeout] + timeout_errors = (requests.exceptions.ReadTimeout,) def __init__(self, session, request, misc_options): """Kicks API call for Requests client From c2da0b9a80a5e8dcc46f12dde33d44a6e3d76687 Mon Sep 17 00:00:00 2001 From: Samuele Maci Date: Sun, 24 Jun 2018 23:53:52 +0200 Subject: [PATCH 3/6] Add hack to allow all exceptions to be extended --- bravado/http_future.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bravado/http_future.py b/bravado/http_future.py index 787f32ab..bea6dd32 100644 --- a/bravado/http_future.py +++ b/bravado/http_future.py @@ -46,14 +46,19 @@ class FutureAdapter(object): timeout_errors = () def _raise_error(self, base_exception_class, class_name_suffix, exception): - error_type = type( + error = type( '{}{}'.format(self.__class__.__name__, class_name_suffix), (exception.__class__, base_exception_class), - dict(), - ) + dict( + # Small hack to allow all exceptions to be generated even if they have parameters in the signature + exception.__dict__, + __init__=lambda *args, **kwargs: None, + ), + )() + six.reraise( - error_type, - error_type(*exception.args), + error.__class__, + error, sys.exc_info()[2], ) From 2fb2c9f05fc179a3abef8c506b8dfb70455589f4 Mon Sep 17 00:00:00 2001 From: Samuele Maci Date: Sun, 24 Jun 2018 14:00:37 +0200 Subject: [PATCH 4/6] Add generalized connection errors support --- bravado/exception.py | 11 ++++ bravado/http_future.py | 8 +++ tests/http_future/HttpFuture/conftest.py | 6 +- tests/integration/conftest.py | 7 +-- tests/integration/fido_client_test.py | 6 +- tests/integration/requests_client_test.py | 69 +++++++++++++++++++++++ 6 files changed, 100 insertions(+), 7 deletions(-) diff --git a/bravado/exception.py b/bravado/exception.py index e009853f..048d3589 100644 --- a/bravado/exception.py +++ b/bravado/exception.py @@ -1,6 +1,13 @@ # -*- coding: utf-8 -*- from six import with_metaclass +try: + from builtins import ConnectionError as base_connection_error +except ImportError: + # ConnectionError was introduced in python 3.3+ + base_connection_error = OSError + + try: from builtins import TimeoutError as base_timeout_error except ImportError: @@ -336,6 +343,10 @@ class BravadoTimeoutError(base_timeout_error): pass +class BravadoConnectionError(base_connection_error): + pass + + class ForcedFallbackResultError(Exception): """This exception will be handled if the option to force returning a fallback result is used.""" diff --git a/bravado/http_future.py b/bravado/http_future.py index bea6dd32..d926bad7 100644 --- a/bravado/http_future.py +++ b/bravado/http_future.py @@ -16,6 +16,7 @@ from msgpack import unpackb from bravado.config import RequestConfig +from bravado.exception import BravadoConnectionError from bravado.exception import BravadoTimeoutError from bravado.exception import ForcedFallbackResultError from bravado.exception import HTTPServerError @@ -44,6 +45,7 @@ class FutureAdapter(object): # Make sure to define the timeout errors associated with your http client timeout_errors = () + connection_errors = () def _raise_error(self, base_exception_class, class_name_suffix, exception): error = type( @@ -65,6 +67,9 @@ def _raise_error(self, base_exception_class, class_name_suffix, exception): def _raise_timeout_error(self, exception): self._raise_error(BravadoTimeoutError, 'Timeout', exception) + def _raise_connection_error(self, exception): + self._raise_error(BravadoConnectionError, 'ConnectionError', exception) + def result(self, timeout=None): """ Must implement a result method which blocks on result retrieval. @@ -82,11 +87,14 @@ def reraise_errors(func): @wraps(func) def wrapper(self, *args, **kwargs): timeout_errors = tuple(self.future.timeout_errors or ()) + connection_errors = tuple(self.future.connection_errors or ()) try: return func(self, *args, **kwargs) except timeout_errors as exception: self.future._raise_timeout_error(exception) + except connection_errors as exception: + self.future._raise_connection_error(exception) return wrapper diff --git a/tests/http_future/HttpFuture/conftest.py b/tests/http_future/HttpFuture/conftest.py index 1baf47d2..e053c366 100644 --- a/tests/http_future/HttpFuture/conftest.py +++ b/tests/http_future/HttpFuture/conftest.py @@ -7,4 +7,8 @@ @pytest.fixture def mock_future_adapter(): - return mock.Mock(spec=FutureAdapter, timeout_errors=None) + return mock.Mock( + spec=FutureAdapter, + timeout_errors=None, + connection_errors=None, + ) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b684aaf6..242b44b2 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -import socket import time -from contextlib import closing from multiprocessing import Process import bottle @@ -243,6 +241,5 @@ def wait_unit_service_starts(url, max_wait_time=10): @pytest.fixture(scope='session') def not_answering_http_server(): - with closing(socket.socket(socket.AF_INET)) as s: - s.bind(('', 0)) - yield 'http://localhost:{port}'.format(port=s.getsockname()[1]) + # Nobody could listen on such address, so TCP 3 way handshake will fail + yield 'http://0.0.0.0' diff --git a/tests/integration/fido_client_test.py b/tests/integration/fido_client_test.py index 9521d780..d5841705 100644 --- a/tests/integration/fido_client_test.py +++ b/tests/integration/fido_client_test.py @@ -4,11 +4,15 @@ from tests.integration import requests_client_test -class TestServerFidoClient(requests_client_test.TestServerRequestsClient): +class TestServerFidoClient(requests_client_test.ServerClientGeneric): http_client_type = FidoClient http_future_adapter_type = FidoFutureAdapter + connection_errors_exceptions = set() @classmethod def encode_expected_response(cls, response): return response + + def cancel_http_future(self, http_future): + http_future.future._eventual_result.cancel() diff --git a/tests/integration/requests_client_test.py b/tests/integration/requests_client_test.py index 639de8a4..3269384f 100644 --- a/tests/integration/requests_client_test.py +++ b/tests/integration/requests_client_test.py @@ -10,6 +10,7 @@ from msgpack import unpackb from bravado.client import SwaggerClient +from bravado.exception import BravadoConnectionError from bravado.exception import BravadoTimeoutError from bravado.requests_client import RequestsClient from bravado.requests_client import RequestsFutureAdapter @@ -27,6 +28,7 @@ class ServerClientGeneric: http_client_type = None http_future_adapter_type = None + connection_errors_exceptions = None @classmethod def setup_class(cls): @@ -34,6 +36,8 @@ def setup_class(cls): raise RuntimeError('Define http_client_type for {}'.format(cls.__name__)) if cls.http_future_adapter_type is None: raise RuntimeError('Define http_future_adapter_type for {}'.format(cls.__name__)) + if cls.connection_errors_exceptions is None: + raise RuntimeError('Define connection_errors_exceptions for {}'.format(cls.__name__)) cls.http_client = cls.http_client_type() @classmethod @@ -43,6 +47,9 @@ def encode_expected_response(cls, response): else: return str(response) + def cancel_http_future(self, http_future): + pass + @pytest.fixture def swagger_client(self, swagger_http_server): return SwaggerClient.from_url( @@ -226,11 +233,71 @@ def test_timeout_errors_are_catchable_with_original_exception_types(self, swagge }).result(timeout=0.01) assert isinstance(excinfo.value, BravadoTimeoutError) + def test_connection_errors_are_thrown_as_BravadoConnectionError(self, not_answering_http_server): + if not self.http_future_adapter_type.connection_errors: + pytest.skip('{} does NOT defines connection_errors'.format(self.http_future_adapter_type)) + + with pytest.raises(BravadoConnectionError): + self.http_client.request({ + 'method': 'GET', + 'url': '{server_address}/sleep?sec=0.1'.format(server_address=not_answering_http_server), + 'params': {}, + 'connect_timeout': 0.001, + 'timeout': 0.01, + }).result(timeout=1) + + def test_connection_errors_exceptions_contains_all_future_adapter_connection_errors(self): + assert set( + type(e) for e in self.connection_errors_exceptions + ) == set(self.http_future_adapter_type.connection_errors) + + def test_connection_errors_are_catchable_with_original_exception_types( + self, not_answering_http_server, + ): + for expected_exception in self.connection_errors_exceptions: + with pytest.raises(type(expected_exception)) as excinfo: + http_future = self.http_client.request({ + 'method': 'GET', + 'url': not_answering_http_server, + 'params': {}, + }) + # Finding a way to force all the http clients to raise + # the expected exception while sending the real request is hard + # so we're mocking the future in order to throw the expected + # exception so we can validate that the exception is catchable + # with the original exception type too + self.cancel_http_future(http_future) + http_future.future.result = mock.Mock( + side_effect=expected_exception, + ) + http_future.result(timeout=0.1) + + # check that the raised exception is catchable as BravadoConnectionError too + assert isinstance(excinfo.value, BravadoConnectionError) + + def test_swagger_client_connection_errors_are_thrown_as_BravadoConnectionError( + self, not_answering_http_server, swagger_client, result_getter, + ): + if not self.http_future_adapter_type.connection_errors: + pytest.skip('{} does NOT defines connection_errors'.format(self.http_future_adapter_type)) + + # override api url to communicate with a non responding http server + swagger_client.swagger_spec.api_url = not_answering_http_server + with pytest.raises(BravadoConnectionError): + result_getter( + swagger_client.json.get_json(_request_options={ + 'connect_timeout': 0.001, + 'timeout': 0.01, + }), + timeout=0.1, + ) + class TestServerRequestsClient(ServerClientGeneric): http_client_type = RequestsClient http_future_adapter_type = RequestsFutureAdapter + connection_errors_exceptions = set() def test_timeout_errors_are_catchable_as_requests_timeout(self, swagger_http_server): with pytest.raises(requests.exceptions.Timeout): @@ -243,6 +310,7 @@ def test_timeout_errors_are_catchable_as_requests_timeout(self, swagger_http_ser class FakeRequestsFutureAdapter(RequestsFutureAdapter): timeout_errors = [] + connection_errors = [] class FakeRequestsClient(RequestsClient): @@ -259,6 +327,7 @@ class TestServerRequestsClientFake(ServerClientGeneric): http_client_type = FakeRequestsClient http_future_adapter_type = FakeRequestsFutureAdapter + connection_errors_exceptions = set() def test_timeout_error_not_throws_BravadoTimeoutError_if_no_timeout_errors_specified(self, swagger_http_server): try: From d75d8ead71b1632aa69dc6b9dbec94e24ecaaf74 Mon Sep 17 00:00:00 2001 From: Samuele Maci Date: Sat, 23 Jun 2018 19:23:33 +0200 Subject: [PATCH 5/6] Define connection errors for requests_client --- bravado/requests_client.py | 1 + tests/integration/requests_client_test.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bravado/requests_client.py b/bravado/requests_client.py index 5d5b36e4..d16470c8 100644 --- a/bravado/requests_client.py +++ b/bravado/requests_client.py @@ -212,6 +212,7 @@ class RequestsFutureAdapter(FutureAdapter): """ timeout_errors = (requests.exceptions.ReadTimeout,) + connection_errors = (requests.exceptions.ConnectionError,) def __init__(self, session, request, misc_options): """Kicks API call for Requests client diff --git a/tests/integration/requests_client_test.py b/tests/integration/requests_client_test.py index 3269384f..855e4861 100644 --- a/tests/integration/requests_client_test.py +++ b/tests/integration/requests_client_test.py @@ -297,7 +297,9 @@ class TestServerRequestsClient(ServerClientGeneric): http_client_type = RequestsClient http_future_adapter_type = RequestsFutureAdapter - connection_errors_exceptions = set() + connection_errors_exceptions = { + requests.exceptions.ConnectionError(), + } def test_timeout_errors_are_catchable_as_requests_timeout(self, swagger_http_server): with pytest.raises(requests.exceptions.Timeout): From f1ea220730b295aa625683eb23fb081fe893562b Mon Sep 17 00:00:00 2001 From: Samuele Maci Date: Sat, 23 Jun 2018 19:42:41 +0200 Subject: [PATCH 6/6] Define connection errors for fido_client --- bravado/fido_client.py | 8 ++++++++ tests/integration/fido_client_test.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/bravado/fido_client.py b/bravado/fido_client.py index 5cf39767..b16d8cbb 100644 --- a/bravado/fido_client.py +++ b/bravado/fido_client.py @@ -8,6 +8,8 @@ import requests import requests.structures import six +import twisted.internet.error +import twisted.web.client from bravado_core.response import IncomingResponse from yelp_bytes import to_bytes @@ -167,6 +169,12 @@ class FidoFutureAdapter(FutureAdapter): """ timeout_errors = (fido.exceptions.HTTPTimeoutError,) + connection_errors = ( + fido.exceptions.TCPConnectionError, + twisted.internet.error.ConnectingCancelledError, + twisted.internet.error.DNSLookupError, + twisted.web.client.RequestNotSent, + ) def __init__(self, eventual_result): self._eventual_result = eventual_result diff --git a/tests/integration/fido_client_test.py b/tests/integration/fido_client_test.py index d5841705..7070c201 100644 --- a/tests/integration/fido_client_test.py +++ b/tests/integration/fido_client_test.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +import fido.exceptions +import twisted.internet.error +import twisted.web.client + from bravado.fido_client import FidoClient from bravado.fido_client import FidoFutureAdapter from tests.integration import requests_client_test @@ -8,7 +12,12 @@ class TestServerFidoClient(requests_client_test.ServerClientGeneric): http_client_type = FidoClient http_future_adapter_type = FidoFutureAdapter - connection_errors_exceptions = set() + connection_errors_exceptions = { + fido.exceptions.TCPConnectionError(), + twisted.internet.error.ConnectingCancelledError('address'), + twisted.internet.error.DNSLookupError(), + twisted.web.client.RequestNotSent(), + } @classmethod def encode_expected_response(cls, response):