summaryrefslogtreecommitdiff
path: root/tests/unit/utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/utils.py')
-rw-r--r--tests/unit/utils.py168
1 files changed, 144 insertions, 24 deletions
diff --git a/tests/unit/utils.py b/tests/unit/utils.py
index 88a214d..9d8aacc 100644
--- a/tests/unit/utils.py
+++ b/tests/unit/utils.py
@@ -15,24 +15,34 @@
import functools
import sys
from requests import RequestException
+from requests.structures import CaseInsensitiveDict
from time import sleep
+import unittest
import testtools
import mock
import six
from six.moves import reload_module
+from six.moves.urllib.parse import urlparse, ParseResult
from swiftclient import client as c
from swiftclient import shell as s
-def fake_get_auth_keystone(os_options, exc=None, **kwargs):
+def fake_get_auth_keystone(expected_os_options=None, exc=None,
+ storage_url='http://url/', token='token',
+ **kwargs):
def fake_get_auth_keystone(auth_url,
user,
key,
actual_os_options, **actual_kwargs):
if exc:
raise exc('test')
- if actual_os_options != os_options:
+ # TODO: some way to require auth_url, user and key?
+ if expected_os_options and actual_os_options != expected_os_options:
return "", None
+ if 'required_kwargs' in kwargs:
+ for k, v in kwargs['required_kwargs'].items():
+ if v != actual_kwargs.get(k):
+ return "", None
if auth_url.startswith("https") and \
auth_url.endswith("invalid-certificate") and \
@@ -45,20 +55,36 @@ def fake_get_auth_keystone(os_options, exc=None, **kwargs):
actual_kwargs['cacert'] is None:
from swiftclient import client as c
raise c.ClientException("unverified-certificate")
- if 'required_kwargs' in kwargs:
- for k, v in kwargs['required_kwargs'].items():
- if v != actual_kwargs.get(k):
- return "", None
- return "http://url/", "token"
+ return storage_url, token
return fake_get_auth_keystone
+class StubResponse(object):
+ """
+ Placeholder structure for use with fake_http_connect's code_iter to modify
+ response attributes (status, body, headers) on a per-request basis.
+ """
+
+ def __init__(self, status=200, body='', headers=None):
+ self.status = status
+ self.body = body
+ self.headers = headers or {}
+
+
def fake_http_connect(*code_iter, **kwargs):
+ """
+ Generate a callable which yields a series of stubbed responses. Because
+ swiftclient will reuse an HTTP connection across pipelined requests it is
+ not always the case that this fake is used strictly for mocking an HTTP
+ connection, but rather each HTTP response (i.e. each call to requests
+ get_response).
+ """
class FakeConn(object):
- def __init__(self, status, etag=None, body='', timestamp='1'):
+ def __init__(self, status, etag=None, body='', timestamp='1',
+ headers=None):
self.status = status
self.reason = 'Fake'
self.host = '1.2.3.4'
@@ -69,6 +95,7 @@ def fake_http_connect(*code_iter, **kwargs):
self.body = body
self.timestamp = timestamp
self._is_closed = True
+ self.headers = headers or {}
def connect(self):
self._is_closed = False
@@ -92,6 +119,8 @@ def fake_http_connect(*code_iter, **kwargs):
return FakeConn(100)
def getheaders(self):
+ if self.headers:
+ return self.headers.items()
headers = {'content-length': len(self.body),
'content-type': 'x-application/test',
'x-timestamp': self.timestamp,
@@ -154,15 +183,20 @@ def fake_http_connect(*code_iter, **kwargs):
if 'give_connect' in kwargs:
kwargs['give_connect'](*args, **ckwargs)
status = next(code_iter)
- etag = next(etag_iter)
- timestamp = next(timestamps_iter)
- if status <= 0:
+ if isinstance(status, StubResponse):
+ fake_conn = FakeConn(status.status, body=status.body,
+ headers=status.headers)
+ else:
+ etag = next(etag_iter)
+ timestamp = next(timestamps_iter)
+ fake_conn = FakeConn(status, etag, body=kwargs.get('body', ''),
+ timestamp=timestamp)
+ if fake_conn.status <= 0:
raise RequestException()
- fake_conn = FakeConn(status, etag, body=kwargs.get('body', ''),
- timestamp=timestamp)
fake_conn.connect()
return fake_conn
+ connect.code_iter = code_iter
return connect
@@ -170,10 +204,14 @@ class MockHttpTest(testtools.TestCase):
def setUp(self):
super(MockHttpTest, self).setUp()
+ self.fake_connect = None
+ self.request_log = []
def fake_http_connection(*args, **kwargs):
+ self.validateMockedRequestsConsumed()
+ self.request_log = []
+ self.fake_connect = fake_http_connect(*args, **kwargs)
_orig_http_connection = c.http_connection
- return_read = kwargs.get('return_read')
query_string = kwargs.get('query_string')
storage_url = kwargs.get('storage_url')
auth_token = kwargs.get('auth_token')
@@ -185,9 +223,28 @@ class MockHttpTest(testtools.TestCase):
self.assertEqual(storage_url, url)
parsed, _conn = _orig_http_connection(url, proxy=proxy)
- conn = fake_http_connect(*args, **kwargs)()
+
+ class RequestsWrapper(object):
+ pass
+ conn = RequestsWrapper()
def request(method, url, *args, **kwargs):
+ try:
+ conn.resp = self.fake_connect()
+ except StopIteration:
+ self.fail('Unexpected %s request for %s' % (
+ method, url))
+ self.request_log.append((parsed, method, url, args,
+ kwargs, conn.resp))
+ conn.host = conn.resp.host
+ conn.isclosed = conn.resp.isclosed
+ conn.resp.has_been_read = False
+ _orig_read = conn.resp.read
+
+ def read(*args, **kwargs):
+ conn.resp.has_been_read = True
+ return _orig_read(*args, **kwargs)
+ conn.resp.read = read
if auth_token:
headers = args[1]
self.assertTrue('X-Auth-Token' in headers)
@@ -198,25 +255,88 @@ class MockHttpTest(testtools.TestCase):
if url.endswith('invalid_cert') and not insecure:
from swiftclient import client as c
raise c.ClientException("invalid_certificate")
- elif exc:
+ if exc:
raise exc
- return
+ return conn.resp
conn.request = request
- conn.has_been_read = False
- _orig_read = conn.read
-
- def read(*args, **kwargs):
- conn.has_been_read = True
- return _orig_read(*args, **kwargs)
- conn.read = return_read or read
+ def getresponse():
+ return conn.resp
+ conn.getresponse = getresponse
return parsed, conn
return wrapper
self.fake_http_connection = fake_http_connection
+ def iter_request_log(self):
+ for parsed, method, path, args, kwargs, resp in self.request_log:
+ parts = parsed._asdict()
+ parts['path'] = path
+ full_path = ParseResult(**parts).geturl()
+ args = list(args)
+ log = dict(zip(('body', 'headers'), args))
+ log.update({
+ 'method': method,
+ 'full_path': full_path,
+ 'parsed_path': urlparse(full_path),
+ 'path': path,
+ 'headers': CaseInsensitiveDict(log.get('headers')),
+ 'resp': resp,
+ 'status': resp.status,
+ })
+ yield log
+
+ orig_assertEqual = unittest.TestCase.assertEqual
+
+ def assertRequests(self, expected_requests):
+ """
+ Make sure some requests were made like you expected, provide a list of
+ expected requests, typically in the form of [(method, path), ...]
+ """
+ real_requests = self.iter_request_log()
+ for expected in expected_requests:
+ method, path = expected[:2]
+ real_request = next(real_requests)
+ if urlparse(path).scheme:
+ match_path = real_request['full_path']
+ else:
+ match_path = real_request['path']
+ self.assertEqual((method, path), (real_request['method'],
+ match_path))
+ if len(expected) > 2:
+ body = expected[2]
+ real_request['expected'] = body
+ err_msg = 'Body mismatch for %(method)s %(path)s, ' \
+ 'expected %(expected)r, and got %(body)r' % real_request
+ self.orig_assertEqual(body, real_request['body'], err_msg)
+
+ if len(expected) > 3:
+ headers = expected[3]
+ for key, value in headers.items():
+ real_request['key'] = key
+ real_request['expected_value'] = value
+ real_request['value'] = real_request['headers'].get(key)
+ err_msg = (
+ 'Header mismatch on %(key)r, '
+ 'expected %(expected_value)r and got %(value)r '
+ 'for %(method)s %(path)s %(headers)r' % real_request)
+ self.orig_assertEqual(value, real_request['value'],
+ err_msg)
+
+ def validateMockedRequestsConsumed(self):
+ if not self.fake_connect:
+ return
+ unused_responses = list(self.fake_connect.code_iter)
+ if unused_responses:
+ self.fail('Unused responses %r' % (unused_responses,))
+
def tearDown(self):
+ self.validateMockedRequestsConsumed()
super(MockHttpTest, self).tearDown()
+ # TODO: this nuke from orbit clean up seems to be encouraging
+ # un-hygienic mocking on the swiftclient.client module; which may lead
+ # to some unfortunate test order dependency bugs by way of the broken
+ # window theory if any other modules are similarly patched
reload_module(c)