diff options
Diffstat (limited to 'test/unit/test_shell.py')
-rw-r--r-- | test/unit/test_shell.py | 3402 |
1 files changed, 3402 insertions, 0 deletions
diff --git a/test/unit/test_shell.py b/test/unit/test_shell.py new file mode 100644 index 0000000..c972281 --- /dev/null +++ b/test/unit/test_shell.py @@ -0,0 +1,3402 @@ +# Copyright (c) 2014 Christian Schwede <christian.schwede@enovance.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import unicode_literals + +import contextlib +from genericpath import getmtime +import getpass +import hashlib +import json +import logging +import mock +import os +import tempfile +import unittest +import textwrap +from time import localtime, mktime, strftime, strptime + +import six +import sys + +import swiftclient +from swiftclient.service import SwiftError +import swiftclient.shell +import swiftclient.utils + +from os.path import basename, dirname +from .utils import ( + CaptureOutput, fake_get_auth_keystone, + FakeKeystone, StubResponse, MockHttpTest) +from swiftclient.utils import ( + EMPTY_ETAG, EXPIRES_ISO8601_FORMAT, + SHORT_EXPIRES_ISO8601_FORMAT, TIME_ERRMSG) + +try: + from requests.packages.urllib3.exceptions import InsecureRequestWarning +except ImportError: + InsecureRequestWarning = None + +if six.PY2: + BUILTIN_OPEN = '__builtin__.open' +else: + BUILTIN_OPEN = 'builtins.open' + +mocked_os_environ = { + 'ST_AUTH': 'http://localhost:8080/auth/v1.0', + 'ST_USER': 'test:tester', + 'ST_KEY': 'testing' +} +clean_os_environ = {} +environ_prefixes = ('ST_', 'OS_') +for key in os.environ: + if any(key.startswith(m) for m in environ_prefixes): + clean_os_environ[key] = '' + + +def _make_args(cmd, opts, os_opts, separator='-', flags=None, cmd_args=None): + """ + Construct command line arguments for given options. + """ + args = [""] + flags = flags or [] + for k, v in opts.items(): + args.append("--" + k.replace("_", "-")) + if v is not None: + args.append(v) + for k, v in os_opts.items(): + args.append("--os" + separator + k.replace("_", separator)) + if v is not None: + args.append(v) + for flag in flags: + args.append('--%s' % flag) + if cmd: + args.append(cmd) + if cmd_args: + args.extend(cmd_args) + return args + + +def _make_env(opts, os_opts): + """ + Construct a dict of environment variables for given options. + """ + env = {} + for k, v in opts.items(): + key = 'ST_' + k.upper().replace('-', '_') + env[key] = v + for k, v in os_opts.items(): + key = 'OS_' + k.upper().replace('-', '_') + env[key] = v + 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 + + +@contextlib.contextmanager +def patch_disable_warnings(): + if InsecureRequestWarning is None: + # If InsecureRequestWarning isn't available, disbale_warnings won't + # be either; they both came in with + # https://github.com/requests/requests/commit/811ee4e and left again + # in https://github.com/requests/requests/commit/8e17600 + yield None + else: + with mock.patch('requests.packages.urllib3.disable_warnings') \ + as patched: + yield patched + + +@mock.patch.dict(os.environ, mocked_os_environ) +class TestShell(unittest.TestCase): + def setUp(self): + super(TestShell, self).setUp() + tmpfile = tempfile.NamedTemporaryFile(delete=False) + self.tmpfile = tmpfile.name + + def tearDown(self): + try: + os.remove(self.tmpfile) + except OSError: + pass + super(TestShell, self).tearDown() + + @mock.patch('swiftclient.service.Connection') + def test_stat_account(self, connection): + argv = ["", "stat"] + return_headers = { + 'x-account-container-count': '1', + 'x-account-object-count': '2', + 'x-account-bytes-used': '3', + 'content-length': 0, + 'date': ''} + connection.return_value.head_account.return_value = return_headers + connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account' + with CaptureOutput() as output: + swiftclient.shell.main(argv) + + self.assertEqual(output.out, + ' Account: AUTH_account\n' + 'Containers: 1\n' + ' Objects: 2\n' + ' Bytes: 3\n') + + @mock.patch('swiftclient.service.Connection') + def test_stat_account_with_headers(self, connection): + argv = ["", "stat", "-H", "Skip-Middleware: Test"] + return_headers = { + 'x-account-container-count': '1', + 'x-account-object-count': '2', + 'x-account-bytes-used': '3', + 'content-length': 0, + 'date': ''} + connection.return_value.head_account.return_value = return_headers + connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account' + with CaptureOutput() as output: + swiftclient.shell.main(argv) + + self.assertEqual(output.out, + ' Account: AUTH_account\n' + 'Containers: 1\n' + ' Objects: 2\n' + ' Bytes: 3\n') + self.assertEqual(connection.return_value.head_account.mock_calls, [ + mock.call(headers={'Skip-Middleware': 'Test'})]) + + @mock.patch('swiftclient.service.Connection') + def test_stat_container(self, connection): + return_headers = { + 'x-container-object-count': '1', + 'x-container-bytes-used': '2', + 'x-container-read': 'test2:tester2', + 'x-container-write': 'test3:tester3', + 'x-container-sync-to': 'other', + 'x-container-sync-key': 'secret', + } + argv = ["", "stat", "container"] + connection.return_value.head_container.return_value = return_headers + connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account' + with CaptureOutput() as output: + swiftclient.shell.main(argv) + + self.assertEqual(output.out, + ' Account: AUTH_account\n' + 'Container: container\n' + ' Objects: 1\n' + ' Bytes: 2\n' + ' Read ACL: test2:tester2\n' + 'Write ACL: test3:tester3\n' + ' Sync To: other\n' + ' Sync Key: secret\n') + + @mock.patch('swiftclient.service.Connection') + def test_stat_container_with_headers(self, connection): + return_headers = { + 'x-container-object-count': '1', + 'x-container-bytes-used': '2', + 'x-container-read': 'test2:tester2', + 'x-container-write': 'test3:tester3', + 'x-container-sync-to': 'other', + 'x-container-sync-key': 'secret', + } + argv = ["", "stat", "container", "-H", "Skip-Middleware: Test"] + connection.return_value.head_container.return_value = return_headers + connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account' + with CaptureOutput() as output: + swiftclient.shell.main(argv) + + self.assertEqual(output.out, + ' Account: AUTH_account\n' + 'Container: container\n' + ' Objects: 1\n' + ' Bytes: 2\n' + ' Read ACL: test2:tester2\n' + 'Write ACL: test3:tester3\n' + ' Sync To: other\n' + ' Sync Key: secret\n') + self.assertEqual(connection.return_value.head_container.mock_calls, [ + mock.call('container', headers={'Skip-Middleware': 'Test'})]) + + @mock.patch('swiftclient.service.Connection') + def test_stat_object(self, connection): + return_headers = { + 'x-object-manifest': 'manifest', + 'etag': 'md5', + 'last-modified': 'yesterday', + 'content-type': 'text/plain', + 'content-length': 42, + } + argv = ["", "stat", "container", "object"] + connection.return_value.head_object.return_value = return_headers + connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account' + + with CaptureOutput() as output: + swiftclient.shell.main(argv) + + self.assertEqual(output.out, + ' Account: AUTH_account\n' + ' Container: container\n' + ' Object: object\n' + ' Content Type: text/plain\n' + 'Content Length: 42\n' + ' Last Modified: yesterday\n' + ' ETag: md5\n' + ' Manifest: manifest\n') + + @mock.patch('swiftclient.service.Connection') + def test_stat_object_with_headers(self, connection): + return_headers = { + 'x-object-manifest': 'manifest', + 'etag': 'md5', + 'last-modified': 'yesterday', + 'content-type': 'text/plain', + 'content-length': 42, + } + argv = ["", "stat", "container", "object", + "-H", "Skip-Middleware: Test"] + connection.return_value.head_object.return_value = return_headers + connection.return_value.url = 'http://127.0.0.1/v1/AUTH_account' + + with CaptureOutput() as output: + swiftclient.shell.main(argv) + + self.assertEqual(output.out, + ' Account: AUTH_account\n' + ' Container: container\n' + ' Object: object\n' + ' Content Type: text/plain\n' + 'Content Length: 42\n' + ' Last Modified: yesterday\n' + ' ETag: md5\n' + ' Manifest: manifest\n') + self.assertEqual(connection.return_value.head_object.mock_calls, [ + mock.call('container', 'object', + headers={'Skip-Middleware': 'Test'})]) + + @mock.patch('swiftclient.service.Connection') + def test_list_json(self, connection): + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container'}]], + [None, [{'name': u'\u263A', 'some-custom-key': 'and value'}]], + [None, []], + ] + + argv = ["", "list", "--json"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + calls = [mock.call(marker='', prefix=None, headers={}), + mock.call(marker='container', prefix=None, headers={})] + connection.return_value.get_account.assert_has_calls(calls) + + listing = [{'name': 'container'}, + {'name': u'\u263A', 'some-custom-key': 'and value'}] + expected = json.dumps(listing, sort_keys=True, indent=2) + '\n' + self.assertEqual(output.out, expected) + + @mock.patch('swiftclient.service.Connection') + def test_list_account(self, connection): + # Test account listing + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container'}]], + [None, []], + ] + + argv = ["", "list"] + + with CaptureOutput() as output: + swiftclient.shell.main(argv) + calls = [mock.call(marker='', prefix=None, headers={}), + mock.call(marker='container', prefix=None, headers={})] + connection.return_value.get_account.assert_has_calls(calls) + + self.assertEqual(output.out, 'container\n') + + @mock.patch('swiftclient.service.Connection') + def test_list_account_with_headers(self, connection): + # Test account listing + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container'}]], + [None, []], + ] + + argv = ["", "list", '-H', 'Skip-Custom-Middleware: True'] + + with CaptureOutput() as output: + swiftclient.shell.main(argv) + calls = [mock.call(marker='', prefix=None, + headers={'Skip-Custom-Middleware': 'True'}), + mock.call(marker='container', prefix=None, + headers={'Skip-Custom-Middleware': 'True'})] + connection.return_value.get_account.assert_has_calls(calls) + + self.assertEqual(output.out, 'container\n') + + @mock.patch('swiftclient.service.Connection') + def test_list_account_long(self, connection): + # Test account listing + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container', 'bytes': 0, 'count': 0}]], + [None, []], + ] + + argv = ["", "list", "--lh"] + with CaptureOutput() as output: + swiftclient.shell.main(argv) + calls = [mock.call(marker='', prefix=None, headers={}), + mock.call(marker='container', prefix=None, headers={})] + connection.return_value.get_account.assert_has_calls(calls) + + self.assertEqual(output.out, + ' 0 0 1970-01-01 00:00:01 container\n' + ' 0 0\n') + + # Now test again, this time without returning metadata + connection.return_value.head_container.return_value = {} + + # Test account listing + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container', 'bytes': 0, 'count': 0}]], + [None, []], + ] + + argv = ["", "list", "--lh"] + with CaptureOutput() as output: + swiftclient.shell.main(argv) + calls = [mock.call(marker='', prefix=None, headers={}), + mock.call(marker='container', prefix=None, headers={})] + connection.return_value.get_account.assert_has_calls(calls) + + self.assertEqual(output.out, + ' 0 0 ????-??-?? ??:??:?? container\n' + ' 0 0\n') + + def test_list_account_totals_error(self): + # No --lh provided: expect info message about incorrect --totals use + argv = ["", "list", "--totals"] + + with CaptureOutput() as output: + self.assertRaises(SystemExit, swiftclient.shell.main, argv) + self.assertEqual(output.err, + "Listing totals only works with -l or --lh.\n") + + @mock.patch('swiftclient.service.Connection') + def test_list_account_totals(self, connection): + + # Test account listing, only total count and size + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container1', 'bytes': 1, 'count': 2}, + {'name': 'container2', 'bytes': 2, 'count': 4}]], + [None, []], + ] + + argv = ["", "list", "--lh", "--totals"] + with CaptureOutput() as output: + swiftclient.shell.main(argv) + calls = [mock.call(marker='', prefix=None, headers={})] + connection.return_value.get_account.assert_has_calls(calls) + self.assertEqual(output.out, ' 6 3\n') + + @mock.patch('swiftclient.service.Connection') + def test_list_container(self, connection): + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object_a'}]], + [None, []], + ] + argv = ["", "list", "container"] + with CaptureOutput() as output: + swiftclient.shell.main(argv) + calls = [ + mock.call('container', marker='', + delimiter=None, prefix=None, headers={}), + mock.call('container', marker='object_a', + delimiter=None, prefix=None, headers={})] + connection.return_value.get_container.assert_has_calls(calls) + + self.assertEqual(output.out, 'object_a\n') + + # Test container listing with --long + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object_a', 'bytes': 0, + 'content_type': 'type/content', + 'last_modified': '123T456'}]], + [None, []], + ] + argv = ["", "list", "container", "--long"] + with CaptureOutput() as output: + swiftclient.shell.main(argv) + calls = [ + mock.call('container', marker='', + delimiter=None, prefix=None, headers={}), + mock.call('container', marker='object_a', + delimiter=None, prefix=None, headers={})] + connection.return_value.get_container.assert_has_calls(calls) + + self.assertEqual(output.out, + ' 0 123 456' + ' type/content object_a\n' + ' 0\n') + + @mock.patch('swiftclient.service.Connection') + def test_list_container_with_headers(self, connection): + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object_a'}]], + [None, []], + ] + argv = ["", "list", "container", "-H", "Skip-Middleware: Test"] + with CaptureOutput() as output: + swiftclient.shell.main(argv) + calls = [ + mock.call('container', marker='', + delimiter=None, prefix=None, + headers={'Skip-Middleware': 'Test'}), + mock.call('container', marker='object_a', + delimiter=None, prefix=None, + headers={'Skip-Middleware': 'Test'})] + connection.return_value.get_container.assert_has_calls(calls) + + self.assertEqual(output.out, 'object_a\n') + + @mock.patch('swiftclient.service.makedirs') + @mock.patch('swiftclient.service.Connection') + def test_download(self, connection, makedirs): + objcontent = six.BytesIO(b'objcontent') + connection.return_value.get_object.side_effect = [ + ({'content-type': 'text/plain', + 'etag': '2cbbfe139a744d6abbe695e17f3c1991'}, + objcontent), + ({'content-type': 'text/plain', + 'etag': EMPTY_ETAG}, + '') + ] + + # Test downloading whole container + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}]], + [None, [{'name': 'pseudo/'}]], + [None, []], + ] + connection.return_value.auth_end_time = 0 + connection.return_value.attempts = 0 + + with mock.patch(BUILTIN_OPEN) as mock_open: + argv = ["", "download", "container"] + swiftclient.shell.main(argv) + calls = [mock.call('container', 'object', + headers={}, resp_chunk_size=65536, + response_dict={}), + mock.call('container', 'pseudo/', + headers={}, resp_chunk_size=65536, + response_dict={})] + connection.return_value.get_object.assert_has_calls( + calls, any_order=True) + mock_open.assert_called_once_with('object', 'wb', 65536) + self.assertEqual([mock.call('pseudo')], makedirs.mock_calls) + makedirs.reset_mock() + + # Test downloading single object + objcontent = six.BytesIO(b'objcontent') + connection.return_value.get_object.side_effect = [ + ({'content-type': 'text/plain', + 'etag': '2cbbfe139a744d6abbe695e17f3c1991'}, + objcontent) + ] + with mock.patch(BUILTIN_OPEN) as mock_open: + argv = ["", "download", "container", "object"] + swiftclient.shell.main(argv) + connection.return_value.get_object.assert_called_with( + 'container', 'object', headers={}, resp_chunk_size=65536, + response_dict={}) + mock_open.assert_called_with('object', 'wb', 65536) + self.assertEqual([], makedirs.mock_calls) + + # Test downloading without md5 checks + objcontent = six.BytesIO(b'objcontent') + connection.return_value.get_object.side_effect = [ + ({'content-type': 'text/plain', + 'etag': '2cbbfe139a744d6abbe695e17f3c1991'}, + objcontent) + ] + with mock.patch(BUILTIN_OPEN) as mock_open, mock.patch( + 'swiftclient.service._SwiftReader') as sr: + argv = ["", "download", "container", "object", "--ignore-check"] + swiftclient.shell.main(argv) + connection.return_value.get_object.assert_called_with( + 'container', 'object', headers={}, resp_chunk_size=65536, + response_dict={}) + mock_open.assert_called_with('object', 'wb', 65536) + sr.assert_called_once_with('object', mock.ANY, mock.ANY, False) + self.assertEqual([], makedirs.mock_calls) + + # Test downloading single object to stdout + objcontent = six.BytesIO(b'objcontent') + connection.return_value.get_object.side_effect = [ + ({'content-type': 'text/plain', + 'etag': '2cbbfe139a744d6abbe695e17f3c1991'}, + objcontent) + ] + with CaptureOutput() as output: + argv = ["", "download", "--output", "-", "container", "object"] + swiftclient.shell.main(argv) + self.assertEqual('objcontent', output.out) + + @mock.patch('swiftclient.service.shuffle') + @mock.patch('swiftclient.service.Connection') + def test_download_shuffle(self, connection, mock_shuffle): + # Test that the container and object lists are shuffled + mock_shuffle.side_effect = lambda l: l + connection.return_value.get_object.return_value = [ + {'content-type': 'text/plain', + 'etag': EMPTY_ETAG}, + ''] + + connection.return_value.get_container.side_effect = [ + (None, [{'name': 'object'}]), + (None, [{'name': 'pseudo/'}]), + (None, []), + ] + connection.return_value.auth_end_time = 0 + connection.return_value.attempts = 0 + connection.return_value.get_account.side_effect = [ + (None, [{'name': 'container'}]), + (None, []) + ] + + with mock.patch(BUILTIN_OPEN) as mock_open: + with mock.patch('swiftclient.service.makedirs') as mock_mkdir: + argv = ["", "download", "--all"] + swiftclient.shell.main(argv) + self.assertEqual(3, mock_shuffle.call_count) + mock_shuffle.assert_any_call(['container']) + mock_shuffle.assert_any_call(['object']) + mock_shuffle.assert_any_call(['pseudo/']) + mock_open.assert_called_once_with('container/object', 'wb', 65536) + self.assertEqual([ + mock.call('container'), + mock.call('container/pseudo'), + ], mock_mkdir.mock_calls) + + # Test that the container and object lists are not shuffled + mock_shuffle.reset_mock() + + connection.return_value.get_container.side_effect = [ + (None, [{'name': 'object'}]), + (None, [{'name': 'pseudo/'}]), + (None, []), + ] + connection.return_value.get_account.side_effect = [ + (None, [{'name': 'container'}]), + (None, []) + ] + + with mock.patch(BUILTIN_OPEN) as mock_open: + with mock.patch('swiftclient.service.makedirs') as mock_mkdir: + argv = ["", "download", "--all", "--no-shuffle"] + swiftclient.shell.main(argv) + self.assertEqual(0, mock_shuffle.call_count) + mock_open.assert_called_once_with('container/object', 'wb', 65536) + self.assertEqual([ + mock.call('container'), + mock.call('container/pseudo'), + ], mock_mkdir.mock_calls) + + @mock.patch('swiftclient.service.Connection') + def test_download_no_content_type(self, connection): + connection.return_value.get_object.return_value = [ + {'etag': EMPTY_ETAG}, + ''] + + # Test downloading whole container + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}]], + [None, [{'name': 'pseudo/'}]], + [None, []], + ] + connection.return_value.auth_end_time = 0 + connection.return_value.attempts = 0 + + with mock.patch(BUILTIN_OPEN) as mock_open: + with mock.patch('swiftclient.service.makedirs') as mock_mkdir: + argv = ["", "download", "container"] + swiftclient.shell.main(argv) + calls = [mock.call('container', 'object', + headers={}, resp_chunk_size=65536, + response_dict={}), + mock.call('container', 'pseudo/', + headers={}, resp_chunk_size=65536, + response_dict={})] + connection.return_value.get_object.assert_has_calls( + calls, any_order=True) + mock_open.assert_called_once_with('object', 'wb', 65536) + self.assertEqual([ + mock.call('pseudo'), + ], mock_mkdir.mock_calls) + + @mock.patch('swiftclient.shell.walk') + @mock.patch('swiftclient.service.Connection') + def test_upload(self, connection, walk): + connection.return_value.head_object.return_value = { + 'content-length': '0'} + connection.return_value.put_object.return_value = EMPTY_ETAG + connection.return_value.attempts = 0 + argv = ["", "upload", "container", self.tmpfile, + "-H", "X-Storage-Policy:one", + "--meta", "Color:Blue"] + swiftclient.shell.main(argv) + connection.return_value.put_container.assert_called_once_with( + 'container', + {'X-Storage-Policy': 'one'}, + response_dict={}) + + connection.return_value.put_object.assert_called_with( + 'container', + self.tmpfile.lstrip('/'), + mock.ANY, + content_length=0, + headers={'x-object-meta-mtime': mock.ANY, + 'X-Storage-Policy': 'one', + 'X-Object-Meta-Color': 'Blue'}, + response_dict={}) + + # upload to pseudo-folder (via <container> param) + argv = ["", "upload", "container/pseudo-folder/nested", self.tmpfile, + "-H", "X-Storage-Policy:one"] + swiftclient.shell.main(argv) + connection.return_value.put_container.assert_called_with( + 'container', + {'X-Storage-Policy': 'one'}, + response_dict={}) + + connection.return_value.put_object.assert_called_with( + 'container', + 'pseudo-folder/nested' + self.tmpfile, + mock.ANY, + content_length=0, + headers={'x-object-meta-mtime': mock.ANY, + 'X-Storage-Policy': 'one'}, + response_dict={}) + + # Upload whole directory + argv = ["", "upload", "container", "/tmp"] + _tmpfile = self.tmpfile + _tmpfile_dir = dirname(_tmpfile) + _tmpfile_base = basename(_tmpfile) + walk.return_value = [(_tmpfile_dir, [], [_tmpfile_base])] + swiftclient.shell.main(argv) + connection.return_value.put_object.assert_called_with( + 'container', + self.tmpfile.lstrip('/'), + mock.ANY, + content_length=0, + headers={'x-object-meta-mtime': mock.ANY}, + response_dict={}) + + # Upload in segments + connection.return_value.head_container.return_value = { + 'x-storage-policy': 'one'} + argv = ["", "upload", "container", self.tmpfile, "-S", "10"] + with open(self.tmpfile, "wb") as fh: + fh.write(b'12345678901234567890') + swiftclient.shell.main(argv) + expected_calls = [mock.call('container', + {'X-Storage-Policy': mock.ANY}, + response_dict={}), + mock.call('container_segments', + {'X-Storage-Policy': mock.ANY}, + response_dict={})] + connection.return_value.put_container.has_calls(expected_calls) + 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={}) + + # upload in segments to pseudo-folder (via <container> param) + connection.reset_mock() + connection.return_value.head_container.return_value = { + 'x-storage-policy': 'one'} + argv = ["", "upload", "container/pseudo-folder/nested", + self.tmpfile, "-S", "10", "--use-slo"] + with open(self.tmpfile, "wb") as fh: + fh.write(b'12345678901234567890') + swiftclient.shell.main(argv) + expected_calls = [mock.call('container', + {}, + response_dict={}), + mock.call('container_segments', + {'X-Storage-Policy': 'one'}, + response_dict={})] + connection.return_value.put_container.assert_has_calls(expected_calls) + connection.return_value.put_object.assert_called_with( + 'container', + 'pseudo-folder/nested' + self.tmpfile, + mock.ANY, + headers={ + 'x-object-meta-mtime': mock.ANY, + }, + query_string='multipart-manifest=put', + response_dict=mock.ANY) + + @mock.patch('swiftclient.service.SwiftService.upload') + def test_upload_object_with_account_readonly(self, upload): + argv = ["", "upload", "container", self.tmpfile] + upload.return_value = [ + {"success": False, + "headers": {}, + "container": 'container', + "action": 'create_container', + "error": swiftclient.ClientException( + 'Container PUT failed', + http_status=403, + http_reason='Forbidden', + http_response_content=b'<html><h1>Forbidden</h1>') + }] + + with CaptureOutput() as output: + swiftclient.shell.main(argv) + self.assertTrue(output.err != '') + warning_msg = "Warning: failed to create container 'container': " \ + "403 Forbidden" + self.assertTrue(output.err.startswith(warning_msg)) + + @mock.patch('swiftclient.service.Connection') + def test_upload_delete_slo_segments(self, connection): + # Upload delete existing segments + connection.return_value.head_container.return_value = { + 'x-storage-policy': 'one'} + connection.return_value.attempts = 0 + argv = ["", "upload", "container", self.tmpfile] + connection.return_value.head_object.side_effect = [ + {'x-static-large-object': 'true', # For the upload call + 'content-length': '2'}, + {'x-static-large-object': 'false', # For the 1st delete call + 'content-length': '2'}, + {'x-static-large-object': 'false', # For the 2nd delete call + 'content-length': '2'} + ] + connection.return_value.get_object.return_value = ( + {}, + b'[{"name": "container1/old_seg1"},' + b' {"name": "container2/old_seg2"}]' + ) + connection.return_value.put_object.return_value = EMPTY_ETAG + # create the delete_object child mock here in attempt to fix + # https://bugs.launchpad.net/python-swiftclient/+bug/1480223 + connection.return_value.delete_object.return_value = None + swiftclient.shell.main(argv) + connection.return_value.put_object.assert_called_with( + 'container', + self.tmpfile.lstrip('/'), + mock.ANY, + content_length=0, + headers={'x-object-meta-mtime': mock.ANY}, + response_dict={}) + expected_delete_calls = [ + mock.call( + 'container1', 'old_seg1', + response_dict={} + ), + mock.call( + 'container2', 'old_seg2', + response_dict={} + ) + ] + self.assertEqual( + sorted(expected_delete_calls), + sorted(connection.return_value.delete_object.mock_calls) + ) + + @mock.patch('swiftclient.service.Connection') + def test_upload_over_symlink_to_slo(self, connection): + # Upload delete existing segments + connection.return_value.head_container.return_value = { + 'x-storage-policy': 'one'} + connection.return_value.attempts = 0 + connection.return_value.head_object.side_effect = [ + {'x-static-large-object': 'true', + 'content-location': '/v1/a/c/manifest', + 'content-length': '2'}, + ] + connection.return_value.get_object.return_value = ( + {'content-location': '/v1/a/c/manifest'}, + b'[{"name": "container1/old_seg1"},' + b' {"name": "container2/old_seg2"}]' + ) + connection.return_value.put_object.return_value = EMPTY_ETAG + connection.return_value.delete_object.return_value = None + argv = ["", "upload", "container", self.tmpfile] + swiftclient.shell.main(argv) + connection.return_value.put_object.assert_called_with( + 'container', + self.tmpfile.lstrip('/'), + mock.ANY, + content_length=0, + headers={'x-object-meta-mtime': mock.ANY}, + response_dict={}) + self.assertEqual([], connection.return_value.delete_object.mock_calls) + + @mock.patch('swiftclient.service.Connection') + def test_upload_leave_slo_segments(self, connection): + # Test upload overwriting a manifest respects --leave-segments + connection.return_value.head_container.return_value = { + 'x-storage-policy': 'one'} + connection.return_value.attempts = 0 + argv = ["", "upload", "container", self.tmpfile, "--leave-segments"] + connection.return_value.head_object.side_effect = [ + {'x-static-large-object': 'true', # For the upload call + 'content-length': '2'}] + connection.return_value.put_object.return_value = ( + 'd41d8cd98f00b204e9800998ecf8427e') + swiftclient.shell.main(argv) + connection.return_value.put_object.assert_called_with( + 'container', + self.tmpfile.lstrip('/'), + mock.ANY, + content_length=0, + headers={'x-object-meta-mtime': mock.ANY}, + response_dict={}) + self.assertFalse(connection.return_value.delete_object.mock_calls) + + @mock.patch('swiftclient.service.Connection') + def test_reupload_leaves_slo_segments(self, connection): + with open(self.tmpfile, "wb") as fh: + fh.write(b'12345678901234567890') + mtime = '{:.6f}'.format(os.path.getmtime(self.tmpfile)) + expected_segments = [ + 'container_segments/{}/slo/{}/20/10/{:08d}'.format( + self.tmpfile[1:], mtime, i) + for i in range(2) + ] + + # Test re-upload overwriting a manifest doesn't remove + # segments it just wrote + connection.return_value.head_container.return_value = { + 'x-storage-policy': 'one'} + connection.return_value.attempts = 0 + argv = ["", "upload", "container", self.tmpfile, + "--use-slo", "-S", "10"] + connection.return_value.head_object.side_effect = [ + {'x-static-large-object': 'true', # For the upload call + 'content-length': '20'}] + connection.return_value.get_object.return_value = ( + {}, + # we've already *got* the expected manifest! + json.dumps([ + {'name': seg} for seg in expected_segments + ]).encode('ascii') + ) + connection.return_value.put_object.return_value = ( + 'd41d8cd98f00b204e9800998ecf8427e') + swiftclient.shell.main(argv) + connection.return_value.put_object.assert_called_with( + 'container', + self.tmpfile[1:], # drop leading / + mock.ANY, + headers={'x-object-meta-mtime': mtime}, + query_string='multipart-manifest=put', + response_dict={}) + self.assertFalse(connection.return_value.delete_object.mock_calls) + + @mock.patch('swiftclient.service.Connection') + def test_upload_delete_dlo_segments(self, connection): + # Upload delete existing segments + connection.return_value.head_container.return_value = { + 'x-storage-policy': 'one'} + connection.return_value.attempts = 0 + argv = ["", "upload", "container", self.tmpfile] + connection.return_value.head_object.side_effect = [ + {'x-object-manifest': 'container1/prefix', + 'content-length': '0'}, + {}, + {} + ] + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'prefix_a', 'bytes': 0, + 'last_modified': '123T456'}]], + # Have multiple pages worth of DLO segments + [None, [{'name': 'prefix_b', 'bytes': 0, + 'last_modified': '123T456'}]], + [None, []] + ] + connection.return_value.put_object.return_value = EMPTY_ETAG + # create the delete_object child mock here in attempt to fix + # https://bugs.launchpad.net/python-swiftclient/+bug/1480223 + connection.return_value.delete_object.return_value = None + swiftclient.shell.main(argv) + connection.return_value.put_object.assert_called_with( + 'container', + self.tmpfile.lstrip('/'), + mock.ANY, + content_length=0, + headers={'x-object-meta-mtime': mock.ANY}, + response_dict={}) + expected_delete_calls = [ + mock.call( + 'container1', 'prefix_a', + response_dict={} + ), + mock.call( + 'container1', 'prefix_b', + response_dict={} + ) + ] + self.assertEqual( + sorted(expected_delete_calls), + sorted(connection.return_value.delete_object.mock_calls) + ) + + @mock.patch('swiftclient.service.Connection') + def test_upload_leave_dlo_segments(self, connection): + # Upload delete existing segments + connection.return_value.head_container.return_value = { + 'x-storage-policy': 'one'} + connection.return_value.attempts = 0 + argv = ["", "upload", "container", self.tmpfile, "--leave-segments"] + connection.return_value.head_object.side_effect = [ + {'x-object-manifest': 'container1/prefix', + 'content-length': '0'}] + connection.return_value.put_object.return_value = ( + 'd41d8cd98f00b204e9800998ecf8427e') + swiftclient.shell.main(argv) + connection.return_value.put_object.assert_called_with( + 'container', + self.tmpfile.lstrip('/'), + mock.ANY, + content_length=0, + headers={'x-object-meta-mtime': mock.ANY}, + response_dict={}) + self.assertFalse(connection.return_value.delete_object.mock_calls) + + @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 + connection.return_value.put_object.return_value = EMPTY_ETAG + 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.shell.io.open') + @mock.patch('swiftclient.service.SwiftService.upload') + def test_upload_from_stdin(self, upload_mock, io_open_mock): + def fake_open(fd, mode): + mock_io = mock.Mock() + mock_io.fileno.return_value = fd + return mock_io + + io_open_mock.side_effect = fake_open + + argv = ["", "upload", "container", "-", "--object-name", "foo"] + swiftclient.shell.main(argv) + upload_mock.assert_called_once_with("container", mock.ANY) + # This is a little convoluted: we want to examine the first call ([0]), + # the argv list([1]), the second parameter ([1]), and the first + # element. This is because the upload method takes a container and a + # list of SwiftUploadObjects. + swift_upload_obj = upload_mock.mock_calls[0][1][1][0] + self.assertEqual(sys.stdin.fileno(), swift_upload_obj.source.fileno()) + io_open_mock.assert_called_once_with(sys.stdin.fileno(), mode='rb') + + @mock.patch('swiftclient.service.SwiftService.upload') + def test_upload_from_stdin_no_name(self, upload_mock): + argv = ["", "upload", "container", "-"] + with CaptureOutput() as out: + self.assertRaises(SystemExit, swiftclient.shell.main, argv) + self.assertEqual(0, len(upload_mock.mock_calls)) + self.assertTrue(out.err.find('object-name must be specified') >= 0) + + @mock.patch('swiftclient.service.SwiftService.upload') + def test_upload_from_stdin_and_others(self, upload_mock): + argv = ["", "upload", "container", "-", "foo", "--object-name", "bar"] + with CaptureOutput() as out: + self.assertRaises(SystemExit, swiftclient.shell.main, argv) + self.assertEqual(0, len(upload_mock.mock_calls)) + self.assertTrue(out.err.find( + 'upload from stdin cannot be used') >= 0) + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 0) + @mock.patch('swiftclient.service.Connection') + def test_delete_bad_threads(self, mock_connection): + mock_connection.return_value.get_container.return_value = (None, []) + mock_connection.return_value.attempts = 0 + + def check_bad(argv): + args, env = _make_cmd( + 'delete', {}, {}, cmd_args=['cont'] + argv) + with mock.patch.dict(os.environ, env): + with CaptureOutput() as output: + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertIn( + 'ERROR: option %s should be a positive integer.' % argv[0], + output.err) + + def check_good(argv): + args, env = _make_cmd( + 'delete', {}, {}, cmd_args=['cont'] + argv) + with mock.patch.dict(os.environ, env): + with CaptureOutput() as output: + swiftclient.shell.main(args) + self.assertEqual('', output.err) + check_bad(["--object-threads", "-1"]) + check_bad(["--object-threads", "0"]) + check_bad(["--container-threads", "-1"]) + check_bad(["--container-threads", "0"]) + check_good(["--object-threads", "1"]) + check_good(["--container-threads", "1"]) + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 1) + @mock.patch('swiftclient.service.Connection') + def test_delete_account(self, connection): + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container'}, {'name': 'container2'}]], + [None, [{'name': 'empty_container'}]], + [None, []], + ] + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}]], + [None, []], + [None, [{'name': 'object'}]], + [None, []], + [None, []], + ] + connection.return_value.attempts = 0 + argv = ["", "delete", "--all"] + connection.return_value.head_object.return_value = {} + connection.return_value.delete_object.return_value = None + swiftclient.shell.main(argv) + connection.return_value.delete_object.assert_has_calls([ + mock.call('container', 'object', query_string=None, + response_dict={}, headers={}), + mock.call('container', 'obj\xe9ct2', query_string=None, + response_dict={}, headers={}), + mock.call('container2', 'object', query_string=None, + response_dict={}, headers={})], any_order=True) + self.assertEqual(3, connection.return_value.delete_object.call_count, + 'Expected 3 calls but found\n%r' + % connection.return_value.delete_object.mock_calls) + self.assertEqual( + connection.return_value.delete_container.mock_calls, [ + mock.call('container', response_dict={}, headers={}), + mock.call('container2', response_dict={}, headers={}), + mock.call('empty_container', response_dict={}, headers={})]) + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 10) + @mock.patch('swiftclient.service.Connection') + def test_delete_bulk_account(self, connection): + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container'}, {'name': 'container2'}]], + [None, [{'name': 'empty_container'}]], + [None, []], + ] + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}, + {'name': 'object3'}]], + [None, []], + [None, [{'name': 'object'}]], + [None, []], + [None, []], + ] + connection.return_value.attempts = 0 + argv = ["", "delete", "--all", "--object-threads", "2"] + connection.return_value.post_account.return_value = {}, ( + b'{"Number Not Found": 0, "Response Status": "200 OK", ' + b'"Errors": [], "Number Deleted": 1, "Response Body": ""}') + swiftclient.shell.main(argv) + self.assertEqual( + 3, len(connection.return_value.post_account.mock_calls), + 'Expected 3 calls but found\n%r' + % connection.return_value.post_account.mock_calls) + # POSTs for same container are made in parallel so expect any order + for expected in [ + mock.call(query_string='bulk-delete', + data=b'/container/object\n/container/obj%C3%A9ct2\n', + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={}), + mock.call(query_string='bulk-delete', + data=b'/container/object3\n', + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={})]: + self.assertIn(expected, + connection.return_value.post_account.mock_calls[:2]) + # POSTs for different containers are made sequentially so expect order + self.assertEqual( + mock.call(query_string='bulk-delete', + data=b'/container2/object\n', + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={}), + connection.return_value.post_account.mock_calls[2]) + self.assertEqual( + connection.return_value.delete_container.mock_calls, [ + mock.call('container', response_dict={}, headers={}), + mock.call('container2', response_dict={}, headers={}), + mock.call('empty_container', response_dict={}, headers={})]) + + @mock.patch('swiftclient.service.Connection') + def test_delete_bulk_account_with_capabilities(self, connection): + connection.return_value.get_capabilities.return_value = { + 'bulk_delete': { + 'max_deletes_per_request': 10000, + 'max_failed_deletes': 1000, + }, + } + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container'}]], + [None, [{'name': 'container2'}]], + [None, [{'name': 'empty_container'}]], + [None, []], + ] + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}, + {'name': 'z_object'}, {'name': 'z_obj\xe9ct2'}]], + [None, []], + [None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}, + {'name': 'z_object'}, {'name': 'z_obj\xe9ct2'}]], + [None, []], + [None, []], + ] + connection.return_value.attempts = 0 + argv = ["", "delete", "--all", "--object-threads", "1"] + connection.return_value.post_account.return_value = {}, ( + b'{"Number Not Found": 0, "Response Status": "200 OK", ' + b'"Errors": [], "Number Deleted": 1, "Response Body": ""}') + swiftclient.shell.main(argv) + self.assertEqual( + connection.return_value.post_account.mock_calls, [ + mock.call(query_string='bulk-delete', + data=b''.join([ + b'/container/object\n', + b'/container/obj%C3%A9ct2\n', + b'/container/z_object\n', + b'/container/z_obj%C3%A9ct2\n' + ]), + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={}), + mock.call(query_string='bulk-delete', + data=b''.join([ + b'/container2/object\n', + b'/container2/obj%C3%A9ct2\n', + b'/container2/z_object\n', + b'/container2/z_obj%C3%A9ct2\n' + ]), + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={})]) + self.assertEqual( + connection.return_value.delete_container.mock_calls, [ + mock.call('container', response_dict={}, headers={}), + mock.call('container2', response_dict={}, headers={}), + mock.call('empty_container', response_dict={}, headers={})]) + self.assertEqual(connection.return_value.get_capabilities.mock_calls, + [mock.call(None)]) # only one /info request + + @mock.patch('swiftclient.service.Connection') + def test_delete_bulk_account_with_capabilities_and_pages(self, connection): + connection.return_value.get_capabilities.return_value = { + 'bulk_delete': { + 'max_deletes_per_request': 2, + 'max_failed_deletes': 1000, + }, + } + connection.return_value.get_account.side_effect = [ + [None, [{'name': 'container'}]], + [None, [{'name': 'container2'}]], + [None, [{'name': 'empty_container'}]], + [None, []], + ] + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}, + {'name': 'z_object'}, {'name': 'z_obj\xe9ct2'}]], + [None, []], + [None, [{'name': 'object'}, {'name': 'obj\xe9ct2'}, + {'name': 'z_object'}, {'name': 'z_obj\xe9ct2'}]], + [None, []], + [None, []], + ] + connection.return_value.attempts = 0 + argv = ["", "delete", "--all", "--object-threads", "1"] + connection.return_value.post_account.return_value = {}, ( + b'{"Number Not Found": 0, "Response Status": "200 OK", ' + b'"Errors": [], "Number Deleted": 1, "Response Body": ""}') + swiftclient.shell.main(argv) + # check that each bulk call was only called with 2 objects + self.assertEqual( + connection.return_value.post_account.mock_calls, [ + mock.call(query_string='bulk-delete', + data=b''.join([ + b'/container/object\n', + b'/container/obj%C3%A9ct2\n', + ]), + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={}), + mock.call(query_string='bulk-delete', + data=b''.join([ + b'/container/z_object\n', + b'/container/z_obj%C3%A9ct2\n' + ]), + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={}), + mock.call(query_string='bulk-delete', + data=b''.join([ + b'/container2/object\n', + b'/container2/obj%C3%A9ct2\n', + ]), + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={}), + mock.call(query_string='bulk-delete', + data=b''.join([ + b'/container2/z_object\n', + b'/container2/z_obj%C3%A9ct2\n' + ]), + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={})]) + self.assertEqual( + connection.return_value.delete_container.mock_calls, [ + mock.call('container', response_dict={}, headers={}), + mock.call('container2', response_dict={}, headers={}), + mock.call('empty_container', response_dict={}, headers={})]) + self.assertEqual(connection.return_value.get_capabilities.mock_calls, + [mock.call(None)]) # only one /info request + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 1) + @mock.patch('swiftclient.service.Connection') + def test_delete_container(self, connection): + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}]], + [None, []], + ] + connection.return_value.attempts = 0 + argv = ["", "delete", "container"] + connection.return_value.head_object.return_value = {} + swiftclient.shell.main(argv) + connection.return_value.delete_container.assert_called_with( + 'container', response_dict={}, headers={}) + connection.return_value.delete_object.assert_called_with( + 'container', 'object', query_string=None, response_dict={}, + headers={}) + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 1) + @mock.patch('swiftclient.service.Connection') + def test_delete_container_headers(self, connection): + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}]], + [None, []], + ] + connection.return_value.attempts = 0 + argv = ["", "delete", "container", "-H", "Skip-Middleware: Test"] + connection.return_value.head_object.return_value = {} + swiftclient.shell.main(argv) + connection.return_value.delete_container.assert_called_with( + 'container', response_dict={}, + headers={'Skip-Middleware': 'Test'}) + connection.return_value.delete_object.assert_called_with( + 'container', 'object', query_string=None, response_dict={}, + headers={'Skip-Middleware': 'Test'}) + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 10) + @mock.patch('swiftclient.service.Connection') + def test_delete_bulk_container(self, connection): + connection.return_value.get_container.side_effect = [ + [None, [{'name': 'object'}]], + [None, []], + ] + connection.return_value.attempts = 0 + argv = ["", "delete", "container"] + connection.return_value.post_account.return_value = {}, ( + b'{"Number Not Found": 0, "Response Status": "200 OK", ' + b'"Errors": [], "Number Deleted": 1, "Response Body": ""}') + swiftclient.shell.main(argv) + connection.return_value.post_account.assert_called_with( + query_string='bulk-delete', data=b'/container/object\n', + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={}) + connection.return_value.delete_container.assert_called_with( + 'container', response_dict={}, headers={}) + + def test_delete_verbose_output_utf8(self): + container = 't\u00e9st_c' + base_argv = ['', '--verbose', 'delete'] + + # simulate container having an object with utf-8 code points in name, + # just returning the object delete result + res = {'success': True, 'response_dict': {}, 'attempts': 2, + 'container': container, 'action': 'delete_object', + 'object': 'obj_t\u00east_o'} + + with mock.patch('swiftclient.shell.SwiftService.delete') as mock_func: + with CaptureOutput() as out: + mock_func.return_value = [res] + swiftclient.shell.main(base_argv + [container.encode('utf-8')]) + + mock_func.assert_called_once_with(container=container) + self.assertTrue(out.out.find( + 'obj_t\u00east_o [after 2 attempts]') >= 0, out) + + # simulate empty container + res = {'success': True, 'response_dict': {}, 'attempts': 2, + 'container': container, 'action': 'delete_container'} + + with mock.patch('swiftclient.shell.SwiftService.delete') as mock_func: + with CaptureOutput() as out: + mock_func.return_value = [res] + swiftclient.shell.main(base_argv + [container.encode('utf-8')]) + + mock_func.assert_called_once_with(container=container) + self.assertTrue(out.out.find( + 't\u00e9st_c [after 2 attempts]') >= 0, out) + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 1) + @mock.patch('swiftclient.service.Connection') + def test_delete_per_object(self, connection): + argv = ["", "delete", "container", "object"] + connection.return_value.head_object.return_value = {} + connection.return_value.attempts = 0 + swiftclient.shell.main(argv) + connection.return_value.delete_object.assert_called_with( + 'container', 'object', query_string=None, response_dict={}, + headers={}) + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 10) + @mock.patch('swiftclient.service.Connection') + def test_delete_bulk_object(self, connection): + argv = ["", "delete", "container", "object"] + connection.return_value.post_account.return_value = {}, ( + b'{"Number Not Found": 0, "Response Status": "200 OK", ' + b'"Errors": [], "Number Deleted": 1, "Response Body": ""}') + connection.return_value.attempts = 0 + swiftclient.shell.main(argv) + connection.return_value.post_account.assert_called_with( + query_string='bulk-delete', data=b'/container/object\n', + headers={'Content-Type': 'text/plain', + 'Accept': 'application/json'}, + response_dict={}) + + def test_delete_verbose_output(self): + del_obj_res = {'success': True, 'response_dict': {}, 'attempts': 2, + 'container': 't\xe9st_c', 'action': 'delete_object', + 'object': 't\xe9st_o'} + + del_seg_res = del_obj_res.copy() + del_seg_res.update({'action': 'delete_segment'}) + + del_con_res = del_obj_res.copy() + del_con_res.update({'action': 'delete_container', 'object': None}) + + test_exc = Exception('t\xe9st_exc') + error_res = del_obj_res.copy() + error_res.update({'success': False, 'error': test_exc, 'object': None}) + + mock_delete = mock.Mock() + base_argv = ['', '--verbose', 'delete'] + + with mock.patch('swiftclient.shell.SwiftService.delete', mock_delete): + with CaptureOutput() as out: + mock_delete.return_value = [del_obj_res] + swiftclient.shell.main(base_argv + ['t\xe9st_c', 't\xe9st_o']) + + mock_delete.assert_called_once_with(container='t\xe9st_c', + objects=['t\xe9st_o']) + self.assertTrue(out.out.find( + 't\xe9st_o [after 2 attempts]') >= 0) + + with CaptureOutput() as out: + mock_delete.return_value = [del_seg_res] + swiftclient.shell.main(base_argv + ['t\xe9st_c', 't\xe9st_o']) + + mock_delete.assert_called_with(container='t\xe9st_c', + objects=['t\xe9st_o']) + self.assertTrue(out.out.find( + 't\xe9st_c/t\xe9st_o [after 2 attempts]') >= 0) + + with CaptureOutput() as out: + mock_delete.return_value = [del_con_res] + swiftclient.shell.main(base_argv + ['t\xe9st_c']) + + mock_delete.assert_called_with(container='t\xe9st_c') + self.assertTrue(out.out.find( + 't\xe9st_c [after 2 attempts]') >= 0) + + with CaptureOutput() as out: + mock_delete.return_value = [error_res] + self.assertRaises(SystemExit, + swiftclient.shell.main, + base_argv + ['t\xe9st_c']) + + mock_delete.assert_called_with(container='t\xe9st_c') + self.assertTrue(out.err.find( + 'Error Deleting: t\xe9st_c: t\xe9st_exc') >= 0) + + @mock.patch('swiftclient.service.Connection') + def test_post_account(self, connection): + argv = ["", "post"] + swiftclient.shell.main(argv) + connection.return_value.post_account.assert_called_with( + headers={}, response_dict={}) + + @mock.patch('swiftclient.service.Connection') + def test_post_account_bad_auth(self, connection): + argv = ["", "post"] + connection.return_value.post_account.side_effect = \ + swiftclient.ClientException( + 'bad auth', http_response_headers={'X-Trans-Id': 'trans_id'}) + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertEqual(output.err, + 'bad auth\nFailed Transaction ID: trans_id\n') + + # do it again with a unicode token + connection.return_value.post_account.side_effect = \ + swiftclient.ClientException( + 'bad auth', http_response_headers={ + 'X-Trans-Id': 'non\u2011utf8'}) + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertEqual(output.err, + 'bad auth\n' + 'Failed Transaction ID: non\u2011utf8\n') + + # do it again with a wonky token + connection.return_value.post_account.side_effect = \ + swiftclient.ClientException( + 'bad auth', http_response_headers={ + 'X-Trans-Id': b'non\xffutf8'}) + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertEqual(output.err, + 'bad auth\nFailed Transaction ID: non%FFutf8\n') + + @mock.patch('swiftclient.service.Connection') + def test_post_account_not_found(self, connection): + argv = ["", "post"] + connection.return_value.post_account.side_effect = \ + swiftclient.ClientException('test', http_status=404) + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertEqual(output.err, 'Account not found\n') + + @mock.patch('swiftclient.service.Connection') + def test_post_container(self, connection): + argv = ["", "post", "container"] + swiftclient.shell.main(argv) + connection.return_value.post_container.assert_called_with( + 'container', headers={}, response_dict={}) + + @mock.patch('swiftclient.service.Connection') + def test_post_container_bad_auth(self, connection): + argv = ["", "post", "container"] + connection.return_value.post_container.side_effect = \ + swiftclient.ClientException('bad auth') + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertEqual(output.err, 'bad auth\n') + + @mock.patch('swiftclient.service.Connection') + def test_post_container_not_found_causes_put(self, connection): + argv = ["", "post", "container"] + connection.return_value.post_container.side_effect = \ + swiftclient.ClientException('test', http_status=404) + swiftclient.shell.main(argv) + self.assertEqual('container', + connection.return_value.put_container.call_args[0][0]) + + def test_post_container_with_bad_name(self): + argv = ["", "post", "conta/iner"] + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + self.assertTrue(output.err != '') + self.assertTrue(output.err.startswith('WARNING: / in')) + + @mock.patch('swiftclient.service.Connection') + def test_post_container_with_options(self, connection): + argv = ["", "post", "container", + "--read-acl", "test2:tester2", + "--write-acl", "test3:tester3 test4", + "--sync-to", "othersite", + "--sync-key", "secret", + ] + swiftclient.shell.main(argv) + connection.return_value.post_container.assert_called_with( + 'container', headers={ + 'X-Container-Write': 'test3:tester3 test4', + 'X-Container-Read': 'test2:tester2', + 'X-Container-Sync-Key': 'secret', + 'X-Container-Sync-To': 'othersite'}, response_dict={}) + + @mock.patch('swiftclient.service.Connection') + def test_post_object(self, connection): + argv = ["", "post", "container", "object", + "--meta", "Color:Blue", + "--header", "content-type:text/plain" + ] + swiftclient.shell.main(argv) + connection.return_value.post_object.assert_called_with( + 'container', 'object', headers={ + 'Content-Type': 'text/plain', + 'X-Object-Meta-Color': 'Blue'}, response_dict={}) + + @mock.patch('swiftclient.service.Connection') + def test_post_object_bad_auth(self, connection): + argv = ["", "post", "container", "object"] + connection.return_value.post_object.side_effect = \ + swiftclient.ClientException("bad auth") + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertEqual(output.err, 'bad auth\n') + + def test_post_object_too_many_args(self): + argv = ["", "post", "container", "object", "bad_arg"] + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertTrue(output.err != '') + self.assertTrue(output.err.startswith('Usage')) + + @mock.patch('swiftclient.service.Connection') + def test_copy_object_no_destination(self, connection): + argv = ["", "copy", "container", "object", + "--meta", "Color:Blue", + "--header", "content-type:text/plain" + ] + with CaptureOutput() as output: + swiftclient.shell.main(argv) + connection.return_value.copy_object.assert_called_with( + 'container', 'object', destination=None, fresh_metadata=False, + headers={ + 'Content-Type': 'text/plain', + 'X-Object-Meta-Color': 'Blue'}, response_dict={}) + self.assertEqual(output.out, 'container/object copied to <self>\n') + + @mock.patch('swiftclient.service.Connection') + def test_copy_object(self, connection): + argv = ["", "copy", "container", "object", + "--meta", "Color:Blue", + "--header", "content-type:text/plain", + "--destination", "/c/o" + ] + with CaptureOutput() as output: + swiftclient.shell.main(argv) + connection.return_value.copy_object.assert_called_with( + 'container', 'object', destination="/c/o", + fresh_metadata=False, + headers={ + 'Content-Type': 'text/plain', + 'X-Object-Meta-Color': 'Blue'}, response_dict={}) + self.assertEqual( + output.out, + 'created container c\ncontainer/object copied to /c/o\n' + ) + + @mock.patch('swiftclient.service.Connection') + def test_copy_object_fresh_metadata(self, connection): + argv = ["", "copy", "container", "object", + "--meta", "Color:Blue", "--fresh-metadata", + "--header", "content-type:text/plain", + "--destination", "/c/o" + ] + swiftclient.shell.main(argv) + connection.return_value.copy_object.assert_called_with( + 'container', 'object', destination="/c/o", fresh_metadata=True, + headers={ + 'Content-Type': 'text/plain', + 'X-Object-Meta-Color': 'Blue'}, response_dict={}) + + @mock.patch('swiftclient.service.Connection') + def test_copy_two_objects(self, connection): + argv = ["", "copy", "container", "object", "object2", + "--meta", "Color:Blue"] + connection.return_value.copy_object.return_value = None + swiftclient.shell.main(argv) + calls = [ + mock.call( + 'container', 'object', destination=None, + fresh_metadata=False, headers={'X-Object-Meta-Color': 'Blue'}, + response_dict={}), + mock.call( + 'container', 'object2', destination=None, + fresh_metadata=False, headers={'X-Object-Meta-Color': 'Blue'}, + response_dict={}) + ] + connection.return_value.copy_object.assert_has_calls( + calls, any_order=True) + self.assertEqual(len(connection.return_value.copy_object.mock_calls), + len(calls)) + + @mock.patch('swiftclient.service.Connection') + def test_copy_two_objects_destination(self, connection): + argv = ["", "copy", "container", "object", "object2", + "--meta", "Color:Blue", "--destination", "/c"] + connection.return_value.copy_object.return_value = None + swiftclient.shell.main(argv) + calls = [ + mock.call( + 'container', 'object', destination="/c/object", + fresh_metadata=False, headers={'X-Object-Meta-Color': 'Blue'}, + response_dict={}), + mock.call( + 'container', 'object2', destination="/c/object2", + fresh_metadata=False, headers={'X-Object-Meta-Color': 'Blue'}, + response_dict={}) + ] + connection.return_value.copy_object.assert_has_calls( + calls, any_order=True) + self.assertEqual(len(connection.return_value.copy_object.mock_calls), + len(calls)) + + @mock.patch('swiftclient.service.Connection') + def test_copy_two_objects_bad_destination(self, connection): + argv = ["", "copy", "container", "object", "object2", + "--meta", "Color:Blue", "--destination", "/c/o"] + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertEqual( + output.err, + 'Combination of multiple objects and destination ' + 'including object is invalid\n') + + @mock.patch('swiftclient.service.Connection') + def test_copy_object_bad_auth(self, connection): + argv = ["", "copy", "container", "object"] + connection.return_value.copy_object.side_effect = \ + swiftclient.ClientException("bad auth") + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertEqual(output.err, 'bad auth\n') + + def test_copy_object_not_enough_args(self): + argv = ["", "copy", "container"] + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertTrue(output.err != '') + self.assertTrue(output.err.startswith('Usage')) + + def test_copy_bad_container(self): + argv = ["", "copy", "cont/ainer", "object"] + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + swiftclient.shell.main(argv) + + self.assertTrue(output.err != '') + self.assertTrue(output.err.startswith('WARN')) + + @mock.patch('swiftclient.shell.generate_temp_url', return_value='') + def test_temp_url(self, temp_url): + argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/o", + "secret_key"] + swiftclient.shell.main(argv) + temp_url.assert_called_with( + '/v1/AUTH_account/c/o', "60", 'secret_key', 'GET', absolute=False, + iso8601=False, prefix=False, ip_range=None) + + @mock.patch('swiftclient.shell.generate_temp_url', return_value='') + def test_temp_url_prefix_based(self, temp_url): + argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/", + "secret_key", "--prefix-based"] + swiftclient.shell.main(argv) + temp_url.assert_called_with( + '/v1/AUTH_account/c/', "60", 'secret_key', 'GET', absolute=False, + iso8601=False, prefix=True, ip_range=None) + + @mock.patch('swiftclient.shell.generate_temp_url', return_value='') + def test_temp_url_iso8601_in(self, temp_url): + dates = ('1970-01-01T00:01:00Z', '1970-01-01T00:01:00', + '1970-01-01') + for d in dates: + argv = ["", "tempurl", "GET", d, "/v1/AUTH_account/c/", + "secret_key"] + swiftclient.shell.main(argv) + temp_url.assert_called_with( + '/v1/AUTH_account/c/', d, 'secret_key', 'GET', absolute=False, + iso8601=False, prefix=False, ip_range=None) + + @mock.patch('swiftclient.shell.generate_temp_url', return_value='') + def test_temp_url_iso8601_out(self, temp_url): + argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/", + "secret_key", "--iso8601"] + swiftclient.shell.main(argv) + temp_url.assert_called_with( + '/v1/AUTH_account/c/', "60", 'secret_key', 'GET', absolute=False, + iso8601=True, prefix=False, ip_range=None) + + @mock.patch('swiftclient.shell.generate_temp_url', return_value='') + def test_absolute_expiry_temp_url(self, temp_url): + argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/o", + "secret_key", "--absolute"] + swiftclient.shell.main(argv) + temp_url.assert_called_with( + '/v1/AUTH_account/c/o', "60", 'secret_key', 'GET', absolute=True, + iso8601=False, prefix=False, ip_range=None) + + @mock.patch('swiftclient.shell.generate_temp_url', return_value='') + def test_temp_url_with_ip_range(self, temp_url): + argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/o", + "secret_key", "--ip-range", "1.2.3.4"] + swiftclient.shell.main(argv) + temp_url.assert_called_with( + '/v1/AUTH_account/c/o', "60", 'secret_key', 'GET', absolute=False, + iso8601=False, prefix=False, ip_range='1.2.3.4') + + def test_temp_url_output(self): + argv = ["", "tempurl", "GET", "60", "/v1/a/c/o", + "secret_key", "--absolute"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + sig = "63bc77a473a1c2ce956548cacf916f292eb9eac3" + expected = "/v1/a/c/o?temp_url_sig=%s&temp_url_expires=60\n" % sig + self.assertEqual(expected, output.out) + + argv = ["", "tempurl", "GET", "60", "http://saio:8080/v1/a/c/o", + "secret_key", "--absolute"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + expected = "http://saio:8080%s" % expected + self.assertEqual(expected, output.out) + + argv = ["", "tempurl", "GET", "60", "/v1/a/c/", + "secret_key", "--absolute", "--prefix"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + sig = '00008c4be1573ba74fc2ab9bce02e3a93d04b349' + expected = ("/v1/a/c/?temp_url_sig=%s&temp_url_expires=60" + "&temp_url_prefix=\n" % sig) + self.assertEqual(expected, output.out) + + argv = ["", "tempurl", "GET", "60", "/v1/a/c/", + "secret_key", "--absolute", "--prefix", '--iso8601'] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + sig = '00008c4be1573ba74fc2ab9bce02e3a93d04b349' + expires = '1970-01-01T00:01:00Z' + expected = ("/v1/a/c/?temp_url_sig=%s&temp_url_expires=%s" + "&temp_url_prefix=\n" % (sig, expires)) + self.assertEqual(expected, output.out) + + dates = ("1970-01-01T00:01:00Z", + strftime(EXPIRES_ISO8601_FORMAT[:-1], localtime(60))) + for d in dates: + argv = ["", "tempurl", "GET", d, "/v1/a/c/o", + "secret_key"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + sig = "63bc77a473a1c2ce956548cacf916f292eb9eac3" + expected = "/v1/a/c/o?temp_url_sig=%s&temp_url_expires=60\n" % sig + self.assertEqual(expected, output.out) + + ts = str(int( + mktime(strptime('2005-05-01', SHORT_EXPIRES_ISO8601_FORMAT)))) + + argv = ["", "tempurl", "GET", ts, "/v1/a/c/", + "secret_key", "--absolute"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + expected = output.out + + argv = ["", "tempurl", "GET", '2005-05-01', "/v1/a/c/", + "secret_key", "--absolute"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + self.assertEqual(expected, output.out) + + argv = ["", "tempurl", "GET", "60", "/v1/a/c/o", + "secret_key", "--absolute", "--ip-range", "1.2.3.4"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + sig = "6a6ec8efa4be53904ecba8d055d841e24a937c98" + expected = ( + "/v1/a/c/o?temp_url_sig=%s&temp_url_expires=60" + "&temp_url_ip_range=1.2.3.4\n" % sig + ) + self.assertEqual(expected, output.out) + + def test_temp_url_error_output(self): + expected = 'path must be full path to an object e.g. /v1/a/c/o\n' + for bad_path in ('/v1/a/c', 'v1/a/c/o', '/v1/a/c/', '/v1/a//o', + 'http://saio/v1/a/c', 'http://v1/a/c/o'): + argv = ["", "tempurl", "GET", "60", bad_path, + "secret_key", "--absolute"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + self.assertEqual(expected, output.err, + 'Expected %r but got %r for path %r' % + (expected, output.err, bad_path)) + + expected = 'path must at least contain /v1/a/c/\n' + argv = ["", "tempurl", "GET", "60", '/v1/a/c', + "secret_key", "--absolute", '--prefix-based'] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + self.assertEqual(expected, output.err, + 'Expected %r but got %r for path %r' % + (expected, output.err, '/v1/a/c')) + + expected = TIME_ERRMSG + '\n' + for bad_time in ('not_an_int', '-1', '2015-05', '2015-05-01T01:00'): + argv = ["", "tempurl", "GET", bad_time, '/v1/a/c/o', + "secret_key", "--absolute"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + self.assertEqual(expected, output.err, + 'Expected %r but got %r for time %r' % + (expected, output.err, bad_time)) + + @mock.patch('swiftclient.service.Connection') + def test_capabilities(self, connection): + argv = ["", "capabilities"] + connection.return_value.get_capabilities.return_value = {'swift': None} + swiftclient.shell.main(argv) + connection.return_value.get_capabilities.assert_called_with(None) + + @mock.patch('swiftclient.service.Connection') + def test_capabilities_json(self, connection): + capabilities = { + 'slo': {'min_segment_size': 1000000}, + 'some': [{'arbitrary': 'nested'}, {'crazy': 'structure'}], + 'swift': {'version': '2.5.0'}} + + connection.return_value.get_capabilities.return_value = capabilities + argv = ["", "capabilities", "--json"] + with CaptureOutput(suppress_systemexit=True) as output: + swiftclient.shell.main(argv) + expected = json.dumps(capabilities, sort_keys=True, indent=2) + '\n' + self.assertEqual(expected, output.out) + connection.return_value.get_capabilities.assert_called_with(None) + + def test_human_readable_upload_segment_size(self): + def _check_expected(x, expected): + actual = x.call_args_list[-1][1]["options"]["segment_size"] + self.assertEqual(int(actual), expected) + + mock_swift = mock.MagicMock(spec=swiftclient.shell.SwiftService) + with mock.patch("swiftclient.shell.SwiftService", mock_swift): + with CaptureOutput(suppress_systemexit=True) as output: + # Test new behaviour with both upper and lower case + # trailing characters + argv = ["", "upload", "-S", "1B", "container", "object"] + swiftclient.shell.main(argv) + _check_expected(mock_swift, 1) + + argv = ["", "upload", "-S", "1K", "container", "object"] + swiftclient.shell.main(argv) + _check_expected(mock_swift, 1024) + + argv = ["", "upload", "-S", "1m", "container", "object"] + swiftclient.shell.main(argv) + _check_expected(mock_swift, 1048576) + + argv = ["", "upload", "-S", "1G", "container", "object"] + swiftclient.shell.main(argv) + _check_expected(mock_swift, 1073741824) + + # Test old behaviour is not affected + argv = ["", "upload", "-S", "12345", "container", "object"] + swiftclient.shell.main(argv) + _check_expected(mock_swift, 12345) + + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + # Test invalid states + argv = ["", "upload", "-S", "1234X", "container", "object"] + swiftclient.shell.main(argv) + self.assertEqual(output.err, "Invalid segment size\n") + output.clear() + + with self.assertRaises(SystemExit): + argv = ["", "upload", "-S", "K1234", "container", "object"] + swiftclient.shell.main(argv) + self.assertEqual(output.err, "Invalid segment size\n") + output.clear() + + with self.assertRaises(SystemExit): + argv = ["", "upload", "-S", "K", "container", "object"] + swiftclient.shell.main(argv) + self.assertEqual(output.err, "Invalid segment size\n") + + def test_negative_upload_segment_size(self): + with CaptureOutput() as output: + with self.assertRaises(SystemExit): + argv = ["", "upload", "-S", "-40", "container", "object"] + swiftclient.shell.main(argv) + self.assertEqual(output.err, "segment-size should be positive\n") + output.clear() + with self.assertRaises(SystemExit): + argv = ["", "upload", "-S=-40K", "container", "object"] + swiftclient.shell.main(argv) + self.assertEqual(output.err, "segment-size should be positive\n") + output.clear() + with self.assertRaises(SystemExit): + argv = ["", "upload", "-S=-40M", "container", "object"] + swiftclient.shell.main(argv) + self.assertEqual(output.err, "segment-size should be positive\n") + output.clear() + with self.assertRaises(SystemExit): + argv = ["", "upload", "-S=-40G", "container", "object"] + swiftclient.shell.main(argv) + self.assertEqual(output.err, "segment-size should be positive\n") + output.clear() + + +class TestSubcommandHelp(unittest.TestCase): + + def test_subcommand_help(self): + for command in swiftclient.shell.commands: + help_var = 'st_%s_help' % command + options_var = 'st_%s_options' % command + self.assertTrue(hasattr(swiftclient.shell, help_var)) + with CaptureOutput() as out: + argv = ['', command, '--help'] + self.assertRaises(SystemExit, swiftclient.shell.main, argv) + expected = 'Usage: swift %s %s\n%s' % ( + command, vars(swiftclient.shell).get(options_var, "\n"), + vars(swiftclient.shell)[help_var]) + self.assertEqual(out.strip('\n'), expected) + + def test_no_help(self): + with CaptureOutput() as out: + argv = ['', 'bad_command', '--help'] + self.assertRaises(SystemExit, swiftclient.shell.main, argv) + expected = 'no such command: bad_command' + self.assertEqual(out.strip('\n'), expected) + + +@mock.patch.dict(os.environ, mocked_os_environ) +class TestDebugAndInfoOptions(unittest.TestCase): + @mock.patch('logging.basicConfig') + @mock.patch('swiftclient.service.Connection') + def test_option_after_posarg(self, connection, mock_logging): + argv = ["", "stat", "--info"] + swiftclient.shell.main(argv) + mock_logging.assert_called_with(level=logging.INFO) + + argv = ["", "stat", "--debug"] + swiftclient.shell.main(argv) + mock_logging.assert_called_with(level=logging.DEBUG) + + @mock.patch('logging.basicConfig') + @mock.patch('swiftclient.service.Connection') + def test_debug_trumps_info(self, connection, mock_logging): + argv_scenarios = (["", "stat", "--info", "--debug"], + ["", "stat", "--debug", "--info"], + ["", "--info", "stat", "--debug"], + ["", "--debug", "stat", "--info"], + ["", "--info", "--debug", "stat"], + ["", "--debug", "--info", "stat"]) + for argv in argv_scenarios: + mock_logging.reset_mock() + swiftclient.shell.main(argv) + try: + mock_logging.assert_called_once_with(level=logging.DEBUG) + except AssertionError: + self.fail('Unexpected call(s) %r for args %r' + % (mock_logging.call_args_list, argv)) + + +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: + if (k in ('ST_KEY', 'ST_USER', 'ST_AUTH') or + k.startswith('OS_')): + self._environ_vars[k] = os.environ.pop(k) + + 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) + return fake_command + + def _verify_opts(self, actual_opts, expected_opts, expected_os_opts=None, + expected_os_opts_dict=None): + """ + Check parsed options are correct. + + :param expected_opts: v1 style options. + :param expected_os_opts: openstack style options. + :param expected_os_opts_dict: openstack options that should be found in + the os_options dict. + """ + expected_os_opts = expected_os_opts or {} + expected_os_opts_dict = expected_os_opts_dict or {} + # check the expected opts are set + for key, v in expected_opts.items(): + actual = actual_opts.get(key) + self.assertEqual(v, actual, 'Expected %s for key %s, found %s' % + (v, key, actual)) + + for key, v in expected_os_opts.items(): + actual = actual_opts.get("os_" + key) + self.assertEqual(v, actual, 'Expected %s for key %s, found %s' % + (v, key, actual)) + + # check the os_options dict values are set + self.assertIn('os_options', actual_opts) + actual_os_opts_dict = actual_opts['os_options'] + expected_os_opts_keys = ['project_name', 'region_name', + 'tenant_name', + 'user_domain_name', 'endpoint_type', + 'object_storage_url', 'project_domain_id', + 'user_id', 'user_domain_id', 'tenant_id', + 'service_type', 'project_id', 'auth_token', + 'project_domain_name'] + for key in expected_os_opts_keys: + self.assertIn(key, actual_os_opts_dict) + cli_key = key + if key == 'object_storage_url': + # exceptions to the pattern... + cli_key = 'storage_url' + if cli_key in expected_os_opts_dict: + expect = expected_os_opts_dict[cli_key] + else: + expect = None + actual = actual_os_opts_dict[key] + self.assertEqual(expect, actual, 'Expected %s for %s, got %s' + % (expect, key, actual)) + for key in actual_os_opts_dict: + self.assertIn(key, expected_os_opts_keys) + + # check that equivalent keys have equal values + equivalents = [('os_username', 'user'), + ('os_auth_url', 'auth'), + ('os_password', 'key')] + for pair in equivalents: + self.assertEqual(actual_opts.get(pair[0]), + actual_opts.get(pair[1])) + + def test_minimum_required_args_v3(self): + opts = {"auth_version": "3"} + os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3"} + + # username with domain is sufficient in args because keystone will + # assume user is in default domain + args = _make_args("stat", opts, os_opts, '-') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], opts, os_opts, {}) + + # check its ok to have user_id instead of username + os_opts = {"password": "secret", + "auth_url": "http://example.com:5000/v3"} + os_opts_dict = {"user_id": "user_ID"} + all_os_opts = os_opts.copy() + all_os_opts.update(os_opts_dict) + + args = _make_args("stat", opts, all_os_opts, '-') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], opts, os_opts, os_opts_dict) + + # check no user credentials required if token and url supplied + os_opts = {} + os_opts_dict = {"storage_url": "http://example.com:8080/v1", + "auth_token": "0123abcd"} + + args = _make_args("stat", opts, os_opts_dict, '-') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], opts, os_opts, os_opts_dict) + + def test_sloppy_versions(self): + os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3", + "identity-api-version": "3.0"} + + # check os_identity_api_version=3.0 is mapped to auth_version=3 + args = _make_args("stat", {}, os_opts, '-') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, {}): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + expected_opts = {'auth_version': '3'} # NB: not '3.0' + expected_os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3"} + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + # check os_identity_api_version=2 is mapped to auth_version=2.0 + # A somewhat contrived scenario - we need to pass in the v1 style opts + # to prevent auth version defaulting to 2.0 due to lack of v1 style + # options. That way we can actually verify that the sloppy 2 was + # interpreted and mapped to 2.0 + os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v2.0", + "identity-api-version": "2"} + opts = {"key": "secret", + "user": "user", + "auth": "http://example.com:5000/v2.0"} + args = _make_args("stat", opts, os_opts, '-') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, {}): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + expected_opts = {'auth_version': '2.0'} # NB: not '2' + expected_os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v2.0"} + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + def test_os_identity_api_version(self): + os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3", + "identity-api-version": "3"} + + # check os_identity_api_version is sufficient in place of auth_version + args = _make_args("stat", {}, os_opts, '-') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, {}): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + expected_opts = {'auth_version': '3'} + expected_os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3"} + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + # check again using environment variables + args = _make_args("stat", {}, {}) + env = _make_env({}, os_opts) + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, env): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + # check that last of auth-version, os-identity-api-version is preferred + args = _make_args("stat", {}, os_opts, '-') + ['--auth-version', '2.0'] + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, {}): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + expected_opts = {'auth_version': '2.0'} + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + # now put auth_version ahead of os-identity-api-version + args = _make_args("stat", {"auth_version": "2.0"}, os_opts, '-') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, {}): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + expected_opts = {'auth_version': '3'} + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + # check that OS_AUTH_VERSION overrides OS_IDENTITY_API_VERSION + args = _make_args("stat", {}, {}) + env = _make_env({}, os_opts) + env.update({'OS_AUTH_VERSION': '2.0'}) + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, env): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + expected_opts = {'auth_version': '2.0'} + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + # check that ST_AUTH_VERSION overrides OS_IDENTITY_API_VERSION + args = _make_args("stat", {}, {}) + env = _make_env({}, os_opts) + env.update({'ST_AUTH_VERSION': '2.0'}) + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, env): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + # check that ST_AUTH_VERSION overrides OS_AUTH_VERSION + args = _make_args("stat", {}, {}) + env = _make_env({}, os_opts) + env.update({'ST_AUTH_VERSION': '2.0', 'OS_AUTH_VERSION': '3'}) + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, env): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], expected_opts, expected_os_opts, {}) + + def test_args_v3(self): + opts = {"auth_version": "3"} + os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3"} + os_opts_dict = {"user_id": "user_ID", + "project_id": "project_ID", + "tenant_id": "tenant_ID", + "project_domain_id": "project_domain_ID", + "user_domain_id": "user_domain_ID", + "tenant_name": "tenant", + "project_name": "project", + "project_domain_name": "project_domain", + "user_domain_name": "user_domain", + "auth_token": "token", + "storage_url": "http://example.com:8080/v1", + "region_name": "region", + "service_type": "service", + "endpoint_type": "endpoint"} + all_os_opts = os_opts.copy() + all_os_opts.update(os_opts_dict) + + # check using hyphen separator + args = _make_args("stat", opts, all_os_opts, '-') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], opts, os_opts, os_opts_dict) + + # check using underscore separator + args = _make_args("stat", opts, all_os_opts, '_') + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], opts, os_opts, os_opts_dict) + + # check using environment variables + args = _make_args("stat", {}, {}) + env = _make_env(opts, all_os_opts) + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, env): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], opts, os_opts, os_opts_dict) + + # check again using OS_AUTH_VERSION instead of ST_AUTH_VERSION + env = _make_env({}, all_os_opts) + env.update({'OS_AUTH_VERSION': '3'}) + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch.dict(os.environ, env): + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self._verify_opts(result[0], opts, os_opts, os_opts_dict) + + def test_command_args_v3(self): + result = [None, None] + fake_command = self._make_fake_command(result) + opts = {"auth_version": "3"} + os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3"} + args = _make_args("stat", opts, os_opts) + with mock.patch('swiftclient.shell.st_stat', fake_command): + swiftclient.shell.main(args) + self.assertEqual(['stat'], result[1]) + with mock.patch('swiftclient.shell.st_stat', fake_command): + args = args + ["container_name"] + swiftclient.shell.main(args) + self.assertEqual(["stat", "container_name"], result[1]) + + def test_insufficient_args_v3(self): + opts = {"auth_version": "3"} + os_opts = {"password": "secret", + "auth_url": "http://example.com:5000/v3"} + args = _make_args("stat", opts, os_opts) + with self.assertRaises(SystemExit) as cm: + swiftclient.shell.main(args) + self.assertIn( + 'Auth version 3 requires either OS_USERNAME or OS_USER_ID', + str(cm.exception)) + + os_opts = {"username": "user", + "auth_url": "http://example.com:5000/v3"} + args = _make_args("stat", opts, os_opts) + with self.assertRaises(SystemExit) as cm: + swiftclient.shell.main(args) + self.assertIn('Auth version 3 requires OS_PASSWORD', str(cm.exception)) + + os_opts = {"username": "user", + "password": "secret"} + args = _make_args("stat", opts, os_opts) + with self.assertRaises(SystemExit) as cm: + swiftclient.shell.main(args) + self.assertIn('Auth version 3 requires OS_AUTH_URL', str(cm.exception)) + + def test_password_prompt(self): + def do_test(opts, os_opts, auth_version): + args = _make_args("stat", opts, os_opts) + result = [None, None] + fake_command = self._make_fake_command(result) + with mock.patch('swiftclient.shell.st_stat', fake_command): + with mock.patch('getpass.getpass', + return_value='input_pwd') as mock_getpass: + swiftclient.shell.main(args) + mock_getpass.assert_called_once_with() + self.assertEqual('input_pwd', result[0]['key']) + self.assertEqual('input_pwd', result[0]['os_password']) + + # ctrl-D + with self.assertRaises(SystemExit) as cm: + with mock.patch('swiftclient.shell.st_stat', fake_command): + with mock.patch('getpass.getpass', + side_effect=EOFError) as mock_getpass: + swiftclient.shell.main(args) + mock_getpass.assert_called_once_with() + self.assertIn( + 'Auth version %s requires' % auth_version, str(cm.exception)) + + # force getpass to think it needs to use raw input + with self.assertRaises(SystemExit) as cm: + with mock.patch('getpass.getpass', getpass.fallback_getpass): + swiftclient.shell.main(args) + self.assertIn( + 'Input stream incompatible', str(cm.exception)) + + opts = {"prompt": None, "user": "bob", "key": "secret", + "auth": "http://example.com:8080/auth/v1.0"} + do_test(opts, {}, '1.0') + os_opts = {"username": "user", + "password": "secret", + "auth_url": "http://example.com:5000/v3"} + opts = {"auth_version": "2.0", "prompt": None} + do_test(opts, os_opts, '2.0') + opts = {"auth_version": "3", "prompt": None} + do_test(opts, os_opts, '3') + + def test_no_tenant_name_or_id_v2(self): + os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3", + "tenant_name": "", + "tenant_id": ""} + + with CaptureOutput() as output: + args = _make_args("stat", {}, os_opts) + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertEqual(output.err.strip(), 'No tenant specified') + + with CaptureOutput() as output: + args = _make_args("stat", {}, os_opts, cmd_args=["testcontainer"]) + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertEqual(output.err.strip(), 'No tenant specified') + + def test_no_tenant_name_or_id_v3(self): + os_opts = {"password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3", + "tenant_name": "", + "tenant_id": ""} + + with CaptureOutput() as output: + args = _make_args("stat", {"auth_version": "3"}, os_opts) + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertEqual(output.err.strip(), + 'No project name or project id specified.') + + with CaptureOutput() as output: + args = _make_args("stat", {"auth_version": "3"}, + os_opts, cmd_args=["testcontainer"]) + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertEqual(output.err.strip(), + 'No project name or project id specified.') + + def test_insufficient_env_vars_v3(self): + args = _make_args("stat", {}, {}) + opts = {"auth_version": "3"} + os_opts = {"password": "secret", + "auth_url": "http://example.com:5000/v3"} + env = _make_env(opts, os_opts) + with mock.patch.dict(os.environ, env): + self.assertRaises(SystemExit, swiftclient.shell.main, args) + + os_opts = {"username": "user", + "auth_url": "http://example.com:5000/v3"} + env = _make_env(opts, os_opts) + with mock.patch.dict(os.environ, env): + self.assertRaises(SystemExit, swiftclient.shell.main, args) + + os_opts = {"username": "user", + "password": "secret"} + env = _make_env(opts, os_opts) + with mock.patch.dict(os.environ, env): + self.assertRaises(SystemExit, swiftclient.shell.main, args) + + def test_help(self): + # --help returns condensed help message + opts = {"help": None} + os_opts = {} + args = _make_args(None, opts, os_opts) + with CaptureOutput() as out: + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertTrue(out.find('[--key <api_key>]') > 0) + self.assertEqual(-1, out.find('--os-username=<auth-user-name>')) + + # --help returns condensed help message, overrides --os-help + opts = {"help": None} + os_opts = {"help": None} + args = _make_args("", opts, os_opts) + with CaptureOutput() as out: + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertTrue(out.find('[--key <api_key>]') > 0) + self.assertEqual(-1, out.find('--os-username=<auth-user-name>')) + + # --os-password, --os-username and --os-auth_url should be ignored + # because --help overrides it + opts = {"help": None} + os_opts = {"help": None, + "password": "secret", + "username": "user", + "auth_url": "http://example.com:5000/v3"} + args = _make_args("", opts, os_opts) + with CaptureOutput() as out: + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertTrue(out.find('[--key <api_key>]') > 0) + self.assertEqual(-1, out.find('--os-username=<auth-user-name>')) + + # --os-help return os options help + opts = {} + args = _make_args("", opts, os_opts) + with CaptureOutput() as out: + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertTrue(out.find('[--key <api_key>]') > 0) + self.assertTrue(out.find('--os-username=<auth-user-name>') > 0) + + +class TestKeystoneOptions(MockHttpTest): + """ + Tests to check that options are passed from the command line or + environment variables through to the keystone client interface. + """ + all_os_opts = {'password': 'secret', + 'username': 'user', + 'auth-url': 'http://example.com:5000/v3', + 'user-domain-name': 'userdomain', + 'user-id': 'userid', + 'user-domain-id': 'userdomainid', + 'tenant-name': 'tenantname', + 'tenant-id': 'tenantid', + 'project-name': 'projectname', + 'project-id': 'projectid', + 'project-domain-id': 'projectdomainid', + 'project-domain-name': 'projectdomain', + 'cacert': 'foo', + 'cert': 'minnie', + 'key': 'mickey'} + catalog_opts = {'service-type': 'my-object-store', + 'endpoint-type': 'public', + 'region-name': 'my-region'} + flags = ['insecure', 'debug'] + + # options that are given default values in code if missing from CLI + defaults = {'auth-version': '2.0', + 'service-type': 'object-store', + 'endpoint-type': 'publicURL'} + + def _build_os_opts(self, keys): + os_opts = {} + for k in keys: + os_opts[k] = self.all_os_opts.get(k, self.catalog_opts.get(k)) + return os_opts + + def _test_options_passed_to_keystone(self, cmd, opts, os_opts, + flags=None, use_env=False, + cmd_args=None, no_auth=False): + 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) + ks_endpoint = 'http://example.com:8080/v1/AUTH_acc' + ks_token = 'fake_auth_token' + # check correct auth version gets used + key = 'auth-version' + fake_ks = FakeKeystone(endpoint=ks_endpoint, token=ks_token) + if no_auth: + fake_ks2 = fake_ks3 = None + elif opts.get(key, self.defaults.get(key)) == '2.0': + fake_ks2 = fake_ks + fake_ks3 = None + else: + fake_ks2 = None + fake_ks3 = fake_ks + # fake_conn will check that storage_url and auth_token are as expected + endpoint = os_opts.get('storage-url', ks_endpoint) + token = os_opts.get('auth-token', ks_token) + fake_conn = self.fake_http_connection(204, headers={}, + storage_url=endpoint, + auth_token=token) + + with mock.patch('swiftclient.client.ksclient_v2', fake_ks2), \ + mock.patch('swiftclient.client.ksclient_v3', fake_ks3), \ + mock.patch('swiftclient.client.http_connection', fake_conn), \ + mock.patch.dict(os.environ, env, clear=True), \ + patch_disable_warnings() as mock_disable_warnings: + try: + swiftclient.shell.main(args) + except SystemExit as e: + self.fail('Unexpected SystemExit: %s' % e) + except SwiftError as err: + self.fail('Unexpected SwiftError: %s' % err) + + if InsecureRequestWarning is not None: + if 'insecure' in flags: + self.assertEqual([mock.call(InsecureRequestWarning)], + mock_disable_warnings.mock_calls) + else: + self.assertEqual([], mock_disable_warnings.mock_calls) + + if no_auth: + # We patched out both keystoneclient versions to be None; + # they *can't* have been used and if we tried to, we would + # have raised ClientExceptions + return + + # check args passed to keystone Client __init__ + self.assertEqual(len(fake_ks.calls), 1) + actual_args = fake_ks.calls[0] + for key in self.all_os_opts.keys(): + expected = os_opts.get(key, self.defaults.get(key)) + key = key.replace('-', '_') + self.assertIn(key, actual_args) + self.assertEqual(expected, actual_args[key], + 'Expected %s for key %s, found %s' + % (expected, key, actual_args[key])) + for flag in flags: + self.assertIn(flag, actual_args) + self.assertTrue(actual_args[flag]) + + check_attr = True + # check args passed to ServiceCatalog.url_for() method + self.assertEqual(len(fake_ks.client.service_catalog.calls), 1) + actual_args = fake_ks.client.service_catalog.calls[0] + for key in self.catalog_opts.keys(): + expected = os_opts.get(key, self.defaults.get(key)) + key = key.replace('-', '_') + if key == 'region_name': + key = 'filter_value' + if expected is None: + check_attr = False + self.assertNotIn(key, actual_args) + self.assertNotIn('attr', actual_args) + continue + self.assertIn(key, actual_args) + self.assertEqual(expected, actual_args[key], + 'Expected %s for key %s, found %s' + % (expected, key, actual_args[key])) + if check_attr: + key, v = 'attr', 'region' + self.assertIn(key, actual_args) + self.assertEqual(v, actual_args[key], + 'Expected %s for key %s, found %s' + % (v, key, actual_args[key])) + + def _test_options(self, opts, os_opts, flags=None, no_auth=False): + # repeat test for different commands using env and command line options + for cmd in ('stat', 'post'): + self._test_options_passed_to_keystone(cmd, opts, os_opts, + flags=flags, no_auth=no_auth) + self._test_options_passed_to_keystone(cmd, opts, os_opts, + flags=flags, use_env=True, + no_auth=no_auth) + + def test_all_args_passed_to_keystone(self): + # check that all possible command line args are passed to keystone + opts = {'auth-version': '3'} + os_opts = dict(self.all_os_opts) + os_opts.update(self.catalog_opts) + self._test_options(opts, os_opts, flags=self.flags) + + opts = {'auth-version': '2.0'} + self._test_options(opts, os_opts, flags=self.flags) + + opts = {} + self.defaults['auth-version'] = '3' + self._test_options(opts, os_opts, flags=self.flags) + + for o in ('user-domain-name', 'user-domain-id', + 'project-domain-name', 'project-domain-id'): + os_opts.pop(o) + self.defaults['auth-version'] = '2.0' + self._test_options(opts, os_opts, flags=self.flags) + + def test_catalog_options_and_flags_not_required_v3(self): + # check that all possible command line args are passed to keystone + opts = {'auth-version': '3'} + os_opts = dict(self.all_os_opts) + self._test_options(opts, os_opts, flags=None) + + def test_ok_option_combinations_v3(self): + opts = {'auth-version': '3'} + keys = ('username', 'password', 'tenant-name', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('user-id', 'password', 'tenant-name', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('user-id', 'password', 'tenant-id', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('user-id', 'password', 'project-name', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('user-id', 'password', 'project-id', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + def test_ok_option_combinations_v2(self): + opts = {'auth-version': '2.0'} + keys = ('username', 'password', 'tenant-name', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('username', 'password', 'tenant-id', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + # allow auth_version to default to 2.0 + opts = {} + keys = ('username', 'password', 'tenant-name', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('username', 'password', 'tenant-id', 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + # ...except when it should be 3 + self.defaults['auth-version'] = '3' + keys = ('username', 'user-domain-name', 'password', 'project-name', + 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('username', 'user-domain-id', 'password', 'project-name', + 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('username', 'project-domain-name', 'password', 'project-name', + 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + keys = ('username', 'project-domain-id', 'password', 'project-name', + 'auth-url') + os_opts = self._build_os_opts(keys) + self._test_options(opts, os_opts) + + def test_url_and_token_provided_on_command_line(self): + endpoint = 'http://alternate.com:8080/v1/AUTH_another' + token = 'alternate_auth_token' + os_opts = {'auth-token': token, + 'storage-url': endpoint} + opts = {'auth-version': '3'} + self._test_options(opts, os_opts, no_auth=True) + + opts = {'auth-version': '2.0'} + self._test_options(opts, os_opts, no_auth=True) + + def test_url_provided_on_command_line(self): + endpoint = 'http://alternate.com:8080/v1/AUTH_another' + os_opts = {'username': 'username', + 'password': 'password', + 'project-name': 'projectname', + 'auth-url': 'http://example.com:5000/v3', + 'storage-url': endpoint} + opts = {'auth-version': '3'} + self._test_options(opts, os_opts) + + opts = {'auth-version': '2.0'} + self._test_options(opts, os_opts) + + +@mock.patch.dict(os.environ, clean_os_environ) +class TestAuth(MockHttpTest): + + def test_pre_authed_request(self): + url = 'https://swift.storage.example.com/v1/AUTH_test' + token = 'AUTH_tk5b6b12' + + pre_auth_env = { + 'OS_STORAGE_URL': url, + 'OS_AUTH_TOKEN': token, + } + fake_conn = self.fake_http_connection(200) + with mock.patch('swiftclient.client.http_connection', new=fake_conn): + with mock.patch.dict(os.environ, pre_auth_env): + argv = ['', 'stat'] + swiftclient.shell.main(argv) + self.assertRequests([ + ('HEAD', url, '', {'x-auth-token': token}), + ]) + + # and again with re-auth + pre_auth_env.update(mocked_os_environ) + pre_auth_env['OS_AUTH_TOKEN'] = 'expired' + fake_conn = self.fake_http_connection(401, 200, 200, headers={ + 'x-auth-token': token + '_new', + 'x-storage-url': url + '_not_used', + }) + with mock.patch.multiple('swiftclient.client', + http_connection=fake_conn, + sleep=mock.DEFAULT): + with mock.patch.dict(os.environ, pre_auth_env): + argv = ['', 'stat'] + swiftclient.shell.main(argv) + self.assertRequests([ + ('HEAD', url, '', { + 'x-auth-token': 'expired', + }), + ('GET', mocked_os_environ['ST_AUTH'], '', { + 'x-auth-user': mocked_os_environ['ST_USER'], + 'x-auth-key': mocked_os_environ['ST_KEY'], + }), + ('HEAD', url, '', { + 'x-auth-token': token + '_new', + }), + ]) + + def test_os_pre_authed_request(self): + url = 'https://swift.storage.example.com/v1/AUTH_test' + token = 'AUTH_tk5b6b12' + + pre_auth_env = { + 'OS_STORAGE_URL': url, + 'OS_AUTH_TOKEN': token, + } + fake_conn = self.fake_http_connection(200) + with mock.patch('swiftclient.client.http_connection', new=fake_conn): + with mock.patch.dict(os.environ, pre_auth_env): + argv = ['', 'stat'] + swiftclient.shell.main(argv) + self.assertRequests([ + ('HEAD', url, '', {'x-auth-token': token}), + ]) + + # and again with re-auth + os_environ = { + 'OS_AUTH_URL': 'https://keystone.example.com/v2.0/', + 'OS_TENANT_NAME': 'demo', + 'OS_USERNAME': 'demo', + 'OS_PASSWORD': 'admin', + } + os_environ.update(pre_auth_env) + os_environ['OS_AUTH_TOKEN'] = 'expired' + + fake_conn = self.fake_http_connection(401, 200) + fake_keystone = fake_get_auth_keystone(storage_url=url + '_not_used', + token=token + '_new') + with mock.patch.multiple('swiftclient.client', + http_connection=fake_conn, + get_auth_keystone=fake_keystone, + sleep=mock.DEFAULT): + with mock.patch.dict(os.environ, os_environ): + argv = ['', 'stat'] + swiftclient.shell.main(argv) + self.assertRequests([ + ('HEAD', url, '', { + 'x-auth-token': 'expired', + }), + ('HEAD', url, '', { + 'x-auth-token': token + '_new', + }), + ]) + + def test_auth(self): + headers = { + 'x-auth-token': 'AUTH_tk5b6b12', + 'x-storage-url': 'https://swift.storage.example.com/v1/AUTH_test', + } + mock_resp = self.fake_http_connection(200, headers=headers) + with mock.patch('swiftclient.client.http_connection', new=mock_resp): + stdout = six.StringIO() + with mock.patch('sys.stdout', new=stdout): + argv = [ + '', + 'auth', + '--auth', 'https://swift.storage.example.com/auth/v1.0', + '--user', 'test:tester', '--key', 'testing', + ] + swiftclient.shell.main(argv) + + expected = """ + export OS_STORAGE_URL=https://swift.storage.example.com/v1/AUTH_test + export OS_AUTH_TOKEN=AUTH_tk5b6b12 + """ + self.assertEqual(textwrap.dedent(expected).lstrip(), + stdout.getvalue()) + + def test_auth_verbose(self): + with mock.patch('swiftclient.client.http_connection') as mock_conn: + stdout = six.StringIO() + with mock.patch('sys.stdout', new=stdout): + argv = [ + '', + 'auth', + '--auth', 'https://swift.storage.example.com/auth/v1.0', + '--user', 'test:tester', '--key', 'te$tin&', + '--verbose', + ] + swiftclient.shell.main(argv) + + expected = """ + export ST_AUTH=https://swift.storage.example.com/auth/v1.0 + export ST_USER=test:tester + export ST_KEY='te$tin&' + """ + self.assertEqual(textwrap.dedent(expected).lstrip(), + stdout.getvalue()) + self.assertEqual([], mock_conn.mock_calls) + + def test_auth_v2(self): + os_options = {'tenant_name': 'demo'} + with mock.patch('swiftclient.client.get_auth_keystone', + new=fake_get_auth_keystone(os_options)): + stdout = six.StringIO() + with mock.patch('sys.stdout', new=stdout): + argv = [ + '', + 'auth', '-V2', + '--auth', 'https://keystone.example.com/v2.0/', + '--os-tenant-name', 'demo', + '--os-username', 'demo', '--os-password', 'admin', + ] + swiftclient.shell.main(argv) + + expected = """ + export OS_STORAGE_URL=http://url/ + export OS_AUTH_TOKEN=token + """ + self.assertEqual(textwrap.dedent(expected).lstrip(), + stdout.getvalue()) + + def test_auth_verbose_v2(self): + with mock.patch('swiftclient.client.get_auth_keystone') \ + as mock_keystone: + stdout = six.StringIO() + with mock.patch('sys.stdout', new=stdout): + argv = [ + '', + 'auth', '-V2', + '--auth', 'https://keystone.example.com/v2.0/', + '--os-tenant-name', 'demo', + '--os-username', 'demo', '--os-password', '$eKr3t', + '--verbose', + ] + swiftclient.shell.main(argv) + + expected = """ + export OS_IDENTITY_API_VERSION=2.0 + export OS_AUTH_VERSION=2.0 + export OS_AUTH_URL=https://keystone.example.com/v2.0/ + export OS_PASSWORD='$eKr3t' + export OS_TENANT_NAME=demo + export OS_USERNAME=demo + """ + self.assertEqual(textwrap.dedent(expected).lstrip(), + stdout.getvalue()) + self.assertEqual([], mock_keystone.mock_calls) + + +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 + self.fake_ks = FakeKeystone( + endpoint='http://example.com:8080/v1/AUTH_bob', + token='bob_token') + + 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 + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 1) + @mock.patch('swiftclient.service.Connection') + def test_upload_bad_threads(self, mock_connection): + mock_connection.return_value.put_object.return_value = EMPTY_ETAG + mock_connection.return_value.attempts = 0 + + def check_bad(argv): + args, env = self._make_cmd( + 'upload', cmd_args=[self.cont, self.obj] + argv) + with mock.patch.dict(os.environ, env): + with CaptureOutput() as output: + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertIn( + 'ERROR: option %s should be a positive integer.' % argv[0], + output.err) + + def check_good(argv): + args, env = self._make_cmd( + 'upload', + cmd_args=[self.cont, self.obj, '--leave-segments'] + argv) + with mock.patch.dict(os.environ, env): + with CaptureOutput() as output: + swiftclient.shell.main(args) + self.assertEqual('', output.err) + check_bad(["--object-threads", "-1"]) + check_bad(["--object-threads", "0"]) + check_bad(["--segment-threads", "-1"]) + check_bad(["--segment-threads", "0"]) + check_good(["--object-threads", "1"]) + check_good(["--segment-threads", "1"]) + + 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.ksclient_v3', self.fake_ks): + 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[1:], out.strip()) + expected_err = "Warning: failed to create container '%s': 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.ksclient_v3', self.fake_ks): + 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[1:], out.strip()) + expected_err = "Warning: failed to create container '%s': 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.ksclient_v3', self.fake_ks): + 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.assertIn(self.obj[1:], out.out) + expected_err = "Warning: failed to create container '%s': 403 Fake" \ + % self.cont + self.assertEqual(expected_err, out.err.strip()) + + def test_segment_upload_with_write_only_access_segments_container(self): + fake_conn = self.fake_http_connection( + 403, # PUT c1 + # HEAD c1 to get storage policy + StubResponse(200, headers={'X-Storage-Policy': 'foo'}), + 403, # PUT c1_segments + 201, # PUT c1_segments/...00 + 201, # PUT c1_segments/...01 + 201, # PUT c1/... + ) + + args, env = self._make_cmd('upload', + cmd_args=[self.cont, self.obj, + '--leave-segments', + '--segment-size=10']) + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): + 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_segments%s/%f/20/10/00000000' % ( + self.cont_path, self.obj, segment_time) + segment_path_1 = '%s_segments%s/%f/20/10/00000001' % ( + self.cont_path, self.obj, 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', self.cont_path + '_segments', '', { + 'X-Auth-Token': 'bob_token', + 'X-Storage-Policy': 'foo', + 'Content-Length': '0', + })) + self.assert_request(('PUT', segment_path_0)) + self.assert_request(('PUT', segment_path_1)) + self.assert_request(('PUT', self.obj_path)) + self.assertIn(self.obj[1:], out.out) + expected_err = ("Warning: failed to create container '%s': 403 Fake\n" + "Warning: failed to create container '%s': 403 Fake" + ) % (self.cont, self.cont + '_segments') + 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.ksclient_v3', self.fake_ks): + 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.assertIn(expected_err, out.err) + self.assertEqual('', out) + + @mock.patch.object(swiftclient.service.SwiftService, + '_bulk_delete_page_size', lambda *a: 1) + @mock.patch('swiftclient.service.Connection') + def test_download_bad_threads(self, mock_connection): + mock_connection.return_value.get_object.return_value = [{}, ''] + mock_connection.return_value.attempts = 0 + + def check_bad(argv): + args, env = self._make_cmd( + 'download', cmd_args=[self.cont, self.obj] + argv) + with mock.patch.dict(os.environ, env): + with CaptureOutput() as output: + self.assertRaises(SystemExit, swiftclient.shell.main, args) + self.assertIn( + 'ERROR: option %s should be a positive integer.' % argv[0], + output.err) + + def check_good(argv): + args, env = self._make_cmd( + 'download', + cmd_args=[self.cont, self.obj, '--no-download'] + argv) + with mock.patch.dict(os.environ, env): + with CaptureOutput() as output: + swiftclient.shell.main(args) + self.assertEqual('', output.err) + check_bad(["--object-threads", "-1"]) + check_bad(["--object-threads", "0"]) + check_bad(["--container-threads", "-1"]) + check_bad(["--container-threads", "0"]) + check_good(["--object-threads", "1"]) + check_good(["--container-threads", "1"]) + + def test_download_with_read_write_access(self): + req_handler = self._fake_cross_account_auth(True, True) + fake_conn = self.fake_http_connection(403, on_request=req_handler, + etags=[EMPTY_ETAG]) + + args, env = self._make_cmd('download', cmd_args=[self.cont, + self.obj.lstrip('/'), + '--no-download']) + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): + 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) + fake_conn = self.fake_http_connection(403, on_request=req_handler, + etags=[EMPTY_ETAG]) + + args, env = self._make_cmd('download', cmd_args=[self.cont, + self.obj.lstrip('/'), + '--no-download']) + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): + 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.ksclient_v3', self.fake_ks): + 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 '%s'" % 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 = b'{}' + resp = StubResponse(403, resp_body, { + 'etag': hashlib.md5(resp_body).hexdigest()}) + fake_conn = self.fake_http_connection(resp, on_request=req_handler) + + args, env = self._make_cmd('download', cmd_args=[self.cont]) + with mock.patch('swiftclient.client.ksclient_v3', self.fake_ks): + 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.ksclient_v3', self.fake_ks): + 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) |