def test_retry_total_none(self): """ if Total is none, connect error should take precedence """ error = ConnectTimeoutError() retry = Retry(connect=2, total=None) retry = retry.increment(error=error) retry = retry.increment(error=error) with pytest.raises(MaxRetryError) as e: retry.increment(error=error) assert e.value.reason == error error = ReadTimeoutError(None, "/", "read timed out") retry = Retry(connect=2, total=None) retry = retry.increment(method="GET", error=error) retry = retry.increment(method="GET", error=error) retry = retry.increment(method="GET", error=error) assert not retry.is_exhausted()
class TestPickle(object): @pytest.mark.parametrize('exception', [ HTTPError(None), MaxRetryError(None, None, None), LocationParseError(None), ConnectTimeoutError(None), HTTPError('foo'), HTTPError('foo', IOError('foo')), MaxRetryError(HTTPConnectionPool('localhost'), '/', None), LocationParseError('fake location'), ClosedPoolError(HTTPConnectionPool('localhost'), None), EmptyPoolError(HTTPConnectionPool('localhost'), None), ReadTimeoutError(HTTPConnectionPool('localhost'), '/', None), ]) def test_exceptions(self, exception): result = pickle.loads(pickle.dumps(exception)) assert isinstance(result, type(exception))
def test_wait_for_etcd_event_conn_failed(self, m_sleep): self.watcher.next_etcd_index = 1 m_resp = Mock() m_resp.modifiedIndex = 123 read_timeout = etcd.EtcdConnectionFailed() read_timeout.cause = ReadTimeoutError(Mock(), "", "") other_error = etcd.EtcdConnectionFailed() other_error.cause = ExpectedException() responses = [ read_timeout, other_error, m_resp, ] self.m_client.read.side_effect = iter(responses) event = self.watcher.wait_for_etcd_event() self.assertEqual(event, m_resp) self.assertEqual(m_sleep.mock_calls, [call(1)])
def test_retry_total_none(self): """ if Total is none, connect error should take precedence """ error = ConnectTimeoutError() retry = Retry(connect=2, total=None) retry = retry.increment(error=error) retry = retry.increment(error=error) try: retry.increment(error=error) self.fail("Failed to raise error.") except MaxRetryError as e: self.assertEqual(e.reason, error) error = ReadTimeoutError(None, "/", "read timed out") retry = Retry(connect=2, total=None) retry = retry.increment(method='GET', error=error) retry = retry.increment(method='GET', error=error) retry = retry.increment(method='GET', error=error) self.assertFalse(retry.is_exhausted())
def test_initial_read_exceptions(self): log.debug("test_initial_read_exceptions") client = stub_etcd.Client() client.add_read_exception(stub_etcd.EtcdException()) client.add_read_exception(ReadTimeoutError("pool", "url", "message")) client.add_read_exception(SocketTimeout()) client.add_read_exception(ConnectTimeoutError()) client.add_read_exception(HTTPError()) client.add_read_exception(HTTPException()) client.add_read_exception(stub_etcd.EtcdClusterIdChanged()) client.add_read_exception(stub_etcd.EtcdEventIndexCleared()) elector = election.Elector(client, "test_basic", "/bloop", interval=5, ttl=15) self._wait_and_stop(client, elector)
def test_read(): """ Test to make sure we interact with etcd correctly """ with patch("etcd.Client", autospec=True) as mock: etcd_client = mock.return_value etcd_return = MagicMock(value="salt") etcd_client.read.return_value = etcd_return client = etcd_util.EtcdClient({}) assert client.read("/salt") == etcd_return etcd_client.read.assert_called_with( "/salt", recursive=False, wait=False, timeout=None ) client.read("salt", True, True, 10, 5) etcd_client.read.assert_called_with( "salt", recursive=True, wait=True, timeout=10, waitIndex=5 ) etcd_client.read.side_effect = etcd.EtcdKeyNotFound with pytest.raises(etcd.EtcdKeyNotFound): client.read("salt") etcd_client.read.side_effect = etcd.EtcdConnectionFailed with pytest.raises(etcd.EtcdConnectionFailed): client.read("salt") etcd_client.read.side_effect = etcd.EtcdValueError with pytest.raises(etcd.EtcdValueError): client.read("salt") etcd_client.read.side_effect = ValueError with pytest.raises(ValueError): client.read("salt") etcd_client.read.side_effect = ReadTimeoutError(None, None, None) with pytest.raises(etcd.EtcdConnectionFailed): client.read("salt") etcd_client.read.side_effect = MaxRetryError(None, None) with pytest.raises(etcd.EtcdConnectionFailed): client.read("salt")
class TestPickle(object): @pytest.mark.parametrize( "exception", [ HTTPError(None), MaxRetryError(None, None, None), LocationParseError(None), ConnectTimeoutError(None), HTTPError("foo"), HTTPError("foo", IOError("foo")), MaxRetryError(HTTPConnectionPool("localhost"), "/", None), LocationParseError("fake location"), ClosedPoolError(HTTPConnectionPool("localhost"), None), EmptyPoolError(HTTPConnectionPool("localhost"), None), ReadTimeoutError(HTTPConnectionPool("localhost"), "/", None), ], ) def test_exceptions(self, exception): result = pickle.loads(pickle.dumps(exception)) assert isinstance(result, type(exception))
def test_history(self): retry = Retry(total=10, method_whitelist=frozenset(['GET', 'POST'])) assert retry.history == tuple() connection_error = ConnectTimeoutError('conntimeout') retry = retry.increment('GET', '/test1', None, connection_error) history = (RequestHistory('GET', '/test1', connection_error, None, None),) assert retry.history == history read_error = ReadTimeoutError(None, "/test2", "read timed out") retry = retry.increment('POST', '/test2', None, read_error) history = (RequestHistory('GET', '/test1', connection_error, None, None), RequestHistory('POST', '/test2', read_error, None, None)) assert retry.history == history response = HTTPResponse(status=500) retry = retry.increment('GET', '/test3', response, None) history = (RequestHistory('GET', '/test1', connection_error, None, None), RequestHistory('POST', '/test2', read_error, None, None), RequestHistory('GET', '/test3', None, 500, None)) assert retry.history == history
def test_error_message(self): retry = Retry(total=0) try: retry = retry.increment(method='GET', error=ReadTimeoutError( None, "/", "read timed out")) raise AssertionError("Should have raised a MaxRetryError") except MaxRetryError as e: assert 'Caused by redirect' not in str(e) self.assertEqual(str(e.reason), 'None: read timed out') retry = Retry(total=1) try: retry = retry.increment('POST', '/') retry = retry.increment('POST', '/') raise AssertionError("Should have raised a MaxRetryError") except MaxRetryError as e: assert 'Caused by redirect' not in str(e) self.assertTrue(isinstance(e.reason, ResponseError), "%s should be a ResponseError" % e.reason) self.assertEqual(str(e.reason), ResponseError.GENERIC_ERROR) retry = Retry(total=1) try: response = HTTPResponse(status=500) retry = retry.increment('POST', '/', response=response) retry = retry.increment('POST', '/', response=response) raise AssertionError("Should have raised a MaxRetryError") except MaxRetryError as e: assert 'Caused by redirect' not in str(e) msg = ResponseError.SPECIFIC_ERROR.format(status_code=500) self.assertEqual(str(e.reason), msg) retry = Retry(connect=1) try: retry = retry.increment(error=ConnectTimeoutError('conntimeout')) retry = retry.increment(error=ConnectTimeoutError('conntimeout')) raise AssertionError("Should have raised a MaxRetryError") except MaxRetryError as e: assert 'Caused by redirect' not in str(e) self.assertEqual(str(e.reason), 'conntimeout')
def test_retries(): """ Tests that, even if I set up 5 retries, there is only one request made since it times out. """ connection_mock = mock.Mock() connection_mock.request.side_effect = ReadTimeoutError(None, "test.com", "Timeout") snuba_pool = FakeConnectionPool( connection=connection_mock, host="www.test.com", port=80, retries=RetrySkipTimeout(total=5, method_whitelist={"GET", "POST"}), timeout=30, maxsize=10, ) with pytest.raises(HTTPError): snuba_pool.urlopen("POST", "/query", body="{}") assert connection_mock.request.call_count == 1
def test_exceptions_with_objects(self): assert self.verify_pickling(HTTPError('foo')) assert self.verify_pickling(HTTPError('foo', IOError('foo'))) assert self.verify_pickling( MaxRetryError(HTTPConnectionPool('localhost'), '/', None)) assert self.verify_pickling(LocationParseError('fake location')) assert self.verify_pickling( ClosedPoolError(HTTPConnectionPool('localhost'), None)) assert self.verify_pickling( EmptyPoolError(HTTPConnectionPool('localhost'), None)) assert self.verify_pickling( HostChangedError(HTTPConnectionPool('localhost'), '/', None)) assert self.verify_pickling( ReadTimeoutError(HTTPConnectionPool('localhost'), '/', None))
def test_history(self): retry = Retry(total=10) self.assertEqual(retry.history, tuple()) connection_error = ConnectTimeoutError('conntimeout') retry = retry.increment('GET', '/test1', None, connection_error) self.assertEqual( retry.history, (RequestHistory('GET', '/test1', connection_error, None, None), )) read_error = ReadTimeoutError(None, "/test2", "read timed out") retry = retry.increment('POST', '/test2', None, read_error) self.assertEqual( retry.history, (RequestHistory('GET', '/test1', connection_error, None, None), RequestHistory('POST', '/test2', read_error, None, None))) response = HTTPResponse(status=500) retry = retry.increment('GET', '/test3', response, None) self.assertEqual( retry.history, (RequestHistory('GET', '/test1', connection_error, None, None), RequestHistory('POST', '/test2', read_error, None, None), RequestHistory('GET', '/test3', None, 500, None)))
def test_read(self): ''' Test to make sure we interact with etcd correctly ''' with patch('etcd.Client', autospec=True) as mock: etcd_client = mock.return_value etcd_return = MagicMock(value='salt') etcd_client.read.return_value = etcd_return client = etcd_util.EtcdClient({}) self.assertEqual(client.read('/salt'), etcd_return) etcd_client.read.assert_called_with('/salt', recursive=False, wait=False, timeout=None) client.read('salt', True, True, 10, 5) etcd_client.read.assert_called_with('salt', recursive=True, wait=True, timeout=10, waitIndex=5) etcd_client.read.side_effect = etcd.EtcdKeyNotFound self.assertRaises(etcd.EtcdKeyNotFound, client.read, 'salt') etcd_client.read.side_effect = etcd.EtcdConnectionFailed self.assertRaises(etcd.EtcdConnectionFailed, client.read, 'salt') etcd_client.read.side_effect = etcd.EtcdValueError self.assertRaises(etcd.EtcdValueError, client.read, 'salt') etcd_client.read.side_effect = ValueError self.assertRaises(ValueError, client.read, 'salt') etcd_client.read.side_effect = ReadTimeoutError(None, None, None) self.assertRaises(etcd.EtcdConnectionFailed, client.read, 'salt') etcd_client.read.side_effect = MaxRetryError(None, None) self.assertRaises(etcd.EtcdConnectionFailed, client.read, 'salt')
def test_history(self) -> None: retry = Retry(total=10, allowed_methods=frozenset(["GET", "POST"])) assert retry.history == tuple() connection_error = ConnectTimeoutError("conntimeout") retry = retry.increment("GET", "/test1", None, connection_error) test_history1 = (RequestHistory("GET", "/test1", connection_error, None, None),) assert retry.history == test_history1 read_error = ReadTimeoutError(DUMMY_POOL, "/test2", "read timed out") retry = retry.increment("POST", "/test2", None, read_error) test_history2 = ( RequestHistory("GET", "/test1", connection_error, None, None), RequestHistory("POST", "/test2", read_error, None, None), ) assert retry.history == test_history2 response = HTTPResponse(status=500) retry = retry.increment("GET", "/test3", response, None) test_history3 = ( RequestHistory("GET", "/test1", connection_error, None, None), RequestHistory("POST", "/test2", read_error, None, None), RequestHistory("GET", "/test3", None, 500, None), ) assert retry.history == test_history3
def test_history(self, expect_retry_deprecation): retry = Retry(total=10, method_whitelist=frozenset(["GET", "POST"])) assert retry.history == tuple() connection_error = ConnectTimeoutError("conntimeout") retry = retry.increment("GET", "/test1", None, connection_error) history = (RequestHistory("GET", "/test1", connection_error, None, None),) assert retry.history == history read_error = ReadTimeoutError(None, "/test2", "read timed out") retry = retry.increment("POST", "/test2", None, read_error) history = ( RequestHistory("GET", "/test1", connection_error, None, None), RequestHistory("POST", "/test2", read_error, None, None), ) assert retry.history == history response = HTTPResponse(status=500) retry = retry.increment("GET", "/test3", response, None) history = ( RequestHistory("GET", "/test1", connection_error, None, None), RequestHistory("POST", "/test2", read_error, None, None), RequestHistory("GET", "/test3", None, 500, None), ) assert retry.history == history
def urlopen(self, method, url, redirect=True, **kw): """ Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen` with custom cross-host redirect logic and only sends the request-uri portion of the ``url``. The given ``url`` parameter must be absolute, such that an appropriate :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. """ #=============================================================================================================== # add by mz error_type = kw.get('error_type') if error_type: from urllib3.exceptions import LocationValueError, HostChangedError, LocationParseError, ConnectTimeoutError from urllib3.exceptions import ProxyError, TimeoutError, ReadTimeoutError, ProtocolError, DecodeError from urllib3.exceptions import ResponseError, ResponseNotChunked, SSLError, HTTPError, HTTPWarning, PoolError from urllib3.exceptions import RequestError, MaxRetryError, TimeoutStateError, NewConnectionError from urllib3.exceptions import EmptyPoolError, ClosedPoolError, SecurityWarning, SubjectAltNameWarning from urllib3.exceptions import InsecureRequestWarning, SystemTimeWarning, InsecurePlatformWarning from urllib3.exceptions import SNIMissingWarning, DependencyWarning, ProxySchemeUnknown, HeaderParsingError get_error = { "LocationValueError": LocationValueError(), "HostChangedError": HostChangedError(pool=1, url=2), "LocationParseError": LocationParseError(url), "ConnectTimeoutError": ConnectTimeoutError(), "ProxyError": ProxyError(), "TimeoutError": TimeoutError(), "ReadTimeoutError": ReadTimeoutError(pool=1, url=2, message="ReadTimeoutError"), "ProtocolError": ProtocolError(), "DecodeError": DecodeError(), "ResponseError": ResponseError(), "ResponseNotChunked": ResponseNotChunked(), "SSLError": SSLError(), "HTTPError": HTTPError(), "HTTPWarning": HTTPWarning(), "PoolError": PoolError(pool=1, message=2), "RequestError": RequestError(pool=1, url=2, message="RequestError"), "MaxRetryError": MaxRetryError(pool=1, url=2, reason=None), "TimeoutStateError": TimeoutStateError(), "NewConnectionError": NewConnectionError(pool=1, message="NewConnectionError"), "EmptyPoolError": EmptyPoolError(pool=1, message="EmptyPoolError"), "ClosedPoolError": ClosedPoolError(pool=1, message="ClosedPoolError"), "SecurityWarning": SecurityWarning(), "SubjectAltNameWarning": SubjectAltNameWarning(), "InsecureRequestWarning": InsecureRequestWarning(), "SystemTimeWarning": SystemTimeWarning(), "InsecurePlatformWarning": InsecurePlatformWarning(), "SNIMissingWarning": SNIMissingWarning(), "DependencyWarning": DependencyWarning(), "ProxySchemeUnknown": ProxySchemeUnknown(scheme=1), "HeaderParsingError": HeaderParsingError(defects=1, unparsed_data=2) } error_ = get_error[error_type] raise error_ #=============================================================================================================== u = parse_url(url) conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) kw['assert_same_host'] = False kw['redirect'] = False if 'headers' not in kw: kw['headers'] = self.headers if self.proxy is not None and u.scheme == "http": response = conn.urlopen(method, url, **kw) else: response = conn.urlopen(method, u.request_uri, **kw) redirect_location = redirect and response.get_redirect_location() if not redirect_location: return response # Support relative URLs for redirecting. redirect_location = urljoin(url, redirect_location) # RFC 2616, Section 10.3.4 if response.status == 303: method = 'GET' log.info("Redirecting %s -> %s" % (url, redirect_location)) kw['retries'] = kw.get('retries', 3) - 1 # Persist retries countdown kw['redirect'] = redirect return self.urlopen(method, redirect_location, **kw)
def test_retry_method_not_allowed(self) -> None: error = ReadTimeoutError(DUMMY_POOL, "/", "read timed out") retry = Retry() with pytest.raises(ReadTimeoutError): retry.increment(method="POST", error=error)
def artifact_func(artifact): raise ReadTimeoutError(unittest.mock.MagicMock(), unittest.mock.MagicMock(), unittest.mock.MagicMock())
def test_retry_method_not_in_whitelist(self): error = ReadTimeoutError(None, "/", "read timed out") retry = Retry() with pytest.raises(ReadTimeoutError): retry.increment(method="POST", error=error)
def http_request(method, url, **kwargs): if url == 'http://localhost:2379/timeout': raise ReadTimeoutError(None, None, None) if url == 'http://localhost:2379/': return MockResponse() raise socket.error
def test_directory_deletion(self): self._run_initial_resync() watcher_req = self.watcher_etcd.get_next_request() with patch("calico.etcddriver.driver.monotonic_time", autospec=True) as m_mon: # The watcher has code to detect tight loops, vary the duration # between timeouts/exceptions so that we trigger that on the first # loop. m_mon.side_effect = iter([ 0.1, # ReadTimeoutError req_end_time 0.1, # req_start_time 0.2, # HTTPError req_end_time 0.2, # req_start_time 30, # HTTPException req_end_time 40, # req_start_time 50, # socket.error req_end_time ]) # For coverage: Nothing happens for a while, poll times out. watcher_req.respond_with_exception(ReadTimeoutError( Mock(), "", "")) with patch("time.sleep", autospec=True) as m_sleep: for exc in [HTTPError(), HTTPException(), socket.error()]: # For coverage: non-timeout errors. watcher_req = self.watcher_etcd.get_next_request() watcher_req.respond_with_exception(exc) self.assertEqual(m_sleep.mock_calls, [call(0.1)]) # For coverage: Then a set to a dir, which should be ignored. watcher_req = self.watcher_etcd.get_next_request() watcher_req.respond_with_data( json.dumps({ "action": "create", "node": { "key": "/calico/v1/foo", "dir": True, "modifiedIndex": 100, } }), 100, 200) # Then a whole directory is deleted. watcher_req = self.watcher_etcd.assert_request( VERSION_DIR, timeout=90, recursive=True, wait_index=101, ) watcher_req.respond_with_value( "/calico/v1/adir", dir=True, value=None, action="delete", mod_index=101, status=300 # For coverage of warning log. ) # Should get individual deletes for each one then a flush. We're # relying on the trie returning sorted results here. self.assert_msg_to_felix(MSG_TYPE_UPDATE, { MSG_KEY_KEY: "/calico/v1/adir/akey", MSG_KEY_VALUE: None, }) self.assert_msg_to_felix(MSG_TYPE_UPDATE, { MSG_KEY_KEY: "/calico/v1/adir/bkey", MSG_KEY_VALUE: None, }) self.assert_msg_to_felix(MSG_TYPE_UPDATE, { MSG_KEY_KEY: "/calico/v1/adir/ckey", MSG_KEY_VALUE: None, }) self.assert_msg_to_felix(MSG_TYPE_UPDATE, { MSG_KEY_KEY: "/calico/v1/adir/ekey", MSG_KEY_VALUE: None, }) self.assert_flush_to_felix() # Check the contents of the trie. keys = set(self.driver._hwms._hwms.keys()) self.assertEqual( keys, set([u'/calico/v1/Ready/', u'/calico/v1/adir2/dkey/']))
def read_timeout_exception(self): return ReadTimeoutError(pool=None, url="https://example.com", message="Test exception")
class TestBaseApiClient(object): def _from_httplib_response_mock(self, status, response_data=None): response_mock = mock.Mock(status=status, headers={}, spec=[ 'get_redirect_location', 'getheader', 'read', 'reason', 'drain_conn' ]) response_mock.get_redirect_location.return_value = None response_mock.getheader.return_value = None response_mock.read.side_effect = [response_data, None, None, None] response_mock.reason = f'Mocked {status} response' return response_mock @pytest.mark.parametrize( "method,exc_factory", chain( ((m, lambda: NewConnectionError(mock.Mock(), "I'm a message")) for m in ( "GET", "PUT", "POST", "PATCH", )), ((m, lambda: ProtocolError(mock.Mock(), "I'm a message")) for m in ( "GET", "PUT", )), ((m, lambda: ReadTimeoutError(mock.Mock(), mock.Mock(), "I'm a message")) for m in ( "GET", "PUT", )), )) @pytest.mark.parametrize('retry_count', range(1, 4)) @mock.patch('urllib3.connectionpool.HTTPConnectionPool._make_request') @mock.patch('dmapiclient.base.BaseAPIClient._RETRIES_BACKOFF_FACTOR', 0) def test_client_retries_on_httperror_and_raises_api_error( self, _make_request, base_client, retry_count, exc_factory, method, ): _make_request.side_effect = exc_factory() with mock.patch('dmapiclient.base.BaseAPIClient._RETRIES', retry_count): with pytest.raises(HTTPError) as e: base_client._request(method, '/') requests = _make_request.call_args_list assert len(requests) == retry_count + 1 assert all((request[0][1], request[0][2]) == (method, '/') for request in requests) assert type(_make_request.side_effect).__name__ in e.value.message assert e.value.status_code == REQUEST_ERROR_STATUS_CODE @pytest.mark.parametrize("exc_factory", ( lambda: ProtocolError(mock.Mock(), "I'm a message"), lambda: ReadTimeoutError(mock.Mock(), mock.Mock(), "I'm a message"), )) @pytest.mark.parametrize("method", ( "POST", "PATCH", )) @pytest.mark.parametrize('retry_count', range(1, 4)) @mock.patch('urllib3.connectionpool.HTTPConnectionPool._make_request') @mock.patch('dmapiclient.base.BaseAPIClient._RETRIES_BACKOFF_FACTOR', 0) def test_client_doesnt_retry_non_whitelisted_methods_on_unsafe_errors( self, _make_request, base_client, retry_count, exc_factory, method, ): _make_request.side_effect = exc_factory() with mock.patch('dmapiclient.base.BaseAPIClient._RETRIES', retry_count): with pytest.raises(HTTPError) as e: base_client._request(method, '/') requests = _make_request.call_args_list assert len(requests) == 1 assert requests[0][0][1] == method assert requests[0][0][2] == "/" assert type(_make_request.side_effect).__name__ in e.value.message assert e.value.status_code == REQUEST_ERROR_STATUS_CODE @pytest.mark.parametrize(('retry_count'), range(1, 4)) @pytest.mark.parametrize(('status'), BaseAPIClient.RETRIES_FORCE_STATUS_CODES) @mock.patch( 'urllib3.connectionpool.HTTPConnectionPool.ResponseCls.from_httplib') @mock.patch('urllib3.connectionpool.HTTPConnectionPool._make_request') @mock.patch('dmapiclient.base.BaseAPIClient._RETRIES_BACKOFF_FACTOR', 0) def test_client_retries_on_status_error_and_raises_api_error( self, _make_request, from_httplib, base_client, status, retry_count): response_mock = self._from_httplib_response_mock(status) from_httplib.return_value = response_mock with mock.patch('dmapiclient.base.BaseAPIClient._RETRIES', retry_count): with pytest.raises(HTTPError) as e: base_client._request("GET", '/') requests = _make_request.call_args_list assert len(requests) == retry_count + 1 assert all((request[0][1], request[0][2]) == ('GET', '/') for request in requests) assert f'{status} Server Error: {response_mock.reason} for url: http://baseurl/\n' in e.value.message assert e.value.status_code == status @mock.patch( 'urllib3.connectionpool.HTTPConnectionPool.ResponseCls.from_httplib') @mock.patch('urllib3.connectionpool.HTTPConnectionPool._make_request') @mock.patch('dmapiclient.base.BaseAPIClient._RETRIES_BACKOFF_FACTOR', 0) def test_client_retries_and_returns_data_if_successful( self, _make_request, from_httplib, base_client): # The third response here would normally be a httplib response object. It's only use is to be passed in to # `from_httplib`, which we're mocking the return of below. `from_httplib` converts a httplib response into a # urllib3 response. The mock object we're returning is a mock for that urllib3 response. _make_request.side_effect = [ ProtocolError(mock.Mock(), '1st error'), ProtocolError(mock.Mock(), '2nd error'), ProtocolError(mock.Mock(), '3nd error'), 'httplib_response - success!', ] from_httplib.return_value = self._from_httplib_response_mock( 200, response_data=b'{"Success?": "Yes!"}') response = base_client._request("GET", '/') requests = _make_request.call_args_list assert len(requests) == 4 assert all((request[0][1], request[0][2]) == ('GET', '/') for request in requests) assert response == {'Success?': 'Yes!'} def test_non_2xx_response_raises_api_error(self, base_client, rmock): rmock.request("GET", "http://baseurl/", json={"error": "Not found"}, status_code=404) with pytest.raises(HTTPError) as e: base_client._request("GET", '/') assert e.value.message == "Not found" assert e.value.status_code == 404 def test_base_error_is_logged(self, base_client): with requests_mock.Mocker() as m: m.register_uri('GET', '/', exc=requests.RequestException()) with pytest.raises(HTTPError) as e: base_client._request("GET", "/") assert e.value.message == "\nRequestException()" assert e.value.status_code == 503 def test_invalid_json_raises_api_error(self, base_client, rmock): rmock.request("GET", "http://baseurl/", text="Internal Error", status_code=200) with pytest.raises(InvalidResponse) as e: base_client._request("GET", '/') assert e.value.message == "No JSON object could be decoded" assert e.value.status_code == 200 def test_user_agent_is_set(self, base_client, rmock): rmock.request("GET", "http://baseurl/", json={}, status_code=200) base_client._request('GET', '/') assert rmock.last_request.headers.get("User-Agent").startswith( "DM-API-Client/") def test_request_always_uses_base_url_scheme(self, base_client, rmock): rmock.request("GET", "http://baseurl/path/", json={}, status_code=200) base_client._request('GET', 'https://host/path/') assert rmock.called def test_null_api_throws(self): bad_client = BaseAPIClient(None, 'auth-token', True) with pytest.raises(ImproperlyConfigured): bad_client._request('GET', '/anything') def test_onwards_request_headers_added_if_available( self, base_client, rmock, app): rmock.get("http://baseurl/_status", json={"status": "ok"}, status_code=200) with app.test_request_context('/'): # add a simple mock callable instead of using a full request implementation request.get_onwards_request_headers = mock.Mock() request.get_onwards_request_headers.return_value = { "Douce": "bronze", "Kennedy": "gold", } base_client.get_status() assert rmock.last_request.headers["Douce"] == "bronze" assert rmock.last_request.headers["kennedy"] == "gold" assert request.get_onwards_request_headers.call_args_list == [ # just a single, arg-less call (), ] def test_onwards_request_headers_not_available(self, base_client, rmock, app): rmock.get("http://baseurl/_status", json={"status": "ok"}, status_code=200) with app.test_request_context('/'): # really just asserting no exception arose from performing a call without get_onwards_request_headers being # available base_client.get_status() def test_request_id_fallback(self, base_client, rmock, app): # request.request_id is an old interface which we're still supporting here just for compatibility rmock.get("http://baseurl/_status", json={"status": "ok"}, status_code=200) app.config["DM_REQUEST_ID_HEADER"] = "Bar" with app.test_request_context('/'): request.request_id = "Ormond" base_client.get_status() assert rmock.last_request.headers["bar"] == "Ormond" @pytest.mark.parametrize("dm_span_id_headers_setting", ( None, ( "X-Brian-Tweedy", "Major-Tweedy", ), )) @pytest.mark.parametrize( "has_request_context", (False, True), ) @mock.patch("dmapiclient.base.logger") def test_child_span_id_not_provided( self, logger, dm_span_id_headers_setting, has_request_context, base_client, rmock, app, ): rmock.get("http://baseurl/_status", json={"status": "ok"}, status_code=200) app.config["DM_SPAN_ID_HEADERS"] = dm_span_id_headers_setting with (app.test_request_context('/') if has_request_context else _empty_context_manager()): if has_request_context: request.get_onwards_request_headers = mock.Mock( return_value={ "impression": "arrested", }) base_client.get_status() assert rmock.called assert logger.log.call_args_list == [ mock.call( logging.DEBUG, "API request {method} {url}", extra={ "method": "GET", "url": "http://baseurl/_status", # childSpanId NOT provided }), mock.call( logging.INFO, "API {api_method} request on {api_url} finished in {api_time}", extra={ "api_method": "GET", "api_url": "http://baseurl/_status", "api_status": 200, "api_time": mock.ANY, # childSpanId NOT provided }), ] @pytest.mark.parametrize( "onwards_request_headers", ( { "X-Brian-Tweedy": "Amiens Street", }, { "major-TWEEDY": "Amiens Street", }, { "Major-Tweedy": "terminus", "x-brian-tweedy": "Amiens Street", }, { # note same header name, different capitalizations "X-BRIAN-TWEEDY": "great northern", "x-brian-tweedy": "Amiens Street", }, )) @pytest.mark.parametrize("response_status", ( 200, 500, )) @mock.patch("dmapiclient.base.logger") def test_child_span_id_provided( self, mock_logger, onwards_request_headers, response_status, base_client, rmock, app, ): rmock.get("http://baseurl/_status", json={"status": "foobar"}, status_code=response_status) app.config["DM_SPAN_ID_HEADERS"] = ( "X-Brian-Tweedy", "major-tweedy", ) with app.test_request_context('/'): request.get_onwards_request_headers = mock.Mock( return_value=onwards_request_headers) try: base_client.get_status() except HTTPError: # it is tested elsewhere whether this exception is raised in the *right* circumstances or not pass assert rmock.called # some of our scenarios test multiple header names differing only by capitalization - we care that the same # span id that was chosen for the log message is the same one that was sent in the onwards request header, # so we need two distinct values which are acceptable either_span_id = RestrictedAny( lambda value: value == "Amiens Street" or value == "great northern") assert mock_logger.log.call_args_list == [ mock.call(logging.DEBUG, "API request {method} {url}", extra={ "method": "GET", "url": "http://baseurl/_status", "childSpanId": either_span_id, }), (mock.call( logging.INFO, "API {api_method} request on {api_url} finished in {api_time}", extra={ "api_method": "GET", "api_url": "http://baseurl/_status", "api_status": response_status, "api_time": mock.ANY, "childSpanId": either_span_id, } ) if response_status == 200 else mock.call( logging.WARNING, "API {api_method} request on {api_url} failed with {api_status} '{api_error}'", extra={ "api_method": "GET", "api_url": "http://baseurl/_status", "api_status": response_status, "api_time": mock.ANY, "api_error": mock.ANY, "childSpanId": either_span_id, }, )) ] # both logging calls should have had the *same* childSpanId value assert mock_logger.log.call_args_list[0][1]["extra"]["childSpanId"] \ == mock_logger.log.call_args_list[1][1]["extra"]["childSpanId"] # that value should be the same one that was sent in the onwards request header assert ( rmock.last_request.headers.get("x-brian-tweedy") or rmock.last_request.headers.get("major-tweedy") ) == mock_logger.log.call_args_list[0][1]["extra"]["childSpanId"] @pytest.mark.parametrize( "thrown_exception", ( # requests can be slightly unpredictable in the exceptions it raises requests.exceptions.ConnectionError( MaxRetryError( mock.Mock(), "http://abc.net", ReadTimeoutError(mock.Mock(), mock.Mock(), mock.Mock()))), requests.exceptions.ConnectionError( ReadTimeoutError(mock.Mock(), mock.Mock(), mock.Mock())), requests.exceptions.ReadTimeout, )) @mock.patch("dmapiclient.base.logger") def test_nowait_times_out( self, mock_logger, base_client, rmock, app, thrown_exception, ): "test the case when a request with client_wait_for_response=False does indeed time out" rmock.post("http://baseurl/services/10000", exc=thrown_exception) retval = base_client._request( "POST", "/services/10000", {"serviceName": "Postcard"}, client_wait_for_response=False, ) assert retval is None assert rmock.called assert tuple( req.timeout for req in rmock.request_history) == (base_client.nowait_timeout, ) assert mock_logger.log.call_args_list == [ mock.call(logging.DEBUG, "API request {method} {url}", extra={ "method": "POST", "url": "http://baseurl/services/10000", }), mock.call( logging.INFO, "API {api_method} request on {api_url} dispatched but ignoring response", extra={ "api_method": "POST", "api_url": "http://baseurl/services/10000", "api_time": mock.ANY, "api_time_incomplete": True, }), ] @mock.patch("dmapiclient.base.logger") def test_nowait_completes( self, mock_logger, base_client, rmock, app, ): "test the case when a request with client_wait_for_response=False completes before it can time out" rmock.post("http://baseurl/services/10000", json={"services": { "id": "10000" }}, status_code=200) retval = base_client._request( "POST", "/services/10000", {"serviceName": "Postcard"}, client_wait_for_response=False, ) assert retval == {"services": {"id": "10000"}} assert rmock.called assert tuple( req.timeout for req in rmock.request_history) == (base_client.nowait_timeout, ) assert mock_logger.log.call_args_list == [ mock.call(logging.DEBUG, "API request {method} {url}", extra={ "method": "POST", "url": "http://baseurl/services/10000", }), mock.call( logging.INFO, "API {api_method} request on {api_url} finished in {api_time}", extra={ "api_method": "POST", "api_url": "http://baseurl/services/10000", "api_time": mock.ANY, "api_status": 200, }), ]
def test_read(client_name, use_v2): """ Test to make sure we interact with etcd correctly """ with patch(client_name, autospec=True) as mock: etcd_client = mock.return_value client = etcd_util.get_conn( {"etcd.require_v2": use_v2, "etcd.encode_values": False} ) if use_v2: etcd_return = MagicMock(value="salt") etcd_client.read.return_value = etcd_return assert client.read("/salt") == etcd_return etcd_client.read.assert_called_with( "/salt", recursive=False, wait=False, timeout=None ) client.read("salt", True, True, 10, 5) etcd_client.read.assert_called_with( "salt", recursive=True, wait=True, timeout=10, waitIndex=5 ) etcd_client.read.side_effect = etcd.EtcdKeyNotFound with pytest.raises(etcd.EtcdKeyNotFound): client.read("salt") etcd_client.read.side_effect = etcd.EtcdConnectionFailed with pytest.raises(etcd.EtcdConnectionFailed): client.read("salt") etcd_client.read.side_effect = etcd.EtcdValueError with pytest.raises(etcd.EtcdValueError): client.read("salt") etcd_client.read.side_effect = ValueError with pytest.raises(ValueError): client.read("salt") etcd_client.read.side_effect = ReadTimeoutError(None, None, None) with pytest.raises(etcd.EtcdConnectionFailed): client.read("salt") etcd_client.read.side_effect = MaxRetryError(None, None) with pytest.raises(etcd.EtcdConnectionFailed): client.read("salt") else: etcd_return = MagicMock(kvs=[MagicMock(value="salt")]) etcd_client.range.return_value = etcd_return assert client.read("/salt") == etcd_return.kvs etcd_client.range.assert_called_with("/salt", prefix=False) etcd_client.range.side_effect = Exception assert client.read("/salt") is None watcher_mock = MagicMock() with patch.object(etcd_client, "Watcher", return_value=watcher_mock): client.read("salt", True, True, 10, 5) etcd_client.range.assert_called_with("/salt", prefix=False) watcher_mock.watch_once.assert_called_with(timeout=10) watcher_mock.watch_once.side_effect = Exception assert client.read("salt", True, True, 10, 5) is None