diff options
Diffstat (limited to 'tests/unit/utils.py')
-rw-r--r-- | tests/unit/utils.py | 168 |
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) |