Exemplo n.º 1
0
    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()
Exemplo n.º 2
0
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))
Exemplo n.º 3
0
 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)])
Exemplo n.º 4
0
    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())
Exemplo n.º 5
0
    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)
Exemplo n.º 6
0
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")
Exemplo n.º 7
0
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))
Exemplo n.º 8
0
    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
Exemplo n.º 9
0
    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')
Exemplo n.º 10
0
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
Exemplo n.º 11
0
    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))
Exemplo n.º 12
0
 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)))
Exemplo n.º 13
0
    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')
Exemplo n.º 14
0
    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
Exemplo n.º 15
0
    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
Exemplo n.º 16
0
    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)
Exemplo n.º 17
0
 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)
Exemplo n.º 18
0
 def artifact_func(artifact):
     raise ReadTimeoutError(unittest.mock.MagicMock(),
                            unittest.mock.MagicMock(),
                            unittest.mock.MagicMock())
Exemplo n.º 19
0
 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
Exemplo n.º 21
0
    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/']))
Exemplo n.º 22
0
 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,
                }),
        ]
Exemplo n.º 24
0
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