Add tests for Cinder volume list pagination
Cinder volume list allows pagination using 'volumes_links' but we are
not currently checking that we get them back in the response or the
validity of those links.
This patch adds pagination tests for lists with and without detail.
Change-Id: Ie24c8013f1d6981c3667a7c34ffb47f296a5fbbb
diff --git a/tempest/api/volume/v2/test_volumes_list.py b/tempest/api/volume/v2/test_volumes_list.py
index 4c774da..d1eb694 100644
--- a/tempest/api/volume/v2/test_volumes_list.py
+++ b/tempest/api/volume/v2/test_volumes_list.py
@@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from six.moves.urllib import parse
+
from tempest.api.volume import base
from tempest import test
@@ -87,3 +89,101 @@
_list_details_with_multiple_params()
_list_details_with_multiple_params(sort_dir='desc')
+
+ def _test_pagination(self, resource, ids=None, limit=1, **kwargs):
+ """Check list pagination functionality for a resource.
+
+ This method requests the list of resources and follows pagination
+ links.
+
+ If an iterable is supplied in ids it will check that all ids are
+ retrieved and that only those are listed, that we will get a next
+ link for an empty page if the number of items is divisible by used
+ limit (this is expected behavior).
+
+ We can specify number of items per request using limit argument.
+ """
+
+ # Get list method for the type of resource from the client
+ client = getattr(self, resource + '_client')
+ method = getattr(client, 'list_' + resource)
+
+ # Include limit in params for list request
+ params = kwargs.pop('params', {})
+ params['limit'] = limit
+
+ # Store remaining items we are expecting from list
+ if ids is not None:
+ remaining = list(ids)
+ else:
+ remaining = None
+
+ # Mark that we are not comming from a next link
+ next = None
+
+ while True:
+ # Get a list page
+ response = method(return_body=True, params=params, **kwargs)
+
+ # If we have to check ids
+ if remaining is not None:
+ # Confirm we receive expected number of elements
+ num_expected = min(len(remaining), limit)
+ self.assertEqual(num_expected, len(response[resource]),
+ 'Requested %(#expect)d but got %(#received)d '
+ % {'#expect': num_expected,
+ '#received': len(response[resource])})
+
+ # For each received element
+ for element in response[resource]:
+ element_id = element['id']
+ # Check it's one of expected ids
+ self.assertIn(element_id,
+ ids,
+ 'Id %(id)s is not in expected ids %(ids)s' %
+ {'id': element_id, 'ids': ids})
+ # If not in remaining, we have received it twice
+ self.assertIn(element_id,
+ remaining,
+ 'Id %s was received twice' % element_id)
+ # We no longer expect it
+ remaining.remove(element_id)
+
+ # If we come from a next link check that absolute url is the same
+ # as the one used for this request
+ if next:
+ self.assertEqual(next, response.response['content-location'])
+
+ # Get next from response
+ next = None
+ for link in response.get(resource + '_links', ()):
+ if link['rel'] == 'next':
+ next = link['href']
+ break
+
+ # Check if we have next and we shouldn't or the other way around
+ if remaining is not None:
+ if remaining or (num_expected and len(ids) % limit == 0):
+ self.assertIsNotNone(next, 'Missing link to next page')
+ else:
+ self.assertIsNone(next, 'Unexpected link to next page')
+
+ # If we can follow to the next page, get params from url to make
+ # request in the form of a relative URL
+ if next:
+ params = parse.urlparse(next).query
+
+ # If cannot follow make sure it's because we have finished
+ else:
+ self.assertListEqual([], remaining or [],
+ 'No more pages reported, but still '
+ 'missing ids %s' % remaining)
+ break
+
+ @test.idempotent_id('e9138a2c-f67b-4796-8efa-635c196d01de')
+ def test_volume_list_details_pagination(self):
+ self._test_pagination('volumes', ids=self.volume_id_list, detail=True)
+
+ @test.idempotent_id('af55e775-8e4b-4feb-8719-215c43b0238c')
+ def test_volume_list_pagination(self):
+ self._test_pagination('volumes', ids=self.volume_id_list, detail=False)
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index 65aa0f4..9731304 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -16,6 +16,7 @@
import json
import time
+import six
from six.moves.urllib import parse as urllib
from tempest_lib import exceptions as lib_exc
@@ -40,18 +41,57 @@
"""Return the element 'attachment' from input volumes."""
return volume['attachments'][0]
- def list_volumes(self, detail=False, params=None):
- """List all the volumes created."""
+ def _ext_get(self, url, key=None, status=200):
+ """Extended get method.
+
+ Retrieves requested url, checks that status is expected status and
+ return a ResponseBody, ResponseBodyList or ResponseBodyData depending
+ on received data's key entry.
+
+ If key is not specified or is None we will return the whole body in a
+ ResponseBody class.
+ """
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(status, resp.status)
+
+ if not key:
+ return service_client.ResponseBody(resp, body)
+ elif isinstance(body[key], dict):
+ return service_client.ResponseBody(resp, body[key])
+ elif isinstance(body[key], list):
+ return service_client.ResponseBodyList(resp, body[key])
+
+ return service_client.ResponseBodyData(resp, body[key])
+
+ def _prepare_params(self, params):
+ """Prepares params for use in get or _ext_get methods.
+
+ If params is a string it will be left as it is, but if it's not it will
+ be urlencoded.
+ """
+ if isinstance(params, six.string_types):
+ return params
+ return urllib.urlencode(params)
+
+ def list_volumes(self, detail=False, params=None, return_body=False):
+ """List all the volumes created.
+
+ Params can be a string (must be urlencoded) or a dictionary.
+ If return_body is True then we will return the whole response body in
+ a ResponseBody class, it it's False or has not been specified we will
+ return only the list of volumes in a ResponseBodyList (inherits from
+ list).
+ """
url = 'volumes'
if detail:
url += '/detail'
if params:
- url += '?%s' % urllib.urlencode(params)
+ url += '?%s' % self._prepare_params(params)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBodyList(resp, body['volumes'])
+ key = None if return_body else 'volumes'
+ return self._ext_get(url, key)
def show_volume(self, volume_id):
"""Returns the details of a single volume."""