Merge "Add Tests for Message & Claim APIs"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index bf0b308..33e3b31 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -845,6 +845,28 @@
 # queues (integer value)
 #max_queues_per_page=20
 
+# The maximum metadata size for a queue (integer value)
+#max_queue_metadata=65536
+
+# The maximum number of queue message per page when listing
+# (or) posting messages (integer value)
+#max_messages_per_page=20
+
+# The maximum size of a message body (integer value)
+#max_message_size=262144
+
+# The maximum number of messages per claim (integer value)
+#max_messages_per_claim=20
+
+# The maximum ttl for a message (integer value)
+#max_message_ttl=1209600
+
+# The maximum ttl for a claim (integer value)
+#max_claim_ttl=43200
+
+# The maximum grace period for a claim (integer value)
+#max_claim_grace=43200
+
 
 [scenario]
 
diff --git a/tempest/api/queuing/base.py b/tempest/api/queuing/base.py
index 5649619..f4ff7f1 100644
--- a/tempest/api/queuing/base.py
+++ b/tempest/api/queuing/base.py
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from tempest.common.utils import data_utils
 from tempest import config
 from tempest.openstack.common import log as logging
 from tempest import test
@@ -89,3 +90,80 @@
         """Wrapper utility that sets the metadata of a queue."""
         resp, body = cls.client.set_queue_metadata(queue_name, rbody)
         return resp, body
+
+    @classmethod
+    def post_messages(cls, queue_name, rbody):
+        '''Wrapper utility that posts messages to a queue.'''
+        resp, body = cls.client.post_messages(queue_name, rbody)
+
+        return resp, body
+
+    @classmethod
+    def list_messages(cls, queue_name):
+        '''Wrapper utility that lists the messages in a queue.'''
+        resp, body = cls.client.list_messages(queue_name)
+
+        return resp, body
+
+    @classmethod
+    def get_single_message(cls, message_uri):
+        '''Wrapper utility that gets a single message.'''
+        resp, body = cls.client.get_single_message(message_uri)
+
+        return resp, body
+
+    @classmethod
+    def get_multiple_messages(cls, message_uri):
+        '''Wrapper utility that gets multiple messages.'''
+        resp, body = cls.client.get_multiple_messages(message_uri)
+
+        return resp, body
+
+    @classmethod
+    def delete_messages(cls, message_uri):
+        '''Wrapper utility that deletes messages.'''
+        resp, body = cls.client.delete_messages(message_uri)
+
+        return resp, body
+
+    @classmethod
+    def post_claims(cls, queue_name, rbody, url_params=False):
+        '''Wrapper utility that claims messages.'''
+        resp, body = cls.client.post_claims(
+            queue_name, rbody, url_params=False)
+
+        return resp, body
+
+    @classmethod
+    def query_claim(cls, claim_uri):
+        '''Wrapper utility that gets a claim.'''
+        resp, body = cls.client.query_claim(claim_uri)
+
+        return resp, body
+
+    @classmethod
+    def update_claim(cls, claim_uri, rbody):
+        '''Wrapper utility that updates a claim.'''
+        resp, body = cls.client.update_claim(claim_uri, rbody)
+
+        return resp, body
+
+    @classmethod
+    def release_claim(cls, claim_uri):
+        '''Wrapper utility that deletes a claim.'''
+        resp, body = cls.client.release_claim(claim_uri)
+
+        return resp, body
+
+    @classmethod
+    def generate_message_body(cls, repeat=1):
+        '''Wrapper utility that sets the metadata of a queue.'''
+        message_ttl = data_utils.rand_int_id(start=60,
+                                             end=CONF.queuing.max_message_ttl)
+
+        key = data_utils.arbitrary_string(size=20, base_text='QueuingKey')
+        value = data_utils.arbitrary_string(size=20, base_text='QueuingValue')
+        message_body = {key: value}
+
+        rbody = ([{'body': message_body, 'ttl': message_ttl}] * repeat)
+        return rbody
diff --git a/tempest/api/queuing/test_claims.py b/tempest/api/queuing/test_claims.py
new file mode 100644
index 0000000..a306623
--- /dev/null
+++ b/tempest/api/queuing/test_claims.py
@@ -0,0 +1,123 @@
+# Copyright (c) 2014 Rackspace, Inc.
+#
+# 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 logging
+import urlparse
+
+from tempest.api.queuing import base
+from tempest.common.utils import data_utils
+from tempest import config
+from tempest import test
+
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+
+
+class TestClaims(base.BaseQueuingTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestClaims, cls).setUpClass()
+        cls.queue_name = data_utils.rand_name('Queues-Test')
+        # Create Queue
+        cls.create_queue(cls.queue_name)
+
+    def _post_and_claim_messages(self, queue_name, repeat=1):
+        # Post Messages
+        message_body = self.generate_message_body(repeat=repeat)
+        self.client.post_messages(queue_name=self.queue_name,
+                                  rbody=message_body)
+
+        # Post Claim
+        claim_ttl = data_utils.rand_int_id(start=60,
+                                           end=CONF.queuing.max_claim_ttl)
+        claim_grace = data_utils.rand_int_id(start=60,
+                                             end=CONF.queuing.max_claim_grace)
+        claim_body = {"ttl": claim_ttl, "grace": claim_grace}
+        resp, body = self.client.post_claims(queue_name=self.queue_name,
+                                             rbody=claim_body)
+
+        return resp, body
+
+    @test.attr(type='smoke')
+    def test_post_claim(self):
+        _, body = self._post_and_claim_messages(queue_name=self.queue_name)
+        claimed_message_uri = body[0]['href']
+
+        # Skipping this step till bug-1331517  is fixed
+        # Get posted claim
+        # self.client.query_claim(claimed_message_uri)
+
+        # Delete Claimed message
+        self.client.delete_messages(claimed_message_uri)
+
+    @test.skip_because(bug="1331517")
+    @test.attr(type='smoke')
+    def test_query_claim(self):
+        # Post a Claim
+        resp, body = self._post_and_claim_messages(queue_name=self.queue_name)
+
+        # Query Claim
+        claim_uri = resp['location']
+        self.client.query_claim(claim_uri)
+
+        # Delete Claimed message
+        claimed_message_uri = body[0]['href']
+        self.delete_messages(claimed_message_uri)
+
+    @test.skip_because(bug="1328111")
+    @test.attr(type='smoke')
+    def test_update_claim(self):
+        # Post a Claim
+        resp, body = self._post_and_claim_messages(queue_name=self.queue_name)
+
+        claim_uri = resp['location']
+        claimed_message_uri = body[0]['href']
+
+        # Update Claim
+        claim_ttl = data_utils.rand_int_id(start=60,
+                                           end=CONF.queuing.max_claim_ttl)
+        update_rbody = {"ttl": claim_ttl}
+
+        self.client.update_claim(claim_uri, rbody=update_rbody)
+
+        # Verify claim ttl >= updated ttl value
+        _, body = self.client.query_claim(claim_uri)
+        updated_claim_ttl = body["ttl"]
+        self.assertTrue(updated_claim_ttl >= claim_ttl)
+
+        # Delete Claimed message
+        self.client.delete_messages(claimed_message_uri)
+
+    @test.attr(type='smoke')
+    def test_release_claim(self):
+        # Post a Claim
+        resp, body = self._post_and_claim_messages(queue_name=self.queue_name)
+        claim_uri = resp['location']
+
+        # Release Claim
+        self.client.release_claim(claim_uri)
+
+        # Delete Claimed message
+        # This will implicitly verify that the claim is deleted.
+        message_uri = urlparse.urlparse(claim_uri).path
+        self.client.delete_messages(message_uri)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.delete_queue(cls.queue_name)
+        super(TestClaims, cls).tearDownClass()
diff --git a/tempest/api/queuing/test_messages.py b/tempest/api/queuing/test_messages.py
new file mode 100644
index 0000000..9546c91
--- /dev/null
+++ b/tempest/api/queuing/test_messages.py
@@ -0,0 +1,122 @@
+# Copyright (c) 2014 Rackspace, Inc.
+#
+# 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 logging
+
+from tempest.api.queuing import base
+from tempest.common.utils import data_utils
+from tempest import config
+from tempest import test
+
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+
+
+class TestMessages(base.BaseQueuingTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestMessages, cls).setUpClass()
+        cls.queue_name = data_utils.rand_name('Queues-Test')
+        # Create Queue
+        cls.client.create_queue(cls.queue_name)
+
+    def _post_messages(self, repeat=CONF.queuing.max_messages_per_page):
+        message_body = self.generate_message_body(repeat=repeat)
+        resp, body = self.post_messages(queue_name=self.queue_name,
+                                        rbody=message_body)
+        return resp, body
+
+    @test.attr(type='smoke')
+    def test_post_messages(self):
+        # Post Messages
+        resp, _ = self._post_messages()
+
+        # Get on the posted messages
+        message_uri = resp['location']
+        resp, _ = self.client.get_multiple_messages(message_uri)
+        # The test has an assertion here, because the response cannot be 204
+        # in this case (the client allows 200 or 204 for this API call).
+        self.assertEqual('200', resp['status'])
+
+    @test.attr(type='smoke')
+    def test_list_messages(self):
+        # Post Messages
+        self._post_messages()
+
+        # List Messages
+        resp, _ = self.list_messages(queue_name=self.queue_name)
+        # The test has an assertion here, because the response cannot be 204
+        # in this case (the client allows 200 or 204 for this API call).
+        self.assertEqual('200', resp['status'])
+
+    @test.attr(type='smoke')
+    def test_get_message(self):
+        # Post Messages
+        _, body = self._post_messages()
+        message_uri = body['resources'][0]
+
+        # Get posted message
+        resp, _ = self.client.get_single_message(message_uri)
+        # The test has an assertion here, because the response cannot be 204
+        # in this case (the client allows 200 or 204 for this API call).
+        self.assertEqual('200', resp['status'])
+
+    @test.attr(type='smoke')
+    def test_get_multiple_messages(self):
+        # Post Messages
+        resp, _ = self._post_messages()
+        message_uri = resp['location']
+
+        # Get posted messages
+        resp, _ = self.client.get_multiple_messages(message_uri)
+        # The test has an assertion here, because the response cannot be 204
+        # in this case (the client allows 200 or 204 for this API call).
+        self.assertEqual('200', resp['status'])
+
+    @test.attr(type='smoke')
+    def test_delete_single_message(self):
+        # Post Messages
+        _, body = self._post_messages()
+        message_uri = body['resources'][0]
+
+        # Delete posted message & verify the delete operration
+        self.client.delete_messages(message_uri)
+
+        message_uri = message_uri.replace('/messages/', '/messages?ids=')
+        resp, _ = self.client.get_multiple_messages(message_uri)
+        # The test has an assertion here, because the response has to be 204
+        # in this case (the client allows 200 or 204 for this API call).
+        self.assertEqual('204', resp['status'])
+
+    @test.attr(type='smoke')
+    def test_delete_multiple_messages(self):
+        # Post Messages
+        resp, _ = self._post_messages()
+        message_uri = resp['location']
+
+        # Delete multiple messages
+        self.client.delete_messages(message_uri)
+        resp, _ = self.client.get_multiple_messages(message_uri)
+        # The test has an assertion here, because the response has to be 204
+        # in this case (the client allows 200 or 204 for this API call).
+        self.assertEqual('204', resp['status'])
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.delete_queue(cls.queue_name)
+        super(TestMessages, cls).tearDownClass()
diff --git a/tempest/api_schema/queuing/v1/queues.py b/tempest/api_schema/queuing/v1/queues.py
index 4630e1c..f0b2691 100644
--- a/tempest/api_schema/queuing/v1/queues.py
+++ b/tempest/api_schema/queuing/v1/queues.py
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+
 list_link = {
     'type': 'object',
     'properties': {
@@ -58,6 +59,11 @@
     }
 }
 
+age = {
+    'type': 'number',
+    'minimum': 0
+}
+
 message_link = {
     'type': 'object',
     'properties': {
@@ -65,7 +71,7 @@
             'type': 'string',
             'format': 'uri'
         },
-        'age': {'type': 'number'},
+        'age': age,
         'created': {
             'type': 'string',
             'format': 'date-time'
@@ -96,3 +102,136 @@
         'required': ['messages']
     }
 }
+
+resource_schema = {
+    'type': 'array',
+    'items': 'string',
+    'minItems': 1
+}
+
+post_messages = {
+    'status_code': [201],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'resources': resource_schema,
+            'partial': {'type': 'boolean'}
+        }
+    },
+    'required': ['resources', 'partial']
+}
+
+message_ttl = {
+    'type': 'number',
+    'minimum': 1
+}
+
+list_messages_links = {
+    'type': 'array',
+    'maxItems': 1,
+    'minItems': 1,
+    'items': {
+        'type': 'object',
+        'properties': {
+            'rel': {'type': 'string'},
+            'href': {'type': 'string'}
+        },
+        'required': ['rel', 'href']
+    }
+}
+
+list_messages_response = {
+    'type': 'array',
+    'minItems': 1,
+    'items': {
+        'type': 'object',
+        'properties': {
+            'href': {'type': 'string'},
+            'ttl': message_ttl,
+            'age': age,
+            'body': {'type': 'object'}
+        },
+        'required': ['href', 'ttl', 'age', 'body']
+    }
+}
+
+list_messages = {
+    'status_code': [200, 204],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'links': list_messages_links,
+            'messages': list_messages_response
+        }
+    },
+    'required': ['links', 'messages']
+}
+
+single_message = {
+    'type': 'object',
+    'properties': {
+        'href': {'type': 'string'},
+        'ttl': message_ttl,
+        'age': age,
+        'body': {'type': 'object'}
+    },
+    'required': ['href', 'ttl', 'age', 'body']
+}
+
+get_single_message = {
+    'status_code': [200],
+    'response_body': single_message
+}
+
+get_multiple_messages = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'array',
+        'items': single_message,
+        'minItems': 1
+    }
+}
+
+messages_claimed = {
+    'type': 'object',
+    'properties': {
+        'href': {
+            'type': 'string',
+            'format': 'uri'
+        },
+        'ttl': message_ttl,
+        'age': {'type': 'number'},
+        'body': {'type': 'object'}
+    },
+    'required': ['href', 'ttl', 'age', 'body']
+}
+
+claim_messages = {
+    'status_code': [201, 204],
+    'response_body': {
+        'type': 'array',
+        'items': messages_claimed,
+        'minItems': 1
+    }
+}
+
+claim_ttl = {
+    'type': 'number',
+    'minimum': 1
+}
+
+query_claim = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'age': {'type': 'number'},
+            'ttl': claim_ttl,
+            'messages': {
+                'type': 'array',
+                'minItems': 1
+            }
+        },
+        'required': ['ttl', 'age', 'messages']
+    }
+}
diff --git a/tempest/config.py b/tempest/config.py
index 7d871cb..06c7bc0 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -455,6 +455,28 @@
                default=20,
                help='The maximum number of queue records per page when '
                     'listing queues'),
+    cfg.IntOpt('max_queue_metadata',
+               default=65536,
+               help='The maximum metadata size for a queue'),
+    cfg.IntOpt('max_messages_per_page',
+               default=20,
+               help='The maximum number of queue message per page when '
+                    'listing (or) posting messages'),
+    cfg.IntOpt('max_message_size',
+               default=262144,
+               help='The maximum size of a message body'),
+    cfg.IntOpt('max_messages_per_claim',
+               default=20,
+               help='The maximum number of messages per claim'),
+    cfg.IntOpt('max_message_ttl',
+               default=1209600,
+               help='The maximum ttl for a message'),
+    cfg.IntOpt('max_claim_ttl',
+               default=43200,
+               help='The maximum ttl for a claim'),
+    cfg.IntOpt('max_claim_grace',
+               default=43200,
+               help='The maximum grace period for a claim'),
 ]
 
 volume_group = cfg.OptGroup(name='volume',
diff --git a/tempest/services/queuing/json/queuing_client.py b/tempest/services/queuing/json/queuing_client.py
index e5978f5..031c9c6 100644
--- a/tempest/services/queuing/json/queuing_client.py
+++ b/tempest/services/queuing/json/queuing_client.py
@@ -14,11 +14,14 @@
 # limitations under the License.
 
 import json
+import urllib
 
 from tempest.api_schema.queuing.v1 import queues as queues_schema
 from tempest.common import rest_client
+from tempest.common.utils import data_utils
 from tempest import config
 
+
 CONF = config.CONF
 
 
@@ -30,11 +33,16 @@
         self.version = '1'
         self.uri_prefix = 'v{0}'.format(self.version)
 
+        client_id = data_utils.rand_uuid_hex()
+        self.headers = {'Client-ID': client_id}
+
     def list_queues(self):
         uri = '{0}/queues'.format(self.uri_prefix)
         resp, body = self.get(uri)
-        body = json.loads(body)
-        self.validate_response(queues_schema.list_queues, resp, body)
+
+        if resp['status'] != '204':
+            body = json.loads(body)
+            self.validate_response(queues_schema.list_queues, resp, body)
         return resp, body
 
     def create_queue(self, queue_name):
@@ -74,3 +82,80 @@
         uri = '{0}/queues/{1}/metadata'.format(self.uri_prefix, queue_name)
         resp, body = self.put(uri, body=json.dumps(rbody))
         return resp, body
+
+    def post_messages(self, queue_name, rbody):
+        uri = '{0}/queues/{1}/messages'.format(self.uri_prefix, queue_name)
+        resp, body = self.post(uri, body=json.dumps(rbody),
+                               extra_headers=True,
+                               headers=self.headers)
+
+        body = json.loads(body)
+        return resp, body
+
+    def list_messages(self, queue_name):
+        uri = '{0}/queues/{1}/messages?echo=True'.format(self.uri_prefix,
+                                                         queue_name)
+        resp, body = self.get(uri, extra_headers=True, headers=self.headers)
+
+        if resp['status'] != '204':
+            body = json.loads(body)
+            self.validate_response(queues_schema.list_messages, resp, body)
+
+        return resp, body
+
+    def get_single_message(self, message_uri):
+        resp, body = self.get(message_uri, extra_headers=True,
+                              headers=self.headers)
+        if resp['status'] != '204':
+            body = json.loads(body)
+            self.validate_response(queues_schema.get_single_message, resp,
+                                   body)
+        return resp, body
+
+    def get_multiple_messages(self, message_uri):
+        resp, body = self.get(message_uri, extra_headers=True,
+                              headers=self.headers)
+
+        if resp['status'] != '204':
+            body = json.loads(body)
+            self.validate_response(queues_schema.get_multiple_messages,
+                                   resp,
+                                   body)
+
+        return resp, body
+
+    def delete_messages(self, message_uri):
+        resp, body = self.delete(message_uri)
+        assert(resp['status'] == '204')
+        return resp, body
+
+    def post_claims(self, queue_name, rbody, url_params=False):
+        uri = '{0}/queues/{1}/claims'.format(self.uri_prefix, queue_name)
+        if url_params:
+            uri += '?%s' % urllib.urlencode(url_params)
+
+        resp, body = self.post(uri, body=json.dumps(rbody),
+                               extra_headers=True,
+                               headers=self.headers)
+
+        body = json.loads(body)
+        self.validate_response(queues_schema.claim_messages, resp, body)
+        return resp, body
+
+    def query_claim(self, claim_uri):
+        resp, body = self.get(claim_uri)
+
+        if resp['status'] != '204':
+            body = json.loads(body)
+            self.validate_response(queues_schema.query_claim, resp, body)
+        return resp, body
+
+    def update_claim(self, claim_uri, rbody):
+        resp, body = self.patch(claim_uri, body=json.dumps(rbody))
+        assert(resp['status'] == '204')
+        return resp, body
+
+    def release_claim(self, claim_uri):
+        resp, body = self.delete(claim_uri)
+        assert(resp['status'] == '204')
+        return resp, body