diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py index a03ef8096..df20cebf3 100644 --- a/keystoneclient/exceptions.py +++ b/keystoneclient/exceptions.py @@ -29,3 +29,15 @@ class CertificateConfigError(Exception): msg = ("Unable to load certificate. " "Ensure your system is configured properly.") super(CertificateConfigError, self).__init__(msg) + + +class ConnectionError(ClientException): + """Something went wrong trying to connect to a server""" + + +class SSLError(ConnectionError): + """An SSL error occurred.""" + + +class Timeout(ClientException): + """The request timed out.""" diff --git a/keystoneclient/httpclient.py b/keystoneclient/httpclient.py index 648b5ca4e..9daeeb94d 100644 --- a/keystoneclient/httpclient.py +++ b/keystoneclient/httpclient.py @@ -21,12 +21,10 @@ OpenStack Client interface. Handles the REST calls and responses. """ -import copy import logging from six.moves.urllib import parse as urlparse import requests -import six try: import keyring @@ -44,89 +42,15 @@ if not hasattr(urlparse, 'parse_qsl'): from keystoneclient import access from keystoneclient import exceptions from keystoneclient.openstack.common import jsonutils +from keystoneclient import session as client_session _logger = logging.getLogger(__name__) - -USER_AGENT = 'python-keystoneclient' - - -def request(url, method='GET', headers=None, original_ip=None, debug=False, - logger=None, **kwargs): - """Perform a http request with standard settings. - - A wrapper around requests.request that adds standard headers like - User-Agent and provides optional debug logging of the request. - - Arguments that are not handled are passed through to the requests library. - - :param string url: The url to make the request of. - :param string method: The http method to use. (eg. 'GET', 'POST') - :param dict headers: Headers to be included in the request. (optional) - :param string original_ip: Mark this request as forwarded for this ip. - (optional) - :param bool debug: Enable debug logging. (Defaults to False) - :param logging.Logger logger: A logger to output to. (optional) - - :raises exceptions.ClientException: For connection failure, or to indicate - an error response code. - - :returns: The response to the request. - """ - - if not headers: - headers = dict() - - if not logger: - logger = _logger - - headers.setdefault('User-Agent', USER_AGENT) - - if original_ip: - headers['Forwarded'] = "for=%s;by=%s" % (original_ip, USER_AGENT) - - if debug: - string_parts = ['curl -i'] - - if not kwargs.get('verify', True): - string_parts.append(' --insecure') - - if method: - string_parts.append(' -X %s' % method) - - string_parts.append(" '%s'" % url) - - if headers: - for header in six.iteritems(headers): - string_parts.append(' -H "%s: %s"' % header) - - data = kwargs.get('data') - if data: - string_parts.append(" -d '%s'" % data) - - logger.debug("REQ: %s\n", "".join(string_parts)) - - try: - resp = requests.request( - method, - url, - headers=headers, - **kwargs) - except requests.ConnectionError as e: - msg = 'Unable to establish connection to %s: %s' % (url, e) - raise exceptions.ClientException(msg) - - if debug: - logger.debug("RESP: [%s] %s\nRESP BODY: %s\n", - resp.status_code, resp.headers, resp.text) - - if resp.status_code >= 400: - logger.debug("Request returned failure status: %s", - resp.status_code) - raise exceptions.from_response(resp, method, url) - - return resp +# These variables are moved and using them via httpclient is deprecated. +# Maintain here for compatibility. +USER_AGENT = client_session.USER_AGENT +request = client_session.request class HTTPClient(object): @@ -139,7 +63,7 @@ class HTTPClient(object): stale_duration=None, user_id=None, user_domain_id=None, user_domain_name=None, domain_id=None, domain_name=None, project_id=None, project_name=None, project_domain_id=None, - project_domain_name=None, trust_id=None): + project_domain_name=None, trust_id=None, session=None): """Construct a new http client :param string user_id: User ID for authentication. (optional) @@ -161,31 +85,17 @@ class HTTPClient(object): :param string auth_url: Identity service endpoint for authorization. :param string region_name: Name of a region to select when choosing an endpoint from the service catalog. - :param integer timeout: Allows customization of the timeout for client - http requests. (optional) + :param integer timeout: DEPRECATED: use session. (optional) :param string endpoint: A user-supplied endpoint URL for the identity service. Lazy-authentication is possible for API service calls if endpoint is set at instantiation. (optional) :param string token: Token for authentication. (optional) - :param string cacert: Path to the Privacy Enhanced Mail (PEM) file - which contains the trusted authority X.509 - certificates needed to established SSL connection - with the identity service. (optional) - :param string key: Path to the Privacy Enhanced Mail (PEM) file which - contains the unencrypted client private key needed - to established two-way SSL connection with the - identity service. (optional) - :param string cert: Path to the Privacy Enhanced Mail (PEM) file which - contains the corresponding X.509 client certificate - needed to established two-way SSL connection with - the identity service. (optional) - :param boolean insecure: Does not perform X.509 certificate validation - when establishing SSL connection with identity - service. default: False (optional) - :param string original_ip: The original IP of the requesting user - which will be sent to identity service in a - 'Forwarded' header. (optional) + :param string cacert: DEPRECATED: use session. (optional) + :param string key: DEPRECATED: use session. (optional) + :param string cert: DEPRECATED: use session. (optional) + :param boolean insecure: DEPRECATED: use session. (optional) + :param string original_ip: DEPRECATED: use session. (optional) :param boolean debug: Enables debug logging of all request and responses to identity service. default False (optional) @@ -210,6 +120,8 @@ class HTTPClient(object): The tenant_id keyword argument is deprecated, use project_id instead. :param string trust_id: Trust ID for trust scoping. (optional) + :param object session: A Session object to be used for + communicating with the identity service. """ # set baseline defaults @@ -230,7 +142,6 @@ class HTTPClient(object): self.auth_url = None self._endpoint = None self._management_url = None - self.timeout = float(timeout) if timeout is not None else None self.trust_id = None @@ -309,16 +220,26 @@ class HTTPClient(object): self._endpoint = endpoint.rstrip('/') self.region_name = region_name - self.original_ip = original_ip - if cacert: - self.verify_cert = cacert - else: - self.verify_cert = True - if insecure: - self.verify_cert = False - self.cert = cert - if cert and key: - self.cert = (cert, key,) + if not session: + verify = cacert or True + if insecure: + verify = False + + session_cert = None + if cert and key: + session_cert = (cert, key) + elif cert: + _logger.warn("Client cert was provided without corresponding " + "key. Ignoring.") + + timeout = float(timeout) if timeout is not None else None + session = client_session.Session(verify=verify, + cert=session_cert, + original_ip=original_ip, + timeout=timeout, + debug=debug) + + self.session = session self.domain = '' # logging setup @@ -601,29 +522,8 @@ class HTTPClient(object): def serialize(self, entity): return jsonutils.dumps(entity) - def request(self, url, method, body=None, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around requests.request to handle tasks such as - setting headers, JSON encoding/decoding, and error handling. - """ - # Copy the kwargs so we can reuse the original in case of redirects - request_kwargs = copy.copy(kwargs) - request_kwargs.setdefault('headers', kwargs.get('headers', {})) - - if body: - request_kwargs['headers']['Content-Type'] = 'application/json' - request_kwargs['data'] = self.serialize(body) - - if self.cert: - request_kwargs.setdefault('cert', self.cert) - if self.timeout is not None: - request_kwargs.setdefault('timeout', self.timeout) - - resp = request(url, method, original_ip=self.original_ip, - verify=self.verify_cert, debug=self.debug_log, - **request_kwargs) - + @staticmethod + def _decode_body(resp): if resp.text: try: body_resp = jsonutils.loads(resp.text) @@ -635,12 +535,33 @@ class HTTPClient(object): _logger.debug("No body was returned.") body_resp = None + return body_resp + + def request(self, url, method, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around requests.request to handle tasks such as + setting headers, JSON encoding/decoding, and error handling. + """ + + try: + kwargs['json'] = kwargs.pop('body') + except KeyError: + pass + + resp = self.session.request(url, method, **kwargs) + + # NOTE(jamielennox): The requests lib will handle the majority of + # redirections. Where it fails is when POSTs are redirected which + # is apparently something handled differently by each browser which + # requests forces us to do the most compliant way (which we don't want) + # see: https://en.wikipedia.org/wiki/Post/Redirect/Get + # Nova and other direct users don't do this. Is it still relevant? if resp.status_code in (301, 302, 305): # Redirected. Reissue the request to the new location. - return self.request(resp.headers['location'], method, body, - **request_kwargs) + return self.request(resp.headers['location'], method, **kwargs) - return resp, body_resp + return resp, self._decode_body(resp) def _cs_request(self, url, method, **kwargs): """Makes an authenticated request to keystone endpoint by @@ -683,3 +604,29 @@ class HTTPClient(object): def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) + + # DEPRECATIONS: The following methods are no longer directly supported + # but maintained for compatibility purposes. + + deprecated_session_variables = {'original_ip': None, + 'cert': None, + 'timeout': None, + 'verify_cert': 'verify'} + + def __getattr__(self, name): + # FIXME(jamielennox): provide a proper deprecated warning + try: + var_name = self.deprecated_session_variables[name] + except KeyError: + raise AttributeError("Unknown Attribute: %s" % name) + + return getattr(self.session, var_name or name) + + def __setattr__(self, name, val): + # FIXME(jamielennox): provide a proper deprecated warning + try: + var_name = self.deprecated_session_variables[name] + except KeyError: + super(HTTPClient, self).__setattr__(name, val) + else: + setattr(self.session, var_name or name) diff --git a/keystoneclient/session.py b/keystoneclient/session.py new file mode 100644 index 000000000..eeedf9b92 --- /dev/null +++ b/keystoneclient/session.py @@ -0,0 +1,201 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +import requests +import six + +from keystoneclient import exceptions +from keystoneclient.openstack.common import jsonutils + +USER_AGENT = 'python-keystoneclient' + +_logger = logging.getLogger(__name__) + + +def request(url, method='GET', **kwargs): + return Session().request(url, method=method, **kwargs) + + +class Session(object): + + user_agent = None + + def __init__(self, session=None, original_ip=None, verify=True, cert=None, + timeout=None, debug=False, user_agent=None): + """Maintains client communication state and common functionality. + + As much as possible the parameters to this class reflect and are passed + directly to the requests library. + + :param string original_ip: The original IP of the requesting user + which will be sent to identity service in a + 'Forwarded' header. (optional) + :param verify: The verification arguments to pass to requests. These + are of the same form as requests expects, so True or + False to verify (or not) against system certificates or + a path to a bundle or CA certs to check against. + (optional, defaults to True) + :param cert: A client certificate to pass to requests. These are of the + same form as requests expects. Either a single filename + containing both the certificate and key or a tuple + containing the path to the certificate then a path to the + key. (optional) + :param float timeout: A timeout to pass to requests. This should be a + numerical value indicating some amount + (or fraction) of seconds or 0 for no timeout. + (optional, defaults to 0) + :param string user_agent: A User-Agent header string to use for the + request. If not provided a default is used. + (optional, defaults to + 'python-keystoneclient') + """ + if not session: + session = requests.Session() + + self.session = session + self.original_ip = original_ip + self.verify = verify + self.cert = cert + self.timeout = None + self.debug = debug + + if timeout is not None: + self.timeout = float(timeout) + + # don't override the class variable if none provided + if user_agent is not None: + self.user_agent = user_agent + + def request(self, url, method, json=None, original_ip=None, debug=None, + logger=None, user_agent=None, **kwargs): + """Send an HTTP request with the specified characteristics. + + Wrapper around `requests.Session.request` to handle tasks such as + setting headers, JSON encoding/decoding, and error handling. + + Arguments that are not handled are passed through to the requests + library. + + :param string url: Fully qualified URL of HTTP request + :param string method: The http method to use. (eg. 'GET', 'POST') + :param string original_ip: Mark this request as forwarded for this ip. + (optional) + :param dict headers: Headers to be included in the request. (optional) + :param bool debug: Enable debug logging. (Defaults to False) + :param kwargs: any other parameter that can be passed to + requests.Session.request (such as `headers`) or `json` + that will be encoded as JSON and used as `data` argument + :param logging.Logger logger: A logger to output to. (optional) + :param json: Some data to be represented as JSON. (optional) + :param string user_agent: A user_agent to use for the request. If + present will override one present in headers. + (optional) + + :raises exceptions.ClientException: For connection failure, or to + indicate an error response code. + + :returns: The response to the request. + """ + + headers = kwargs.setdefault('headers', dict()) + + if self.cert: + kwargs.setdefault('cert', self.cert) + + if self.timeout is not None: + kwargs.setdefault('timeout', self.timeout) + + if user_agent: + headers['User-Agent'] = user_agent + elif self.user_agent: + user_agent = headers.setdefault('User-Agent', self.user_agent) + else: + user_agent = headers.setdefault('User-Agent', USER_AGENT) + + if self.original_ip: + headers.setdefault('Forwarded', + 'for=%s;by=%s' % (self.original_ip, user_agent)) + + if json is not None: + headers['Content-Type'] = 'application/json' + kwargs['data'] = jsonutils.dumps(json) + + if not logger: + logger = _logger + + if debug is None: + debug = self.debug + + kwargs.setdefault('verify', self.verify) + + if debug: + string_parts = ['curl -i'] + + if method: + string_parts.extend([' -X ', method]) + + string_parts.extend([' ', url]) + + if headers: + for header in six.iteritems(headers): + string_parts.append(' -H "%s: %s"' % header) + + logger.debug('REQ: %s', ''.join(string_parts)) + + data = kwargs.get('data') + if data: + logger.debug('REQ BODY: %s', data) + + try: + resp = self.session.request(method, url, **kwargs) + except requests.exceptions.SSLError: + msg = 'SSL exception connecting to %s' % url + raise exceptions.SSLError(msg) + except requests.exceptions.Timeout: + msg = 'Request to %s timed out' % url + raise exceptions.Timeout(msg) + except requests.exceptions.ConnectionError: + msg = 'Unable to establish connection to %s' % url + raise exceptions.ConnectionError(msg) + + if debug: + logger.debug('RESP: [%s] %s\nRESP BODY: %s\n', + resp.status_code, resp.headers, resp.text) + + if resp.status_code >= 400: + logger.debug('Request returned failure status: %s', + resp.status_code) + raise exceptions.from_response(resp, method, url) + + return resp + + def head(self, url, **kwargs): + return self.request(url, 'HEAD', **kwargs) + + def get(self, url, **kwargs): + return self.request(url, 'GET', **kwargs) + + def post(self, url, **kwargs): + return self.request(url, 'POST', **kwargs) + + def put(self, url, **kwargs): + return self.request(url, 'PUT', **kwargs) + + def delete(self, url, **kwargs): + return self.request(url, 'DELETE', **kwargs) + + def patch(self, url, **kwargs): + return self.request(url, 'PATCH', **kwargs) diff --git a/keystoneclient/tests/test_https.py b/keystoneclient/tests/test_https.py index 1477720b6..b4a955ba3 100644 --- a/keystoneclient/tests/test_https.py +++ b/keystoneclient/tests/test_https.py @@ -17,13 +17,13 @@ import mock import requests from keystoneclient import httpclient +from keystoneclient import session from keystoneclient.tests import utils FAKE_RESPONSE = utils.TestResponse({ "status_code": 200, "text": '{"hi": "there"}', }) -MOCK_REQUEST = mock.Mock(return_value=(FAKE_RESPONSE)) REQUEST_URL = 'https://127.0.0.1:5000/hi' RESPONSE_BODY = '{"hi": "there"}' @@ -55,56 +55,59 @@ class ClientTest(utils.TestCase): self.request_patcher.stop() super(ClientTest, self).tearDown() - def test_get(self): + @mock.patch.object(session.requests.Session, 'request') + def test_get(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE cl = get_authed_client() - with mock.patch.object(requests, "request", MOCK_REQUEST): - resp, body = cl.get("/hi") + resp, body = cl.get("/hi") - # this may become too tightly couple later - mock_args, mock_kwargs = MOCK_REQUEST.call_args + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args - self.assertEqual(mock_args[0], 'GET') - self.assertEqual(mock_args[1], REQUEST_URL) - self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') - self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) - self.assertEqual(mock_kwargs['verify'], 'ca.pem') + self.assertEqual(mock_args[0], 'GET') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') - # Automatic JSON parsing - self.assertEqual(body, {"hi": "there"}) + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) - def test_post(self): + @mock.patch.object(session.requests.Session, 'request') + def test_post(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE cl = get_authed_client() - with mock.patch.object(requests, "request", MOCK_REQUEST): - cl.post("/hi", body=[1, 2, 3]) + cl.post("/hi", body=[1, 2, 3]) - # this may become too tightly couple later - mock_args, mock_kwargs = MOCK_REQUEST.call_args + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args - self.assertEqual(mock_args[0], 'POST') - self.assertEqual(mock_args[1], REQUEST_URL) - self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') - self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') - self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) - self.assertEqual(mock_kwargs['verify'], 'ca.pem') + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') - def test_post_auth(self): - with mock.patch.object(requests, "request", MOCK_REQUEST): - cl = httpclient.HTTPClient( - username="username", password="password", tenant_id="tenant", - auth_url="auth_test", cacert="ca.pem", key="key.pem", - cert="cert.pem") - cl.management_url = "https://127.0.0.1:5000" - cl.auth_token = "token" - cl.post("/hi", body=[1, 2, 3]) + @mock.patch.object(session.requests.Session, 'request') + def test_post_auth(self, MOCK_REQUEST): + MOCK_REQUEST.return_value = FAKE_RESPONSE + cl = httpclient.HTTPClient( + username="username", password="password", tenant_id="tenant", + auth_url="auth_test", cacert="ca.pem", key="key.pem", + cert="cert.pem") + cl.management_url = "https://127.0.0.1:5000" + cl.auth_token = "token" + cl.post("/hi", body=[1, 2, 3]) - # this may become too tightly couple later - mock_args, mock_kwargs = MOCK_REQUEST.call_args + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args - self.assertEqual(mock_args[0], 'POST') - self.assertEqual(mock_args[1], REQUEST_URL) - self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') - self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') - self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) - self.assertEqual(mock_kwargs['verify'], 'ca.pem') + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') diff --git a/keystoneclient/tests/test_session.py b/keystoneclient/tests/test_session.py new file mode 100644 index 000000000..74fab7864 --- /dev/null +++ b/keystoneclient/tests/test_session.py @@ -0,0 +1,140 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import httpretty +import mock + +from keystoneclient import exceptions +from keystoneclient import session as client_session +from keystoneclient.tests import utils + + +class SessionTests(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/' + + @httpretty.activate + def test_get(self): + session = client_session.Session() + self.stub_url(httpretty.GET, body='response') + resp = session.get(self.TEST_URL) + + self.assertEqual(httpretty.GET, httpretty.last_request().method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + + @httpretty.activate + def test_post(self): + session = client_session.Session() + self.stub_url(httpretty.POST, body='response') + resp = session.post(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual(httpretty.POST, httpretty.last_request().method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + self.assertRequestBodyIs(json={'hello': 'world'}) + + @httpretty.activate + def test_head(self): + session = client_session.Session() + self.stub_url(httpretty.HEAD) + resp = session.head(self.TEST_URL) + + self.assertEqual(httpretty.HEAD, httpretty.last_request().method) + self.assertTrue(resp.ok) + self.assertRequestBodyIs('') + + @httpretty.activate + def test_put(self): + session = client_session.Session() + self.stub_url(httpretty.PUT, body='response') + resp = session.put(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual(httpretty.PUT, httpretty.last_request().method) + self.assertEqual(resp.text, 'response') + self.assertTrue(resp.ok) + self.assertRequestBodyIs(json={'hello': 'world'}) + + @httpretty.activate + def test_delete(self): + session = client_session.Session() + self.stub_url(httpretty.DELETE, body='response') + resp = session.delete(self.TEST_URL) + + self.assertEqual(httpretty.DELETE, httpretty.last_request().method) + self.assertTrue(resp.ok) + self.assertEqual(resp.text, 'response') + + @httpretty.activate + def test_patch(self): + session = client_session.Session() + self.stub_url(httpretty.PATCH, body='response') + resp = session.patch(self.TEST_URL, json={'hello': 'world'}) + + self.assertEqual(httpretty.PATCH, httpretty.last_request().method) + self.assertTrue(resp.ok) + self.assertEqual(resp.text, 'response') + self.assertRequestBodyIs(json={'hello': 'world'}) + + @httpretty.activate + def test_user_agent(self): + session = client_session.Session(user_agent='test-agent') + self.stub_url(httpretty.GET, body='response') + resp = session.get(self.TEST_URL) + + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'test-agent') + + resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}) + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'new-agent') + + resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}, + user_agent='overrides-agent') + self.assertTrue(resp.ok) + self.assertRequestHeaderEqual('User-Agent', 'overrides-agent') + + @httpretty.activate + def test_http_session_opts(self): + session = client_session.Session(cert='cert.pem', timeout=5, + verify='certs') + + FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'}) + RESP = mock.Mock(return_value=FAKE_RESP) + + with mock.patch.object(session.session, 'request', RESP) as mocked: + session.post(self.TEST_URL, data='value') + + mock_args, mock_kwargs = mocked.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], self.TEST_URL) + self.assertEqual(mock_kwargs['data'], 'value') + self.assertEqual(mock_kwargs['cert'], 'cert.pem') + self.assertEqual(mock_kwargs['verify'], 'certs') + self.assertEqual(mock_kwargs['timeout'], 5) + + @httpretty.activate + def test_not_found(self): + session = client_session.Session() + self.stub_url(httpretty.GET, status=404) + self.assertRaises(exceptions.NotFound, session.get, self.TEST_URL) + + @httpretty.activate + def test_server_error(self): + session = client_session.Session() + self.stub_url(httpretty.GET, status=500) + self.assertRaises(exceptions.InternalServerError, + session.get, self.TEST_URL) diff --git a/keystoneclient/tests/test_shell.py b/keystoneclient/tests/test_shell.py index e4b999b7f..17a4a09ce 100644 --- a/keystoneclient/tests/test_shell.py +++ b/keystoneclient/tests/test_shell.py @@ -25,6 +25,7 @@ import testtools from testtools import matchers from keystoneclient import exceptions +from keystoneclient import session from keystoneclient import shell as openstack_shell from keystoneclient.tests import utils from keystoneclient.v2_0 import shell as shell_v2_0 @@ -423,7 +424,8 @@ class ShellTest(utils.TestCase): 'endpoints': [], }) request_mock = mock.MagicMock(return_value=response_mock) - with mock.patch('requests.request', request_mock): + with mock.patch.object(session.requests.Session, 'request', + request_mock): shell(('--timeout 2 --os-token=blah --os-endpoint=blah' ' --os-auth-url=blah.com endpoint-list')) request_mock.assert_called_with(mock.ANY, mock.ANY,