diff --git a/tests/utils.py b/tests/utils.py index 8f8c255d..b2849e5f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,6 +16,7 @@ import copy import json import six +import six.moves.urllib.parse as urlparse import testtools from glanceclient.v2.schemas import Schema @@ -28,11 +29,13 @@ class FakeAPI(object): def _request(self, method, url, headers=None, data=None, content_length=None): - call = (method, url, headers or {}, data) + call = build_call_record(method, sort_url_by_query_keys(url), + headers or {}, data) if content_length is not None: call = tuple(list(call) + [content_length]) self.calls.append(call) - fixture = self.fixtures[url][method] + + fixture = self.fixtures[sort_url_by_query_keys(url)][method] data = fixture[1] if isinstance(fixture[1], six.string_types): @@ -165,3 +168,41 @@ class FakeNoTTYStdout(FakeTTYStdout): def isatty(self): return False + + +def sort_url_by_query_keys(url): + """A helper function which sorts the keys of the query string of a url. + For example, an input of '/v2/tasks?sort_key=id&sort_dir=asc&limit=10' + returns '/v2/tasks?limit=10&sort_dir=asc&sort_key=id'. This is to + prevent non-deterministic ordering of the query string causing + problems with unit tests. + :param url: url which will be ordered by query keys + :returns url: url with ordered query keys + """ + parsed = urlparse.urlparse(url) + queries = urlparse.parse_qsl(parsed.query, True) + sorted_query = sorted(queries, key=lambda x: x[0]) + + encoded_sorted_query = urlparse.urlencode(sorted_query, True) + + url_parts = (parsed.scheme, parsed.netloc, parsed.path, + parsed.params, encoded_sorted_query, + parsed.fragment) + + return urlparse.urlunparse(url_parts) + + +def build_call_record(method, url, headers, data): + """Key the request body be ordered if it's a dict type. + """ + if isinstance(data, dict): + data = sorted(data.items()) + if isinstance(data, six.string_types): + # NOTE(flwang): For image update, the data will be a 'list' which + # contains operation dict, such as: [{"op": "remove", "path": "/a"}] + try: + data = json.loads(data) + except ValueError: + return (method, url, headers or {}, data) + data = [sorted(d.items()) for d in data] + return (method, url, headers or {}, data) diff --git a/tests/v1/test_image_members.py b/tests/v1/test_image_members.py index 20921e30..8cf8f55d 100644 --- a/tests/v1/test_image_members.py +++ b/tests/v1/test_image_members.py @@ -92,7 +92,8 @@ class ImageMemberManagerTest(testtools.TestCase): def test_create(self): self.mgr.create(self.image, '1', can_share=True) expect_body = {'member': {'can_share': True}} - expect = [('PUT', '/v1/images/1/members/1', {}, expect_body)] + expect = [('PUT', '/v1/images/1/members/1', {}, + sorted(expect_body.items()))] self.assertEqual(expect, self.api.calls) def test_replace(self): @@ -101,7 +102,8 @@ class ImageMemberManagerTest(testtools.TestCase): {'member_id': '3'}, ] self.mgr.replace(self.image, body) - expect = [('PUT', '/v1/images/1/members', {}, {'memberships': body})] + expect = [('PUT', '/v1/images/1/members', {}, + sorted({'memberships': body}.items()))] self.assertEqual(expect, self.api.calls) def test_replace_objects(self): @@ -118,5 +120,6 @@ class ImageMemberManagerTest(testtools.TestCase): {'member_id': '3', 'can_share': True}, ], } - expect = [('PUT', '/v1/images/1/members', {}, expect_body)] + expect = [('PUT', '/v1/images/1/members', {}, + sorted(expect_body.items()))] self.assertEqual(expect, self.api.calls) diff --git a/tests/v1/test_images.py b/tests/v1/test_images.py index 5c1be09e..1cf1794f 100644 --- a/tests/v1/test_images.py +++ b/tests/v1/test_images.py @@ -158,7 +158,7 @@ fixtures = { ]}, ), }, - '/v1/images/detail?marker=a&limit=20': { + '/v1/images/detail?limit=20&marker=a': { 'GET': ( {}, {'images': [ @@ -187,7 +187,7 @@ fixtures = { ]}, ), }, - '/v1/images/detail?marker=a&limit=1': { + '/v1/images/detail?limit=1&marker=a': { 'GET': ( {}, {'images': [ @@ -216,7 +216,7 @@ fixtures = { ]}, ), }, - '/v1/images/detail?marker=b&limit=2': { + '/v1/images/detail?limit=2&marker=b': { 'GET': ( {}, {'images': [ @@ -245,7 +245,7 @@ fixtures = { ]}, ), }, - '/v1/images/detail?property-ping=pong&limit=20': + '/v1/images/detail?limit=20&property-ping=pong': { 'GET': ( {}, @@ -258,7 +258,7 @@ fixtures = { ]}, ), }, - '/v1/images/detail?sort_dir=desc&limit=20': { + '/v1/images/detail?limit=20&sort_dir=desc': { 'GET': ( {}, {'images': [ @@ -275,7 +275,7 @@ fixtures = { ]}, ), }, - '/v1/images/detail?sort_key=name&limit=20': { + '/v1/images/detail?limit=20&sort_key=name': { 'GET': ( {}, {'images': [ @@ -441,7 +441,7 @@ class ImageManagerTest(testtools.TestCase): images = list(self.mgr.list(page_size=2)) expect = [ ('GET', '/v1/images/detail?limit=2', {}, None), - ('GET', '/v1/images/detail?marker=b&limit=2', {}, None), + ('GET', '/v1/images/detail?limit=2&marker=b', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(3, len(images)) @@ -459,7 +459,7 @@ class ImageManagerTest(testtools.TestCase): images = list(self.mgr.list(page_size=1, limit=2)) expect = [ ('GET', '/v1/images/detail?limit=1', {}, None), - ('GET', '/v1/images/detail?marker=a&limit=1', {}, None), + ('GET', '/v1/images/detail?limit=1&marker=a', {}, None), ] self.assertEqual(2, len(images)) self.assertEqual('a', images[0].id) @@ -468,7 +468,7 @@ class ImageManagerTest(testtools.TestCase): def test_list_with_marker(self): list(self.mgr.list(marker='a')) - url = '/v1/images/detail?marker=a&limit=20' + url = '/v1/images/detail?limit=20&marker=a' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) @@ -480,19 +480,19 @@ class ImageManagerTest(testtools.TestCase): def test_list_with_property_filters(self): list(self.mgr.list(filters={'properties': {'ping': 'pong'}})) - url = '/v1/images/detail?property-ping=pong&limit=20' + url = '/v1/images/detail?limit=20&property-ping=pong' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_sort_dir(self): list(self.mgr.list(sort_dir='desc')) - url = '/v1/images/detail?sort_dir=desc&limit=20' + url = '/v1/images/detail?limit=20&sort_dir=desc' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_sort_key(self): list(self.mgr.list(sort_key='name')) - url = '/v1/images/detail?sort_key=name&limit=20' + url = '/v1/images/detail?limit=20&sort_key=name' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) diff --git a/tests/v2/test_images.py b/tests/v2/test_images.py index 8e300748..a314a24f 100644 --- a/tests/v2/test_images.py +++ b/tests/v2/test_images.py @@ -14,7 +14,6 @@ # under the License. import errno -import json import six import testtools @@ -226,7 +225,7 @@ data_fixtures = { {'images': []}, ), }, - '/v2/images?owner=%s&limit=%d' % (_OWNER_ID, images.DEFAULT_PAGE_SIZE): { + '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _OWNER_ID): { 'GET': ( {}, {'images': [ @@ -236,14 +235,14 @@ data_fixtures = { ]}, ), }, - '/v2/images?owner=%s&limit=%d' % (_BOGUS_ID, images.DEFAULT_PAGE_SIZE): { + '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): { 'GET': ( {}, {'images': []}, ), }, - '/v2/images?owner=%s&limit=%d&member_status=pending&visibility=shared' - % (_BOGUS_ID, images.DEFAULT_PAGE_SIZE): { + '/v2/images?limit=%d&member_status=pending&owner=%s&visibility=shared' + % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): { 'GET': ( {}, {'images': [ @@ -551,7 +550,7 @@ class TestController(testtools.TestCase): 'image_size': 3} expect = [('PUT', '/v2/images/%s/file' % image_id, {'Content-Type': 'application/octet-stream'}, - body)] + sorted(body.items()))] self.assertEqual(expect, self.api.calls) def test_data_without_checksum(self): @@ -596,7 +595,8 @@ class TestController(testtools.TestCase): expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } - expect_body = '[{"path": "/name", "value": "pong", "op": "replace"}]' + expect_body = [[('op', 'replace'), ('path', '/name'), + ('value', 'pong')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), @@ -615,7 +615,7 @@ class TestController(testtools.TestCase): expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } - expect_body = '[{"path": "/finn", "value": "human", "op": "add"}]' + expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), @@ -634,7 +634,7 @@ class TestController(testtools.TestCase): expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } - expect_body = '[{"path": "/barney", "op": "remove"}]' + expect_body = [[('op', 'remove'), ('path', '/barney')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), @@ -655,8 +655,8 @@ class TestController(testtools.TestCase): expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } - expect_body = ('[{"path": "/barney", "value": "miller", ' - '"op": "replace"}]') + expect_body = ([[('op', 'replace'), ('path', '/barney'), + ('value', 'miller')]]) expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), @@ -677,7 +677,7 @@ class TestController(testtools.TestCase): expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } - expect_body = '[{"path": "/finn", "value": "human", "op": "add"}]' + expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), @@ -702,7 +702,7 @@ class TestController(testtools.TestCase): expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } - expect_body = '[{"path": "/color", "value": "red", "op": "add"}]' + expect_body = [[('op', 'add'), ('path', '/color'), ('value', 'red')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), @@ -718,7 +718,8 @@ class TestController(testtools.TestCase): expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } - expect_body = '[{"path": "/color", "value": "blue", "op": "replace"}]' + expect_body = [[('op', 'replace'), ('path', '/color'), + ('value', 'blue')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), @@ -755,10 +756,11 @@ class TestController(testtools.TestCase): def _patch_req(self, image_id, patch_body): c_type = 'application/openstack-images-v2.1-json-patch' + data = [sorted(d.items()) for d in patch_body] return ('PATCH', '/v2/images/%s' % image_id, {'Content-Type': c_type}, - json.dumps(patch_body)) + data) def test_add_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' diff --git a/tests/v2/test_metadefs_namespaces.py b/tests/v2/test_metadefs_namespaces.py index 8e1fcf5d..4267dd95 100644 --- a/tests/v2/test_metadefs_namespaces.py +++ b/tests/v2/test_metadefs_namespaces.py @@ -85,7 +85,7 @@ data_fixtures = { } ) }, - "/v2/metadefs/namespaces?marker=%s&limit=1" % NAMESPACE7: { + "/v2/metadefs/namespaces?limit=1&marker=%s" % NAMESPACE7: { "GET": ( {}, { diff --git a/tests/v2/test_metadefs_objects.py b/tests/v2/test_metadefs_objects.py index ca2f8c8a..701d5621 100644 --- a/tests/v2/test_metadefs_objects.py +++ b/tests/v2/test_metadefs_objects.py @@ -273,8 +273,8 @@ class TestObjectController(testtools.TestCase): def test_get_object(self): obj = self.controller.get(NAMESPACE1, OBJECT1) self.assertEqual(OBJECT1, obj.name) - self.assertEqual([PROPERTY1, PROPERTY2], - list(six.iterkeys(obj.properties))) + self.assertEqual(sorted([PROPERTY1, PROPERTY2]), + sorted(list(six.iterkeys(obj.properties)))) def test_create_object(self): properties = { diff --git a/tests/v2/test_metadefs_properties.py b/tests/v2/test_metadefs_properties.py index 7c50f139..d2ac25dd 100644 --- a/tests/v2/test_metadefs_properties.py +++ b/tests/v2/test_metadefs_properties.py @@ -245,7 +245,7 @@ class TestPropertyController(testtools.TestCase): properties = list(self.controller.list(NAMESPACE1)) actual = [prop.name for prop in properties] - self.assertEqual([PROPERTY1, PROPERTY2], actual) + self.assertEqual(sorted([PROPERTY1, PROPERTY2]), sorted(actual)) def test_get_property(self): prop = self.controller.get(NAMESPACE1, PROPERTY1) diff --git a/tests/v2/test_schemas.py b/tests/v2/test_schemas.py index ff7ddc2c..2ea6bdb5 100644 --- a/tests/v2/test_schemas.py +++ b/tests/v2/test_schemas.py @@ -170,7 +170,6 @@ class TestSchemaBasedModel(testtools.TestCase): patch = original.patch expected = '[{"path": "/color", "op": "remove"}]' self.assertTrue(compare_json_patches(patch, expected)) - self.assertEqual(expected, patch) def test_patch_should_add_missing_custom_properties(self): obj = { diff --git a/tests/v2/test_tasks.py b/tests/v2/test_tasks.py index e4f2a1e5..2302f9b1 100644 --- a/tests/v2/test_tasks.py +++ b/tests/v2/test_tasks.py @@ -112,7 +112,7 @@ fixtures = { }, ), }, - '/v2/tasks?owner=%s&limit=%d' % (_OWNER_ID, tasks.DEFAULT_PAGE_SIZE): { + '/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _OWNER_ID): { 'GET': ( {}, {'tasks': [ @@ -122,7 +122,7 @@ fixtures = { ]}, ), }, - '/v2/tasks?status=processing&limit=%d' % (tasks.DEFAULT_PAGE_SIZE): { + '/v2/tasks?limit=%d&status=processing' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ @@ -132,7 +132,7 @@ fixtures = { ]}, ), }, - '/v2/tasks?type=import&limit=%d' % (tasks.DEFAULT_PAGE_SIZE): { + '/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ @@ -149,7 +149,7 @@ fixtures = { ]}, ), }, - '/v2/tasks?status=fake&limit=%d' % (tasks.DEFAULT_PAGE_SIZE): { + '/v2/tasks?limit=%d&status=fake' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ @@ -166,8 +166,7 @@ fixtures = { ]}, ), }, - '/v2/tasks?owner=%s&limit=%d' % (_FAKE_OWNER_ID, - tasks.DEFAULT_PAGE_SIZE): + '/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _FAKE_OWNER_ID): { 'GET': ({}, {'tasks': []},