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
This commit is contained in:
parent
5d57018707
commit
9593d4b58a
@ -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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
@ -388,6 +405,28 @@ class TestShell(unittest.TestCase):
|
||||
'x-object-meta-mtime': mock.ANY},
|
||||
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 = [
|
||||
@ -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)
|
||||
|
@ -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)
|
||||
self.assert_request_equal(expected, real_request)
|
||||
|
||||
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 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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user