Add tests for testing swift bulk middleware

Add tests for two functions of Swift bulk operations.
- PUT operation to extract containers and objects on Swift cluster from
  uploaded archived file
- DELETE operation to delete multiple containers and objects listed in
  the request body

To implement these tests, I modify delete operation in rest_client.py
to include request body in HTTP DELETE request and fix minor bugs.

Change-Id: I263d903f14fe777153a543640a54464f93d6c008
Implements: blueprint test-swift-bulk-middleware
Related-Bug: #1240856
diff --git a/tempest/api/object_storage/test_account_bulk.py b/tempest/api/object_storage/test_account_bulk.py
new file mode 100644
index 0000000..5fde76a
--- /dev/null
+++ b/tempest/api/object_storage/test_account_bulk.py
@@ -0,0 +1,136 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NTT Corporation
+#
+#    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.
+
+import tarfile
+import tempfile
+
+from tempest.api.object_storage import base
+from tempest.common import custom_matchers
+from tempest import test
+
+
+class BulkTest(base.BaseObjectTest):
+
+    def setUp(self):
+        super(BulkTest, self).setUp()
+        self.containers = []
+
+    def tearDown(self):
+        self.delete_containers(self.containers)
+        super(BulkTest, self).tearDown()
+
+    def _create_archive(self):
+        # Create an archived file for bulk upload testing.
+        # Directory and files contained in the directory correspond to
+        # container and subsidiary objects.
+        tmp_dir = tempfile.mkdtemp()
+        tmp_file = tempfile.mkstemp(dir=tmp_dir)
+
+        # Extract a container name and an object name
+        container_name = tmp_dir.split("/")[-1]
+        object_name = tmp_file[1].split("/")[-1]
+
+        # Create tar file
+        tarpath = tempfile.NamedTemporaryFile(suffix=".tar")
+        tar = tarfile.open(None, 'w', tarpath)
+        tar.add(tmp_dir, arcname=container_name)
+        tar.close()
+        tarpath.flush()
+
+        return tarpath.name, container_name, object_name
+
+    @test.attr(type='gate')
+    def test_extract_archive(self):
+        # Test bulk operation of file upload with an archived file
+        filepath, container_name, object_name = self._create_archive()
+
+        params = {'extract-archive': 'tar'}
+        with open(filepath) as fh:
+            mydata = fh.read()
+            resp, body = self.account_client.create_account(data=mydata,
+                                                            params=params)
+
+        self.containers.append(container_name)
+
+        self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+
+        # When uploading an archived file with the bulk operation, the response
+        # does not contain 'content-length' header. This is the special case,
+        # therefore the existence of response headers is checked without
+        # custom matcher.
+        self.assertIn('transfer-encoding', resp)
+        self.assertIn('content-type', resp)
+        self.assertIn('x-trans-id', resp)
+        self.assertIn('date', resp)
+
+        # Check only the format of common headers with custom matcher
+        self.assertThat(resp, custom_matchers.AreAllWellFormatted())
+
+        param = {'format': 'json'}
+        resp, body = self.account_client.list_account_containers(param)
+
+        self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Account', 'GET')
+
+        self.assertIn(container_name, [b['name'] for b in body])
+
+        param = {'format': 'json'}
+        resp, contents_list = self.container_client.list_container_contents(
+            container_name, param)
+
+        self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Container', 'GET')
+
+        self.assertIn(object_name, [c['name'] for c in contents_list])
+
+    @test.attr(type='gate')
+    def test_bulk_delete(self):
+        # Test bulk operation of deleting multiple files
+        filepath, container_name, object_name = self._create_archive()
+
+        params = {'extract-archive': 'tar'}
+        with open(filepath) as fh:
+            mydata = fh.read()
+            resp, body = self.account_client.create_account(data=mydata,
+                                                            params=params)
+
+        data = '%s/%s\n%s' % (container_name, object_name, container_name)
+        params = {'bulk-delete': ''}
+        resp, body = self.account_client.delete_account(data=data,
+                                                        params=params)
+
+        self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+
+        # When deleting multiple files using the bulk operation, the response
+        # does not contain 'content-length' header. This is the special case,
+        # therefore the existence of response headers is checked without
+        # custom matcher.
+        self.assertIn('transfer-encoding', resp)
+        self.assertIn('content-type', resp)
+        self.assertIn('x-trans-id', resp)
+        self.assertIn('date', resp)
+
+        # Check only the format of common headers with custom matcher
+        self.assertThat(resp, custom_matchers.AreAllWellFormatted())
+
+        # Check if a container is deleted
+        param = {'format': 'txt'}
+        resp, body = self.account_client.list_account_containers(param)
+
+        self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Account', 'GET')
+
+        self.assertNotIn(container_name, body)
diff --git a/tempest/common/custom_matchers.py b/tempest/common/custom_matchers.py
index 81b5153..f43f55f 100644
--- a/tempest/common/custom_matchers.py
+++ b/tempest/common/custom_matchers.py
@@ -133,6 +133,8 @@
                 return InvalidFormat(key, value)
             elif key == 'etag' and not value.isalnum():
                 return InvalidFormat(key, value)
+            elif key == 'transfer-encoding' and not value == 'chunked':
+                return InvalidFormat(key, value)
 
         return None
 
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 9aca2ff..ba88d9b 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -304,8 +304,8 @@
     def get(self, url, headers=None):
         return self.request('GET', url, headers)
 
-    def delete(self, url, headers=None):
-        return self.request('DELETE', url, headers)
+    def delete(self, url, headers=None, body=None):
+        return self.request('DELETE', url, headers, body)
 
     def patch(self, url, body, headers):
         return self.request('PATCH', url, headers, body)
diff --git a/tempest/services/object_storage/account_client.py b/tempest/services/object_storage/account_client.py
index 94b55c3..8260e2f 100644
--- a/tempest/services/object_storage/account_client.py
+++ b/tempest/services/object_storage/account_client.py
@@ -30,6 +30,37 @@
         self.service = self.config.object_storage.catalog_type
         self.format = 'json'
 
+    def create_account(self, data=None,
+                       params=None,
+                       metadata={},
+                       remove_metadata={},
+                       metadata_prefix='X-Account-Meta-',
+                       remove_metadata_prefix='X-Remove-Account-Meta-'):
+        """Create an account."""
+        url = ''
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        headers = {}
+        for key in metadata:
+            headers[metadata_prefix + key] = metadata[key]
+        for key in remove_metadata:
+            headers[remove_metadata_prefix + key] = remove_metadata[key]
+
+        resp, body = self.put(url, data, headers)
+        return resp, body
+
+    def delete_account(self, data=None, params=None):
+        """Delete an account."""
+        url = ''
+        if params:
+            if 'bulk-delete' in params:
+                url += 'bulk-delete&'
+            url = '?%s%s' % (url, urllib.urlencode(params))
+
+        resp, body = self.delete(url, headers=None, body=data)
+        return resp, body
+
     def list_account_metadata(self):
         """
         HEAD on the storage URL
@@ -92,7 +123,9 @@
 
         url = '?' + urllib.urlencode(params)
         resp, body = self.get(url)
-        body = json.loads(body)
+
+        if params and params.get('format') == 'json':
+            body = json.loads(body)
         return resp, body