summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlistair Coles <alistair.coles@hp.com>2014-09-29 18:26:33 +0100
committerAlistair Coles <alistair.coles@hp.com>2015-01-06 16:13:39 +0000
commit9593d4b58a5e9f240e26d7873d3cc251c7d51f71 (patch)
treebf2e4135070d7afe5d2bc6f16c29668773967400
parent5d5701870702a554dcea61213999670ee15f4ea8 (diff)
downloadpython-swiftclient-9593d4b58a5e9f240e26d7873d3cc251c7d51f71.tar.gz
Fix cross account upload using --os-storage-url
Removes an account stat from the object upload path. This stat fails when user is not account admin even though the user may have container ACL permission to write objects. Reduces the severity of the CLI output message when upload fails to create the given container (this is not an error since the container may exist - the user just does not have permission to PUT or POST the container). Changes the 'swift upload' exit return code from 1 to 0 if container PUT fails but object PUT succeeds. For segment uploads, makes the attempt to create the segment container conditional on it not being the same as the manifest container. This avoids an unnecessary container PUT. Fixes another bug that became apparent: with segmented upload a container HEAD may be attempted to determine the policy to be used for the segment container. When this failed the result dict has headers=None which was causing an exception in the shell result handler. Add unit tests for object upload/download and container list with --os-storage-url option. Closes-Bug: #1371650 Change-Id: If1f8a02ee7459ea2158ffa6e958f67d299ec529e
-rw-r--r--swiftclient/multithreading.py10
-rw-r--r--swiftclient/service.py48
-rwxr-xr-xswiftclient/shell.py37
-rw-r--r--tests/unit/test_shell.py352
-rw-r--r--tests/unit/utils.py82
5 files changed, 448 insertions, 81 deletions
diff --git a/swiftclient/multithreading.py b/swiftclient/multithreading.py
index ade0f7b..6e7f143 100644
--- a/swiftclient/multithreading.py
+++ b/swiftclient/multithreading.py
@@ -96,10 +96,16 @@ class OutputManager(object):
item = item.encode('utf8')
print(item, file=stream)
- def _print_error(self, item):
- self.error_count += 1
+ def _print_error(self, item, count=1):
+ self.error_count += count
return self._print(item, stream=self.error_stream)
+ def warning(self, msg, *fmt_args):
+ # print to error stream but do not increment error count
+ if fmt_args:
+ msg = msg % fmt_args
+ self.error_print_pool.submit(self._print_error, msg, count=0)
+
class MultiThreadingManager(object):
"""
diff --git a/swiftclient/service.py b/swiftclient/service.py
index dddb7db..eff22a8 100644
--- a/swiftclient/service.py
+++ b/swiftclient/service.py
@@ -1175,11 +1175,6 @@ class SwiftService(object):
except ValueError:
raise SwiftError('Segment size should be an integer value')
- # Does the account exist?
- account_stat = self.stat(options=options)
- if not account_stat["success"]:
- raise account_stat["error"]
-
# Try to create the container, just in case it doesn't exist. If this
# fails, it might just be because the user doesn't have container PUT
# permissions, so we'll ignore any error. If there's really a problem,
@@ -1206,28 +1201,29 @@ class SwiftService(object):
seg_container = container + '_segments'
if options['segment_container']:
seg_container = options['segment_container']
- if not policy_header:
- # Since no storage policy was specified on the command line,
- # rather than just letting swift pick the default storage
- # policy, we'll try to create the segments container with the
- # same as the upload container
- create_containers = [
- self.thread_manager.container_pool.submit(
- self._create_container_job, seg_container,
- policy_source=container
- )
- ]
- else:
- create_containers = [
- self.thread_manager.container_pool.submit(
- self._create_container_job, seg_container,
- headers=policy_header
- )
- ]
+ if seg_container != container:
+ if not policy_header:
+ # Since no storage policy was specified on the command
+ # line, rather than just letting swift pick the default
+ # storage policy, we'll try to create the segments
+ # container with the same policy as the upload container
+ create_containers = [
+ self.thread_manager.container_pool.submit(
+ self._create_container_job, seg_container,
+ policy_source=container
+ )
+ ]
+ else:
+ create_containers = [
+ self.thread_manager.container_pool.submit(
+ self._create_container_job, seg_container,
+ headers=policy_header
+ )
+ ]
- for r in interruptable_as_completed(create_containers):
- res = r.result()
- yield res
+ for r in interruptable_as_completed(create_containers):
+ res = r.result()
+ yield res
# We maintain a results queue here and a separate thread to monitor
# the futures because we want to get results back from potential
diff --git a/swiftclient/shell.py b/swiftclient/shell.py
index c3f4628..6b3ee3f 100755
--- a/swiftclient/shell.py
+++ b/swiftclient/shell.py
@@ -817,16 +817,14 @@ def st_upload(parser, args, output_manager):
)
else:
error = r['error']
- if isinstance(error, SwiftError):
- output_manager.error("%s" % error)
- elif isinstance(error, ClientException):
- if r['action'] == "create_container":
- if 'X-Storage-Policy' in r['headers']:
- output_manager.error(
- 'Error trying to create container %s with '
- 'Storage Policy %s', container,
- r['headers']['X-Storage-Policy'].strip()
- )
+ if 'action' in r and r['action'] == "create_container":
+ # it is not an error to be unable to create the
+ # container so print a warning and carry on
+ if isinstance(error, ClientException):
+ if (r['headers'] and
+ 'X-Storage-Policy' in r['headers']):
+ msg = ' with Storage Policy %s' % \
+ r['headers']['X-Storage-Policy'].strip()
else:
msg = ' '.join(str(x) for x in (
error.http_status, error.http_reason)
@@ -835,20 +833,15 @@ def st_upload(parser, args, output_manager):
if msg:
msg += ': '
msg += error.http_response_content[:60]
- output_manager.error(
- 'Error trying to create container %r: %s',
- container, msg
- )
+ msg = ': %s' % msg
else:
- output_manager.error("%s" % error)
+ msg = ': %s' % error
+ output_manager.warning(
+ 'Warning: failed to create container '
+ '%r%s', container, msg
+ )
else:
- if r['action'] == "create_container":
- output_manager.error(
- 'Error trying to create container %r: %s',
- container, error
- )
- else:
- output_manager.error("%s" % error)
+ output_manager.error("%s" % error)
except SwiftError as e:
output_manager.error("%s" % e)
diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py
index 65530bc..0090a97 100644
--- a/tests/unit/test_shell.py
+++ b/tests/unit/test_shell.py
@@ -12,7 +12,9 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+from genericpath import getmtime
+import hashlib
import mock
import os
import tempfile
@@ -83,6 +85,21 @@ def _make_env(opts, os_opts):
return env
+def _make_cmd(cmd, opts, os_opts, use_env=False, flags=None, cmd_args=None):
+ flags = flags or []
+ if use_env:
+ # set up fake environment variables and make a minimal command line
+ env = _make_env(opts, os_opts)
+ args = _make_args(cmd, {}, {}, separator='-', flags=flags,
+ cmd_args=cmd_args)
+ else:
+ # set up empty environment and make full command line
+ env = {}
+ args = _make_args(cmd, opts, os_opts, separator='-', flags=flags,
+ cmd_args=cmd_args)
+ return args, env
+
+
@mock.patch.dict(os.environ, mocked_os_environ)
class TestShell(unittest.TestCase):
def __init__(self, *args, **kwargs):
@@ -389,6 +406,28 @@ class TestShell(unittest.TestCase):
response_dict={})
@mock.patch('swiftclient.service.Connection')
+ def test_upload_segments_to_same_container(self, connection):
+ # Upload in segments to same container
+ connection.return_value.head_object.return_value = {
+ 'content-length': '0'}
+ connection.return_value.attempts = 0
+ argv = ["", "upload", "container", self.tmpfile, "-S", "10",
+ "-C", "container"]
+ with open(self.tmpfile, "wb") as fh:
+ fh.write(b'12345678901234567890')
+ swiftclient.shell.main(argv)
+ connection.return_value.put_container.assert_called_once_with(
+ 'container', {}, response_dict={})
+ connection.return_value.put_object.assert_called_with(
+ 'container',
+ self.tmpfile.lstrip('/'),
+ '',
+ content_length=0,
+ headers={'x-object-manifest': mock.ANY,
+ 'x-object-meta-mtime': mock.ANY},
+ response_dict={})
+
+ @mock.patch('swiftclient.service.Connection')
def test_delete_account(self, connection):
connection.return_value.get_account.side_effect = [
[None, [{'name': 'container'}]],
@@ -692,10 +731,11 @@ class TestSubcommandHelp(unittest.TestCase):
self.assertEqual(out.strip('\n'), expected)
-class TestParsing(unittest.TestCase):
-
- def setUp(self):
- super(TestParsing, self).setUp()
+class TestBase(unittest.TestCase):
+ """
+ Provide some common methods to subclasses
+ """
+ def _remove_swift_env_vars(self):
self._environ_vars = {}
keys = list(os.environ.keys())
for k in keys:
@@ -703,9 +743,20 @@ class TestParsing(unittest.TestCase):
or k.startswith('OS_')):
self._environ_vars[k] = os.environ.pop(k)
- def tearDown(self):
+ def _replace_swift_env_vars(self):
os.environ.update(self._environ_vars)
+
+class TestParsing(TestBase):
+
+ def setUp(self):
+ super(TestParsing, self).setUp()
+ self._remove_swift_env_vars()
+
+ def tearDown(self):
+ self._replace_swift_env_vars()
+ super(TestParsing, self).tearDown()
+
def _make_fake_command(self, result):
def fake_command(parser, args, thread_manager):
result[0], result[1] = swiftclient.shell.parse_args(parser, args)
@@ -1339,3 +1390,294 @@ class TestAuth(MockHttpTest):
'x-auth-token': token + '_new',
}),
])
+
+
+class TestCrossAccountObjectAccess(TestBase, MockHttpTest):
+ """
+ Tests to verify use of --os-storage-url will actually
+ result in the object request being sent despite account
+ read/write access and container write access being denied.
+ """
+ def setUp(self):
+ super(TestCrossAccountObjectAccess, self).setUp()
+ self._remove_swift_env_vars()
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
+ temp_file.file.write(b'01234567890123456789')
+ temp_file.file.flush()
+ self.obj = temp_file.name
+ self.url = 'http://alternate.com:8080/v1'
+
+ # account tests will attempt to access
+ self.account = 'AUTH_alice'
+
+ # keystone returns endpoint for another account
+ fake_ks = FakeKeystone(endpoint='http://example.com:8080/v1/AUTH_bob',
+ token='bob_token')
+ self.fake_ks_import = _make_fake_import_keystone_client(fake_ks)
+
+ self.cont = 'c1'
+ self.cont_path = '/v1/%s/%s' % (self.account, self.cont)
+ self.obj_path = '%s%s' % (self.cont_path, self.obj)
+
+ self.os_opts = {'username': 'bob',
+ 'password': 'password',
+ 'project-name': 'proj_bob',
+ 'auth-url': 'http://example.com:5000/v3',
+ 'storage-url': '%s/%s' % (self.url, self.account)}
+ self.opts = {'auth-version': '3'}
+
+ def tearDown(self):
+ try:
+ os.remove(self.obj)
+ except OSError:
+ pass
+ self._replace_swift_env_vars()
+ super(TestCrossAccountObjectAccess, self).tearDown()
+
+ def _make_cmd(self, cmd, cmd_args=None):
+ return _make_cmd(cmd, self.opts, self.os_opts, cmd_args=cmd_args)
+
+ def _fake_cross_account_auth(self, read_ok, write_ok):
+ def on_request(method, path, *args, **kwargs):
+ """
+ Modify response code to 200 if cross account permissions match.
+ """
+ status = 403
+ if (path.startswith('/v1/%s/%s' % (self.account, self.cont))
+ and read_ok and method in ('GET', 'HEAD')):
+ status = 200
+ elif (path.startswith('/v1/%s/%s%s'
+ % (self.account, self.cont, self.obj))
+ and write_ok and method in ('PUT', 'POST', 'DELETE')):
+ status = 200
+ return status
+ return on_request
+
+ def test_upload_with_read_write_access(self):
+ req_handler = self._fake_cross_account_auth(True, True)
+ fake_conn = self.fake_http_connection(403, 403,
+ on_request=req_handler)
+
+ args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj,
+ '--leave-segments'])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ except SystemExit as e:
+ self.fail('Unexpected SystemExit: %s' % e)
+
+ self.assertRequests([('PUT', self.cont_path),
+ ('PUT', self.obj_path)])
+ self.assertEqual(self.obj, out.strip())
+ expected_err = 'Warning: failed to create container %r: 403 Fake' \
+ % self.cont
+ self.assertEqual(expected_err, out.err.strip())
+
+ def test_upload_with_write_only_access(self):
+ req_handler = self._fake_cross_account_auth(False, True)
+ fake_conn = self.fake_http_connection(403, 403,
+ on_request=req_handler)
+
+ args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj,
+ '--leave-segments'])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ except SystemExit as e:
+ self.fail('Unexpected SystemExit: %s' % e)
+
+ self.assertRequests([('PUT', self.cont_path),
+ ('PUT', self.obj_path)])
+ self.assertEqual(self.obj, out.strip())
+ expected_err = 'Warning: failed to create container %r: 403 Fake' \
+ % self.cont
+ self.assertEqual(expected_err, out.err.strip())
+
+ def test_segment_upload_with_write_only_access(self):
+ req_handler = self._fake_cross_account_auth(False, True)
+ fake_conn = self.fake_http_connection(403, 403, 403, 403,
+ on_request=req_handler)
+
+ args, env = self._make_cmd('upload',
+ cmd_args=[self.cont, self.obj,
+ '--leave-segments',
+ '--segment-size=10',
+ '--segment-container=%s'
+ % self.cont])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ except SystemExit as e:
+ self.fail('Unexpected SystemExit: %s' % e)
+
+ segment_time = getmtime(self.obj)
+ segment_path_0 = '%s/%f/20/10/00000000' % (self.obj_path, segment_time)
+ segment_path_1 = '%s/%f/20/10/00000001' % (self.obj_path, segment_time)
+ # Note that the order of segment PUTs cannot be asserted, so test for
+ # existence in request log individually
+ self.assert_request(('PUT', self.cont_path))
+ self.assert_request(('PUT', segment_path_0))
+ self.assert_request(('PUT', segment_path_1))
+ self.assert_request(('PUT', self.obj_path))
+ self.assertTrue(self.obj in out.out)
+ expected_err = 'Warning: failed to create container %r: 403 Fake' \
+ % self.cont
+ self.assertEqual(expected_err, out.err.strip())
+
+ def test_upload_with_no_access(self):
+ fake_conn = self.fake_http_connection(403, 403)
+
+ args, env = self._make_cmd('upload', cmd_args=[self.cont, self.obj,
+ '--leave-segments'])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ self.fail('Expected SystemExit')
+ except SystemExit:
+ pass
+
+ self.assertRequests([('PUT', self.cont_path),
+ ('PUT', self.obj_path)])
+ expected_err = 'Object PUT failed: http://1.2.3.4%s 403 Fake' \
+ % self.obj_path
+ self.assertTrue(expected_err in out.err)
+ self.assertEqual('', out)
+
+ def test_download_with_read_write_access(self):
+ req_handler = self._fake_cross_account_auth(True, True)
+ empty_str_etag = 'd41d8cd98f00b204e9800998ecf8427e'
+ fake_conn = self.fake_http_connection(403, on_request=req_handler,
+ etags=[empty_str_etag])
+
+ args, env = self._make_cmd('download', cmd_args=[self.cont,
+ self.obj.lstrip('/'),
+ '--no-download'])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ except SystemExit as e:
+ self.fail('Unexpected SystemExit: %s' % e)
+
+ self.assertRequests([('GET', self.obj_path)])
+ self.assertTrue(out.out.startswith(self.obj.lstrip('/')))
+ self.assertEqual('', out.err)
+
+ def test_download_with_read_only_access(self):
+ req_handler = self._fake_cross_account_auth(True, False)
+ empty_str_etag = 'd41d8cd98f00b204e9800998ecf8427e'
+ fake_conn = self.fake_http_connection(403, on_request=req_handler,
+ etags=[empty_str_etag])
+
+ args, env = self._make_cmd('download', cmd_args=[self.cont,
+ self.obj.lstrip('/'),
+ '--no-download'])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ except SystemExit as e:
+ self.fail('Unexpected SystemExit: %s' % e)
+
+ self.assertRequests([('GET', self.obj_path)])
+ self.assertTrue(out.out.startswith(self.obj.lstrip('/')))
+ self.assertEqual('', out.err)
+
+ def test_download_with_no_access(self):
+ fake_conn = self.fake_http_connection(403)
+ args, env = self._make_cmd('download', cmd_args=[self.cont,
+ self.obj.lstrip('/'),
+ '--no-download'])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ self.fail('Expected SystemExit')
+ except SystemExit:
+ pass
+
+ self.assertRequests([('GET', self.obj_path)])
+ path = '%s%s' % (self.cont, self.obj)
+ expected_err = 'Error downloading object %r' % path
+ self.assertTrue(out.err.startswith(expected_err))
+ self.assertEqual('', out)
+
+ def test_list_with_read_access(self):
+ req_handler = self._fake_cross_account_auth(True, False)
+ resp_body = '{}'
+ m = hashlib.md5()
+ m.update(resp_body.encode())
+ etag = m.hexdigest()
+ fake_conn = self.fake_http_connection(403, on_request=req_handler,
+ etags=[etag],
+ body=resp_body)
+
+ args, env = self._make_cmd('download', cmd_args=[self.cont])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ except SystemExit as e:
+ self.fail('Unexpected SystemExit: %s' % e)
+
+ self.assertRequests([('GET', '%s?format=json' % self.cont_path)])
+ self.assertEqual('', out)
+ self.assertEqual('', out.err)
+
+ def test_list_with_no_access(self):
+ fake_conn = self.fake_http_connection(403)
+
+ args, env = self._make_cmd('download', cmd_args=[self.cont])
+ with mock.patch('swiftclient.client._import_keystone_client',
+ self.fake_ks_import):
+ with mock.patch('swiftclient.client.http_connection', fake_conn):
+ with mock.patch.dict(os.environ, env):
+ with CaptureOutput() as out:
+ try:
+ swiftclient.shell.main(args)
+ self.fail('Expected SystemExit')
+ except SystemExit:
+ pass
+
+ self.assertRequests([('GET', '%s?format=json' % self.cont_path)])
+ self.assertEqual('', out)
+ self.assertTrue(out.err.startswith('Container GET failed:'))
+
+
+class TestCrossAccountObjectAccessUsingEnv(TestCrossAccountObjectAccess):
+ """
+ Repeat super-class tests using environment variables rather than command
+ line to set options.
+ """
+
+ def _make_cmd(self, cmd, cmd_args=None):
+ return _make_cmd(cmd, self.opts, self.os_opts, cmd_args=cmd_args,
+ use_env=True)
diff --git a/tests/unit/utils.py b/tests/unit/utils.py
index 9d8aacc..201a8a8 100644
--- a/tests/unit/utils.py
+++ b/tests/unit/utils.py
@@ -216,6 +216,7 @@ class MockHttpTest(testtools.TestCase):
storage_url = kwargs.get('storage_url')
auth_token = kwargs.get('auth_token')
exc = kwargs.get('exc')
+ on_request = kwargs.get('on_request')
def wrapper(url, proxy=None, cacert=None, insecure=False,
ssl_compression=True):
@@ -245,6 +246,9 @@ class MockHttpTest(testtools.TestCase):
conn.resp.has_been_read = True
return _orig_read(*args, **kwargs)
conn.resp.read = read
+ if on_request:
+ status = on_request(method, url, *args, **kwargs)
+ conn.resp.status = status
if auth_token:
headers = args[1]
self.assertTrue('X-Auth-Token' in headers)
@@ -258,7 +262,12 @@ class MockHttpTest(testtools.TestCase):
if exc:
raise exc
return conn.resp
+
+ def putrequest(path, data=None, headers=None, **kwargs):
+ request('PUT', path, data, headers, **kwargs)
+
conn.request = request
+ conn.putrequest = putrequest
def getresponse():
return conn.resp
@@ -288,6 +297,34 @@ class MockHttpTest(testtools.TestCase):
orig_assertEqual = unittest.TestCase.assertEqual
+ def assert_request_equal(self, expected, real_request):
+ method, path = expected[:2]
+ 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 assertRequests(self, expected_requests):
"""
Make sure some requests were made like you expected, provide a list of
@@ -295,33 +332,26 @@ class MockHttpTest(testtools.TestCase):
"""
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)
+ self.assert_request_equal(expected, real_request)
+
+ def assert_request(self, expected_request):
+ """
+ Make sure a request was made as expected. Provide the
+ expected request in the form of [(method, path), ...]
+ """
+ real_requests = self.iter_request_log()
+ for real_request in real_requests:
+ try:
+ self.assert_request_equal(expected_request, real_request)
+ break
+ except AssertionError:
+ pass
+ else:
+ raise AssertionError(
+ "Expected request %s not found in actual requests %s"
+ % (expected_request, self.request_log)
+ )
def validateMockedRequestsConsumed(self):
if not self.fake_connect: