From 95056d1ef49730d98eda1fc246b7e4bb1070a311 Mon Sep 17 00:00:00 2001
From: Alistair Coles <alistair.coles@hp.com>
Date: Mon, 20 Oct 2014 10:41:34 +0100
Subject: [PATCH] Make swift post output an error message when failing

swiftclient.shell.st_post was ignoring the result dict
returned from SwiftService.post and therefore giving no
indication when posts failed e.g. due to invalid auth
credentials.

This patch ensures that SwiftService always returns a
result_dict from post() and then checks the result dict.
On failure the shell now outputs the ClientException
message and exits with error code 1.

Also adds unit tests and cleans up some unnecessary mocked
return values in existing tests.

Closes-Bug: 1383243

Change-Id: I111150eb3d026c8d01c2cac13d3613ca7304e5b9
---
 swiftclient/service.py   | 131 +++++++++++++++++++++++------------------------
 swiftclient/shell.py     |  14 ++---
 tests/unit/test_shell.py |  67 ++++++++++++++++++++++--
 3 files changed, 134 insertions(+), 78 deletions(-)

Index: python-swiftclient-2.3.1/swiftclient/service.py
===================================================================
--- python-swiftclient-2.3.1.orig/swiftclient/service.py
+++ python-swiftclient-2.3.1/swiftclient/service.py
@@ -594,44 +594,31 @@ class SwiftService(object):
                         'error': err,
                         'response_dict': response_dict
                     })
-                    return res
-        else:
-            if not objects:
-                res["action"] = "post_container"
-                response_dict = {}
-                headers = split_headers(
-                    options['meta'], 'X-Container-Meta-')
-                headers.update(
-                    split_headers(options['header'], ''))
-                if options['read_acl'] is not None:
-                    headers['X-Container-Read'] = options['read_acl']
-                if options['write_acl'] is not None:
-                    headers['X-Container-Write'] = options['write_acl']
-                if options['sync_to'] is not None:
-                    headers['X-Container-Sync-To'] = options['sync_to']
-                if options['sync_key'] is not None:
-                    headers['X-Container-Sync-Key'] = options['sync_key']
-                res['headers'] = headers
-                try:
-                    post = self.thread_manager.container_pool.submit(
-                        self._post_container_job, container,
-                        headers, response_dict
-                    )
-                    get_future_result(post)
-                except ClientException as err:
-                    if err.http_status != 404:
-                        res.update({
-                            'action': 'post_container',
-                            'success': False,
-                            'error': err,
-                            'response_dict': response_dict
-                        })
-                        return res
-                    raise SwiftError(
-                        "Container '%s' not found" % container,
-                        container=container
-                    )
-                except Exception as err:
+            return res
+        if not objects:
+            res["action"] = "post_container"
+            response_dict = {}
+            headers = split_headers(
+                options['meta'], 'X-Container-Meta-')
+            headers.update(
+                split_headers(options['header'], ''))
+            if options['read_acl'] is not None:
+                headers['X-Container-Read'] = options['read_acl']
+            if options['write_acl'] is not None:
+                headers['X-Container-Write'] = options['write_acl']
+            if options['sync_to'] is not None:
+                headers['X-Container-Sync-To'] = options['sync_to']
+            if options['sync_key'] is not None:
+                headers['X-Container-Sync-Key'] = options['sync_key']
+            res['headers'] = headers
+            try:
+                post = self.thread_manager.container_pool.submit(
+                    self._post_container_job, container,
+                    headers, response_dict
+                )
+                get_future_result(post)
+            except ClientException as err:
+                if err.http_status != 404:
                     res.update({
                         'action': 'post_container',
                         'success': False,
@@ -639,37 +626,49 @@ class SwiftService(object):
                         'response_dict': response_dict
                     })
                     return res
-            else:
-                post_futures = []
-                post_objects = self._make_post_objects(objects)
-                for post_object in post_objects:
-                    obj = post_object.object_name
-                    obj_options = post_object.options
-                    response_dict = {}
-                    headers = split_headers(
-                        options['meta'], 'X-Object-Meta-')
-                    # add header options to the headers object for the request.
-                    headers.update(
-                        split_headers(options['header'], ''))
-                    if obj_options is not None:
-                        if 'meta' in obj_options:
-                            headers.update(
-                                split_headers(
-                                    obj_options['meta'], 'X-Object-Meta'
-                                )
-                            )
-                        if 'headers' in obj_options:
-                            headers.update(
-                                split_headers(obj_options['header'], '')
+                raise SwiftError(
+                    "Container '%s' not found" % container,
+                    container=container
+                )
+            except Exception as err:
+                res.update({
+                    'action': 'post_container',
+                    'success': False,
+                    'error': err,
+                    'response_dict': response_dict
+                })
+            return res
+        else:
+            post_futures = []
+            post_objects = self._make_post_objects(objects)
+            for post_object in post_objects:
+                obj = post_object.object_name
+                obj_options = post_object.options
+                response_dict = {}
+                headers = split_headers(
+                    options['meta'], 'X-Object-Meta-')
+                # add header options to the headers object for the request.
+                headers.update(
+                    split_headers(options['header'], ''))
+                if obj_options is not None:
+                    if 'meta' in obj_options:
+                        headers.update(
+                            split_headers(
+                                obj_options['meta'], 'X-Object-Meta'
                             )
+                        )
+                    if 'headers' in obj_options:
+                        headers.update(
+                            split_headers(obj_options['header'], '')
+                        )
 
-                    post = self.thread_manager.object_uu_pool.submit(
-                        self._post_object_job, container, obj,
-                        headers, response_dict
-                    )
-                    post_futures.append(post)
+                post = self.thread_manager.object_uu_pool.submit(
+                    self._post_object_job, container, obj,
+                    headers, response_dict
+                )
+                post_futures.append(post)
 
-                return ResultsIterator(post_futures)
+            return ResultsIterator(post_futures)
 
     @staticmethod
     def _make_post_objects(objects):
Index: python-swiftclient-2.3.1/swiftclient/shell.py
===================================================================
--- python-swiftclient-2.3.1.orig/swiftclient/shell.py
+++ python-swiftclient-2.3.1/swiftclient/shell.py
@@ -543,8 +543,7 @@ If the container is not found, it will b
 
 Positional arguments:
   [container]           Name of container to post to.
-  [object]              Name of object to post. Specify multiple times
-                        for multiple objects.
+  [object]              Name of object to post.
 
 Optional arguments:
   --read-acl <acl>      Read ACL for containers. Quick summary of ACL syntax:
@@ -596,7 +595,7 @@ def st_post(parser, args, output_manager
     with SwiftService(options=_opts) as swift:
         try:
             if not args:
-                swift.post()
+                result = swift.post()
             else:
                 container = args[0]
                 if '/' in container:
@@ -612,15 +611,16 @@ def st_post(parser, args, output_manager
                         results_iterator = swift.post(
                             container=container, objects=objects
                         )
-                        for result in results_iterator:  # only 1 result
-                            if not result["success"]:
-                                raise(result["error"])
+                        result = next(results_iterator)
                     else:
                         output_manager.error(
                             'Usage: %s post %s\n%s', BASENAME,
                             st_post_options, st_post_help)
+                        return
                 else:
-                    swift.post(container=container)
+                    result = swift.post(container=container)
+            if not result["success"]:
+                raise(result["error"])
 
         except SwiftError as e:
             output_manager.error(e.value)
Index: python-swiftclient-2.3.1/tests/unit/test_shell.py
===================================================================
--- python-swiftclient-2.3.1.orig/tests/unit/test_shell.py
+++ python-swiftclient-2.3.1/tests/unit/test_shell.py
@@ -378,26 +378,68 @@ class TestShell(unittest.TestCase):
     @mock.patch('swiftclient.service.Connection')
     def test_post_account(self, connection):
         argv = ["", "post"]
-        connection.return_value.head_object.return_value = {}
         swiftclient.shell.main(argv)
         connection.return_value.post_account.assert_called_with(
             headers={}, response_dict={})
 
+    @mock.patch('swiftclient.shell.OutputManager.error')
+    @mock.patch('swiftclient.service.Connection')
+    def test_post_account_bad_auth(self, connection, error):
+        argv = ["", "post"]
+        connection.return_value.post_account.side_effect = \
+            swiftclient.ClientException('bad auth')
+        swiftclient.shell.main(argv)
+        error.assert_called_with('bad auth')
+
+    @mock.patch('swiftclient.shell.OutputManager.error')
+    @mock.patch('swiftclient.service.Connection')
+    def test_post_account_not_found(self, connection, error):
+        argv = ["", "post"]
+        connection.return_value.post_account.side_effect = \
+            swiftclient.ClientException('test', http_status=404)
+        swiftclient.shell.main(argv)
+        error.assert_called_with('Account not found')
+
+    @mock.patch('swiftclient.service.Connection')
+    def test_post_container(self, connection):
         argv = ["", "post", "container"]
-        connection.return_value.head_object.return_value = {}
         swiftclient.shell.main(argv)
         connection.return_value.post_container.assert_called_with(
             'container', headers={}, response_dict={})
 
+    @mock.patch('swiftclient.shell.OutputManager.error')
     @mock.patch('swiftclient.service.Connection')
-    def test_post_container(self, connection):
+    def test_post_container_bad_auth(self, connection, error):
+        argv = ["", "post", "container"]
+        connection.return_value.post_container.side_effect = \
+            swiftclient.ClientException('bad auth')
+        swiftclient.shell.main(argv)
+        error.assert_called_with('bad auth')
+
+    @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])
+
+    @mock.patch('swiftclient.shell.OutputManager.error')
+    def test_post_container_with_bad_name(self, error):
+        argv = ["", "post", "conta/iner"]
+        swiftclient.shell.main(argv)
+        self.assertTrue(error.called)
+        self.assertTrue(error.call_args[0][0].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",
                 ]
-        connection.return_value.head_object.return_value = {}
         swiftclient.shell.main(argv)
         connection.return_value.post_container.assert_called_with(
             'container', headers={
@@ -412,13 +454,28 @@ class TestShell(unittest.TestCase):
                 "--meta", "Color:Blue",
                 "--header", "content-type:text/plain"
                 ]
-        connection.return_value.head_object.return_value = {}
         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.shell.OutputManager.error')
+    @mock.patch('swiftclient.service.Connection')
+    def test_post_object_bad_auth(self, connection, error):
+        argv = ["", "post", "container", "object"]
+        connection.return_value.post_object.side_effect = \
+            swiftclient.ClientException("bad auth")
+        swiftclient.shell.main(argv)
+        error.assert_called_with('bad auth')
+
+    @mock.patch('swiftclient.shell.OutputManager.error')
+    def test_post_object_too_many_args(self, error):
+        argv = ["", "post", "container", "object", "bad_arg"]
+        swiftclient.shell.main(argv)
+        self.assertTrue(error.called)
+        self.assertTrue(error.call_args[0][0].startswith('Usage'))
+
     @mock.patch('swiftclient.shell.generate_temp_url')
     def test_temp_url(self, temp_url):
         argv = ["", "tempurl", "GET", "60", "/v1/AUTH_account/c/o",
