summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2015-02-02 08:30:40 +0000
committerGerrit Code Review <review@openstack.org>2015-02-02 08:30:40 +0000
commit21473f1bc475fa69aa9d1cdd6b60cc827c4f7f1b (patch)
treea644c4ca51a352f11bf16d4ccfd2c7a90bc0f14d
parenteef91b35139411fcef31855ce0ebe4407a2de70b (diff)
parent9593d4b58a5e9f240e26d7873d3cc251c7d51f71 (diff)
downloadpython-swiftclient-21473f1bc475fa69aa9d1cdd6b60cc827c4f7f1b.tar.gz
Merge "Fix cross account upload using --os-storage-url"
-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 630ec8d..32d3bd0 100644
--- a/swiftclient/service.py
+++ b/swiftclient/service.py
@@ -1180,11 +1180,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,
@@ -1211,28 +1206,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 6513263..40cd77b 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):
@@ -416,6 +433,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'}]],
@@ -719,10 +758,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:
@@ -730,9 +770,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)
@@ -1366,3 +1417,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: