Check HTTP response headers of Swift API in detail

All HTTP response headers for each Swift API are checked their existence
and style. For checking, create custom_matchers.py file in common directory
and implement specific matchers so that they can be reused.

Change-Id: I7dcd87c814cf657999796bda75482505112b4edb
Closes-Bug: #1240856
diff --git a/tempest/api/object_storage/base.py b/tempest/api/object_storage/base.py
index c6639c2..e7cb806 100644
--- a/tempest/api/object_storage/base.py
+++ b/tempest/api/object_storage/base.py
@@ -18,6 +18,7 @@
 
 from tempest.api.identity.base import DataGenerator
 from tempest import clients
+from tempest.common import custom_matchers
 from tempest.common import isolated_creds
 from tempest import exceptions
 import tempest.test
@@ -120,3 +121,12 @@
                 container_client.delete_container(cont)
             except exceptions.NotFound:
                 pass
+
+    def assertHeaders(self, resp, target, method):
+        """
+        Common method to check the existence and the format of common response
+        headers
+        """
+        self.assertThat(resp, custom_matchers.ExistsAllResponseHeaders(
+                        target, method))
+        self.assertThat(resp, custom_matchers.AreAllWellFormatted())
diff --git a/tempest/api/object_storage/test_account_services.py b/tempest/api/object_storage/test_account_services.py
index 615b179..12c823b 100644
--- a/tempest/api/object_storage/test_account_services.py
+++ b/tempest/api/object_storage/test_account_services.py
@@ -46,6 +46,7 @@
         params = {'format': 'json'}
         resp, container_list = \
             self.account_client.list_account_containers(params=params)
+        self.assertHeaders(resp, 'Account', 'GET')
 
         self.assertIsNotNone(container_list)
         container_names = [c['name'] for c in container_list]
@@ -59,6 +60,8 @@
             params = {'limit': limit}
             resp, container_list = \
                 self.account_client.list_account_containers(params=params)
+            self.assertHeaders(resp, 'Account', 'GET')
+
             self.assertEqual(len(container_list), limit)
 
     @attr(type='smoke')
@@ -70,10 +73,15 @@
         params = {'marker': self.containers[-1]}
         resp, container_list = \
             self.account_client.list_account_containers(params=params)
+        self.assertHeaders(resp, 'Account', 'GET')
+
         self.assertEqual(len(container_list), 0)
+
         params = {'marker': self.containers[self.containers_count / 2]}
         resp, container_list = \
             self.account_client.list_account_containers(params=params)
+        self.assertHeaders(resp, 'Account', 'GET')
+
         self.assertEqual(len(container_list), self.containers_count / 2 - 1)
 
     @attr(type='smoke')
@@ -85,10 +93,13 @@
         params = {'end_marker': self.containers[0]}
         resp, container_list = \
             self.account_client.list_account_containers(params=params)
+        self.assertHeaders(resp, 'Account', 'GET')
         self.assertEqual(len(container_list), 0)
+
         params = {'end_marker': self.containers[self.containers_count / 2]}
         resp, container_list = \
             self.account_client.list_account_containers(params=params)
+        self.assertHeaders(resp, 'Account', 'GET')
         self.assertEqual(len(container_list), self.containers_count / 2)
 
     @attr(type='smoke')
@@ -101,6 +112,8 @@
                       'limit': limit}
             resp, container_list = \
                 self.account_client.list_account_containers(params=params)
+            self.assertHeaders(resp, 'Account', 'GET')
+
             self.assertTrue(len(container_list) <= limit, str(container_list))
 
     @attr(type='smoke')
@@ -108,9 +121,7 @@
         # list all account metadata
         resp, metadata = self.account_client.list_account_metadata()
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
-        self.assertIn('x-account-object-count', resp)
-        self.assertIn('x-account-container-count', resp)
-        self.assertIn('x-account-bytes-used', resp)
+        self.assertHeaders(resp, 'Account', 'HEAD')
 
     @attr(type='smoke')
     def test_create_and_delete_account_metadata(self):
@@ -120,8 +131,11 @@
         resp, _ = self.account_client.create_account_metadata(
             metadata={header: data})
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Account', 'POST')
 
         resp, _ = self.account_client.list_account_metadata()
+        self.assertHeaders(resp, 'Account', 'HEAD')
+
         self.assertIn('x-account-meta-' + header, resp)
         self.assertEqual(resp['x-account-meta-' + header], data)
 
@@ -129,8 +143,10 @@
         resp, _ = \
             self.account_client.delete_account_metadata(metadata=[header])
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Account', 'POST')
 
         resp, _ = self.account_client.list_account_metadata()
+        self.assertHeaders(resp, 'Account', 'HEAD')
         self.assertNotIn('x-account-meta-' + header, resp)
 
     @attr(type=['negative', 'gate'])
diff --git a/tempest/api/object_storage/test_container_services.py b/tempest/api/object_storage/test_container_services.py
index 4fae953..04de072 100644
--- a/tempest/api/object_storage/test_container_services.py
+++ b/tempest/api/object_storage/test_container_services.py
@@ -38,6 +38,7 @@
         resp, body = self.container_client.create_container(container_name)
         self.containers.append(container_name)
         self.assertIn(resp['status'], ('202', '201'))
+        self.assertHeaders(resp, 'Container', 'PUT')
 
     @attr(type='smoke')
     def test_delete_container(self):
@@ -48,6 +49,8 @@
         # delete container
         resp, _ = self.container_client.delete_container(container_name)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Container', 'DELETE')
+
         self.containers.remove(container_name)
 
     @attr(type='smoke')
@@ -76,6 +79,8 @@
             self.container_client.\
             list_container_contents(container_name, params=params)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Container', 'GET')
+
         self.assertIsNotNone(object_list)
 
         object_names = [obj['name'] for obj in object_list]
@@ -97,11 +102,14 @@
             self.container_client.update_container_metadata(container_name,
                                                             metadata=metadata)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Container', 'POST')
 
         # list container metadata
         resp, _ = self.container_client.list_container_metadata(
             container_name)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Container', 'HEAD')
+
         self.assertIn('x-container-meta-name', resp)
         self.assertIn('x-container-meta-description', resp)
         self.assertEqual(resp['x-container-meta-name'], 'Pictures')
@@ -112,9 +120,12 @@
             container_name,
             metadata=metadata.keys())
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Container', 'POST')
 
         # check if the metadata are no longer there
         resp, _ = self.container_client.list_container_metadata(container_name)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Container', 'HEAD')
+
         self.assertNotIn('x-container-meta-name', resp)
         self.assertNotIn('x-container-meta-description', resp)
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index 7626af1..c7fdd0e 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -16,8 +16,10 @@
 #    under the License.
 
 import hashlib
+import re
 
 from tempest.api.object_storage import base
+from tempest.common import custom_matchers
 from tempest.common.utils import data_utils
 from tempest.test import attr
 from tempest.test import HTTP_SUCCESS
@@ -60,6 +62,7 @@
         resp, _ = self.object_client.create_object(self.container_name,
                                                    object_name, data)
         self.assertEqual(resp['status'], '201')
+        self.assertHeaders(resp, 'Object', 'PUT')
 
     @attr(type='smoke')
     def test_delete_object(self):
@@ -72,6 +75,7 @@
         resp, _ = self.object_client.delete_object(self.container_name,
                                                    object_name)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Object', 'DELETE')
 
     @attr(type='smoke')
     def test_object_metadata(self):
@@ -89,11 +93,14 @@
         resp, _ = self.object_client.update_object_metadata(
             self.container_name, object_name, orig_metadata)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Object', 'POST')
 
         # get object metadata
         resp, resp_metadata = self.object_client.list_object_metadata(
             self.container_name, object_name)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Object', 'HEAD')
+
         actual_meta_key = 'x-object-meta-' + meta_key
         self.assertIn(actual_meta_key, resp)
         self.assertEqual(resp[actual_meta_key], meta_value)
@@ -111,6 +118,8 @@
         resp, body = self.object_client.get_object(self.container_name,
                                                    object_name)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Object', 'GET')
+
         self.assertEqual(body, data)
 
     @attr(type='smoke')
@@ -133,6 +142,8 @@
         resp, _ = self.object_client.copy_object_in_same_container(
             self.container_name, src_object_name, dst_object_name)
         self.assertEqual(resp['status'], '201')
+        self.assertHeaders(resp, 'Object', 'PUT')
+
         # check data
         resp, body = self.object_client.get_object(self.container_name,
                                                    dst_object_name)
@@ -156,6 +167,8 @@
         resp, _ = self.object_client.copy_object_in_same_container(
             self.container_name, object_name, object_name, metadata)
         self.assertEqual(resp['status'], '201')
+        self.assertHeaders(resp, 'Object', 'PUT')
+
         # check the content type
         resp, _ = self.object_client.list_object_metadata(self.container_name,
                                                           object_name)
@@ -180,6 +193,17 @@
                                                         src_object_name,
                                                         dst_object_name)
         self.assertEqual(resp['status'], '201')
+        self.assertHeaders(resp, 'Object', 'COPY')
+
+        self.assertIn('last-modified', resp)
+        self.assertIn('x-copied-from', resp)
+        self.assertIn('x-copied-from-last-modified', resp)
+        self.assertNotEqual(len(resp['last-modified']), 0)
+        self.assertEqual(
+            resp['x-copied-from'],
+            self.container_name + "/" + src_object_name)
+        self.assertNotEqual(len(resp['x-copied-from-last-modified']), 0)
+
         # check data
         resp, body = self.object_client.get_object(self.container_name,
                                                    dst_object_name)
@@ -210,11 +234,15 @@
                                                             object_name,
                                                             orig_metadata)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Object', 'POST')
+
         # copy object from source container to destination container
         resp, _ = self.object_client.copy_object_across_containers(
             src_container_name, object_name, dst_container_name,
             object_name)
         self.assertEqual(resp['status'], '201')
+        self.assertHeaders(resp, 'Object', 'PUT')
+
         # check if object is present in destination container
         resp, body = self.object_client.get_object(dst_container_name,
                                                    object_name)
@@ -238,13 +266,34 @@
         # creating a manifest file
         metadata = {'X-Object-Manifest': '%s/%s/'
                     % (self.container_name, object_name)}
-        self.object_client.create_object(self.container_name,
-                                         object_name, data='')
+        resp, _ = self.object_client.create_object(self.container_name,
+                                                   object_name, data='')
+        self.assertHeaders(resp, 'Object', 'PUT')
+
         resp, _ = self.object_client.update_object_metadata(
             self.container_name, object_name, metadata, metadata_prefix='')
+        self.assertHeaders(resp, 'Object', 'POST')
+
         resp, _ = self.object_client.list_object_metadata(
             self.container_name, object_name)
+
+        # Check only the existence of common headers with custom matcher
+        self.assertThat(resp, custom_matchers.ExistsAllResponseHeaders(
+                        'Object', 'HEAD'))
         self.assertIn('x-object-manifest', resp)
+
+        # Etag value of a large object is enclosed in double-quotations.
+        # This is a special case, therefore the formats of response headers
+        # are checked without a custom matcher.
+        self.assertTrue(resp['etag'].startswith('\"'))
+        self.assertTrue(resp['etag'].endswith('\"'))
+        self.assertTrue(resp['etag'].strip('\"').isalnum())
+        self.assertTrue(re.match("^\d+\.?\d*\Z", resp['x-timestamp']))
+        self.assertNotEqual(len(resp['content-type']), 0)
+        self.assertTrue(re.match("^tx[0-9a-f]*-[0-9a-f]*$",
+                                 resp['x-trans-id']))
+        self.assertNotEqual(len(resp['date']), 0)
+        self.assertEqual(resp['accept-ranges'], 'bytes')
         self.assertEqual(resp['x-object-manifest'],
                          '%s/%s/' % (self.container_name, object_name))
 
@@ -270,12 +319,23 @@
         resp, _ = self.object_client.get(url, headers=headers)
         self.assertEqual(resp['status'], '304')
 
+        # When the file is not downloaded from Swift server, response does
+        # not contain 'X-Timestamp' header. This is the special case, therefore
+        # the existence of response headers is checked without custom matcher.
+        self.assertIn('content-type', resp)
+        self.assertIn('x-trans-id', resp)
+        self.assertIn('date', resp)
+        self.assertIn('accept-ranges', resp)
+        # Check only the format of common headers with custom matcher
+        self.assertThat(resp, custom_matchers.AreAllWellFormatted())
+
         # local copy is different, download
         local_data = "something different"
         md5 = hashlib.md5(local_data).hexdigest()
         headers = {'If-None-Match': md5}
         resp, body = self.object_client.get(url, headers=headers)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Object', 'GET')
 
 
 class PublicObjectTest(base.BaseObjectTest):
@@ -298,6 +358,8 @@
         resp_meta, body = self.container_client.update_container_metadata(
             self.container_name, metadata=cont_headers, metadata_prefix='')
         self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp_meta, 'Container', 'POST')
+
         # create object
         object_name = data_utils.rand_name(name='Object')
         data = data_utils.arbitrary_string(size=len(object_name),
@@ -305,17 +367,22 @@
         resp, _ = self.object_client.create_object(self.container_name,
                                                    object_name, data)
         self.assertEqual(resp['status'], '201')
+        self.assertHeaders(resp, 'Object', 'PUT')
 
         # list container metadata
         resp_meta, _ = self.container_client.list_container_metadata(
             self.container_name)
-        self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp_meta, 'Container', 'HEAD')
+
         self.assertIn('x-container-read', resp_meta)
         self.assertEqual(resp_meta['x-container-read'], '.r:*,.rlistings')
 
         # trying to get object with empty headers as it is public readable
         resp, body = self.custom_object_client.get_object(
             self.container_name, object_name, metadata={})
+        self.assertHeaders(resp, 'Object', 'GET')
+
         self.assertEqual(body, data)
 
     @attr(type='smoke')
@@ -327,6 +394,7 @@
             self.container_name, metadata=cont_headers,
             metadata_prefix='')
         self.assertIn(int(resp_meta['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp_meta, 'Container', 'POST')
 
         # create object
         object_name = data_utils.rand_name(name='Object')
@@ -335,11 +403,14 @@
         resp, _ = self.object_client.create_object(self.container_name,
                                                    object_name, data)
         self.assertEqual(resp['status'], '201')
+        self.assertHeaders(resp, 'Object', 'PUT')
 
         # list container metadata
         resp, _ = self.container_client.list_container_metadata(
             self.container_name)
         self.assertIn(int(resp['status']), HTTP_SUCCESS)
+        self.assertHeaders(resp, 'Container', 'HEAD')
+
         self.assertIn('x-container-read', resp)
         self.assertEqual(resp['x-container-read'], '.r:*,.rlistings')
 
@@ -350,4 +421,6 @@
         resp, body = self.custom_object_client.get_object(
             self.container_name, object_name,
             metadata=headers)
+        self.assertHeaders(resp, 'Object', 'GET')
+
         self.assertEqual(body, data)
diff --git a/tempest/common/custom_matchers.py b/tempest/common/custom_matchers.py
new file mode 100644
index 0000000..307d5db
--- /dev/null
+++ b/tempest/common/custom_matchers.py
@@ -0,0 +1,154 @@
+# 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 re
+
+
+class ExistsAllResponseHeaders(object):
+    """
+    Specific matcher to check the existence of Swift's response headers
+
+    This matcher checks the existence of common headers for each HTTP method
+    or the target, which means account, container or object.
+    When checking the existence of 'specific' headers such as
+    X-Account-Meta-* or X-Object-Manifest for example, those headers must be
+    checked in each test code.
+    """
+
+    def __init__(self, target, method):
+        """
+        param: target Account/Container/Object
+        param: method PUT/GET/HEAD/DELETE/COPY/POST
+        """
+        self.target = target
+        self.method = method
+
+    def match(self, actual):
+        """
+        param: actual HTTP response headers
+        """
+        # Check common headers for all HTTP methods
+        if 'content-length' not in actual:
+            return NonExistentHeader('content-length')
+        if 'content-type' not in actual:
+            return NonExistentHeader('content-type')
+        if 'x-trans-id' not in actual:
+            return NonExistentHeader('x-trans-id')
+        if 'date' not in actual:
+            return NonExistentHeader('date')
+
+        # Check headers for a specific method or target
+        if self.method == 'GET' or self.method == 'HEAD':
+            if 'x-timestamp' not in actual:
+                return NonExistentHeader('x-timestamp')
+            if 'accept-ranges' not in actual:
+                return NonExistentHeader('accept-ranges')
+            if self.target == 'Account':
+                if 'x-account-bytes-used' not in actual:
+                    return NonExistentHeader('x-account-bytes-used')
+                if 'x-account-container-count' not in actual:
+                    return NonExistentHeader('x-account-container-count')
+                if 'x-account-object-count' not in actual:
+                    return NonExistentHeader('x-account-object-count')
+            elif self.target == 'Container':
+                if 'x-container-bytes-used' not in actual:
+                    return NonExistentHeader('x-container-bytes-used')
+                if 'x-container-object-count' not in actual:
+                    return NonExistentHeader('x-container-object-count')
+            elif self.target == 'Object':
+                if 'etag' not in actual:
+                    return NonExistentHeader('etag')
+        elif self.method == 'PUT' or self.method == 'COPY':
+            if self.target == 'Object':
+                if 'etag' not in actual:
+                    return NonExistentHeader('etag')
+
+        return None
+
+
+class NonExistentHeader(object):
+    """
+    Informs an error message for end users in the case of missing a
+    certain header in Swift's responses
+    """
+
+    def __init__(self, header):
+        self.header = header
+
+    def describe(self):
+        return "%s header does not exist" % self.header
+
+    def get_details(self):
+        return {}
+
+
+class AreAllWellFormatted(object):
+    """
+    Specific matcher to check the correctness of formats of values of Swift's
+    response headers
+
+    This matcher checks the format of values of response headers.
+    When checking the format of values of 'specific' headers such as
+    X-Account-Meta-* or X-Object-Manifest for example, those values must be
+    checked in each test code.
+    """
+
+    def match(self, actual):
+        for key, value in actual.iteritems():
+            if key == 'content-length' and not value.isdigit():
+                return InvalidFormat(key, value)
+            elif key == 'x-timestamp' and not re.match("^\d+\.?\d*\Z", value):
+                return InvalidFormat(key, value)
+            elif key == 'x-account-bytes-used' and not value.isdigit():
+                return InvalidFormat(key, value)
+            elif key == 'x-account-container-count' and not value.isdigit():
+                return InvalidFormat(key, value)
+            elif key == 'x-account-object-count' and not value.isdigit():
+                return InvalidFormat(key, value)
+            elif key == 'x-container-bytes-used' and not value.isdigit():
+                return InvalidFormat(key, value)
+            elif key == 'x-container-object-count' and not value.isdigit():
+                return InvalidFormat(key, value)
+            elif key == 'content-type' and not value:
+                return InvalidFormat(key, value)
+            elif key == 'x-trans-id' and \
+                not re.match("^tx[0-9a-f]*-[0-9a-f]*$", value):
+                return InvalidFormat(key, value)
+            elif key == 'date' and not value:
+                return InvalidFormat(key, value)
+            elif key == 'accept-ranges' and not value == 'bytes':
+                return InvalidFormat(key, value)
+            elif key == 'etag' and not value.isalnum():
+                return InvalidFormat(key, value)
+
+        return None
+
+
+class InvalidFormat(object):
+    """
+    Informs an error message for end users if a format of a certain header
+    is invalid
+    """
+
+    def __init__(self, key, value):
+        self.key = key
+        self.value = value
+
+    def describe(self):
+        return "InvalidFormat (%s, %s)" % (self.key, self.value)
+
+    def get_details(self):
+        return {}