From 2c5dcf9bf19d16e9ac8125ea571a94b4f37a3298 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Mon, 30 Sep 2013 10:35:55 +1000 Subject: [PATCH] Extract a base Session object A wrapper around a number of connection variables. This will be extended later with principals such as Kerberos authentication and http sessions. The intent is that this session object will become the basis for all other client library communications in OpenStack (as keystone wants to control things like authentication for everybody). Change-Id: I8ee728c49d554659d7057ebf17d0f8ceea4d7d8e Part of: blueprint auth-plugins --- keystoneclient/exceptions.py | 12 ++ keystoneclient/httpclient.py | 225 ++++++++++----------------- keystoneclient/session.py | 201 ++++++++++++++++++++++++ keystoneclient/tests/test_https.py | 85 +++++----- keystoneclient/tests/test_session.py | 140 +++++++++++++++++ keystoneclient/tests/test_shell.py | 4 +- 6 files changed, 486 insertions(+), 181 deletions(-) create mode 100644 keystoneclient/session.py create mode 100644 keystoneclient/tests/test_session.py 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,