summaryrefslogtreecommitdiff
path: root/glanceclient
diff options
context:
space:
mode:
authorStuart McLaren <stuart.mclaren@hp.com>2015-04-17 14:02:33 +0000
committerStuart McLaren <stuart.mclaren@hp.com>2015-04-18 17:42:20 +0000
commitf2a8a520e76a129039b3c4043aeb8db75582b8c8 (patch)
tree1bdef9cffd95d747c515d16e6ea0bcea90cf8b54 /glanceclient
parent825c4a5df2e32a2d7c1665f0924cc5b9fa675673 (diff)
downloadpython-glanceclient-f2a8a520e76a129039b3c4043aeb8db75582b8c8.tar.gz
Move unit tests to standard directory
This patch moves the glanceclient unit tests to the standard directory (xxxclient/tests/unit) in preparation for adding functional gate tests 'check-glanceclient-dsvm-functional' in the same vein as existing client tests for other projects, eg: * check-novaclient-dsvm-functional * check-keystoneclient-dsvm-functional * check-neutronclient-dsvm-functional Change-Id: I29d4b9e3a428c851575ee9afde40d6df583456c4
Diffstat (limited to 'glanceclient')
-rw-r--r--glanceclient/tests/__init__.py0
-rw-r--r--glanceclient/tests/unit/__init__.py0
-rw-r--r--glanceclient/tests/unit/test_base.py57
-rw-r--r--glanceclient/tests/unit/test_client.py46
-rw-r--r--glanceclient/tests/unit/test_exc.py70
-rw-r--r--glanceclient/tests/unit/test_http.py326
-rw-r--r--glanceclient/tests/unit/test_progressbar.py75
-rw-r--r--glanceclient/tests/unit/test_shell.py512
-rw-r--r--glanceclient/tests/unit/test_ssl.py452
-rw-r--r--glanceclient/tests/unit/test_utils.py165
-rw-r--r--glanceclient/tests/unit/v1/__init__.py0
-rw-r--r--glanceclient/tests/unit/v1/test_image_members.py125
-rw-r--r--glanceclient/tests/unit/v1/test_images.py963
-rw-r--r--glanceclient/tests/unit/v1/test_shell.py503
-rw-r--r--glanceclient/tests/unit/v2/__init__.py0
-rw-r--r--glanceclient/tests/unit/v2/test_images.py1098
-rw-r--r--glanceclient/tests/unit/v2/test_members.py120
-rw-r--r--glanceclient/tests/unit/v2/test_metadefs_namespaces.py674
-rw-r--r--glanceclient/tests/unit/v2/test_metadefs_objects.py323
-rw-r--r--glanceclient/tests/unit/v2/test_metadefs_properties.py300
-rw-r--r--glanceclient/tests/unit/v2/test_metadefs_resource_types.py186
-rw-r--r--glanceclient/tests/unit/v2/test_schemas.py231
-rw-r--r--glanceclient/tests/unit/v2/test_shell_v2.py1094
-rw-r--r--glanceclient/tests/unit/v2/test_tags.py81
-rw-r--r--glanceclient/tests/unit/v2/test_tasks.py285
-rw-r--r--glanceclient/tests/unit/var/ca.crt34
-rw-r--r--glanceclient/tests/unit/var/certificate.crt66
-rw-r--r--glanceclient/tests/unit/var/expired-cert.crt35
-rw-r--r--glanceclient/tests/unit/var/privatekey.key51
-rw-r--r--glanceclient/tests/unit/var/wildcard-certificate.crt61
-rw-r--r--glanceclient/tests/unit/var/wildcard-san-certificate.crt54
-rw-r--r--glanceclient/tests/utils.py215
32 files changed, 8202 insertions, 0 deletions
diff --git a/glanceclient/tests/__init__.py b/glanceclient/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glanceclient/tests/__init__.py
diff --git a/glanceclient/tests/unit/__init__.py b/glanceclient/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glanceclient/tests/unit/__init__.py
diff --git a/glanceclient/tests/unit/test_base.py b/glanceclient/tests/unit/test_base.py
new file mode 100644
index 0000000..4a97de8
--- /dev/null
+++ b/glanceclient/tests/unit/test_base.py
@@ -0,0 +1,57 @@
+# Copyright 2013 OpenStack Foundation
+# Copyright (C) 2013 Yahoo! Inc.
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient.openstack.common.apiclient import base
+
+
+class TestBase(testtools.TestCase):
+
+ def test_resource_repr(self):
+ r = base.Resource(None, dict(foo="bar", baz="spam"))
+ self.assertEqual("<Resource baz=spam, foo=bar>", repr(r))
+
+ def test_getid(self):
+ self.assertEqual(4, base.getid(4))
+
+ class TmpObject(object):
+ id = 4
+ self.assertEqual(4, base.getid(TmpObject))
+
+ def test_two_resources_with_same_id_are_equal(self):
+ # Two resources of the same type with the same id: equal
+ r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
+ r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
+ self.assertEqual(r1, r2)
+
+ def test_two_resources_with_eq_info_are_equal(self):
+ # Two resources with no ID: equal if their info is equal
+ r1 = base.Resource(None, {'name': 'joe', 'age': 12})
+ r2 = base.Resource(None, {'name': 'joe', 'age': 12})
+ self.assertEqual(r1, r2)
+
+ def test_two_resources_with_diff_id_are_not_equal(self):
+ # Two resources with diff ID: not equal
+ r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
+ r2 = base.Resource(None, {'id': 2, 'name': 'hello'})
+ self.assertNotEqual(r1, r2)
+
+ def test_two_resources_with_not_eq_info_are_not_equal(self):
+ # Two resources with no ID: not equal if their info is not equal
+ r1 = base.Resource(None, {'name': 'bill', 'age': 21})
+ r2 = base.Resource(None, {'name': 'joe', 'age': 12})
+ self.assertNotEqual(r1, r2)
diff --git a/glanceclient/tests/unit/test_client.py b/glanceclient/tests/unit/test_client.py
new file mode 100644
index 0000000..62daaf5
--- /dev/null
+++ b/glanceclient/tests/unit/test_client.py
@@ -0,0 +1,46 @@
+# Copyright 2014 Red Hat, Inc.
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient import client
+from glanceclient.v1 import client as v1
+from glanceclient.v2 import client as v2
+
+
+class ClientTest(testtools.TestCase):
+
+ def test_no_endpoint_error(self):
+ self.assertRaises(ValueError, client.Client, None)
+
+ def test_endpoint(self):
+ gc = client.Client(1, "http://example.com")
+ self.assertEqual("http://example.com", gc.http_client.endpoint)
+ self.assertIsInstance(gc, v1.Client)
+
+ def test_versioned_endpoint(self):
+ gc = client.Client(1, "http://example.com/v2")
+ self.assertEqual("http://example.com", gc.http_client.endpoint)
+ self.assertIsInstance(gc, v1.Client)
+
+ def test_versioned_endpoint_no_version(self):
+ gc = client.Client(endpoint="http://example.com/v2")
+ self.assertEqual("http://example.com", gc.http_client.endpoint)
+ self.assertIsInstance(gc, v2.Client)
+
+ def test_versioned_endpoint_with_minor_revision(self):
+ gc = client.Client(2.2, "http://example.com/v2.1")
+ self.assertEqual("http://example.com", gc.http_client.endpoint)
+ self.assertIsInstance(gc, v2.Client)
diff --git a/glanceclient/tests/unit/test_exc.py b/glanceclient/tests/unit/test_exc.py
new file mode 100644
index 0000000..575c62b
--- /dev/null
+++ b/glanceclient/tests/unit/test_exc.py
@@ -0,0 +1,70 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 mock
+import testtools
+
+from glanceclient import exc
+
+HTML_MSG = """<html>
+ <head>
+ <title>404 Entity Not Found</title>
+ </head>
+ <body>
+ <h1>404 Entity Not Found</h1>
+ Entity could not be found
+ <br /><br />
+ </body>
+</html>"""
+
+
+class TestHTTPExceptions(testtools.TestCase):
+ def test_from_response(self):
+ """exc.from_response should return instance of an HTTP exception."""
+ mock_resp = mock.Mock()
+ mock_resp.status_code = 400
+ out = exc.from_response(mock_resp)
+ self.assertIsInstance(out, exc.HTTPBadRequest)
+
+ def test_handles_json(self):
+ """exc.from_response should not print JSON."""
+ mock_resp = mock.Mock()
+ mock_resp.status_code = 413
+ mock_resp.json.return_value = {
+ "overLimit": {
+ "code": 413,
+ "message": "OverLimit Retry...",
+ "details": "Error Details...",
+ "retryAt": "2014-12-03T13:33:06Z"
+ }
+ }
+ mock_resp.headers = {
+ "content-type": "application/json"
+ }
+ err = exc.from_response(mock_resp, "Non-empty body")
+ self.assertIsInstance(err, exc.HTTPOverLimit)
+ self.assertEqual("OverLimit Retry...", err.details)
+
+ def test_handles_html(self):
+ """exc.from_response should not print HTML."""
+ mock_resp = mock.Mock()
+ mock_resp.status_code = 404
+ mock_resp.text = HTML_MSG
+ mock_resp.headers = {
+ "content-type": "text/html"
+ }
+ err = exc.from_response(mock_resp, HTML_MSG)
+ self.assertIsInstance(err, exc.HTTPNotFound)
+ self.assertEqual("404 Entity Not Found: Entity could not be found",
+ err.details)
diff --git a/glanceclient/tests/unit/test_http.py b/glanceclient/tests/unit/test_http.py
new file mode 100644
index 0000000..e8bfaaa
--- /dev/null
+++ b/glanceclient/tests/unit/test_http.py
@@ -0,0 +1,326 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 json
+
+import mock
+import requests
+from requests_mock.contrib import fixture
+import six
+from six.moves.urllib import parse
+import testtools
+from testtools import matchers
+import types
+
+import glanceclient
+from glanceclient.common import http
+from glanceclient.common import https
+from glanceclient import exc
+from glanceclient.tests import utils
+
+
+class TestClient(testtools.TestCase):
+
+ def setUp(self):
+ super(TestClient, self).setUp()
+ self.mock = self.useFixture(fixture.Fixture())
+
+ self.endpoint = 'http://example.com:9292'
+ self.ssl_endpoint = 'https://example.com:9292'
+ self.client = http.HTTPClient(self.endpoint, token=u'abc123')
+
+ def test_identity_headers_and_token(self):
+ identity_headers = {
+ 'X-Auth-Token': 'auth_token',
+ 'X-User-Id': 'user',
+ 'X-Tenant-Id': 'tenant',
+ 'X-Roles': 'roles',
+ 'X-Identity-Status': 'Confirmed',
+ 'X-Service-Catalog': 'service_catalog',
+ }
+ #with token
+ kwargs = {'token': u'fake-token',
+ 'identity_headers': identity_headers}
+ http_client_object = http.HTTPClient(self.endpoint, **kwargs)
+ self.assertEqual('auth_token', http_client_object.auth_token)
+ self.assertTrue(http_client_object.identity_headers.
+ get('X-Auth-Token') is None)
+
+ def test_identity_headers_and_no_token_in_header(self):
+ identity_headers = {
+ 'X-User-Id': 'user',
+ 'X-Tenant-Id': 'tenant',
+ 'X-Roles': 'roles',
+ 'X-Identity-Status': 'Confirmed',
+ 'X-Service-Catalog': 'service_catalog',
+ }
+ #without X-Auth-Token in identity headers
+ kwargs = {'token': u'fake-token',
+ 'identity_headers': identity_headers}
+ http_client_object = http.HTTPClient(self.endpoint, **kwargs)
+ self.assertEqual(u'fake-token', http_client_object.auth_token)
+ self.assertTrue(http_client_object.identity_headers.
+ get('X-Auth-Token') is None)
+
+ def test_identity_headers_and_no_token_in_session_header(self):
+ # Tests that if token or X-Auth-Token are not provided in the kwargs
+ # when creating the http client, the session headers don't contain
+ # the X-Auth-Token key.
+ identity_headers = {
+ 'X-User-Id': 'user',
+ 'X-Tenant-Id': 'tenant',
+ 'X-Roles': 'roles',
+ 'X-Identity-Status': 'Confirmed',
+ 'X-Service-Catalog': 'service_catalog',
+ }
+ kwargs = {'identity_headers': identity_headers}
+ http_client_object = http.HTTPClient(self.endpoint, **kwargs)
+ self.assertIsNone(http_client_object.auth_token)
+ self.assertNotIn('X-Auth-Token', http_client_object.session.headers)
+
+ def test_identity_headers_are_passed(self):
+ # Tests that if token or X-Auth-Token are not provided in the kwargs
+ # when creating the http client, the session headers don't contain
+ # the X-Auth-Token key.
+ identity_headers = {
+ 'X-User-Id': b'user',
+ 'X-Tenant-Id': b'tenant',
+ 'X-Roles': b'roles',
+ 'X-Identity-Status': b'Confirmed',
+ 'X-Service-Catalog': b'service_catalog',
+ }
+ kwargs = {'identity_headers': identity_headers}
+ http_client = http.HTTPClient(self.endpoint, **kwargs)
+
+ path = '/v1/images/my-image'
+ self.mock.get(self.endpoint + path)
+ http_client.get(path)
+
+ headers = self.mock.last_request.headers
+ for k, v in six.iteritems(identity_headers):
+ self.assertEqual(v, headers[k])
+
+ def test_connection_refused(self):
+ """
+ Should receive a CommunicationError if connection refused.
+ And the error should list the host and port that refused the
+ connection
+ """
+ def cb(request, context):
+ raise requests.exceptions.ConnectionError()
+
+ path = '/v1/images/detail?limit=20'
+ self.mock.get(self.endpoint + path, text=cb)
+
+ comm_err = self.assertRaises(glanceclient.exc.CommunicationError,
+ self.client.get,
+ '/v1/images/detail?limit=20')
+
+ self.assertIn(self.endpoint, comm_err.message)
+
+ def test_http_encoding(self):
+ path = '/v1/images/detail'
+ text = 'Ok'
+ self.mock.get(self.endpoint + path, text=text,
+ headers={"Content-Type": "text/plain"})
+
+ headers = {"test": u'ni\xf1o'}
+ resp, body = self.client.get(path, headers=headers)
+ self.assertEqual(text, resp.text)
+
+ def test_headers_encoding(self):
+ value = u'ni\xf1o'
+ headers = {"test": value, "none-val": None}
+ encoded = self.client.encode_headers(headers)
+ self.assertEqual(b"ni\xc3\xb1o", encoded[b"test"])
+ self.assertNotIn("none-val", encoded)
+
+ def test_raw_request(self):
+ " Verify the path being used for HTTP requests reflects accurately. "
+ headers = {"Content-Type": "text/plain"}
+ text = 'Ok'
+ path = '/v1/images/detail'
+
+ self.mock.get(self.endpoint + path, text=text, headers=headers)
+
+ resp, body = self.client.get('/v1/images/detail', headers=headers)
+ self.assertEqual(headers, resp.headers)
+ self.assertEqual(text, resp.text)
+
+ def test_parse_endpoint(self):
+ endpoint = 'http://example.com:9292'
+ test_client = http.HTTPClient(endpoint, token=u'adc123')
+ actual = test_client.parse_endpoint(endpoint)
+ expected = parse.SplitResult(scheme='http',
+ netloc='example.com:9292', path='',
+ query='', fragment='')
+ self.assertEqual(expected, actual)
+
+ def test_get_connections_kwargs_http(self):
+ endpoint = 'http://example.com:9292'
+ test_client = http.HTTPClient(endpoint, token=u'adc123')
+ self.assertEqual(test_client.timeout, 600.0)
+
+ def test_http_chunked_request(self):
+ text = "Ok"
+ data = six.StringIO(text)
+ path = '/v1/images/'
+ self.mock.post(self.endpoint + path, text=text)
+
+ headers = {"test": u'chunked_request'}
+ resp, body = self.client.post(path, headers=headers, data=data)
+ self.assertIsInstance(self.mock.last_request.body, types.GeneratorType)
+ self.assertEqual(text, resp.text)
+
+ def test_http_json(self):
+ data = {"test": "json_request"}
+ path = '/v1/images'
+ text = 'OK'
+ self.mock.post(self.endpoint + path, text=text)
+
+ headers = {"test": u'chunked_request'}
+ resp, body = self.client.post(path, headers=headers, data=data)
+
+ self.assertEqual(text, resp.text)
+ self.assertIsInstance(self.mock.last_request.body, six.string_types)
+ self.assertEqual(data, json.loads(self.mock.last_request.body))
+
+ def test_http_chunked_response(self):
+ data = "TEST"
+ path = '/v1/images/'
+ self.mock.get(self.endpoint + path, body=six.StringIO(data),
+ headers={"Content-Type": "application/octet-stream"})
+
+ resp, body = self.client.get(path)
+ self.assertTrue(isinstance(body, types.GeneratorType))
+ self.assertEqual([data], list(body))
+
+ def test_log_http_response_with_non_ascii_char(self):
+ try:
+ response = 'Ok'
+ headers = {"Content-Type": "text/plain",
+ "test": "value1\xa5\xa6"}
+ fake = utils.FakeResponse(headers, six.StringIO(response))
+ self.client.log_http_response(fake)
+ except UnicodeDecodeError as e:
+ self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
+
+ def test_log_curl_request_with_non_ascii_char(self):
+ try:
+ headers = {'header1': 'value1\xa5\xa6'}
+ body = 'examplebody\xa5\xa6'
+ self.client.log_curl_request('GET', '/api/v1/\xa5', headers, body,
+ None)
+ except UnicodeDecodeError as e:
+ self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
+
+ @mock.patch('glanceclient.common.http.LOG.debug')
+ def test_log_curl_request_with_body_and_header(self, mock_log):
+ hd_name = 'header1'
+ hd_val = 'value1'
+ headers = {hd_name: hd_val}
+ body = 'examplebody'
+ self.client.log_curl_request('GET', '/api/v1/', headers, body, None)
+ self.assertTrue(mock_log.called, 'LOG.debug never called')
+ self.assertTrue(mock_log.call_args[0],
+ 'LOG.debug called with no arguments')
+ hd_regex = ".*\s-H\s+'\s*%s\s*:\s*%s\s*'.*" % (hd_name, hd_val)
+ self.assertThat(mock_log.call_args[0][0],
+ matchers.MatchesRegex(hd_regex),
+ 'header not found in curl command')
+ body_regex = ".*\s-d\s+'%s'\s.*" % body
+ self.assertThat(mock_log.call_args[0][0],
+ matchers.MatchesRegex(body_regex),
+ 'body not found in curl command')
+
+ def _test_log_curl_request_with_certs(self, mock_log, key, cert, cacert):
+ headers = {'header1': 'value1'}
+ http_client_object = http.HTTPClient(self.ssl_endpoint, key_file=key,
+ cert_file=cert, cacert=cacert,
+ token='fake-token')
+ http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
+ None)
+ self.assertTrue(mock_log.called, 'LOG.debug never called')
+ self.assertTrue(mock_log.call_args[0],
+ 'LOG.debug called with no arguments')
+
+ needles = {'key': key, 'cert': cert, 'cacert': cacert}
+ for option, value in six.iteritems(needles):
+ if value:
+ regex = ".*\s--%s\s+('%s'|%s).*" % (option, value, value)
+ self.assertThat(mock_log.call_args[0][0],
+ matchers.MatchesRegex(regex),
+ 'no --%s option in curl command' % option)
+ else:
+ regex = ".*\s--%s\s+.*" % option
+ self.assertThat(mock_log.call_args[0][0],
+ matchers.Not(matchers.MatchesRegex(regex)),
+ 'unexpected --%s option in curl command' %
+ option)
+
+ @mock.patch('glanceclient.common.http.LOG.debug')
+ def test_log_curl_request_with_all_certs(self, mock_log):
+ self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1',
+ 'cacert2')
+
+ @mock.patch('glanceclient.common.http.LOG.debug')
+ def test_log_curl_request_with_some_certs(self, mock_log):
+ self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', None)
+
+ @mock.patch('glanceclient.common.http.LOG.debug')
+ def test_log_curl_request_with_insecure_param(self, mock_log):
+ headers = {'header1': 'value1'}
+ http_client_object = http.HTTPClient(self.ssl_endpoint, insecure=True,
+ token='fake-token')
+ http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
+ None)
+ self.assertTrue(mock_log.called, 'LOG.debug never called')
+ self.assertTrue(mock_log.call_args[0],
+ 'LOG.debug called with no arguments')
+ self.assertThat(mock_log.call_args[0][0],
+ matchers.MatchesRegex('.*\s-k\s.*'),
+ 'no -k option in curl command')
+
+ @mock.patch('glanceclient.common.http.LOG.debug')
+ def test_log_curl_request_with_token_header(self, mock_log):
+ fake_token = 'fake-token'
+ headers = {'X-Auth-Token': fake_token}
+ http_client_object = http.HTTPClient(self.endpoint,
+ identity_headers=headers)
+ http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
+ None)
+ self.assertTrue(mock_log.called, 'LOG.debug never called')
+ self.assertTrue(mock_log.call_args[0],
+ 'LOG.debug called with no arguments')
+ token_regex = '.*%s.*' % fake_token
+ self.assertThat(mock_log.call_args[0][0],
+ matchers.Not(matchers.MatchesRegex(token_regex)),
+ 'token found in LOG.debug parameter')
+
+
+class TestVerifiedHTTPSConnection(testtools.TestCase):
+ """Test fixture for glanceclient.common.http.VerifiedHTTPSConnection."""
+
+ def test_setcontext_unable_to_load_cacert(self):
+ """Add this UT case with Bug#1265730."""
+ self.assertRaises(exc.SSLConfigurationError,
+ https.VerifiedHTTPSConnection,
+ "127.0.0.1",
+ None,
+ None,
+ None,
+ "gx_cacert",
+ None,
+ False,
+ True)
diff --git a/glanceclient/tests/unit/test_progressbar.py b/glanceclient/tests/unit/test_progressbar.py
new file mode 100644
index 0000000..1dd42a0
--- /dev/null
+++ b/glanceclient/tests/unit/test_progressbar.py
@@ -0,0 +1,75 @@
+# Copyright 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 sys
+
+import six
+import testtools
+
+from glanceclient.common import progressbar
+from glanceclient.tests import utils as test_utils
+
+
+class TestProgressBarWrapper(testtools.TestCase):
+
+ def test_iter_iterator_display_progress_bar(self):
+ size = 100
+ iterator = iter('X' * 100)
+ saved_stdout = sys.stdout
+ try:
+ sys.stdout = output = test_utils.FakeTTYStdout()
+ # Consume iterator.
+ data = list(progressbar.VerboseIteratorWrapper(iterator, size))
+ self.assertEqual(['X'] * 100, data)
+ self.assertEqual(
+ '[%s>] 100%%\n' % ('=' * 29),
+ output.getvalue()
+ )
+ finally:
+ sys.stdout = saved_stdout
+
+ def test_iter_file_display_progress_bar(self):
+ size = 98304
+ file_obj = six.StringIO('X' * size)
+ saved_stdout = sys.stdout
+ try:
+ sys.stdout = output = test_utils.FakeTTYStdout()
+ file_obj = progressbar.VerboseFileWrapper(file_obj, size)
+ chunksize = 1024
+ chunk = file_obj.read(chunksize)
+ while chunk:
+ chunk = file_obj.read(chunksize)
+ self.assertEqual(
+ '[%s>] 100%%\n' % ('=' * 29),
+ output.getvalue()
+ )
+ finally:
+ sys.stdout = saved_stdout
+
+ def test_iter_file_no_tty(self):
+ size = 98304
+ file_obj = six.StringIO('X' * size)
+ saved_stdout = sys.stdout
+ try:
+ sys.stdout = output = test_utils.FakeNoTTYStdout()
+ file_obj = progressbar.VerboseFileWrapper(file_obj, size)
+ chunksize = 1024
+ chunk = file_obj.read(chunksize)
+ while chunk:
+ chunk = file_obj.read(chunksize)
+ # If stdout is not a tty progress bar should do nothing.
+ self.assertEqual('', output.getvalue())
+ finally:
+ sys.stdout = saved_stdout
diff --git a/glanceclient/tests/unit/test_shell.py b/glanceclient/tests/unit/test_shell.py
new file mode 100644
index 0000000..ef15561
--- /dev/null
+++ b/glanceclient/tests/unit/test_shell.py
@@ -0,0 +1,512 @@
+# Copyright 2013 OpenStack Foundation
+# Copyright (C) 2013 Yahoo! Inc.
+# All Rights Reserved.
+#
+# 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 argparse
+import os
+import sys
+import uuid
+
+import fixtures
+from keystoneclient import exceptions as ks_exc
+from keystoneclient import fixture as ks_fixture
+import mock
+import requests
+from requests_mock.contrib import fixture as rm_fixture
+import six
+
+from glanceclient import exc
+from glanceclient import shell as openstack_shell
+
+from glanceclient.tests import utils
+#NOTE (esheffield) Used for the schema caching tests
+from glanceclient.v2 import schemas as schemas
+import json
+
+
+DEFAULT_IMAGE_URL = 'http://127.0.0.1:5000/'
+DEFAULT_USERNAME = 'username'
+DEFAULT_PASSWORD = 'password'
+DEFAULT_TENANT_ID = 'tenant_id'
+DEFAULT_TENANT_NAME = 'tenant_name'
+DEFAULT_PROJECT_ID = '0123456789'
+DEFAULT_USER_DOMAIN_NAME = 'user_domain_name'
+DEFAULT_UNVERSIONED_AUTH_URL = 'http://127.0.0.1:5000/'
+DEFAULT_V2_AUTH_URL = '%sv2.0' % DEFAULT_UNVERSIONED_AUTH_URL
+DEFAULT_V3_AUTH_URL = '%sv3' % DEFAULT_UNVERSIONED_AUTH_URL
+DEFAULT_AUTH_TOKEN = ' 3bcc3d3a03f44e3d8377f9247b0ad155'
+TEST_SERVICE_URL = 'http://127.0.0.1:5000/'
+
+FAKE_V2_ENV = {'OS_USERNAME': DEFAULT_USERNAME,
+ 'OS_PASSWORD': DEFAULT_PASSWORD,
+ 'OS_TENANT_NAME': DEFAULT_TENANT_NAME,
+ 'OS_AUTH_URL': DEFAULT_V2_AUTH_URL,
+ 'OS_IMAGE_URL': DEFAULT_IMAGE_URL}
+
+FAKE_V3_ENV = {'OS_USERNAME': DEFAULT_USERNAME,
+ 'OS_PASSWORD': DEFAULT_PASSWORD,
+ 'OS_PROJECT_ID': DEFAULT_PROJECT_ID,
+ 'OS_USER_DOMAIN_NAME': DEFAULT_USER_DOMAIN_NAME,
+ 'OS_AUTH_URL': DEFAULT_V3_AUTH_URL,
+ 'OS_IMAGE_URL': DEFAULT_IMAGE_URL}
+
+TOKEN_ID = uuid.uuid4().hex
+
+V2_TOKEN = ks_fixture.V2Token(token_id=TOKEN_ID)
+V2_TOKEN.set_scope()
+_s = V2_TOKEN.add_service('image', name='glance')
+_s.add_endpoint(DEFAULT_IMAGE_URL)
+
+V3_TOKEN = ks_fixture.V3Token()
+V3_TOKEN.set_project_scope()
+_s = V3_TOKEN.add_service('image', name='glance')
+_s.add_standard_endpoints(public=DEFAULT_IMAGE_URL)
+
+
+class ShellTest(utils.TestCase):
+ # auth environment to use
+ auth_env = FAKE_V2_ENV.copy()
+ # expected auth plugin to invoke
+ token_url = DEFAULT_V2_AUTH_URL + '/tokens'
+
+ # Patch os.environ to avoid required auth info
+ def make_env(self, exclude=None):
+ env = dict((k, v) for k, v in self.auth_env.items() if k != exclude)
+ self.useFixture(fixtures.MonkeyPatch('os.environ', env))
+
+ def setUp(self):
+ super(ShellTest, self).setUp()
+ global _old_env
+ _old_env, os.environ = os.environ, self.auth_env
+
+ self.requests = self.useFixture(rm_fixture.Fixture())
+
+ json_list = ks_fixture.DiscoveryList(DEFAULT_UNVERSIONED_AUTH_URL)
+ self.requests.get(DEFAULT_IMAGE_URL, json=json_list, status_code=300)
+
+ json_v2 = {'version': ks_fixture.V2Discovery(DEFAULT_V2_AUTH_URL)}
+ self.requests.get(DEFAULT_V2_AUTH_URL, json=json_v2)
+
+ json_v3 = {'version': ks_fixture.V3Discovery(DEFAULT_V3_AUTH_URL)}
+ self.requests.get(DEFAULT_V3_AUTH_URL, json=json_v3)
+
+ self.v2_auth = self.requests.post(DEFAULT_V2_AUTH_URL + '/tokens',
+ json=V2_TOKEN)
+
+ headers = {'X-Subject-Token': TOKEN_ID}
+ self.v3_auth = self.requests.post(DEFAULT_V3_AUTH_URL + '/auth/tokens',
+ headers=headers,
+ json=V3_TOKEN)
+
+ global shell, _shell, assert_called, assert_called_anytime
+ _shell = openstack_shell.OpenStackImagesShell()
+ shell = lambda cmd: _shell.main(cmd.split())
+
+ def tearDown(self):
+ super(ShellTest, self).tearDown()
+ global _old_env
+ os.environ = _old_env
+
+ def shell(self, argstr, exitcodes=(0,)):
+ orig = sys.stdout
+ orig_stderr = sys.stderr
+ try:
+ sys.stdout = six.StringIO()
+ sys.stderr = six.StringIO()
+ _shell = openstack_shell.OpenStackImagesShell()
+ _shell.main(argstr.split())
+ except SystemExit:
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ self.assertIn(exc_value.code, exitcodes)
+ finally:
+ stdout = sys.stdout.getvalue()
+ sys.stdout.close()
+ sys.stdout = orig
+ stderr = sys.stderr.getvalue()
+ sys.stderr.close()
+ sys.stderr = orig_stderr
+ return (stdout, stderr)
+
+ def test_help_unknown_command(self):
+ shell = openstack_shell.OpenStackImagesShell()
+ argstr = 'help foofoo'
+ self.assertRaises(exc.CommandError, shell.main, argstr.split())
+
+ def test_help(self):
+ shell = openstack_shell.OpenStackImagesShell()
+ argstr = 'help'
+ actual = shell.main(argstr.split())
+ self.assertEqual(0, actual)
+
+ def test_help_on_subcommand_error(self):
+ self.assertRaises(exc.CommandError, shell, 'help bad')
+
+ def test_get_base_parser(self):
+ test_shell = openstack_shell.OpenStackImagesShell()
+ actual_parser = test_shell.get_base_parser()
+ description = 'Command-line interface to the OpenStack Images API.'
+ expected = argparse.ArgumentParser(
+ prog='glance', usage=None,
+ description=description,
+ conflict_handler='error',
+ add_help=False,
+ formatter_class=openstack_shell.HelpFormatter,)
+ # NOTE(guochbo): Can't compare ArgumentParser instances directly
+ # Convert ArgumentPaser to string first.
+ self.assertEqual(str(expected), str(actual_parser))
+
+ @mock.patch.object(openstack_shell.OpenStackImagesShell,
+ '_get_versioned_client')
+ def test_cert_and_key_args_interchangeable(self,
+ mock_versioned_client):
+ # make sure --os-cert and --os-key are passed correctly
+ args = '--os-cert mycert --os-key mykey image-list'
+ shell(args)
+ assert mock_versioned_client.called
+ ((api_version, args), kwargs) = mock_versioned_client.call_args
+ self.assertEqual('mycert', args.os_cert)
+ self.assertEqual('mykey', args.os_key)
+
+ # make sure we get the same thing with --cert-file and --key-file
+ args = '--cert-file mycertfile --key-file mykeyfile image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ assert mock_versioned_client.called
+ ((api_version, args), kwargs) = mock_versioned_client.call_args
+ self.assertEqual('mycertfile', args.os_cert)
+ self.assertEqual('mykeyfile', args.os_key)
+
+ @mock.patch('glanceclient.v1.client.Client')
+ def test_no_auth_with_token_and_image_url_with_v1(self, v1_client):
+ # test no authentication is required if both token and endpoint url
+ # are specified
+ args = ('--os-auth-token mytoken --os-image-url https://image:1234/v1 '
+ 'image-list')
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ assert v1_client.called
+ (args, kwargs) = v1_client.call_args
+ self.assertEqual('mytoken', kwargs['token'])
+ self.assertEqual('https://image:1234', args[0])
+
+ @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas')
+ def test_no_auth_with_token_and_image_url_with_v2(self,
+ cache_schemas):
+ with mock.patch('glanceclient.v2.client.Client') as v2_client:
+ # test no authentication is required if both token and endpoint url
+ # are specified
+ args = ('--os-auth-token mytoken '
+ '--os-image-url https://image:1234/v2 '
+ '--os-image-api-version 2 image-list')
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ ((args), kwargs) = v2_client.call_args
+ self.assertEqual('https://image:1234', args[0])
+ self.assertEqual('mytoken', kwargs['token'])
+
+ def _assert_auth_plugin_args(self):
+ # make sure our auth plugin is invoked with the correct args
+ self.assertEqual(1, self.v2_auth.call_count)
+ self.assertFalse(self.v3_auth.called)
+
+ body = json.loads(self.v2_auth.last_request.body)
+
+ self.assertEqual(self.auth_env['OS_TENANT_NAME'],
+ body['auth']['tenantName'])
+ self.assertEqual(self.auth_env['OS_USERNAME'],
+ body['auth']['passwordCredentials']['username'])
+ self.assertEqual(self.auth_env['OS_PASSWORD'],
+ body['auth']['passwordCredentials']['password'])
+
+ @mock.patch('glanceclient.v1.client.Client')
+ def test_auth_plugin_invocation_with_v1(self, v1_client):
+ args = 'image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ self._assert_auth_plugin_args()
+
+ @mock.patch('glanceclient.v2.client.Client')
+ @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas')
+ def test_auth_plugin_invocation_with_v2(self,
+ v2_client,
+ cache_schemas):
+ args = '--os-image-api-version 2 image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ self._assert_auth_plugin_args()
+
+ @mock.patch('glanceclient.v1.client.Client')
+ def test_auth_plugin_invocation_with_unversioned_auth_url_with_v1(
+ self, v1_client):
+ args = '--os-auth-url %s image-list' % DEFAULT_UNVERSIONED_AUTH_URL
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ self._assert_auth_plugin_args()
+
+ @mock.patch('glanceclient.v2.client.Client')
+ @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas')
+ def test_auth_plugin_invocation_with_unversioned_auth_url_with_v2(
+ self, v2_client, cache_schemas):
+ args = ('--os-auth-url %s --os-image-api-version 2 '
+ 'image-list') % DEFAULT_UNVERSIONED_AUTH_URL
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ self._assert_auth_plugin_args()
+
+ @mock.patch('sys.stdin', side_effect=mock.MagicMock)
+ @mock.patch('getpass.getpass', return_value='password')
+ def test_password_prompted_with_v2(self, mock_getpass, mock_stdin):
+ self.requests.post(self.token_url, exc=requests.ConnectionError)
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.make_env(exclude='OS_PASSWORD')
+ self.assertRaises(ks_exc.ConnectionRefused,
+ glance_shell.main, ['image-list'])
+ # Make sure we are actually prompted.
+ mock_getpass.assert_called_with('OS Password: ')
+
+ @mock.patch('sys.stdin', side_effect=mock.MagicMock)
+ @mock.patch('getpass.getpass', side_effect=EOFError)
+ def test_password_prompted_ctrlD_with_v2(self, mock_getpass, mock_stdin):
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.make_env(exclude='OS_PASSWORD')
+ # We should get Command Error because we mock Ctl-D.
+ self.assertRaises(exc.CommandError, glance_shell.main, ['image-list'])
+ # Make sure we are actually prompted.
+ mock_getpass.assert_called_with('OS Password: ')
+
+ @mock.patch(
+ 'glanceclient.shell.OpenStackImagesShell._get_keystone_session')
+ @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas')
+ def test_no_auth_with_proj_name(self, cache_schemas, session):
+ with mock.patch('glanceclient.v2.client.Client'):
+ args = ('--os-project-name myname '
+ '--os-project-domain-name mydomain '
+ '--os-project-domain-id myid '
+ '--os-image-api-version 2 image-list')
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ ((args), kwargs) = session.call_args
+ self.assertEqual('myname', kwargs['project_name'])
+ self.assertEqual('mydomain', kwargs['project_domain_name'])
+ self.assertEqual('myid', kwargs['project_domain_id'])
+
+ @mock.patch.object(openstack_shell.OpenStackImagesShell, 'main')
+ def test_shell_keyboard_interrupt(self, mock_glance_shell):
+ # Ensure that exit code is 130 for KeyboardInterrupt
+ try:
+ mock_glance_shell.side_effect = KeyboardInterrupt()
+ openstack_shell.main()
+ except SystemExit as ex:
+ self.assertEqual(130, ex.code)
+
+ @mock.patch('glanceclient.v1.client.Client')
+ def test_auth_plugin_invocation_without_username_with_v1(self, v1_client):
+ self.make_env(exclude='OS_USERNAME')
+ args = 'image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.assertRaises(exc.CommandError, glance_shell.main, args.split())
+
+ @mock.patch('glanceclient.v2.client.Client')
+ def test_auth_plugin_invocation_without_username_with_v2(self, v2_client):
+ self.make_env(exclude='OS_USERNAME')
+ args = '--os-image-api-version 2 image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.assertRaises(exc.CommandError, glance_shell.main, args.split())
+
+ @mock.patch('glanceclient.v1.client.Client')
+ def test_auth_plugin_invocation_without_auth_url_with_v1(self, v1_client):
+ self.make_env(exclude='OS_AUTH_URL')
+ args = 'image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.assertRaises(exc.CommandError, glance_shell.main, args.split())
+
+ @mock.patch('glanceclient.v2.client.Client')
+ def test_auth_plugin_invocation_without_auth_url_with_v2(self, v2_client):
+ self.make_env(exclude='OS_AUTH_URL')
+ args = '--os-image-api-version 2 image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.assertRaises(exc.CommandError, glance_shell.main, args.split())
+
+ @mock.patch('glanceclient.v1.client.Client')
+ def test_auth_plugin_invocation_without_tenant_with_v1(self, v1_client):
+ if 'OS_TENANT_NAME' in os.environ:
+ self.make_env(exclude='OS_TENANT_NAME')
+ if 'OS_PROJECT_ID' in os.environ:
+ self.make_env(exclude='OS_PROJECT_ID')
+ args = 'image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.assertRaises(exc.CommandError, glance_shell.main, args.split())
+
+ @mock.patch('glanceclient.v2.client.Client')
+ def test_auth_plugin_invocation_without_tenant_with_v2(self, v2_client):
+ if 'OS_TENANT_NAME' in os.environ:
+ self.make_env(exclude='OS_TENANT_NAME')
+ if 'OS_PROJECT_ID' in os.environ:
+ self.make_env(exclude='OS_PROJECT_ID')
+ args = '--os-image-api-version 2 image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.assertRaises(exc.CommandError, glance_shell.main, args.split())
+
+
+class ShellTestWithKeystoneV3Auth(ShellTest):
+ # auth environment to use
+ auth_env = FAKE_V3_ENV.copy()
+ token_url = DEFAULT_V3_AUTH_URL + '/auth/tokens'
+
+ def _assert_auth_plugin_args(self):
+ self.assertFalse(self.v2_auth.called)
+ self.assertEqual(1, self.v3_auth.call_count)
+
+ body = json.loads(self.v3_auth.last_request.body)
+ user = body['auth']['identity']['password']['user']
+
+ self.assertEqual(self.auth_env['OS_USERNAME'], user['name'])
+ self.assertEqual(self.auth_env['OS_PASSWORD'], user['password'])
+ self.assertEqual(self.auth_env['OS_USER_DOMAIN_NAME'],
+ user['domain']['name'])
+ self.assertEqual(self.auth_env['OS_PROJECT_ID'],
+ body['auth']['scope']['project']['id'])
+
+ @mock.patch('glanceclient.v1.client.Client')
+ def test_auth_plugin_invocation_with_v1(self, v1_client):
+ args = 'image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ self._assert_auth_plugin_args()
+
+ @mock.patch('glanceclient.v2.client.Client')
+ @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas')
+ def test_auth_plugin_invocation_with_v2(self, v2_client, cache_schemas):
+ args = '--os-image-api-version 2 image-list'
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ glance_shell.main(args.split())
+ self._assert_auth_plugin_args()
+
+ @mock.patch('keystoneclient.discover.Discover',
+ side_effect=ks_exc.ClientException())
+ def test_api_discovery_failed_with_unversioned_auth_url(self,
+ discover):
+ args = '--os-auth-url %s image-list' % DEFAULT_UNVERSIONED_AUTH_URL
+ glance_shell = openstack_shell.OpenStackImagesShell()
+ self.assertRaises(exc.CommandError, glance_shell.main, args.split())
+
+ def test_bash_completion(self):
+ stdout, stderr = self.shell('bash_completion')
+ # just check we have some output
+ required = [
+ '--status',
+ 'image-create',
+ 'help',
+ '--size']
+ for r in required:
+ self.assertIn(r, stdout.split())
+ avoided = [
+ 'bash_completion',
+ 'bash-completion']
+ for r in avoided:
+ self.assertNotIn(r, stdout.split())
+
+
+class ShellCacheSchemaTest(utils.TestCase):
+ def setUp(self):
+ super(ShellCacheSchemaTest, self).setUp()
+ self._mock_client_setup()
+ self._mock_shell_setup()
+ self.cache_dir = '/dir_for_cached_schema'
+ self.cache_files = [self.cache_dir + '/image_schema.json',
+ self.cache_dir + '/namespace_schema.json',
+ self.cache_dir + '/resource_type_schema.json']
+
+ def tearDown(self):
+ super(ShellCacheSchemaTest, self).tearDown()
+
+ def _mock_client_setup(self):
+ self.schema_dict = {
+ 'name': 'image',
+ 'properties': {
+ 'name': {'type': 'string', 'description': 'Name of image'},
+ },
+ }
+
+ self.client = mock.Mock()
+ self.client.schemas.get.return_value = schemas.Schema(self.schema_dict)
+
+ def _mock_shell_setup(self):
+ mocked_get_client = mock.MagicMock(return_value=self.client)
+ self.shell = openstack_shell.OpenStackImagesShell()
+ self.shell._get_versioned_client = mocked_get_client
+
+ def _make_args(self, args):
+ class Args():
+ def __init__(self, entries):
+ self.__dict__.update(entries)
+
+ return Args(args)
+
+ @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
+ @mock.patch('os.path.exists', return_value=True)
+ def test_cache_schemas_gets_when_forced(self, exists_mock):
+ options = {
+ 'get_schema': True
+ }
+
+ self.shell._cache_schemas(self._make_args(options),
+ home_dir=self.cache_dir)
+
+ self.assertEqual(12, open.mock_calls.__len__())
+ self.assertEqual(mock.call(self.cache_files[0], 'w'),
+ open.mock_calls[0])
+ self.assertEqual(mock.call(self.cache_files[1], 'w'),
+ open.mock_calls[4])
+ self.assertEqual(mock.call().write(json.dumps(self.schema_dict)),
+ open.mock_calls[2])
+ self.assertEqual(mock.call().write(json.dumps(self.schema_dict)),
+ open.mock_calls[6])
+
+ @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
+ @mock.patch('os.path.exists', side_effect=[True, False, False, False])
+ def test_cache_schemas_gets_when_not_exists(self, exists_mock):
+ options = {
+ 'get_schema': False
+ }
+
+ self.shell._cache_schemas(self._make_args(options),
+ home_dir=self.cache_dir)
+
+ self.assertEqual(12, open.mock_calls.__len__())
+ self.assertEqual(mock.call(self.cache_files[0], 'w'),
+ open.mock_calls[0])
+ self.assertEqual(mock.call(self.cache_files[1], 'w'),
+ open.mock_calls[4])
+ self.assertEqual(mock.call().write(json.dumps(self.schema_dict)),
+ open.mock_calls[2])
+ self.assertEqual(mock.call().write(json.dumps(self.schema_dict)),
+ open.mock_calls[6])
+
+ @mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
+ @mock.patch('os.path.exists', return_value=True)
+ def test_cache_schemas_leaves_when_present_not_forced(self, exists_mock):
+ options = {
+ 'get_schema': False
+ }
+
+ self.shell._cache_schemas(self._make_args(options),
+ home_dir=self.cache_dir)
+
+ os.path.exists.assert_any_call(self.cache_dir)
+ os.path.exists.assert_any_call(self.cache_files[0])
+ os.path.exists.assert_any_call(self.cache_files[1])
+ self.assertEqual(4, exists_mock.call_count)
+ self.assertEqual(0, open.mock_calls.__len__())
diff --git a/glanceclient/tests/unit/test_ssl.py b/glanceclient/tests/unit/test_ssl.py
new file mode 100644
index 0000000..907b7bf
--- /dev/null
+++ b/glanceclient/tests/unit/test_ssl.py
@@ -0,0 +1,452 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 os
+
+from OpenSSL import crypto
+from OpenSSL import SSL
+try:
+ from requests.packages.urllib3 import poolmanager
+except ImportError:
+ from urllib3 import poolmanager
+import six
+import ssl
+import testtools
+import threading
+
+from glanceclient.common import http
+from glanceclient.common import https
+
+from glanceclient import Client
+from glanceclient import exc
+
+if six.PY3 is True:
+ import socketserver
+else:
+ import SocketServer as socketserver
+
+
+TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ 'var'))
+
+
+class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
+ def handle(self):
+ self.request.recv(1024)
+ response = b'somebytes'
+ self.request.sendall(response)
+
+
+class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
+ def get_request(self):
+ key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ (_sock, addr) = socketserver.TCPServer.get_request(self)
+ sock = ssl.wrap_socket(_sock,
+ certfile=cert_file,
+ keyfile=key_file,
+ ca_certs=cacert,
+ server_side=True,
+ cert_reqs=ssl.CERT_REQUIRED)
+ return sock, addr
+
+
+class TestHTTPSVerifyCert(testtools.TestCase):
+ """Check 'requests' based ssl verification occurs
+
+ The requests library performs SSL certificate validation,
+ however there is still a need to check that the glance
+ client is properly integrated with requests so that
+ cert validation actually happens.
+ """
+ def setUp(self):
+ # Rather than spinning up a new process, we create
+ # a thread to perform client/server interaction.
+ # This should run more quickly.
+ super(TestHTTPSVerifyCert, self).setUp()
+ server = ThreadedTCPServer(('127.0.0.1', 0),
+ ThreadedTCPRequestHandler)
+ __, self.port = server.server_address
+ server_thread = threading.Thread(target=server.serve_forever)
+ server_thread.daemon = True
+ server_thread.start()
+
+ def test_v1_requests_cert_verification(self):
+ """v1 regression test for bug 115260."""
+ port = self.port
+ url = 'https://0.0.0.0:%d' % port
+
+ try:
+ client = Client('1', url,
+ insecure=False,
+ ssl_compression=True)
+ client.images.get('image123')
+ self.fail('No SSL exception raised')
+ except exc.CommunicationError as e:
+ if 'certificate verify failed' not in e.message:
+ self.fail('No certificate failure message received')
+ except Exception as e:
+ self.fail('Unexpected exception raised')
+
+ def test_v1_requests_cert_verification_no_compression(self):
+ """v1 regression test for bug 115260."""
+ port = self.port
+ url = 'https://0.0.0.0:%d' % port
+
+ try:
+ client = Client('1', url,
+ insecure=False,
+ ssl_compression=False)
+ client.images.get('image123')
+ self.fail('No SSL exception raised')
+ except SSL.Error as e:
+ if 'certificate verify failed' not in str(e):
+ self.fail('No certificate failure message received')
+ except Exception as e:
+ self.fail('Unexpected exception raised')
+
+ def test_v2_requests_cert_verification(self):
+ """v2 regression test for bug 115260."""
+ port = self.port
+ url = 'https://0.0.0.0:%d' % port
+
+ try:
+ gc = Client('2', url,
+ insecure=False,
+ ssl_compression=True)
+ gc.images.get('image123')
+ self.fail('No SSL exception raised')
+ except exc.CommunicationError as e:
+ if 'certificate verify failed' not in e.message:
+ self.fail('No certificate failure message received')
+ except Exception as e:
+ self.fail('Unexpected exception raised')
+
+ def test_v2_requests_cert_verification_no_compression(self):
+ """v2 regression test for bug 115260."""
+ port = self.port
+ url = 'https://0.0.0.0:%d' % port
+
+ try:
+ gc = Client('2', url,
+ insecure=False,
+ ssl_compression=False)
+ gc.images.get('image123')
+ self.fail('No SSL exception raised')
+ except SSL.Error as e:
+ if 'certificate verify failed' not in str(e):
+ self.fail('No certificate failure message received')
+ except Exception as e:
+ self.fail('Unexpected exception raised')
+
+
+class TestVerifiedHTTPSConnection(testtools.TestCase):
+ def test_ssl_init_ok(self):
+ """
+ Test VerifiedHTTPSConnection class init
+ """
+ key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ try:
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ key_file=key_file,
+ cert_file=cert_file,
+ cacert=cacert)
+ except exc.SSLConfigurationError:
+ self.fail('Failed to init VerifiedHTTPSConnection.')
+
+ def test_ssl_init_cert_no_key(self):
+ """
+ Test VerifiedHTTPSConnection: absence of SSL key file.
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ try:
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ cert_file=cert_file,
+ cacert=cacert)
+ self.fail('Failed to raise assertion.')
+ except exc.SSLConfigurationError:
+ pass
+
+ def test_ssl_init_key_no_cert(self):
+ """
+ Test VerifiedHTTPSConnection: absence of SSL cert file.
+ """
+ key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ try:
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ key_file=key_file,
+ cacert=cacert)
+ except exc.SSLConfigurationError:
+ pass
+ except Exception:
+ self.fail('Failed to init VerifiedHTTPSConnection.')
+
+ def test_ssl_init_bad_key(self):
+ """
+ Test VerifiedHTTPSConnection: bad key.
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ key_file = os.path.join(TEST_VAR_DIR, 'badkey.key')
+ try:
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ key_file=key_file,
+ cert_file=cert_file,
+ cacert=cacert)
+ self.fail('Failed to raise assertion.')
+ except exc.SSLConfigurationError:
+ pass
+
+ def test_ssl_init_bad_cert(self):
+ """
+ Test VerifiedHTTPSConnection: bad cert.
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ try:
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ cert_file=cert_file,
+ cacert=cacert)
+ self.fail('Failed to raise assertion.')
+ except exc.SSLConfigurationError:
+ pass
+
+ def test_ssl_init_bad_ca(self):
+ """
+ Test VerifiedHTTPSConnection: bad CA.
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'badca.crt')
+ try:
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ cert_file=cert_file,
+ cacert=cacert)
+ self.fail('Failed to raise assertion.')
+ except exc.SSLConfigurationError:
+ pass
+
+ def test_ssl_cert_cname(self):
+ """
+ Test certificate: CN match
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM,
+ open(cert_file).read())
+ # The expected cert should have CN=0.0.0.0
+ self.assertEqual('0.0.0.0', cert.get_subject().commonName)
+ try:
+ conn = https.VerifiedHTTPSConnection('0.0.0.0', 0)
+ https.do_verify_callback(None, cert, 0, 0, 1, host=conn.host)
+ except Exception:
+ self.fail('Unexpected exception.')
+
+ def test_ssl_cert_cname_wildcard(self):
+ """
+ Test certificate: wildcard CN match
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'wildcard-certificate.crt')
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM,
+ open(cert_file).read())
+ # The expected cert should have CN=*.pong.example.com
+ self.assertEqual('*.pong.example.com', cert.get_subject().commonName)
+ try:
+ conn = https.VerifiedHTTPSConnection('ping.pong.example.com', 0)
+ https.do_verify_callback(None, cert, 0, 0, 1, host=conn.host)
+ except Exception:
+ self.fail('Unexpected exception.')
+
+ def test_ssl_cert_subject_alt_name(self):
+ """
+ Test certificate: SAN match
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM,
+ open(cert_file).read())
+ # The expected cert should have CN=0.0.0.0
+ self.assertEqual('0.0.0.0', cert.get_subject().commonName)
+ try:
+ conn = https.VerifiedHTTPSConnection('alt1.example.com', 0)
+ https.do_verify_callback(None, cert, 0, 0, 1, host=conn.host)
+ except Exception:
+ self.fail('Unexpected exception.')
+
+ try:
+ conn = https.VerifiedHTTPSConnection('alt2.example.com', 0)
+ https.do_verify_callback(None, cert, 0, 0, 1, host=conn.host)
+ except Exception:
+ self.fail('Unexpected exception.')
+
+ def test_ssl_cert_subject_alt_name_wildcard(self):
+ """
+ Test certificate: wildcard SAN match
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'wildcard-san-certificate.crt')
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM,
+ open(cert_file).read())
+ # The expected cert should have CN=0.0.0.0
+ self.assertEqual('0.0.0.0', cert.get_subject().commonName)
+ try:
+ conn = https.VerifiedHTTPSConnection('alt1.example.com', 0)
+ https.do_verify_callback(None, cert, 0, 0, 1, host=conn.host)
+ except Exception:
+ self.fail('Unexpected exception.')
+
+ try:
+ conn = https.VerifiedHTTPSConnection('alt2.example.com', 0)
+ https.do_verify_callback(None, cert, 0, 0, 1, host=conn.host)
+ except Exception:
+ self.fail('Unexpected exception.')
+
+ try:
+ conn = https.VerifiedHTTPSConnection('alt3.example.net', 0)
+ https.do_verify_callback(None, cert, 0, 0, 1, host=conn.host)
+ self.fail('Failed to raise assertion.')
+ except exc.SSLCertificateError:
+ pass
+
+ def test_ssl_cert_mismatch(self):
+ """
+ Test certificate: bogus host
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM,
+ open(cert_file).read())
+ # The expected cert should have CN=0.0.0.0
+ self.assertEqual('0.0.0.0', cert.get_subject().commonName)
+ try:
+ conn = https.VerifiedHTTPSConnection('mismatch.example.com', 0)
+ except Exception:
+ self.fail('Failed to init VerifiedHTTPSConnection.')
+
+ self.assertRaises(exc.SSLCertificateError,
+ https.do_verify_callback, None, cert, 0, 0, 1,
+ host=conn.host)
+
+ def test_ssl_expired_cert(self):
+ """
+ Test certificate: out of date cert
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'expired-cert.crt')
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM,
+ open(cert_file).read())
+ # The expected expired cert has CN=openstack.example.com
+ self.assertEqual('openstack.example.com',
+ cert.get_subject().commonName)
+ try:
+ conn = https.VerifiedHTTPSConnection('openstack.example.com', 0)
+ except Exception:
+ raise
+ self.fail('Failed to init VerifiedHTTPSConnection.')
+ self.assertRaises(exc.SSLCertificateError,
+ https.do_verify_callback, None, cert, 0, 0, 1,
+ host=conn.host)
+
+ def test_ssl_broken_key_file(self):
+ """
+ Test verify exception is raised.
+ """
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ key_file = 'fake.key'
+ self.assertRaises(
+ exc.SSLConfigurationError,
+ https.VerifiedHTTPSConnection, '127.0.0.1',
+ 0, key_file=key_file,
+ cert_file=cert_file, cacert=cacert)
+
+ def test_ssl_init_ok_with_insecure_true(self):
+ """
+ Test VerifiedHTTPSConnection class init
+ """
+ key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ try:
+ https.VerifiedHTTPSConnection(
+ '127.0.0.1', 0,
+ key_file=key_file,
+ cert_file=cert_file,
+ cacert=cacert, insecure=True)
+ except exc.SSLConfigurationError:
+ self.fail('Failed to init VerifiedHTTPSConnection.')
+
+ def test_ssl_init_ok_with_ssl_compression_false(self):
+ """
+ Test VerifiedHTTPSConnection class init
+ """
+ key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ try:
+ https.VerifiedHTTPSConnection(
+ '127.0.0.1', 0,
+ key_file=key_file,
+ cert_file=cert_file,
+ cacert=cacert, ssl_compression=False)
+ except exc.SSLConfigurationError:
+ self.fail('Failed to init VerifiedHTTPSConnection.')
+
+ def test_ssl_init_non_byte_string(self):
+ """
+ Test VerifiedHTTPSConnection class non byte string
+
+ Reproduces bug #1301849
+ """
+ key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
+ cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
+ cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
+ # Note: we reproduce on python 2.6/2.7, on 3.3 the bug doesn't occur.
+ key_file = key_file.encode('ascii', 'strict').decode('utf-8')
+ cert_file = cert_file.encode('ascii', 'strict').decode('utf-8')
+ cacert = cacert.encode('ascii', 'strict').decode('utf-8')
+ try:
+ https.VerifiedHTTPSConnection('127.0.0.1', 0,
+ key_file=key_file,
+ cert_file=cert_file,
+ cacert=cacert)
+ except exc.SSLConfigurationError:
+ self.fail('Failed to init VerifiedHTTPSConnection.')
+
+
+class TestRequestsIntegration(testtools.TestCase):
+
+ def test_pool_patch(self):
+ client = http.HTTPClient("https://localhost",
+ ssl_compression=True)
+ self.assertNotEqual(https.HTTPSConnectionPool,
+ poolmanager.pool_classes_by_scheme["https"])
+
+ adapter = client.session.adapters.get("https://")
+ self.assertFalse(isinstance(adapter, https.HTTPSAdapter))
+
+ adapter = client.session.adapters.get("glance+https://")
+ self.assertFalse(isinstance(adapter, https.HTTPSAdapter))
+
+ def test_custom_https_adapter(self):
+ client = http.HTTPClient("https://localhost",
+ ssl_compression=False)
+ self.assertNotEqual(https.HTTPSConnectionPool,
+ poolmanager.pool_classes_by_scheme["https"])
+
+ adapter = client.session.adapters.get("https://")
+ self.assertFalse(isinstance(adapter, https.HTTPSAdapter))
+
+ adapter = client.session.adapters.get("glance+https://")
+ self.assertTrue(isinstance(adapter, https.HTTPSAdapter))
diff --git a/glanceclient/tests/unit/test_utils.py b/glanceclient/tests/unit/test_utils.py
new file mode 100644
index 0000000..564d7f6
--- /dev/null
+++ b/glanceclient/tests/unit/test_utils.py
@@ -0,0 +1,165 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 sys
+
+import six
+# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
+from six.moves import range
+import testtools
+
+from glanceclient.common import utils
+
+
+class TestUtils(testtools.TestCase):
+
+ def test_make_size_human_readable(self):
+ self.assertEqual("106B", utils.make_size_human_readable(106))
+ self.assertEqual("1000kB", utils.make_size_human_readable(1024000))
+ self.assertEqual("1MB", utils.make_size_human_readable(1048576))
+ self.assertEqual("1.4GB", utils.make_size_human_readable(1476395008))
+ self.assertEqual("9.3MB", utils.make_size_human_readable(9761280))
+
+ def test_get_new_file_size(self):
+ size = 98304
+ file_obj = six.StringIO('X' * size)
+ try:
+ self.assertEqual(size, utils.get_file_size(file_obj))
+ # Check that get_file_size didn't change original file position.
+ self.assertEqual(0, file_obj.tell())
+ finally:
+ file_obj.close()
+
+ def test_get_consumed_file_size(self):
+ size, consumed = 98304, 304
+ file_obj = six.StringIO('X' * size)
+ file_obj.seek(consumed)
+ try:
+ self.assertEqual(size, utils.get_file_size(file_obj))
+ # Check that get_file_size didn't change original file position.
+ self.assertEqual(consumed, file_obj.tell())
+ finally:
+ file_obj.close()
+
+ def test_prettytable(self):
+ class Struct:
+ def __init__(self, **entries):
+ self.__dict__.update(entries)
+
+ # test that the prettytable output is wellformatted (left-aligned)
+ columns = ['ID', 'Name']
+ val = ['Name1', 'another', 'veeeery long']
+ images = [Struct(**{'id': i ** 16, 'name': val[i]})
+ for i in range(len(val))]
+
+ saved_stdout = sys.stdout
+ try:
+ sys.stdout = output_list = six.StringIO()
+ utils.print_list(images, columns)
+
+ sys.stdout = output_dict = six.StringIO()
+ utils.print_dict({'K': 'k', 'Key': 'veeeeeeeeeeeeeeeeeeeeeeee'
+ 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
+ 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
+ 'eeeeeeeeeeeery long value'},
+ max_column_width=60)
+
+ finally:
+ sys.stdout = saved_stdout
+
+ self.assertEqual('''\
++-------+--------------+
+| ID | Name |
++-------+--------------+
+| | Name1 |
+| 1 | another |
+| 65536 | veeeery long |
++-------+--------------+
+''',
+ output_list.getvalue())
+
+ self.assertEqual('''\
++----------+--------------------------------------------------------------+
+| Property | Value |
++----------+--------------------------------------------------------------+
+| K | k |
+| Key | veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee |
+| | eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee |
+| | ery long value |
++----------+--------------------------------------------------------------+
+''',
+ output_dict.getvalue())
+
+ def test_exception_to_str(self):
+ class FakeException(Exception):
+ def __str__(self):
+ raise UnicodeError()
+
+ ret = utils.exception_to_str(Exception('error message'))
+ self.assertEqual('error message', ret)
+
+ ret = utils.exception_to_str(Exception('\xa5 error message'))
+ if six.PY2:
+ self.assertEqual(' error message', ret)
+ else:
+ self.assertEqual('\xa5 error message', ret)
+
+ ret = utils.exception_to_str(FakeException('\xa5 error message'))
+ self.assertEqual("Caught '%(exception)s' exception." %
+ {'exception': 'FakeException'}, ret)
+
+ def test_schema_args_with_list_types(self):
+ # NOTE(flaper87): Regression for bug
+ # https://bugs.launchpad.net/python-glanceclient/+bug/1401032
+
+ def schema_getter(_type='string', enum=False):
+ prop = {
+ 'type': ['null', _type],
+ 'description': 'Test schema (READ-ONLY)',
+ }
+
+ if enum:
+ prop['enum'] = [None, 'opt-1', 'opt-2']
+
+ def actual_getter():
+ return {
+ 'additionalProperties': False,
+ 'required': ['name'],
+ 'name': 'test_schema',
+ 'properties': {
+ 'test': prop,
+ }
+ }
+
+ return actual_getter
+
+ def dummy_func():
+ pass
+
+ decorated = utils.schema_args(schema_getter())(dummy_func)
+ arg, opts = decorated.__dict__['arguments'][0]
+ self.assertIn('--test', arg)
+ self.assertEqual(str, opts['type'])
+
+ decorated = utils.schema_args(schema_getter('integer'))(dummy_func)
+ arg, opts = decorated.__dict__['arguments'][0]
+ self.assertIn('--test', arg)
+ self.assertEqual(int, opts['type'])
+
+ decorated = utils.schema_args(schema_getter(enum=True))(dummy_func)
+ arg, opts = decorated.__dict__['arguments'][0]
+ self.assertIn('--test', arg)
+ self.assertEqual(str, opts['type'])
+ self.assertIn('None, opt-1, opt-2', opts['help'])
diff --git a/glanceclient/tests/unit/v1/__init__.py b/glanceclient/tests/unit/v1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glanceclient/tests/unit/v1/__init__.py
diff --git a/glanceclient/tests/unit/v1/test_image_members.py b/glanceclient/tests/unit/v1/test_image_members.py
new file mode 100644
index 0000000..d6e3150
--- /dev/null
+++ b/glanceclient/tests/unit/v1/test_image_members.py
@@ -0,0 +1,125 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient.tests import utils
+import glanceclient.v1.image_members
+import glanceclient.v1.images
+
+
+fixtures = {
+ '/v1/images/1/members': {
+ 'GET': (
+ {},
+ {'members': [
+ {'member_id': '1', 'can_share': False},
+ ]},
+ ),
+ 'PUT': ({}, None),
+ },
+ '/v1/images/1/members/1': {
+ 'GET': (
+ {},
+ {'member': {
+ 'member_id': '1',
+ 'can_share': False,
+ }},
+ ),
+ 'PUT': ({}, None),
+ 'DELETE': ({}, None),
+ },
+ '/v1/shared-images/1': {
+ 'GET': (
+ {},
+ {'shared_images': [
+ {'image_id': '1', 'can_share': False},
+ ]},
+ ),
+ },
+}
+
+
+class ImageMemberManagerTest(testtools.TestCase):
+
+ def setUp(self):
+ super(ImageMemberManagerTest, self).setUp()
+ self.api = utils.FakeAPI(fixtures)
+ self.mgr = glanceclient.v1.image_members.ImageMemberManager(self.api)
+ self.image = glanceclient.v1.images.Image(self.api, {'id': '1'}, True)
+
+ def test_list_by_image(self):
+ members = self.mgr.list(image=self.image)
+ expect = [('GET', '/v1/images/1/members', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(1, len(members))
+ self.assertEqual('1', members[0].member_id)
+ self.assertEqual('1', members[0].image_id)
+ self.assertEqual(False, members[0].can_share)
+
+ def test_list_by_member(self):
+ resource_class = glanceclient.v1.image_members.ImageMember
+ member = resource_class(self.api, {'member_id': '1'}, True)
+ self.mgr.list(member=member)
+ expect = [('GET', '/v1/shared-images/1', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_get(self):
+ member = self.mgr.get(self.image, '1')
+ expect = [('GET', '/v1/images/1/members/1', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('1', member.member_id)
+ self.assertEqual('1', member.image_id)
+ self.assertEqual(False, member.can_share)
+
+ def test_delete(self):
+ self.mgr.delete('1', '1')
+ expect = [('DELETE', '/v1/images/1/members/1', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+
+ 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', {},
+ sorted(expect_body.items()))]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_replace(self):
+ body = [
+ {'member_id': '2', 'can_share': False},
+ {'member_id': '3'},
+ ]
+ self.mgr.replace(self.image, body)
+ expect = [('PUT', '/v1/images/1/members', {},
+ sorted({'memberships': body}.items()))]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_replace_objects(self):
+ body = [
+ glanceclient.v1.image_members.ImageMember(
+ self.mgr, {'member_id': '2', 'can_share': False}, True),
+ glanceclient.v1.image_members.ImageMember(
+ self.mgr, {'member_id': '3', 'can_share': True}, True),
+ ]
+ self.mgr.replace(self.image, body)
+ expect_body = {
+ 'memberships': [
+ {'member_id': '2', 'can_share': False},
+ {'member_id': '3', 'can_share': True},
+ ],
+ }
+ expect = [('PUT', '/v1/images/1/members', {},
+ sorted(expect_body.items()))]
+ self.assertEqual(expect, self.api.calls)
diff --git a/glanceclient/tests/unit/v1/test_images.py b/glanceclient/tests/unit/v1/test_images.py
new file mode 100644
index 0000000..90849d1
--- /dev/null
+++ b/glanceclient/tests/unit/v1/test_images.py
@@ -0,0 +1,963 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 errno
+import json
+import testtools
+
+import six
+from six.moves.urllib import parse
+
+from glanceclient.tests import utils
+from glanceclient.v1 import client
+from glanceclient.v1 import images
+from glanceclient.v1 import shell
+
+
+fixtures = {
+ '/v1/images': {
+ 'POST': (
+ {
+ 'location': '/v1/images/1',
+ 'x-openstack-request-id': 'req-1234',
+ },
+ json.dumps(
+ {'image': {
+ 'id': '1',
+ 'name': 'image-1',
+ 'container_format': 'ovf',
+ 'disk_format': 'vhd',
+ 'owner': 'asdf',
+ 'size': '1024',
+ 'min_ram': '512',
+ 'min_disk': '10',
+ 'properties': {'a': 'b', 'c': 'd'},
+ 'is_public': False,
+ 'protected': False,
+ 'deleted': False,
+ }},
+ ),
+ ),
+ },
+ '/v1/images/detail?limit=20': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'a',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?is_public=None&limit=20': {
+ 'GET': (
+ {'x-openstack-request-id': 'req-1234'},
+ {'images': [
+ {
+ 'id': 'a',
+ 'owner': 'A',
+ 'is_public': 'True',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'owner': 'B',
+ 'is_public': 'False',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'c',
+ 'is_public': 'False',
+ 'name': 'image-3',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?is_public=None&limit=5': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'a',
+ 'owner': 'A',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'owner': 'B',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b2',
+ 'owner': 'B',
+ 'name': 'image-3',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'c',
+ 'name': 'image-3',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=5': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'a',
+ 'owner': 'A',
+ 'is_public': 'False',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'owner': 'A',
+ 'is_public': 'False',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b2',
+ 'owner': 'B',
+ 'name': 'image-3',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'c',
+ 'is_public': 'True',
+ 'name': 'image-3',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=20&marker=a': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'b',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'c',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=1': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'a',
+ 'name': 'image-0',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=1&marker=a': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'b',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=2': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'a',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=2&marker=b': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'c',
+ 'name': 'image-3',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=20&name=foo': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'a',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=20&property-ping=pong':
+ {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '1',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=20&sort_dir=desc': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'a',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+ '/v1/images/detail?limit=20&sort_key=name': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': 'a',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]},
+ ),
+ },
+
+ '/v1/images/1': {
+ 'HEAD': (
+ {
+ 'x-image-meta-id': '1',
+ 'x-image-meta-name': 'image-1',
+ 'x-image-meta-property-arch': 'x86_64',
+ 'x-image-meta-is_public': 'false',
+ 'x-image-meta-protected': 'false',
+ 'x-image-meta-deleted': 'false',
+ },
+ None),
+ 'GET': (
+ {},
+ 'XXX',
+ ),
+ 'PUT': (
+ {},
+ json.dumps(
+ {'image': {
+ 'id': '1',
+ 'name': 'image-2',
+ 'container_format': 'ovf',
+ 'disk_format': 'vhd',
+ 'owner': 'asdf',
+ 'size': '1024',
+ 'min_ram': '512',
+ 'min_disk': '10',
+ 'properties': {'a': 'b', 'c': 'd'},
+ 'is_public': False,
+ 'protected': False,
+ }},
+ ),
+ ),
+ 'DELETE': ({}, None),
+ },
+ '/v1/images/2': {
+ 'HEAD': (
+ {
+ 'x-image-meta-id': '2'
+ },
+ None,
+ ),
+ 'GET': (
+ {
+ 'x-image-meta-checksum': 'wrong'
+ },
+ 'YYY',
+ ),
+ },
+ '/v1/images/3': {
+ 'HEAD': (
+ {
+ 'x-image-meta-id': '3',
+ 'x-image-meta-name': u"ni\xf1o"
+ },
+ None,
+ ),
+ 'GET': (
+ {
+ 'x-image-meta-checksum': '0745064918b49693cca64d6b6a13d28a'
+ },
+ 'ZZZ',
+ ),
+ },
+ '/v1/images/4': {
+ 'HEAD': (
+ {
+ 'x-image-meta-id': '4',
+ 'x-image-meta-name': 'image-4',
+ 'x-image-meta-property-arch': 'x86_64',
+ 'x-image-meta-is_public': 'false',
+ 'x-image-meta-protected': 'false',
+ 'x-image-meta-deleted': 'false',
+ 'x-openstack-request-id': 'req-1234',
+ },
+ None),
+ 'GET': (
+ {
+ 'x-openstack-request-id': 'req-1234',
+ },
+ 'XXX',
+ ),
+ 'PUT': (
+ {
+ 'x-openstack-request-id': 'req-1234',
+ },
+ json.dumps(
+ {'image': {
+ 'id': '4',
+ 'name': 'image-4',
+ 'container_format': 'ovf',
+ 'disk_format': 'vhd',
+ 'owner': 'asdf',
+ 'size': '1024',
+ 'min_ram': '512',
+ 'min_disk': '10',
+ 'properties': {'a': 'b', 'c': 'd'},
+ 'is_public': False,
+ 'protected': False,
+ }},
+ ),
+ ),
+ 'DELETE': (
+ {
+ 'x-openstack-request-id': 'req-1234',
+ },
+ None),
+ },
+ '/v1/images/v2_created_img': {
+ 'PUT': (
+ {},
+ json.dumps({
+ "image": {
+ "status": "queued",
+ "deleted": False,
+ "container_format": "bare",
+ "min_ram": 0,
+ "updated_at": "2013-12-20T01:51:45",
+ "owner": "foo",
+ "min_disk": 0,
+ "is_public": False,
+ "deleted_at": None,
+ "id": "v2_created_img",
+ "size": None,
+ "name": "bar",
+ "checksum": None,
+ "created_at": "2013-12-20T01:50:38",
+ "disk_format": "qcow2",
+ "properties": {},
+ "protected": False
+ }
+ })
+ ),
+ },
+}
+
+
+class ImageManagerTest(testtools.TestCase):
+
+ def setUp(self):
+ super(ImageManagerTest, self).setUp()
+ self.api = utils.FakeAPI(fixtures)
+ self.mgr = images.ImageManager(self.api)
+
+ def test_paginated_list(self):
+ images = list(self.mgr.list(page_size=2))
+ expect = [
+ ('GET', '/v1/images/detail?limit=2', {}, None),
+ ('GET', '/v1/images/detail?limit=2&marker=b', {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(3, len(images))
+ self.assertEqual('a', images[0].id)
+ self.assertEqual('b', images[1].id)
+ self.assertEqual('c', images[2].id)
+
+ def test_list_with_limit_less_than_page_size(self):
+ results = list(self.mgr.list(page_size=2, limit=1))
+ expect = [('GET', '/v1/images/detail?limit=2', {}, None)]
+ self.assertEqual(1, len(results))
+ self.assertEqual(expect, self.api.calls)
+
+ def test_list_with_limit_greater_than_page_size(self):
+ images = list(self.mgr.list(page_size=1, limit=2))
+ expect = [
+ ('GET', '/v1/images/detail?limit=1', {}, None),
+ ('GET', '/v1/images/detail?limit=1&marker=a', {}, None),
+ ]
+ self.assertEqual(2, len(images))
+ self.assertEqual('a', images[0].id)
+ self.assertEqual('b', images[1].id)
+ self.assertEqual(expect, self.api.calls)
+
+ def test_list_with_marker(self):
+ list(self.mgr.list(marker='a'))
+ url = '/v1/images/detail?limit=20&marker=a'
+ expect = [('GET', url, {}, None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_list_with_filter(self):
+ list(self.mgr.list(filters={'name': "foo"}))
+ url = '/v1/images/detail?limit=20&name=foo'
+ expect = [('GET', url, {}, None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_list_with_property_filters(self):
+ list(self.mgr.list(filters={'properties': {'ping': 'pong'}}))
+ 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?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?limit=20&sort_key=name'
+ expect = [('GET', url, {}, None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_get(self):
+ image = self.mgr.get('1')
+ expect = [('HEAD', '/v1/images/1', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('1', image.id)
+ self.assertEqual('image-1', image.name)
+ self.assertEqual(False, image.is_public)
+ self.assertEqual(False, image.protected)
+ self.assertEqual(False, image.deleted)
+ self.assertEqual({u'arch': u'x86_64'}, image.properties)
+
+ def test_get_int(self):
+ image = self.mgr.get(1)
+ expect = [('HEAD', '/v1/images/1', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('1', image.id)
+ self.assertEqual('image-1', image.name)
+ self.assertEqual(False, image.is_public)
+ self.assertEqual(False, image.protected)
+ self.assertEqual(False, image.deleted)
+ self.assertEqual({u'arch': u'x86_64'}, image.properties)
+
+ def test_get_encoding(self):
+ image = self.mgr.get('3')
+ self.assertEqual(u"ni\xf1o", image.name)
+
+ def test_get_req_id(self):
+ params = {'return_req_id': []}
+ self.mgr.get('4', **params)
+ expect_req_id = ['req-1234']
+ self.assertEqual(expect_req_id, params['return_req_id'])
+
+ def test_data(self):
+ data = ''.join([b for b in self.mgr.data('1', do_checksum=False)])
+ expect = [('GET', '/v1/images/1', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('XXX', data)
+
+ expect += [('GET', '/v1/images/1', {}, None)]
+ data = ''.join([b for b in self.mgr.data('1')])
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('XXX', data)
+
+ def test_data_with_wrong_checksum(self):
+ data = ''.join([b for b in self.mgr.data('2', do_checksum=False)])
+ expect = [('GET', '/v1/images/2', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('YYY', data)
+
+ expect += [('GET', '/v1/images/2', {}, None)]
+ data = self.mgr.data('2')
+ self.assertEqual(expect, self.api.calls)
+ try:
+ data = ''.join([b for b in data])
+ self.fail('data did not raise an error.')
+ except IOError as e:
+ self.assertEqual(errno.EPIPE, e.errno)
+ msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong'
+ self.assertTrue(msg in str(e))
+
+ def test_data_req_id(self):
+ params = {
+ 'do_checksum': False,
+ 'return_req_id': [],
+ }
+ ''.join([b for b in self.mgr.data('4', **params)])
+ expect_req_id = ['req-1234']
+ self.assertEqual(expect_req_id, params['return_req_id'])
+
+ def test_data_with_checksum(self):
+ data = ''.join([b for b in self.mgr.data('3', do_checksum=False)])
+ expect = [('GET', '/v1/images/3', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('ZZZ', data)
+
+ expect += [('GET', '/v1/images/3', {}, None)]
+ data = ''.join([b for b in self.mgr.data('3')])
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('ZZZ', data)
+
+ def test_delete(self):
+ self.mgr.delete('1')
+ expect = [('DELETE', '/v1/images/1', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_delete_req_id(self):
+ params = {
+ 'return_req_id': []
+ }
+ self.mgr.delete('4', **params)
+ expect = [('DELETE', '/v1/images/4', {}, None)]
+ self.assertEqual(self.api.calls, expect)
+ expect_req_id = ['req-1234']
+ self.assertEqual(expect_req_id, params['return_req_id'])
+
+ def test_create_without_data(self):
+ params = {
+ 'id': '1',
+ 'name': 'image-1',
+ 'container_format': 'ovf',
+ 'disk_format': 'vhd',
+ 'owner': 'asdf',
+ 'size': 1024,
+ 'min_ram': 512,
+ 'min_disk': 10,
+ 'copy_from': 'http://example.com',
+ 'properties': {'a': 'b', 'c': 'd'},
+ }
+ image = self.mgr.create(**params)
+ expect_headers = {
+ 'x-image-meta-id': '1',
+ 'x-image-meta-name': 'image-1',
+ 'x-image-meta-container_format': 'ovf',
+ 'x-image-meta-disk_format': 'vhd',
+ 'x-image-meta-owner': 'asdf',
+ 'x-image-meta-size': '1024',
+ 'x-image-meta-min_ram': '512',
+ 'x-image-meta-min_disk': '10',
+ 'x-glance-api-copy-from': 'http://example.com',
+ 'x-image-meta-property-a': 'b',
+ 'x-image-meta-property-c': 'd',
+ }
+ expect = [('POST', '/v1/images', expect_headers, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('1', image.id)
+ self.assertEqual('image-1', image.name)
+ self.assertEqual('ovf', image.container_format)
+ self.assertEqual('vhd', image.disk_format)
+ self.assertEqual('asdf', image.owner)
+ self.assertEqual(1024, image.size)
+ self.assertEqual(512, image.min_ram)
+ self.assertEqual(10, image.min_disk)
+ self.assertEqual(False, image.is_public)
+ self.assertEqual(False, image.protected)
+ self.assertEqual(False, image.deleted)
+ self.assertEqual({'a': 'b', 'c': 'd'}, image.properties)
+
+ def test_create_with_data(self):
+ image_data = six.StringIO('XXX')
+ self.mgr.create(data=image_data)
+ expect_headers = {'x-image-meta-size': '3'}
+ expect = [('POST', '/v1/images', expect_headers, image_data)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_create_req_id(self):
+ params = {
+ 'id': '4',
+ 'name': 'image-4',
+ 'container_format': 'ovf',
+ 'disk_format': 'vhd',
+ 'owner': 'asdf',
+ 'size': 1024,
+ 'min_ram': 512,
+ 'min_disk': 10,
+ 'copy_from': 'http://example.com',
+ 'properties': {'a': 'b', 'c': 'd'},
+ 'return_req_id': [],
+ }
+ image = self.mgr.create(**params)
+ expect_headers = {
+ 'x-image-meta-id': '4',
+ 'x-image-meta-name': 'image-4',
+ 'x-image-meta-container_format': 'ovf',
+ 'x-image-meta-disk_format': 'vhd',
+ 'x-image-meta-owner': 'asdf',
+ 'x-image-meta-size': '1024',
+ 'x-image-meta-min_ram': '512',
+ 'x-image-meta-min_disk': '10',
+ 'x-glance-api-copy-from': 'http://example.com',
+ 'x-image-meta-property-a': 'b',
+ 'x-image-meta-property-c': 'd',
+ }
+ expect = [('POST', '/v1/images', expect_headers, None)]
+ self.assertEqual(self.api.calls, expect)
+ self.assertEqual(image.id, '1')
+ expect_req_id = ['req-1234']
+ self.assertEqual(expect_req_id, params['return_req_id'])
+
+ def test_update(self):
+ fields = {
+ 'name': 'image-2',
+ 'container_format': 'ovf',
+ 'disk_format': 'vhd',
+ 'owner': 'asdf',
+ 'size': 1024,
+ 'min_ram': 512,
+ 'min_disk': 10,
+ 'copy_from': 'http://example.com',
+ 'properties': {'a': 'b', 'c': 'd'},
+ 'deleted': False,
+ }
+ image = self.mgr.update('1', **fields)
+ expect_hdrs = {
+ 'x-image-meta-name': 'image-2',
+ 'x-image-meta-container_format': 'ovf',
+ 'x-image-meta-disk_format': 'vhd',
+ 'x-image-meta-owner': 'asdf',
+ 'x-image-meta-size': '1024',
+ 'x-image-meta-min_ram': '512',
+ 'x-image-meta-min_disk': '10',
+ 'x-glance-api-copy-from': 'http://example.com',
+ 'x-image-meta-property-a': 'b',
+ 'x-image-meta-property-c': 'd',
+ 'x-image-meta-deleted': 'False',
+ 'x-glance-registry-purge-props': 'false',
+ }
+ expect = [('PUT', '/v1/images/1', expect_hdrs, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('1', image.id)
+ self.assertEqual('image-2', image.name)
+ self.assertEqual(1024, image.size)
+ self.assertEqual(512, image.min_ram)
+ self.assertEqual(10, image.min_disk)
+
+ def test_update_with_data(self):
+ image_data = six.StringIO('XXX')
+ self.mgr.update('1', data=image_data)
+ expect_headers = {'x-image-meta-size': '3',
+ 'x-glance-registry-purge-props': 'false'}
+ expect = [('PUT', '/v1/images/1', expect_headers, image_data)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_update_with_purge_props(self):
+ self.mgr.update('1', purge_props=True)
+ expect_headers = {'x-glance-registry-purge-props': 'true'}
+ expect = [('PUT', '/v1/images/1', expect_headers, None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_update_with_purge_props_false(self):
+ self.mgr.update('1', purge_props=False)
+ expect_headers = {'x-glance-registry-purge-props': 'false'}
+ expect = [('PUT', '/v1/images/1', expect_headers, None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_update_req_id(self):
+ fields = {
+ 'purge_props': True,
+ 'return_req_id': [],
+ }
+ self.mgr.update('4', **fields)
+ expect_headers = {'x-glance-registry-purge-props': 'true'}
+ expect = [('PUT', '/v1/images/4', expect_headers, None)]
+ self.assertEqual(self.api.calls, expect)
+ expect_req_id = ['req-1234']
+ self.assertEqual(expect_req_id, fields['return_req_id'])
+
+ def test_image_meta_from_headers_encoding(self):
+ value = u"ni\xf1o"
+ if six.PY2:
+ fields = {"x-image-meta-name": "ni\xc3\xb1o"}
+ else:
+ fields = {"x-image-meta-name": value}
+ headers = self.mgr._image_meta_from_headers(fields)
+ self.assertEqual(value, headers["name"])
+
+ def test_image_list_with_owner(self):
+ images = self.mgr.list(owner='A', page_size=20)
+ image_list = list(images)
+ self.assertEqual('A', image_list[0].owner)
+ self.assertEqual('a', image_list[0].id)
+ self.assertEqual(1, len(image_list))
+
+ def test_image_list_with_owner_req_id(self):
+ fields = {
+ 'owner': 'A',
+ 'return_req_id': [],
+ }
+ images = self.mgr.list(**fields)
+ next(images)
+ self.assertEqual(fields['return_req_id'], ['req-1234'])
+
+ def test_image_list_with_notfound_owner(self):
+ images = self.mgr.list(owner='X', page_size=20)
+ self.assertEqual(0, len(list(images)))
+
+ def test_image_list_with_empty_string_owner(self):
+ images = self.mgr.list(owner='', page_size=20)
+ image_list = list(images)
+ self.assertRaises(AttributeError, lambda: image_list[0].owner)
+ self.assertEqual('c', image_list[0].id)
+ self.assertEqual(1, len(image_list))
+
+ def test_image_list_with_unspecified_owner(self):
+ images = self.mgr.list(owner=None, page_size=5)
+ image_list = list(images)
+ self.assertEqual('A', image_list[0].owner)
+ self.assertEqual('a', image_list[0].id)
+ self.assertEqual('A', image_list[1].owner)
+ self.assertEqual('b', image_list[1].id)
+ self.assertEqual('B', image_list[2].owner)
+ self.assertEqual('b2', image_list[2].id)
+ self.assertRaises(AttributeError, lambda: image_list[3].owner)
+ self.assertEqual('c', image_list[3].id)
+ self.assertEqual(4, len(image_list))
+
+ def test_image_list_with_owner_and_limit(self):
+ images = self.mgr.list(owner='B', page_size=5, limit=1)
+ image_list = list(images)
+ self.assertEqual('B', image_list[0].owner)
+ self.assertEqual('b', image_list[0].id)
+ self.assertEqual(1, len(image_list))
+
+ def test_image_list_all_tenants(self):
+ images = self.mgr.list(is_public=None, page_size=5)
+ image_list = list(images)
+ self.assertEqual('A', image_list[0].owner)
+ self.assertEqual('a', image_list[0].id)
+ self.assertEqual('B', image_list[1].owner)
+ self.assertEqual('b', image_list[1].id)
+ self.assertEqual('B', image_list[2].owner)
+ self.assertEqual('b2', image_list[2].id)
+ self.assertRaises(AttributeError, lambda: image_list[3].owner)
+ self.assertEqual('c', image_list[3].id)
+ self.assertEqual(4, len(image_list))
+
+ def test_update_v2_created_image_using_v1(self):
+ fields_to_update = {
+ 'name': 'bar',
+ 'container_format': 'bare',
+ 'disk_format': 'qcow2',
+ }
+ image = self.mgr.update('v2_created_img', **fields_to_update)
+ expect_hdrs = {
+ 'x-image-meta-name': 'bar',
+ 'x-image-meta-container_format': 'bare',
+ 'x-image-meta-disk_format': 'qcow2',
+ 'x-glance-registry-purge-props': 'false',
+ }
+ expect = [('PUT', '/v1/images/v2_created_img', expect_hdrs, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('v2_created_img', image.id)
+ self.assertEqual('bar', image.name)
+ self.assertEqual(0, image.size)
+ self.assertEqual('bare', image.container_format)
+ self.assertEqual('qcow2', image.disk_format)
+
+
+class ImageTest(testtools.TestCase):
+ def setUp(self):
+ super(ImageTest, self).setUp()
+ self.api = utils.FakeAPI(fixtures)
+ self.mgr = images.ImageManager(self.api)
+
+ def test_delete(self):
+ image = self.mgr.get('1')
+ image.delete()
+ expect = [
+ ('HEAD', '/v1/images/1', {}, None),
+ ('HEAD', '/v1/images/1', {}, None),
+ ('DELETE', '/v1/images/1', {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_update(self):
+ image = self.mgr.get('1')
+ image.update(name='image-5')
+ expect = [
+ ('HEAD', '/v1/images/1', {}, None),
+ ('HEAD', '/v1/images/1', {}, None),
+ ('PUT', '/v1/images/1',
+ {'x-image-meta-name': 'image-5',
+ 'x-glance-registry-purge-props': 'false'}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_data(self):
+ image = self.mgr.get('1')
+ data = ''.join([b for b in image.data()])
+ expect = [
+ ('HEAD', '/v1/images/1', {}, None),
+ ('HEAD', '/v1/images/1', {}, None),
+ ('GET', '/v1/images/1', {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('XXX', data)
+
+ data = ''.join([b for b in image.data(do_checksum=False)])
+ expect += [('GET', '/v1/images/1', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('XXX', data)
+
+ def test_data_with_wrong_checksum(self):
+ image = self.mgr.get('2')
+ data = ''.join([b for b in image.data(do_checksum=False)])
+ expect = [
+ ('HEAD', '/v1/images/2', {}, None),
+ ('HEAD', '/v1/images/2', {}, None),
+ ('GET', '/v1/images/2', {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('YYY', data)
+
+ data = image.data()
+ expect += [('GET', '/v1/images/2', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ try:
+ data = ''.join([b for b in image.data()])
+ self.fail('data did not raise an error.')
+ except IOError as e:
+ self.assertEqual(errno.EPIPE, e.errno)
+ msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong'
+ self.assertTrue(msg in str(e))
+
+ def test_data_with_checksum(self):
+ image = self.mgr.get('3')
+ data = ''.join([b for b in image.data(do_checksum=False)])
+ expect = [
+ ('HEAD', '/v1/images/3', {}, None),
+ ('HEAD', '/v1/images/3', {}, None),
+ ('GET', '/v1/images/3', {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('ZZZ', data)
+
+ data = ''.join([b for b in image.data()])
+ expect += [('GET', '/v1/images/3', {}, None)]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual('ZZZ', data)
+
+
+class ParameterFakeAPI(utils.FakeAPI):
+ image_list = {'images': [
+ {
+ 'id': 'a',
+ 'name': 'image-1',
+ 'properties': {'arch': 'x86_64'},
+ },
+ {
+ 'id': 'b',
+ 'name': 'image-2',
+ 'properties': {'arch': 'x86_64'},
+ },
+ ]}
+
+ def get(self, url, **kwargs):
+ self.url = url
+ return utils.FakeResponse({}), ParameterFakeAPI.image_list
+
+
+class FakeArg(object):
+ def __init__(self, arg_dict):
+ self.arg_dict = arg_dict
+ self.fields = arg_dict.keys()
+
+ def __getattr__(self, name):
+ if name in self.arg_dict:
+ return self.arg_dict[name]
+ else:
+ return None
+
+
+class UrlParameterTest(testtools.TestCase):
+
+ def setUp(self):
+ super(UrlParameterTest, self).setUp()
+ self.api = ParameterFakeAPI({})
+ self.gc = client.Client("http://fakeaddress.com")
+ self.gc.images = images.ImageManager(self.api)
+
+ def test_is_public_list(self):
+ shell.do_image_list(self.gc, FakeArg({"is_public": "True"}))
+ parts = parse.urlparse(self.api.url)
+ qs_dict = parse.parse_qs(parts.query)
+ self.assertTrue('is_public' in qs_dict)
+ self.assertTrue(qs_dict['is_public'][0].lower() == "true")
diff --git a/glanceclient/tests/unit/v1/test_shell.py b/glanceclient/tests/unit/v1/test_shell.py
new file mode 100644
index 0000000..342dce1
--- /dev/null
+++ b/glanceclient/tests/unit/v1/test_shell.py
@@ -0,0 +1,503 @@
+# Copyright 2013 OpenStack Foundation
+# Copyright (C) 2013 Yahoo! Inc.
+# All Rights Reserved.
+#
+# 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 argparse
+import json
+import os
+import six
+import subprocess
+import tempfile
+import testtools
+
+import mock
+
+from glanceclient import exc
+from glanceclient import shell
+
+import glanceclient.v1.client as client
+import glanceclient.v1.images
+import glanceclient.v1.shell as v1shell
+
+from glanceclient.tests import utils
+
+if six.PY3:
+ import io
+ file_type = io.IOBase
+else:
+ file_type = file
+
+fixtures = {
+ '/v1/images/96d2c7e1-de4e-4612-8aa2-ba26610c804e': {
+ 'PUT': (
+ {
+ 'Location': 'http://fakeaddress.com:9292/v1/images/'
+ '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
+ 'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
+ 'X-Openstack-Request-Id':
+ 'req-b645039d-e1c7-43e5-b27b-2d18a173c42b',
+ 'Date': 'Mon, 29 Apr 2013 10:24:32 GMT'
+ },
+ json.dumps({
+ 'image': {
+ 'status': 'active', 'name': 'testimagerename',
+ 'deleted': False,
+ 'container_format': 'ami',
+ 'created_at': '2013-04-25T15:47:43',
+ 'disk_format': 'ami',
+ 'updated_at': '2013-04-29T10:24:32',
+ 'id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
+ 'min_disk': 0,
+ 'protected': False,
+ 'min_ram': 0,
+ 'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
+ 'owner': '1310db0cce8f40b0987a5acbe139765a',
+ 'is_public': True,
+ 'deleted_at': None,
+ 'properties': {
+ 'kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568',
+ 'ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114'
+ },
+ 'size': 25165824
+ }
+ })
+ ),
+ 'HEAD': (
+ {
+ 'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
+ 'x-image-meta-status': 'active'
+ },
+ None
+ ),
+ 'GET': (
+ {
+ 'x-image-meta-status': 'active',
+ 'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a',
+ 'x-image-meta-name': 'cirros-0.3.1-x86_64-uec',
+ 'x-image-meta-container_format': 'ami',
+ 'x-image-meta-created_at': '2013-04-25T15:47:43',
+ 'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
+ 'location': 'http://fakeaddress.com:9292/v1/images/'
+ '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
+ 'x-image-meta-min_ram': '0',
+ 'x-image-meta-updated_at': '2013-04-25T15:47:43',
+ 'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
+ 'x-image-meta-property-ramdisk_id':
+ 'b759bee9-0669-4394-a05c-fa2529b1c114',
+ 'date': 'Mon, 29 Apr 2013 09:25:17 GMT',
+ 'x-image-meta-property-kernel_id':
+ '1b108400-65d8-4762-9ea4-1bf6c7be7568',
+ 'x-openstack-request-id':
+ 'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f',
+ 'x-image-meta-deleted': 'False',
+ 'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
+ 'x-image-meta-protected': 'False',
+ 'x-image-meta-min_disk': '0',
+ 'x-image-meta-size': '25165824',
+ 'x-image-meta-is_public': 'True',
+ 'content-type': 'text/html; charset=UTF-8',
+ 'x-image-meta-disk_format': 'ami',
+ },
+ None
+ )
+ },
+ '/v1/images/44d2c7e1-de4e-4612-8aa2-ba26610c444f': {
+ 'PUT': (
+ {
+ 'Location': 'http://fakeaddress.com:9292/v1/images/'
+ '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
+ 'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
+ 'X-Openstack-Request-Id':
+ 'req-b645039d-e1c7-43e5-b27b-2d18a173c42b',
+ 'Date': 'Mon, 29 Apr 2013 10:24:32 GMT'
+ },
+ json.dumps({
+ 'image': {
+ 'status': 'queued', 'name': 'testimagerename',
+ 'deleted': False,
+ 'container_format': 'ami',
+ 'created_at': '2013-04-25T15:47:43',
+ 'disk_format': 'ami',
+ 'updated_at': '2013-04-29T10:24:32',
+ 'id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
+ 'min_disk': 0,
+ 'protected': False,
+ 'min_ram': 0,
+ 'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
+ 'owner': '1310db0cce8f40b0987a5acbe139765a',
+ 'is_public': True,
+ 'deleted_at': None,
+ 'properties': {
+ 'kernel_id':
+ '1b108400-65d8-4762-9ea4-1bf6c7be7568',
+ 'ramdisk_id':
+ 'b759bee9-0669-4394-a05c-fa2529b1c114'
+ },
+ 'size': 25165824
+ }
+ })
+ ),
+ 'HEAD': (
+ {
+ 'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
+ 'x-image-meta-status': 'queued'
+ },
+ None
+ ),
+ 'GET': (
+ {
+ 'x-image-meta-status': 'queued',
+ 'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a',
+ 'x-image-meta-name': 'cirros-0.3.1-x86_64-uec',
+ 'x-image-meta-container_format': 'ami',
+ 'x-image-meta-created_at': '2013-04-25T15:47:43',
+ 'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
+ 'location': 'http://fakeaddress.com:9292/v1/images/'
+ '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
+ 'x-image-meta-min_ram': '0',
+ 'x-image-meta-updated_at': '2013-04-25T15:47:43',
+ 'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
+ 'x-image-meta-property-ramdisk_id':
+ 'b759bee9-0669-4394-a05c-fa2529b1c114',
+ 'date': 'Mon, 29 Apr 2013 09:25:17 GMT',
+ 'x-image-meta-property-kernel_id':
+ '1b108400-65d8-4762-9ea4-1bf6c7be7568',
+ 'x-openstack-request-id':
+ 'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f',
+ 'x-image-meta-deleted': 'False',
+ 'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
+ 'x-image-meta-protected': 'False',
+ 'x-image-meta-min_disk': '0',
+ 'x-image-meta-size': '25165824',
+ 'x-image-meta-is_public': 'True',
+ 'content-type': 'text/html; charset=UTF-8',
+ 'x-image-meta-disk_format': 'ami',
+ },
+ None
+ )
+ },
+ '/v1/images/detail?limit=20&name=70aa106f-3750-4d7c-a5ce-0a535ac08d0a': {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '70aa106f-3750-4d7c-a5ce-0a535ac08d0a',
+ 'name': 'imagedeleted',
+ 'deleted': True,
+ 'status': 'deleted',
+ },
+ ]},
+ ),
+ },
+ '/v1/images/70aa106f-3750-4d7c-a5ce-0a535ac08d0a': {
+ 'HEAD': (
+ {
+ 'x-image-meta-id': '70aa106f-3750-4d7c-a5ce-0a535ac08d0a',
+ 'x-image-meta-status': 'deleted'
+ },
+ None
+ )
+ }
+}
+
+
+class ShellInvalidEndpointandParameterTest(utils.TestCase):
+
+ # Patch os.environ to avoid required auth info.
+ def setUp(self):
+ """Run before each test."""
+ super(ShellInvalidEndpointandParameterTest, self).setUp()
+ self.old_environment = os.environ.copy()
+ os.environ = {
+ 'OS_USERNAME': 'username',
+ 'OS_PASSWORD': 'password',
+ 'OS_TENANT_ID': 'tenant_id',
+ 'OS_TOKEN_ID': 'test',
+ 'OS_AUTH_URL': 'http://127.0.0.1:5000/v2.0/',
+ 'OS_AUTH_TOKEN': 'pass',
+ 'OS_IMAGE_API_VERSION': '1',
+ 'OS_REGION_NAME': 'test',
+ 'OS_IMAGE_URL': 'http://is.invalid'}
+
+ self.shell = shell.OpenStackImagesShell()
+
+ def tearDown(self):
+ super(ShellInvalidEndpointandParameterTest, self).tearDown()
+ os.environ = self.old_environment
+
+ def run_command(self, cmd):
+ self.shell.main(cmd.split())
+
+ def assert_called(self, method, url, body=None, **kwargs):
+ return self.shell.cs.assert_called(method, url, body, **kwargs)
+
+ def assert_called_anytime(self, method, url, body=None):
+ return self.shell.cs.assert_called_anytime(method, url, body)
+
+ def test_image_list_invalid_endpoint(self):
+ self.assertRaises(
+ exc.CommunicationError, self.run_command, 'image-list')
+
+ def test_image_create_invalid_endpoint(self):
+ self.assertRaises(
+ exc.CommunicationError,
+ self.run_command, 'image-create')
+
+ def test_image_delete_invalid_endpoint(self):
+ self.assertRaises(
+ exc.CommunicationError,
+ self.run_command, 'image-delete <fake>')
+
+ def test_image_download_invalid_endpoint(self):
+ self.assertRaises(
+ exc.CommunicationError,
+ self.run_command, 'image-download <fake>')
+
+ def test_members_list_invalid_endpoint(self):
+ self.assertRaises(
+ exc.CommunicationError,
+ self.run_command, 'member-list --image-id fake')
+
+ def test_image_show_invalid_endpoint(self):
+ self.assertRaises(
+ exc.CommunicationError,
+ self.run_command, 'image-show --human-readable <IMAGE_ID>')
+
+ def test_member_create_invalid_endpoint(self):
+ self.assertRaises(
+ exc.CommunicationError,
+ self.run_command,
+ 'member-create --can-share <IMAGE_ID> <TENANT_ID>')
+
+ def test_member_delete_invalid_endpoint(self):
+ self.assertRaises(
+ exc.CommunicationError,
+ self.run_command,
+ 'member-delete <IMAGE_ID> <TENANT_ID>')
+
+ @mock.patch('sys.stderr')
+ def test_image_create_invalid_size_parameter(self, __):
+ self.assertRaises(
+ SystemExit,
+ self.run_command, 'image-create --size 10gb')
+
+ @mock.patch('sys.stderr')
+ def test_image_create_invalid_ram_parameter(self, __):
+ self.assertRaises(
+ SystemExit,
+ self.run_command, 'image-create --min-ram 10gb')
+
+ @mock.patch('sys.stderr')
+ def test_image_create_invalid_min_disk_parameter(self, __):
+ self.assertRaises(
+ SystemExit,
+ self.run_command, 'image-create --min-disk 10gb')
+
+ @mock.patch('sys.stderr')
+ def test_image_update_invalid_size_parameter(self, __):
+ self.assertRaises(
+ SystemExit,
+ self.run_command, 'image-update --size 10gb')
+
+ @mock.patch('sys.stderr')
+ def test_image_update_invalid_min_disk_parameter(self, __):
+ self.assertRaises(
+ SystemExit,
+ self.run_command, 'image-update --min-disk 10gb')
+
+ @mock.patch('sys.stderr')
+ def test_image_update_invalid_ram_parameter(self, __):
+ self.assertRaises(
+ SystemExit,
+ self.run_command, 'image-update --min-ram 10gb')
+
+ @mock.patch('sys.stderr')
+ def test_image_list_invalid_min_size_parameter(self, __):
+ self.assertRaises(
+ SystemExit,
+ self.run_command, 'image-list --size-min 10gb')
+
+ @mock.patch('sys.stderr')
+ def test_image_list_invalid_max_size_parameter(self, __):
+ self.assertRaises(
+ SystemExit,
+ self.run_command, 'image-list --size-max 10gb')
+
+
+class ShellStdinHandlingTests(testtools.TestCase):
+
+ def _fake_update_func(self, *args, **kwargs):
+ '''Function to replace glanceclient.images.update,
+ to determine the parameters that would be supplied with the update
+ request
+ '''
+
+ # Store passed in args
+ self.collected_args = (args, kwargs)
+
+ # Return the first arg, which is an image,
+ # as do_image_update expects this.
+ return args[0]
+
+ def setUp(self):
+ super(ShellStdinHandlingTests, self).setUp()
+ self.api = utils.FakeAPI(fixtures)
+ self.gc = client.Client("http://fakeaddress.com")
+ self.gc.images = glanceclient.v1.images.ImageManager(self.api)
+
+ # Store real stdin, so it can be restored in tearDown.
+ self.real_sys_stdin_fd = os.dup(0)
+
+ # Replace stdin with a FD that points to /dev/null.
+ dev_null = open('/dev/null')
+ self.dev_null_fd = dev_null.fileno()
+ os.dup2(dev_null.fileno(), 0)
+
+ # Replace the image update function with a fake,
+ # so that we can tell if the data field was set correctly.
+ self.real_update_func = self.gc.images.update
+ self.collected_args = []
+ self.gc.images.update = self._fake_update_func
+
+ def tearDown(self):
+ """Restore stdin and gc.images.update to their pretest states."""
+ super(ShellStdinHandlingTests, self).tearDown()
+
+ def try_close(fd):
+ try:
+ os.close(fd)
+ except OSError:
+ # Already closed
+ pass
+
+ # Restore stdin
+ os.dup2(self.real_sys_stdin_fd, 0)
+
+ # Close duplicate stdin handle
+ try_close(self.real_sys_stdin_fd)
+
+ # Close /dev/null handle
+ try_close(self.dev_null_fd)
+
+ # Restore the real image update function
+ self.gc.images.update = self.real_update_func
+
+ def _do_update(self, image='96d2c7e1-de4e-4612-8aa2-ba26610c804e'):
+ """call v1/shell's do_image_update function."""
+
+ v1shell.do_image_update(
+ self.gc, argparse.Namespace(
+ image=image,
+ name='testimagerename',
+ property={},
+ purge_props=False,
+ human_readable=False,
+ file=None,
+ progress=False
+ )
+ )
+
+ def test_image_delete_deleted(self):
+ self.assertRaises(
+ exc.CommandError,
+ v1shell.do_image_delete,
+ self.gc,
+ argparse.Namespace(
+ images=['70aa106f-3750-4d7c-a5ce-0a535ac08d0a']
+ )
+ )
+
+ def test_image_update_closed_stdin(self):
+ """Supply glanceclient with a closed stdin, and perform an image
+ update to an active image. Glanceclient should not attempt to read
+ stdin.
+ """
+
+ # NOTE(hughsaunders) Close stdin, which is repointed to /dev/null by
+ # setUp()
+ os.close(0)
+
+ self._do_update()
+
+ self.assertTrue(
+ 'data' not in self.collected_args[1]
+ or self.collected_args[1]['data'] is None
+ )
+
+ def test_image_update_opened_stdin(self):
+ """Supply glanceclient with a stdin, and perform an image
+ update to an active image. Glanceclient should not allow it.
+ """
+
+ self.assertRaises(
+ SystemExit,
+ v1shell.do_image_update,
+ self.gc,
+ argparse.Namespace(
+ image='96d2c7e1-de4e-4612-8aa2-ba26610c804e',
+ property={},
+ )
+ )
+
+ def test_image_update_data_is_read_from_file(self):
+ """Ensure that data is read from a file."""
+
+ try:
+
+ # NOTE(hughsaunders) Create a tmpfile, write some data to it and
+ # set it as stdin
+ f = open(tempfile.mktemp(), 'w+')
+ f.write('Some Data')
+ f.flush()
+ f.seek(0)
+ os.dup2(f.fileno(), 0)
+
+ self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f')
+
+ self.assertTrue('data' in self.collected_args[1])
+ self.assertIsInstance(self.collected_args[1]['data'], file_type)
+ self.assertEqual('Some Data',
+ self.collected_args[1]['data'].read())
+
+ finally:
+ try:
+ f.close()
+ os.remove(f.name)
+ except Exception:
+ pass
+
+ def test_image_update_data_is_read_from_pipe(self):
+ """Ensure that data is read from a pipe."""
+
+ try:
+
+ # NOTE(hughsaunders): Setup a pipe, duplicate it to stdin
+ # ensure it is read.
+ process = subprocess.Popen(['/bin/echo', 'Some Data'],
+ stdout=subprocess.PIPE)
+ os.dup2(process.stdout.fileno(), 0)
+
+ self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f')
+
+ self.assertTrue('data' in self.collected_args[1])
+ self.assertIsInstance(self.collected_args[1]['data'], file_type)
+ self.assertEqual('Some Data\n',
+ self.collected_args[1]['data'].read())
+
+ finally:
+ try:
+ process.stdout.close()
+ except OSError:
+ pass
diff --git a/glanceclient/tests/unit/v2/__init__.py b/glanceclient/tests/unit/v2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glanceclient/tests/unit/v2/__init__.py
diff --git a/glanceclient/tests/unit/v2/test_images.py b/glanceclient/tests/unit/v2/test_images.py
new file mode 100644
index 0000000..7fc7558
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_images.py
@@ -0,0 +1,1098 @@
+# Copyright 2012 OpenStack Foundation.
+# All Rights Reserved.
+#
+# 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 errno
+
+import testtools
+
+from glanceclient import exc
+from glanceclient.tests import utils
+from glanceclient.v2 import images
+
+_CHKSUM = '93264c3edf5972c9f1cb309543d38a5c'
+_CHKSUM1 = '54264c3edf5972c9f1cb309453d38a46'
+
+_TAG1 = 'power'
+_TAG2 = '64bit'
+
+_BOGUS_ID = '63e7f218-29de-4477-abdc-8db7c9533188'
+_EVERYTHING_ID = '802cbbb7-0379-4c38-853f-37302b5e3d29'
+_OWNED_IMAGE_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf'
+_OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f'
+_PRIVATE_ID = 'e33560a7-3964-4de5-8339-5a24559f99ab'
+_PUBLIC_ID = '857806e7-05b6-48e0-9d40-cb0e6fb727b9'
+_SHARED_ID = '331ac905-2a38-44c5-a83d-653db8f08313'
+_STATUS_REJECTED_ID = 'f3ea56ff-d7e4-4451-998c-1e3d33539c8e'
+
+data_fixtures = {
+ '/v2/schemas/image': {
+ 'GET': (
+ {},
+ {
+ 'name': 'image',
+ 'properties': {
+ 'id': {},
+ 'name': {},
+ 'locations': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'metadata': {'type': 'object'},
+ 'url': {'type': 'string'},
+ },
+ 'required': ['url', 'metadata'],
+ },
+ },
+ 'color': {'type': 'string', 'is_base': False},
+ },
+ 'additionalProperties': {'type': 'string'},
+ },
+ ),
+ },
+ '/v2/images?limit=%d' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=2': {
+ 'GET': (
+ {},
+ {
+ 'images': [
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ ],
+ 'next': ('/v2/images?limit=2&'
+ 'marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'),
+ },
+ ),
+ },
+ '/v2/images?limit=1': {
+ 'GET': (
+ {},
+ {
+ 'images': [
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ ],
+ 'next': ('/v2/images?limit=1&'
+ 'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'),
+ },
+ ),
+ },
+ ('/v2/images?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ ]},
+ ),
+ },
+ ('/v2/images?limit=1&marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'): {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '3f99bf80-2ee6-47cf-acfe-1f1fabb7e811',
+ 'name': 'image-3',
+ },
+ ]},
+ ),
+ },
+ '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1': {
+ 'GET': (
+ {},
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ ),
+ 'PATCH': (
+ {},
+ '',
+ ),
+ },
+ '/v2/images/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': {
+ 'GET': (
+ {},
+ {
+ 'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3',
+ 'name': 'image-3',
+ 'barney': 'rubble',
+ 'george': 'jetson',
+ 'color': 'red',
+ },
+ ),
+ 'PATCH': (
+ {},
+ '',
+ ),
+ },
+ '/v2/images': {
+ 'POST': (
+ {},
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ ),
+ },
+ '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a': {
+ 'DELETE': (
+ {},
+ {
+ 'id': '87b634c1-f893-33c9-28a9-e5673c99239a',
+ },
+ ),
+ },
+ '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/file': {
+ 'PUT': (
+ {},
+ '',
+ ),
+ },
+ '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205/file': {
+ 'GET': (
+ {},
+ 'A',
+ ),
+ },
+ '/v2/images/66fb18d6-db27-11e1-a1eb-080027cbe205/file': {
+ 'GET': (
+ {
+ 'content-md5': 'wrong'
+ },
+ 'BB',
+ ),
+ },
+ '/v2/images/1b1c6366-dd57-11e1-af0f-02163e68b1d8/file': {
+ 'GET': (
+ {
+ 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae'
+ },
+ 'CCC',
+ ),
+ },
+ '/v2/images?limit=%d&visibility=public' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': _PUBLIC_ID,
+ 'harvey': 'lipshitz',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&visibility=private' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': _PRIVATE_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&visibility=shared' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': _SHARED_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&member_status=rejected' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': _STATUS_REJECTED_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&member_status=pending' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': []},
+ ),
+ },
+ '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _OWNER_ID): {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': _OWNED_IMAGE_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): {
+ 'GET': (
+ {},
+ {'images': []},
+ ),
+ },
+ '/v2/images?limit=%d&member_status=pending&owner=%s&visibility=shared'
+ % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': _EVERYTHING_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/images?checksum=%s&limit=%d' % (_CHKSUM, images.DEFAULT_PAGE_SIZE): {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ }
+ ]},
+ ),
+ },
+ '/v2/images?checksum=%s&limit=%d' % (_CHKSUM1, images.DEFAULT_PAGE_SIZE): {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?checksum=wrong&limit=%d' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': []},
+ ),
+ },
+ '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG1): {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ }
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG2): {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&tag=%s&tag=%s' % (images.DEFAULT_PAGE_SIZE,
+ _TAG1, _TAG2):
+ {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ }
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&tag=fake' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': []},
+ ),
+ },
+ '/v2/images/a2b83adc-888e-11e3-8872-78acc0b951d8': {
+ 'GET': (
+ {},
+ {
+ 'id': 'a2b83adc-888e-11e3-8872-78acc0b951d8',
+ 'name': 'image-location-tests',
+ 'locations': [{u'url': u'http://foo.com/',
+ u'metadata': {u'foo': u'foometa'}},
+ {u'url': u'http://bar.com/',
+ u'metadata': {u'bar': u'barmeta'}}],
+ },
+ ),
+ 'PATCH': (
+ {},
+ '',
+ )
+ },
+ '/v2/images?limit=%d&os_distro=NixOS' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '8b052954-c76c-4e02-8e90-be89a70183a8',
+ 'name': 'image-5',
+ 'os_distro': 'NixOS',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&my_little_property=cant_be_this_cute' %
+ images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': []},
+ ),
+ },
+ '/v2/images?limit=%d&sort_key=name' % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&sort_key=name&sort_key=id'
+ % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image',
+ },
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&sort_dir=desc&sort_key=id'
+ % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&sort_dir=desc&sort_key=name&sort_key=id'
+ % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&sort_dir=desc&sort_dir=asc&sort_key=name&sort_key=id'
+ % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ ]},
+ ),
+ },
+ '/v2/images?limit=%d&sort=name%%3Adesc%%2Csize%%3Aasc'
+ % images.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'images': [
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'name': 'image-2',
+ },
+ {
+ 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1',
+ 'name': 'image-1',
+ },
+ ]},
+ ),
+ },
+}
+
+schema_fixtures = {
+ 'image': {
+ 'GET': (
+ {},
+ {
+ 'name': 'image',
+ 'properties': {
+ 'id': {},
+ 'name': {},
+ 'locations': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'metadata': {'type': 'object'},
+ 'url': {'type': 'string'},
+ },
+ 'required': ['url', 'metadata'],
+ }
+ },
+ 'color': {'type': 'string', 'is_base': False},
+ 'tags': {'type': 'array'},
+ },
+ 'additionalProperties': {'type': 'string'},
+ }
+ )
+ }
+}
+
+
+class TestController(testtools.TestCase):
+ def setUp(self):
+ super(TestController, self).setUp()
+ self.api = utils.FakeAPI(data_fixtures)
+ self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
+ self.controller = images.Controller(self.api, self.schema_api)
+
+ def test_list_images(self):
+ # NOTE(bcwaldon):cast to list since the controller returns a generator
+ images = list(self.controller.list())
+ self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id)
+ self.assertEqual('image-1', images[0].name)
+ self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id)
+ self.assertEqual('image-2', images[1].name)
+
+ def test_list_images_paginated(self):
+ # NOTE(bcwaldon):cast to list since the controller returns a generator
+ images = list(self.controller.list(page_size=1))
+ self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id)
+ self.assertEqual('image-1', images[0].name)
+ self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id)
+ self.assertEqual('image-2', images[1].name)
+
+ def test_list_images_paginated_with_limit(self):
+ # NOTE(bcwaldon):cast to list since the controller returns a generator
+ images = list(self.controller.list(limit=3, page_size=2))
+ self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id)
+ self.assertEqual('image-1', images[0].name)
+ self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id)
+ self.assertEqual('image-2', images[1].name)
+ self.assertEqual('3f99bf80-2ee6-47cf-acfe-1f1fabb7e811', images[2].id)
+ self.assertEqual('image-3', images[2].name)
+ self.assertEqual(3, len(images))
+
+ def test_list_images_visibility_public(self):
+ filters = {'filters': {'visibility': 'public'}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(_PUBLIC_ID, images[0].id)
+
+ def test_list_images_visibility_private(self):
+ filters = {'filters': {'visibility': 'private'}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(_PRIVATE_ID, images[0].id)
+
+ def test_list_images_visibility_shared(self):
+ filters = {'filters': {'visibility': 'shared'}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(_SHARED_ID, images[0].id)
+
+ def test_list_images_member_status_rejected(self):
+ filters = {'filters': {'member_status': 'rejected'}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(_STATUS_REJECTED_ID, images[0].id)
+
+ def test_list_images_for_owner(self):
+ filters = {'filters': {'owner': _OWNER_ID}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(_OWNED_IMAGE_ID, images[0].id)
+
+ def test_list_images_for_checksum_single_image(self):
+ fake_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
+ filters = {'filters': {'checksum': _CHKSUM}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(1, len(images))
+ self.assertEqual('%s' % fake_id, images[0].id)
+
+ def test_list_images_for_checksum_multiple_images(self):
+ fake_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ fake_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'
+ filters = {'filters': {'checksum': _CHKSUM1}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(2, len(images))
+ self.assertEqual('%s' % fake_id1, images[0].id)
+ self.assertEqual('%s' % fake_id2, images[1].id)
+
+ def test_list_images_for_wrong_checksum(self):
+ filters = {'filters': {'checksum': 'wrong'}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(0, len(images))
+
+ def test_list_images_for_bogus_owner(self):
+ filters = {'filters': {'owner': _BOGUS_ID}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual([], images)
+
+ def test_list_images_for_bunch_of_filters(self):
+ filters = {'filters': {'owner': _BOGUS_ID,
+ 'visibility': 'shared',
+ 'member_status': 'pending'}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(_EVERYTHING_ID, images[0].id)
+
+ def test_list_images_filters_encoding(self):
+ filters = {"owner": u"ni\xf1o"}
+ try:
+ list(self.controller.list(filters=filters))
+ except KeyError:
+ # NOTE(flaper87): It raises KeyError because there's
+ # no fixture supporting this query:
+ # /v2/images?owner=ni%C3%B1o&limit=20
+ # We just want to make sure filters are correctly encoded.
+ pass
+ self.assertEqual(b"ni\xc3\xb1o", filters["owner"])
+
+ def test_list_images_for_tag_single_image(self):
+ img_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
+ filters = {'filters': {'tag': [_TAG1]}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(1, len(images))
+ self.assertEqual('%s' % img_id, images[0].id)
+
+ def test_list_images_for_tag_multiple_images(self):
+ img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ img_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'
+ filters = {'filters': {'tag': [_TAG2]}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(2, len(images))
+ self.assertEqual('%s' % img_id1, images[0].id)
+ self.assertEqual('%s' % img_id2, images[1].id)
+
+ def test_list_images_for_multi_tags(self):
+ img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ filters = {'filters': {'tag': [_TAG1, _TAG2]}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(1, len(images))
+ self.assertEqual('%s' % img_id1, images[0].id)
+
+ def test_list_images_for_non_existent_tag(self):
+ filters = {'filters': {'tag': ['fake']}}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(0, len(images))
+
+ def test_list_images_for_invalid_tag(self):
+ filters = {'filters': {'tag': [[]]}}
+
+ self.assertRaises(exc.HTTPBadRequest,
+ list,
+ self.controller.list(**filters))
+
+ def test_list_images_with_single_sort_key(self):
+ img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ sort_key = 'name'
+ images = list(self.controller.list(sort_key=sort_key))
+ self.assertEqual(2, len(images))
+ self.assertEqual('%s' % img_id1, images[0].id)
+
+ def test_list_with_multiple_sort_keys(self):
+ img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ sort_key = ['name', 'id']
+ images = list(self.controller.list(sort_key=sort_key))
+ self.assertEqual(2, len(images))
+ self.assertEqual('%s' % img_id1, images[0].id)
+
+ def test_list_images_with_desc_sort_dir(self):
+ img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ sort_key = 'id'
+ sort_dir = 'desc'
+ images = list(self.controller.list(sort_key=sort_key,
+ sort_dir=sort_dir))
+ self.assertEqual(2, len(images))
+ self.assertEqual('%s' % img_id1, images[1].id)
+
+ def test_list_images_with_multiple_sort_keys_and_one_sort_dir(self):
+ img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ sort_key = ['name', 'id']
+ sort_dir = 'desc'
+ images = list(self.controller.list(sort_key=sort_key,
+ sort_dir=sort_dir))
+ self.assertEqual(2, len(images))
+ self.assertEqual('%s' % img_id1, images[1].id)
+
+ def test_list_images_with_multiple_sort_dirs(self):
+ img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ sort_key = ['name', 'id']
+ sort_dir = ['desc', 'asc']
+ images = list(self.controller.list(sort_key=sort_key,
+ sort_dir=sort_dir))
+ self.assertEqual(2, len(images))
+ self.assertEqual('%s' % img_id1, images[1].id)
+
+ def test_list_images_with_new_sorting_syntax(self):
+ img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1'
+ sort = 'name:desc,size:asc'
+ images = list(self.controller.list(sort=sort))
+ self.assertEqual(2, len(images))
+ self.assertEqual('%s' % img_id1, images[1].id)
+
+ def test_list_images_sort_dirs_fewer_than_keys(self):
+ sort_key = ['name', 'id', 'created_at']
+ sort_dir = ['desc', 'asc']
+ self.assertRaises(exc.HTTPBadRequest,
+ list,
+ self.controller.list(
+ sort_key=sort_key,
+ sort_dir=sort_dir))
+
+ def test_list_images_combined_syntax(self):
+ sort_key = ['name', 'id']
+ sort_dir = ['desc', 'asc']
+ sort = 'name:asc'
+ self.assertRaises(exc.HTTPBadRequest,
+ list,
+ self.controller.list(
+ sort=sort,
+ sort_key=sort_key,
+ sort_dir=sort_dir))
+
+ def test_list_images_new_sorting_syntax_invalid_key(self):
+ sort = 'INVALID:asc'
+ self.assertRaises(exc.HTTPBadRequest,
+ list,
+ self.controller.list(
+ sort=sort))
+
+ def test_list_images_new_sorting_syntax_invalid_direction(self):
+ sort = 'name:INVALID'
+ self.assertRaises(exc.HTTPBadRequest,
+ list,
+ self.controller.list(
+ sort=sort))
+
+ def test_list_images_for_property(self):
+ filters = {'filters': dict([('os_distro', 'NixOS')])}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(1, len(images))
+
+ def test_list_images_for_non_existent_property(self):
+ filters = {'filters': dict([('my_little_property',
+ 'cant_be_this_cute')])}
+ images = list(self.controller.list(**filters))
+ self.assertEqual(0, len(images))
+
+ def test_get_image(self):
+ image = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1')
+ self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id)
+ self.assertEqual('image-1', image.name)
+
+ def test_create_image(self):
+ properties = {
+ 'name': 'image-1'
+ }
+ image = self.controller.create(**properties)
+ self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id)
+ self.assertEqual('image-1', image.name)
+
+ def test_create_bad_additionalProperty_type(self):
+ properties = {
+ 'name': 'image-1',
+ 'bad_prop': True,
+ }
+ with testtools.ExpectedException(TypeError):
+ self.controller.create(**properties)
+
+ def test_delete_image(self):
+ self.controller.delete('87b634c1-f893-33c9-28a9-e5673c99239a')
+ expect = [
+ ('DELETE',
+ '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a',
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_data_upload(self):
+ image_data = 'CCC'
+ image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f'
+ self.controller.upload(image_id, image_data)
+ expect = [('PUT', '/v2/images/%s/file' % image_id,
+ {'Content-Type': 'application/octet-stream'},
+ image_data)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_data_upload_w_size(self):
+ image_data = 'CCC'
+ image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f'
+ self.controller.upload(image_id, image_data, image_size=3)
+ body = {'image_data': image_data,
+ 'image_size': 3}
+ expect = [('PUT', '/v2/images/%s/file' % image_id,
+ {'Content-Type': 'application/octet-stream'},
+ sorted(body.items()))]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_data_without_checksum(self):
+ body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205',
+ do_checksum=False)
+ body = ''.join([b for b in body])
+ self.assertEqual('A', body)
+
+ body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205')
+ body = ''.join([b for b in body])
+ self.assertEqual('A', body)
+
+ def test_data_with_wrong_checksum(self):
+ body = self.controller.data('66fb18d6-db27-11e1-a1eb-080027cbe205',
+ do_checksum=False)
+ body = ''.join([b for b in body])
+ self.assertEqual('BB', body)
+
+ body = self.controller.data('66fb18d6-db27-11e1-a1eb-080027cbe205')
+ try:
+ body = ''.join([b for b in body])
+ self.fail('data did not raise an error.')
+ except IOError as e:
+ self.assertEqual(errno.EPIPE, e.errno)
+ msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong'
+ self.assertTrue(msg in str(e))
+
+ def test_data_with_checksum(self):
+ body = self.controller.data('1b1c6366-dd57-11e1-af0f-02163e68b1d8',
+ do_checksum=False)
+ body = ''.join([b for b in body])
+ self.assertEqual('CCC', body)
+
+ body = self.controller.data('1b1c6366-dd57-11e1-af0f-02163e68b1d8')
+ body = ''.join([b for b in body])
+ self.assertEqual('CCC', body)
+
+ def test_update_replace_prop(self):
+ image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
+ params = {'name': 'pong'}
+ image = self.controller.update(image_id, **params)
+ expect_hdrs = {
+ 'Content-Type': 'application/openstack-images-v2.1-json-patch',
+ }
+ 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),
+ ('GET', '/v2/images/%s' % image_id, {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(image_id, image.id)
+ # NOTE(bcwaldon):due to limitations of our fake api framework, the name
+ # will not actually change - yet in real life it will...
+ self.assertEqual('image-1', image.name)
+
+ def test_update_add_prop(self):
+ image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
+ params = {'finn': 'human'}
+ image = self.controller.update(image_id, **params)
+ expect_hdrs = {
+ 'Content-Type': 'application/openstack-images-v2.1-json-patch',
+ }
+ 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),
+ ('GET', '/v2/images/%s' % image_id, {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(image_id, image.id)
+ # NOTE(bcwaldon):due to limitations of our fake api framework, the name
+ # will not actually change - yet in real life it will...
+ self.assertEqual('image-1', image.name)
+
+ def test_update_remove_prop(self):
+ image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
+ remove_props = ['barney']
+ image = self.controller.update(image_id, remove_props)
+ expect_hdrs = {
+ 'Content-Type': 'application/openstack-images-v2.1-json-patch',
+ }
+ expect_body = [[('op', 'remove'), ('path', '/barney')]]
+ expect = [
+ ('GET', '/v2/images/%s' % image_id, {}, None),
+ ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
+ ('GET', '/v2/images/%s' % image_id, {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(image_id, image.id)
+ # NOTE(bcwaldon):due to limitations of our fake api framework, the name
+ # will not actually change - yet in real life it will...
+ self.assertEqual('image-3', image.name)
+
+ def test_update_replace_remove_same_prop(self):
+ image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
+ # Updating a property takes precedence over removing a property
+ params = {'barney': 'miller'}
+ remove_props = ['barney']
+ image = self.controller.update(image_id, remove_props, **params)
+ expect_hdrs = {
+ 'Content-Type': 'application/openstack-images-v2.1-json-patch',
+ }
+ 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),
+ ('GET', '/v2/images/%s' % image_id, {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(image_id, image.id)
+ # NOTE(bcwaldon):due to limitations of our fake api framework, the name
+ # will not actually change - yet in real life it will...
+ self.assertEqual('image-3', image.name)
+
+ def test_update_add_remove_same_prop(self):
+ image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
+ # Adding a property takes precedence over removing a property
+ params = {'finn': 'human'}
+ remove_props = ['finn']
+ image = self.controller.update(image_id, remove_props, **params)
+ expect_hdrs = {
+ 'Content-Type': 'application/openstack-images-v2.1-json-patch',
+ }
+ 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),
+ ('GET', '/v2/images/%s' % image_id, {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(image_id, image.id)
+ # NOTE(bcwaldon):due to limitations of our fake api framework, the name
+ # will not actually change - yet in real life it will...
+ self.assertEqual('image-3', image.name)
+
+ def test_update_bad_additionalProperty_type(self):
+ image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
+ params = {'name': 'pong', 'bad_prop': False}
+ with testtools.ExpectedException(TypeError):
+ self.controller.update(image_id, **params)
+
+ def test_update_add_custom_property(self):
+ image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
+ params = {'color': 'red'}
+ image = self.controller.update(image_id, **params)
+ expect_hdrs = {
+ 'Content-Type': 'application/openstack-images-v2.1-json-patch',
+ }
+ 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),
+ ('GET', '/v2/images/%s' % image_id, {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(image_id, image.id)
+
+ def test_update_replace_custom_property(self):
+ image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3'
+ params = {'color': 'blue'}
+ image = self.controller.update(image_id, **params)
+ expect_hdrs = {
+ 'Content-Type': 'application/openstack-images-v2.1-json-patch',
+ }
+ 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),
+ ('GET', '/v2/images/%s' % image_id, {}, None),
+ ]
+ self.assertEqual(expect, self.api.calls)
+ self.assertEqual(image_id, image.id)
+
+ def test_location_ops_when_server_disabled_location_ops(self):
+ # Location operations should not be allowed if server has not
+ # enabled location related operations
+ image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1'
+ estr = 'The administrator has disabled API access to image locations'
+ url = 'http://bar.com/'
+ meta = {'bar': 'barmeta'}
+
+ e = self.assertRaises(exc.HTTPBadRequest,
+ self.controller.add_location,
+ image_id, url, meta)
+ self.assertTrue(estr in str(e))
+
+ e = self.assertRaises(exc.HTTPBadRequest,
+ self.controller.delete_locations,
+ image_id, set([url]))
+ self.assertTrue(estr in str(e))
+
+ e = self.assertRaises(exc.HTTPBadRequest,
+ self.controller.update_location,
+ image_id, url, meta)
+ self.assertTrue(estr in str(e))
+
+ def _empty_get(self, image_id):
+ return ('GET', '/v2/images/%s' % image_id, {}, None)
+
+ 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},
+ data)
+
+ def test_add_location(self):
+ image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
+ new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}}
+ add_patch = {'path': '/locations/-', 'value': new_loc, 'op': 'add'}
+ self.controller.add_location(image_id, **new_loc)
+ self.assertEqual(self.api.calls, [
+ self._empty_get(image_id),
+ self._patch_req(image_id, [add_patch]),
+ self._empty_get(image_id)
+ ])
+
+ def test_add_duplicate_location(self):
+ image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
+ new_loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'newfoo'}}
+ err_str = 'A location entry at %s already exists' % new_loc['url']
+
+ err = self.assertRaises(exc.HTTPConflict,
+ self.controller.add_location,
+ image_id, **new_loc)
+ self.assertIn(err_str, str(err))
+
+ def test_remove_location(self):
+ image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
+ url_set = set(['http://foo.com/', 'http://bar.com/'])
+ del_patches = [{'path': '/locations/1', 'op': 'remove'},
+ {'path': '/locations/0', 'op': 'remove'}]
+ self.controller.delete_locations(image_id, url_set)
+ self.assertEqual(self.api.calls, [
+ self._empty_get(image_id),
+ self._patch_req(image_id, del_patches)
+ ])
+
+ def test_remove_missing_location(self):
+ image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
+ url_set = set(['http://spam.ham/'])
+ err_str = 'Unknown URL(s): %s' % list(url_set)
+
+ err = self.assertRaises(exc.HTTPNotFound,
+ self.controller.delete_locations,
+ image_id, url_set)
+ self.assertTrue(err_str in str(err))
+
+ def test_update_location(self):
+ image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
+ new_loc = {'url': 'http://foo.com/', 'metadata': {'spam': 'ham'}}
+ fixture_idx = '/v2/images/%s' % (image_id)
+ orig_locations = data_fixtures[fixture_idx]['GET'][1]['locations']
+ loc_map = dict([(l['url'], l) for l in orig_locations])
+ loc_map[new_loc['url']] = new_loc
+ mod_patch = [{'path': '/locations', 'op': 'replace',
+ 'value': []},
+ {'path': '/locations', 'op': 'replace',
+ 'value': list(loc_map.values())}]
+ self.controller.update_location(image_id, **new_loc)
+ self.assertEqual(self.api.calls, [
+ self._empty_get(image_id),
+ self._patch_req(image_id, mod_patch),
+ self._empty_get(image_id)
+ ])
+
+ def test_update_tags(self):
+ image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
+ tag_map = {'tags': ['tag01', 'tag02', 'tag03']}
+
+ image = self.controller.update(image_id, **tag_map)
+
+ expected_body = [{'path': '/tags', 'op': 'replace',
+ 'value': tag_map['tags']}]
+ expected = [
+ self._empty_get(image_id),
+ self._patch_req(image_id, expected_body),
+ self._empty_get(image_id)
+ ]
+ self.assertEqual(expected, self.api.calls)
+ self.assertEqual(image_id, image.id)
+
+ def test_update_missing_location(self):
+ image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
+ new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}}
+ err_str = 'Unknown URL: %s' % new_loc['url']
+ err = self.assertRaises(exc.HTTPNotFound,
+ self.controller.update_location,
+ image_id, **new_loc)
+ self.assertTrue(err_str in str(err))
diff --git a/glanceclient/tests/unit/v2/test_members.py b/glanceclient/tests/unit/v2/test_members.py
new file mode 100644
index 0000000..cf56a36
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_members.py
@@ -0,0 +1,120 @@
+# Copyright 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient.tests import utils
+from glanceclient.v2 import image_members
+
+
+IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1'
+MEMBER = '11223344-5566-7788-9911-223344556677'
+
+
+data_fixtures = {
+ '/v2/images/{image}/members'.format(image=IMAGE): {
+ 'GET': (
+ {},
+ {'members': [
+ {
+ 'image_id': IMAGE,
+ 'member_id': MEMBER,
+ },
+ ]},
+ ),
+ 'POST': (
+ {},
+ {
+ 'image_id': IMAGE,
+ 'member_id': MEMBER,
+ 'status': 'pending'
+ }
+ )
+ },
+ '/v2/images/{image}/members/{mem}'.format(image=IMAGE, mem=MEMBER): {
+ 'DELETE': (
+ {},
+ None,
+ ),
+ 'PUT': (
+ {},
+ {
+ 'image_id': IMAGE,
+ 'member_id': MEMBER,
+ 'status': 'accepted'
+ }
+ ),
+ }
+}
+
+schema_fixtures = {
+ 'member': {
+ 'GET': (
+ {},
+ {
+ 'name': 'member',
+ 'properties': {
+ 'image_id': {},
+ 'member_id': {}
+ }
+ },
+ )
+ }
+}
+
+
+class TestController(testtools.TestCase):
+ def setUp(self):
+ super(TestController, self).setUp()
+ self.api = utils.FakeAPI(data_fixtures)
+ self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
+ self.controller = image_members.Controller(self.api, self.schema_api)
+
+ def test_list_image_members(self):
+ image_id = IMAGE
+ #NOTE(iccha): cast to list since the controller returns a generator
+ image_members = list(self.controller.list(image_id))
+ self.assertEqual(IMAGE, image_members[0].image_id)
+ self.assertEqual(MEMBER, image_members[0].member_id)
+
+ def test_delete_image_member(self):
+ image_id = IMAGE
+ member_id = MEMBER
+ self.controller.delete(image_id, member_id)
+ expect = [
+ ('DELETE',
+ '/v2/images/{image}/members/{mem}'.format(image=IMAGE,
+ mem=MEMBER),
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_update_image_members(self):
+ image_id = IMAGE
+ member_id = MEMBER
+ status = 'accepted'
+ image_member = self.controller.update(image_id, member_id, status)
+ self.assertEqual(IMAGE, image_member.image_id)
+ self.assertEqual(MEMBER, image_member.member_id)
+ self.assertEqual(status, image_member.status)
+
+ def test_create_image_members(self):
+ image_id = IMAGE
+ member_id = MEMBER
+ status = 'pending'
+ image_member = self.controller.create(image_id, member_id)
+ self.assertEqual(IMAGE, image_member.image_id)
+ self.assertEqual(MEMBER, image_member.member_id)
+ self.assertEqual(status, image_member.status)
diff --git a/glanceclient/tests/unit/v2/test_metadefs_namespaces.py b/glanceclient/tests/unit/v2/test_metadefs_namespaces.py
new file mode 100644
index 0000000..ecc05ee
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_metadefs_namespaces.py
@@ -0,0 +1,674 @@
+# Copyright 2012 OpenStack Foundation.
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient.tests import utils
+from glanceclient.v2 import metadefs
+
+NAMESPACE1 = 'Namespace1'
+NAMESPACE2 = 'Namespace2'
+NAMESPACE3 = 'Namespace3'
+NAMESPACE4 = 'Namespace4'
+NAMESPACE5 = 'Namespace5'
+NAMESPACE6 = 'Namespace6'
+NAMESPACE7 = 'Namespace7'
+NAMESPACE8 = 'Namespace8'
+NAMESPACENEW = 'NamespaceNew'
+RESOURCE_TYPE1 = 'ResourceType1'
+RESOURCE_TYPE2 = 'ResourceType2'
+OBJECT1 = 'Object1'
+PROPERTY1 = 'Property1'
+PROPERTY2 = 'Property2'
+
+
+def _get_namespace_fixture(ns_name, rt_name=RESOURCE_TYPE1, **kwargs):
+ ns = {
+ "display_name": "Flavor Quota",
+ "description": "DESCRIPTION1",
+ "self": "/v2/metadefs/namespaces/%s" % ns_name,
+ "namespace": ns_name,
+ "visibility": "public",
+ "protected": True,
+ "owner": "admin",
+ "resource_types": [
+ {
+ "name": rt_name
+ }
+ ],
+ "schema": "/v2/schemas/metadefs/namespace",
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+
+ ns.update(kwargs)
+
+ return ns
+
+data_fixtures = {
+ "/v2/metadefs/namespaces?limit=20": {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=20",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE1),
+ _get_namespace_fixture(NAMESPACE2),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces"
+ }
+ )
+ },
+ "/v2/metadefs/namespaces?limit=1": {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=1",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE7),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces",
+ "next": "/v2/metadefs/namespaces?marker=%s&limit=1"
+ % NAMESPACE7,
+ }
+ )
+ },
+ "/v2/metadefs/namespaces?limit=1&marker=%s" % NAMESPACE7: {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=2",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE8),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces"
+ }
+ )
+ },
+ "/v2/metadefs/namespaces?limit=2&marker=%s" % NAMESPACE6: {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=2",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE7),
+ _get_namespace_fixture(NAMESPACE8),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces"
+ }
+ )
+ },
+ "/v2/metadefs/namespaces?limit=20&sort_dir=asc": {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=1",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE1),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces"
+ }
+ )
+ },
+ "/v2/metadefs/namespaces?limit=20&sort_key=created_at": {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=1",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE1),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces"
+ }
+ )
+ },
+ "/v2/metadefs/namespaces?limit=20&resource_types=%s" % RESOURCE_TYPE1: {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=20",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE3),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces"
+ }
+ )
+ },
+ "/v2/metadefs/namespaces?limit=20&resource_types="
+ "%s%%2C%s" % (RESOURCE_TYPE1, RESOURCE_TYPE2): {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=20",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE4),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces"
+ }
+ )
+ },
+ "/v2/metadefs/namespaces?limit=20&visibility=private": {
+ "GET": (
+ {},
+ {
+ "first": "/v2/metadefs/namespaces?limit=20",
+ "namespaces": [
+ _get_namespace_fixture(NAMESPACE5),
+ ],
+ "schema": "/v2/schemas/metadefs/namespaces"
+ }
+ )
+ },
+ "/v2/metadefs/namespaces": {
+ "POST": (
+ {},
+ {
+ "display_name": "Flavor Quota",
+ "description": "DESCRIPTION1",
+ "self": "/v2/metadefs/namespaces/%s" % 'NamespaceNew',
+ "namespace": 'NamespaceNew',
+ "visibility": "public",
+ "protected": True,
+ "owner": "admin",
+ "schema": "/v2/schemas/metadefs/namespace",
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+ )
+ },
+ "/v2/metadefs/namespaces/%s" % NAMESPACE1: {
+ "GET": (
+ {},
+ {
+ "display_name": "Flavor Quota",
+ "description": "DESCRIPTION1",
+ "objects": [
+ {
+ "description": "DESCRIPTION2",
+ "name": "OBJECT1",
+ "self": "/v2/metadefs/namespaces/%s/objects/" %
+ OBJECT1,
+ "required": [],
+ "properties": {
+ PROPERTY1: {
+ "type": "integer",
+ "description": "DESCRIPTION3",
+ "title": "Quota: CPU Shares"
+ },
+ PROPERTY2: {
+ "minimum": 1000,
+ "type": "integer",
+ "description": "DESCRIPTION4",
+ "maximum": 1000000,
+ "title": "Quota: CPU Period"
+ },
+ },
+ "schema": "/v2/schemas/metadefs/object"
+ }
+ ],
+ "self": "/v2/metadefs/namespaces/%s" % NAMESPACE1,
+ "namespace": NAMESPACE1,
+ "visibility": "public",
+ "protected": True,
+ "owner": "admin",
+ "resource_types": [
+ {
+ "name": RESOURCE_TYPE1
+ }
+ ],
+ "schema": "/v2/schemas/metadefs/namespace",
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+ ),
+ "PUT": (
+ {},
+ {
+ "display_name": "Flavor Quota",
+ "description": "DESCRIPTION1",
+ "objects": [
+ {
+ "description": "DESCRIPTION2",
+ "name": "OBJECT1",
+ "self": "/v2/metadefs/namespaces/%s/objects/" %
+ OBJECT1,
+ "required": [],
+ "properties": {
+ PROPERTY1: {
+ "type": "integer",
+ "description": "DESCRIPTION3",
+ "title": "Quota: CPU Shares"
+ },
+ PROPERTY2: {
+ "minimum": 1000,
+ "type": "integer",
+ "description": "DESCRIPTION4",
+ "maximum": 1000000,
+ "title": "Quota: CPU Period"
+ },
+ },
+ "schema": "/v2/schemas/metadefs/object"
+ }
+ ],
+ "self": "/v2/metadefs/namespaces/%s" % NAMESPACENEW,
+ "namespace": NAMESPACENEW,
+ "visibility": "public",
+ "protected": True,
+ "owner": "admin",
+ "resource_types": [
+ {
+ "name": RESOURCE_TYPE1
+ }
+ ],
+ "schema": "/v2/schemas/metadefs/namespace",
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+ ),
+ "DELETE": (
+ {},
+ {}
+ )
+ },
+ "/v2/metadefs/namespaces/%s?resource_type=%s" % (NAMESPACE6,
+ RESOURCE_TYPE1):
+ {
+ "GET": (
+ {},
+ {
+ "display_name": "Flavor Quota",
+ "description": "DESCRIPTION1",
+ "objects": [],
+ "self": "/v2/metadefs/namespaces/%s" % NAMESPACE1,
+ "namespace": NAMESPACE6,
+ "visibility": "public",
+ "protected": True,
+ "owner": "admin",
+ "resource_types": [
+ {
+ "name": RESOURCE_TYPE1
+ }
+ ],
+ "schema": "/v2/schemas/metadefs/namespace",
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+ ),
+ },
+}
+
+schema_fixtures = {
+ "metadefs/namespace":
+ {
+ "GET": (
+ {},
+ {
+ "additionalProperties": False,
+ "definitions": {
+ "property": {
+ "additionalProperties": {
+ "required": [
+ "title",
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "additionalItems": {
+ "type": "boolean"
+ },
+ "enum": {
+ "type": "array"
+ },
+ "description": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "default": {},
+ "minLength": {
+ "$ref": "#/definitions/"
+ "positiveIntegerDefault0"
+ },
+ "required": {
+ "$ref": "#/definitions/stringArray"
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "minItems": {
+ "$ref": "#/definitions/"
+ "positiveIntegerDefault0"
+ },
+ "readonly": {
+ "type": "boolean"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "maxItems": {
+ "$ref": "#/definitions/"
+ "positiveInteger"
+ },
+ "maxLength": {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ "uniqueItems": {
+ "default": False,
+ "type": "boolean"
+ },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "items": {
+ "type": "object",
+ "properties": {
+ "enum": {
+ "type": "array"
+ },
+ "type": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "number",
+ "object",
+ "string",
+ "null"
+ ],
+ "type": "string"
+ }
+ }
+ },
+ "type": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "number",
+ "object",
+ "string",
+ "null"
+ ],
+ "type": "string"
+ }
+ }
+ },
+ "type": "object"
+ },
+ "positiveIntegerDefault0": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ {
+ "default": 0
+ }
+ ]
+ },
+ "stringArray": {
+ "uniqueItems": True,
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "positiveInteger": {
+ "minimum": 0,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "namespace"
+ ],
+ "name": "namespace",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "Provides a user friendly description "
+ "of the namespace.",
+ "maxLength": 500
+ },
+ "updated_at": {
+ "type": "string",
+ "description": "Date and time of the last namespace "
+ "modification (READ-ONLY)",
+ "format": "date-time"
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "private"
+ ],
+ "type": "string",
+ "description": "Scope of namespace accessibility."
+ },
+ "self": {
+ "type": "string"
+ },
+ "objects": {
+ "items": {
+ "type": "object",
+ "properties": {
+ "properties": {
+ "$ref": "#/definitions/property"
+ },
+ "required": {
+ "$ref": "#/definitions/stringArray"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ }
+ },
+ "type": "array"
+ },
+ "owner": {
+ "type": "string",
+ "description": "Owner of the namespace.",
+ "maxLength": 255
+ },
+ "resource_types": {
+ "items": {
+ "type": "object",
+ "properties": {
+ "prefix": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "metadata_type": {
+ "type": "string"
+ }
+ }
+ },
+ "type": "array"
+ },
+ "properties": {
+ "$ref": "#/definitions/property"
+ },
+ "display_name": {
+ "type": "string",
+ "description": "The user friendly name for the "
+ "namespace. Used by UI if available.",
+ "maxLength": 80
+ },
+ "created_at": {
+ "type": "string",
+ "description": "Date and time of namespace creation "
+ "(READ-ONLY)",
+ "format": "date-time"
+ },
+ "namespace": {
+ "type": "string",
+ "description": "The unique namespace text.",
+ "maxLength": 80
+ },
+ "protected": {
+ "type": "boolean",
+ "description": "If true, namespace will not be "
+ "deletable."
+ },
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ ),
+ }
+}
+
+
+class TestNamespaceController(testtools.TestCase):
+ def setUp(self):
+ super(TestNamespaceController, self).setUp()
+ self.api = utils.FakeAPI(data_fixtures)
+ self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
+ self.controller = metadefs.NamespaceController(self.api,
+ self.schema_api)
+
+ def test_list_namespaces(self):
+ namespaces = list(self.controller.list())
+
+ self.assertEqual(2, len(namespaces))
+ self.assertEqual(NAMESPACE1, namespaces[0]['namespace'])
+ self.assertEqual(NAMESPACE2, namespaces[1]['namespace'])
+
+ def test_list_namespaces_paginate(self):
+ namespaces = list(self.controller.list(page_size=1))
+
+ self.assertEqual(2, len(namespaces))
+ self.assertEqual(NAMESPACE7, namespaces[0]['namespace'])
+ self.assertEqual(NAMESPACE8, namespaces[1]['namespace'])
+
+ def test_list_with_limit_greater_than_page_size(self):
+ namespaces = list(self.controller.list(page_size=1, limit=2))
+ self.assertEqual(2, len(namespaces))
+ self.assertEqual(NAMESPACE7, namespaces[0]['namespace'])
+ self.assertEqual(NAMESPACE8, namespaces[1]['namespace'])
+
+ def test_list_with_marker(self):
+ namespaces = list(self.controller.list(marker=NAMESPACE6, page_size=2))
+ self.assertEqual(2, len(namespaces))
+ self.assertEqual(NAMESPACE7, namespaces[0]['namespace'])
+ self.assertEqual(NAMESPACE8, namespaces[1]['namespace'])
+
+ def test_list_with_sort_dir(self):
+ namespaces = list(self.controller.list(sort_dir='asc', limit=1))
+ self.assertEqual(1, len(namespaces))
+ self.assertEqual(NAMESPACE1, namespaces[0]['namespace'])
+
+ def test_list_with_sort_dir_invalid(self):
+ # NOTE(TravT): The clients work by returning an iterator.
+ # Invoking the iterator is what actually executes the logic.
+ ns_iterator = self.controller.list(sort_dir='foo')
+ self.assertRaises(ValueError, next, ns_iterator)
+
+ def test_list_with_sort_key(self):
+ namespaces = list(self.controller.list(sort_key='created_at', limit=1))
+ self.assertEqual(1, len(namespaces))
+ self.assertEqual(NAMESPACE1, namespaces[0]['namespace'])
+
+ def test_list_with_sort_key_invalid(self):
+ # NOTE(TravT): The clients work by returning an iterator.
+ # Invoking the iterator is what actually executes the logic.
+ ns_iterator = self.controller.list(sort_key='foo')
+ self.assertRaises(ValueError, next, ns_iterator)
+
+ def test_list_namespaces_with_one_resource_type_filter(self):
+ namespaces = list(self.controller.list(
+ filters={
+ 'resource_types': [RESOURCE_TYPE1]
+ }
+ ))
+
+ self.assertEqual(1, len(namespaces))
+ self.assertEqual(NAMESPACE3, namespaces[0]['namespace'])
+
+ def test_list_namespaces_with_multiple_resource_types_filter(self):
+ namespaces = list(self.controller.list(
+ filters={
+ 'resource_types': [RESOURCE_TYPE1, RESOURCE_TYPE2]
+ }
+ ))
+
+ self.assertEqual(1, len(namespaces))
+ self.assertEqual(NAMESPACE4, namespaces[0]['namespace'])
+
+ def test_list_namespaces_with_visibility_filter(self):
+ namespaces = list(self.controller.list(
+ filters={
+ 'visibility': 'private'
+ }
+ ))
+
+ self.assertEqual(1, len(namespaces))
+ self.assertEqual(NAMESPACE5, namespaces[0]['namespace'])
+
+ def test_get_namespace(self):
+ namespace = self.controller.get(NAMESPACE1)
+ self.assertEqual(NAMESPACE1, namespace.namespace)
+ self.assertTrue(namespace.protected)
+
+ def test_get_namespace_with_resource_type(self):
+ namespace = self.controller.get(NAMESPACE6,
+ resource_type=RESOURCE_TYPE1)
+ self.assertEqual(NAMESPACE6, namespace.namespace)
+ self.assertTrue(namespace.protected)
+
+ def test_create_namespace(self):
+ properties = {
+ 'namespace': NAMESPACENEW
+ }
+ namespace = self.controller.create(**properties)
+
+ self.assertEqual(NAMESPACENEW, namespace.namespace)
+ self.assertTrue(namespace.protected)
+
+ def test_create_namespace_invalid_data(self):
+ properties = {}
+
+ self.assertRaises(TypeError, self.controller.create, **properties)
+
+ def test_create_namespace_invalid_property(self):
+ properties = {'namespace': 'NewNamespace', 'protected': '123'}
+
+ self.assertRaises(TypeError, self.controller.create, **properties)
+
+ def test_update_namespace(self):
+ properties = {'display_name': 'My Updated Name'}
+ namespace = self.controller.update(NAMESPACE1, **properties)
+
+ self.assertEqual(NAMESPACE1, namespace.namespace)
+
+ def test_update_namespace_invalid_property(self):
+ properties = {'protected': '123'}
+
+ self.assertRaises(TypeError, self.controller.update, NAMESPACE1,
+ **properties)
+
+ def test_delete_namespace(self):
+ self.controller.delete(NAMESPACE1)
+ expect = [
+ ('DELETE',
+ '/v2/metadefs/namespaces/%s' % NAMESPACE1,
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
diff --git a/glanceclient/tests/unit/v2/test_metadefs_objects.py b/glanceclient/tests/unit/v2/test_metadefs_objects.py
new file mode 100644
index 0000000..c565b3c
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_metadefs_objects.py
@@ -0,0 +1,323 @@
+# Copyright 2012 OpenStack Foundation.
+# All Rights Reserved.
+#
+# 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 six
+import testtools
+
+from glanceclient.tests import utils
+from glanceclient.v2 import metadefs
+
+NAMESPACE1 = 'Namespace1'
+OBJECT1 = 'Object1'
+OBJECT2 = 'Object2'
+OBJECTNEW = 'ObjectNew'
+PROPERTY1 = 'Property1'
+PROPERTY2 = 'Property2'
+PROPERTY3 = 'Property3'
+PROPERTY4 = 'Property4'
+
+
+def _get_object_fixture(ns_name, obj_name, **kwargs):
+ obj = {
+ "description": "DESCRIPTION",
+ "name": obj_name,
+ "self": "/v2/metadefs/namespaces/%s/objects/%s" %
+ (ns_name, obj_name),
+ "required": [],
+ "properties": {
+ PROPERTY1: {
+ "type": "integer",
+ "description": "DESCRIPTION",
+ "title": "Quota: CPU Shares"
+ },
+ PROPERTY2: {
+ "minimum": 1000,
+ "type": "integer",
+ "description": "DESCRIPTION",
+ "maximum": 1000000,
+ "title": "Quota: CPU Period"
+ }},
+ "schema": "/v2/schemas/metadefs/object",
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+
+ obj.update(kwargs)
+
+ return obj
+
+data_fixtures = {
+ "/v2/metadefs/namespaces/%s/objects" % NAMESPACE1: {
+ "GET": (
+ {},
+ {
+ "objects": [
+ _get_object_fixture(NAMESPACE1, OBJECT1),
+ _get_object_fixture(NAMESPACE1, OBJECT2)
+ ],
+ "schema": "v2/schemas/metadefs/objects"
+ }
+ ),
+ "POST": (
+ {},
+ _get_object_fixture(NAMESPACE1, OBJECTNEW)
+ ),
+ "DELETE": (
+ {},
+ {}
+ )
+ },
+ "/v2/metadefs/namespaces/%s/objects/%s" % (NAMESPACE1, OBJECT1): {
+ "GET": (
+ {},
+ _get_object_fixture(NAMESPACE1, OBJECT1)
+ ),
+ "PUT": (
+ {},
+ _get_object_fixture(NAMESPACE1, OBJECT1)
+ ),
+ "DELETE": (
+ {},
+ {}
+ )
+ }
+}
+
+schema_fixtures = {
+ "metadefs/object": {
+ "GET": (
+ {},
+ {
+ "additionalProperties": False,
+ "definitions": {
+ "property": {
+ "additionalProperties": {
+ "required": [
+ "title",
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "additionalItems": {
+ "type": "boolean"
+ },
+ "enum": {
+ "type": "array"
+ },
+ "description": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "default": {},
+ "minLength": {
+ "$ref": "#/definitions/positiveInteger"
+ "Default0"
+ },
+ "required": {
+ "$ref": "#/definitions/stringArray"
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "minItems": {
+ "$ref": "#/definitions/positiveInteger"
+ "Default0"
+ },
+ "readonly": {
+ "type": "boolean"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "maxItems": {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ "maxLength": {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ "uniqueItems": {
+ "default": False,
+ "type": "boolean"
+ },
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "items": {
+ "type": "object",
+ "properties": {
+ "enum": {
+ "type": "array"
+ },
+ "type": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "number",
+ "object",
+ "string",
+ "null"
+ ],
+ "type": "string"
+ }
+ }
+ },
+ "type": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "number",
+ "object",
+ "string",
+ "null"
+ ],
+ "type": "string"
+ }
+ }
+ },
+ "type": "object"
+ },
+ "positiveIntegerDefault0": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ {
+ "default": 0
+ }
+ ]
+ },
+ "stringArray": {
+ "uniqueItems": True,
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "positiveInteger": {
+ "minimum": 0,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "name": "object",
+ "properties": {
+ "created_at": {
+ "type": "string",
+ "description": "Date and time of object creation "
+ "(READ-ONLY)",
+ "format": "date-time"
+ },
+ "description": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "self": {
+ "type": "string"
+ },
+ "required": {
+ "$ref": "#/definitions/stringArray"
+ },
+ "properties": {
+ "$ref": "#/definitions/property"
+ },
+ "schema": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": "string",
+ "description": "Date and time of the last object "
+ "modification (READ-ONLY)",
+ "format": "date-time"
+ },
+ }
+ }
+ )
+ }
+}
+
+
+class TestObjectController(testtools.TestCase):
+ def setUp(self):
+ super(TestObjectController, self).setUp()
+ self.api = utils.FakeAPI(data_fixtures)
+ self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
+ self.controller = metadefs.ObjectController(self.api, self.schema_api)
+
+ def test_list_object(self):
+ objects = list(self.controller.list(NAMESPACE1))
+
+ actual = [obj.name for obj in objects]
+ self.assertEqual([OBJECT1, OBJECT2], actual)
+
+ def test_get_object(self):
+ obj = self.controller.get(NAMESPACE1, OBJECT1)
+ self.assertEqual(OBJECT1, obj.name)
+ self.assertEqual(sorted([PROPERTY1, PROPERTY2]),
+ sorted(list(six.iterkeys(obj.properties))))
+
+ def test_create_object(self):
+ properties = {
+ 'name': OBJECTNEW,
+ 'description': 'DESCRIPTION'
+ }
+ obj = self.controller.create(NAMESPACE1, **properties)
+ self.assertEqual(OBJECTNEW, obj.name)
+
+ def test_create_object_invalid_property(self):
+ properties = {
+ 'namespace': NAMESPACE1
+ }
+ self.assertRaises(TypeError, self.controller.create, **properties)
+
+ def test_update_object(self):
+ properties = {
+ 'description': 'UPDATED_DESCRIPTION'
+ }
+ obj = self.controller.update(NAMESPACE1, OBJECT1, **properties)
+ self.assertEqual(OBJECT1, obj.name)
+
+ def test_update_object_invalid_property(self):
+ properties = {
+ 'required': 'INVALID'
+ }
+ self.assertRaises(TypeError, self.controller.update, NAMESPACE1,
+ OBJECT1, **properties)
+
+ def test_delete_object(self):
+ self.controller.delete(NAMESPACE1, OBJECT1)
+ expect = [
+ ('DELETE',
+ '/v2/metadefs/namespaces/%s/objects/%s' % (NAMESPACE1, OBJECT1),
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_delete_all_objects(self):
+ self.controller.delete_all(NAMESPACE1)
+ expect = [
+ ('DELETE',
+ '/v2/metadefs/namespaces/%s/objects' % NAMESPACE1,
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
diff --git a/glanceclient/tests/unit/v2/test_metadefs_properties.py b/glanceclient/tests/unit/v2/test_metadefs_properties.py
new file mode 100644
index 0000000..388bf93
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_metadefs_properties.py
@@ -0,0 +1,300 @@
+# Copyright 2012 OpenStack Foundation.
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient.tests import utils
+from glanceclient.v2 import metadefs
+
+NAMESPACE1 = 'Namespace1'
+PROPERTY1 = 'Property1'
+PROPERTY2 = 'Property2'
+PROPERTYNEW = 'PropertyNew'
+
+data_fixtures = {
+ "/v2/metadefs/namespaces/%s/properties" % NAMESPACE1: {
+ "GET": (
+ {},
+ {
+ "properties": {
+ PROPERTY1: {
+ "default": "1",
+ "type": "integer",
+ "description": "Number of cores.",
+ "title": "cores"
+ },
+ PROPERTY2: {
+ "items": {
+ "enum": [
+ "Intel",
+ "AMD"
+ ],
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Specifies the CPU manufacturer.",
+ "title": "Vendor"
+ },
+ }
+ }
+ ),
+ "POST": (
+ {},
+ {
+ "items": {
+ "enum": [
+ "Intel",
+ "AMD"
+ ],
+ "type": "string"
+ },
+ "type": "array",
+ "description": "UPDATED_DESCRIPTION",
+ "title": "Vendor",
+ "name": PROPERTYNEW
+ }
+ ),
+ "DELETE": (
+ {},
+ {}
+ )
+ },
+ "/v2/metadefs/namespaces/%s/properties/%s" % (NAMESPACE1, PROPERTY1): {
+ "GET": (
+ {},
+ {
+ "items": {
+ "enum": [
+ "Intel",
+ "AMD"
+ ],
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Specifies the CPU manufacturer.",
+ "title": "Vendor"
+ }
+ ),
+ "PUT": (
+ {},
+ {
+ "items": {
+ "enum": [
+ "Intel",
+ "AMD"
+ ],
+ "type": "string"
+ },
+ "type": "array",
+ "description": "UPDATED_DESCRIPTION",
+ "title": "Vendor"
+ }
+ ),
+ "DELETE": (
+ {},
+ {}
+ )
+ }
+}
+
+schema_fixtures = {
+ "metadefs/property": {
+ "GET": (
+ {},
+ {
+ "additionalProperties": False,
+ "definitions": {
+ "positiveIntegerDefault0": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ {
+ "default": 0
+ }
+ ]
+ },
+ "stringArray": {
+ "minItems": 1,
+ "items": {
+ "type": "string"
+ },
+ "uniqueItems": True,
+ "type": "array"
+ },
+ "positiveInteger": {
+ "minimum": 0,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "name",
+ "title",
+ "type"
+ ],
+ "name": "property",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "minLength": {
+ "$ref": "#/definitions/positiveIntegerDefault0"
+ },
+ "enum": {
+ "type": "array"
+ },
+ "minimum": {
+ "type": "number"
+ },
+ "maxItems": {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ "maxLength": {
+ "$ref": "#/definitions/positiveInteger"
+ },
+ "uniqueItems": {
+ "default": False,
+ "type": "boolean"
+ },
+ "additionalItems": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "default": {},
+ "pattern": {
+ "type": "string",
+ "format": "regex"
+ },
+ "required": {
+ "$ref": "#/definitions/stringArray"
+ },
+ "maximum": {
+ "type": "number"
+ },
+ "minItems": {
+ "$ref": "#/definitions/positiveIntegerDefault0"
+ },
+ "readonly": {
+ "type": "boolean"
+ },
+ "items": {
+ "type": "object",
+ "properties": {
+ "enum": {
+ "type": "array"
+ },
+ "type": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "number",
+ "object",
+ "string",
+ "null"
+ ],
+ "type": "string"
+ }
+ }
+ },
+ "type": {
+ "enum": [
+ "array",
+ "boolean",
+ "integer",
+ "number",
+ "object",
+ "string",
+ "null"
+ ],
+ "type": "string"
+ }
+ }
+ }
+ )
+ }
+}
+
+
+class TestPropertyController(testtools.TestCase):
+ def setUp(self):
+ super(TestPropertyController, self).setUp()
+ self.api = utils.FakeAPI(data_fixtures)
+ self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
+ self.controller = metadefs.PropertyController(self.api,
+ self.schema_api)
+
+ def test_list_property(self):
+ properties = list(self.controller.list(NAMESPACE1))
+
+ actual = [prop.name for prop in properties]
+ self.assertEqual(sorted([PROPERTY1, PROPERTY2]), sorted(actual))
+
+ def test_get_property(self):
+ prop = self.controller.get(NAMESPACE1, PROPERTY1)
+ self.assertEqual(PROPERTY1, prop.name)
+
+ def test_create_property(self):
+ properties = {
+ 'name': PROPERTYNEW,
+ 'title': 'TITLE',
+ 'type': 'string'
+ }
+ obj = self.controller.create(NAMESPACE1, **properties)
+ self.assertEqual(PROPERTYNEW, obj.name)
+
+ def test_create_property_invalid_property(self):
+ properties = {
+ 'namespace': NAMESPACE1
+ }
+ self.assertRaises(TypeError, self.controller.create, **properties)
+
+ def test_update_property(self):
+ properties = {
+ 'description': 'UPDATED_DESCRIPTION'
+ }
+ prop = self.controller.update(NAMESPACE1, PROPERTY1, **properties)
+ self.assertEqual(PROPERTY1, prop.name)
+
+ def test_update_property_invalid_property(self):
+ properties = {
+ 'type': 'INVALID'
+ }
+ self.assertRaises(TypeError, self.controller.update, NAMESPACE1,
+ PROPERTY1, **properties)
+
+ def test_delete_property(self):
+ self.controller.delete(NAMESPACE1, PROPERTY1)
+ expect = [
+ ('DELETE',
+ '/v2/metadefs/namespaces/%s/properties/%s' % (NAMESPACE1,
+ PROPERTY1),
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_delete_all_properties(self):
+ self.controller.delete_all(NAMESPACE1)
+ expect = [
+ ('DELETE',
+ '/v2/metadefs/namespaces/%s/properties' % NAMESPACE1,
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
diff --git a/glanceclient/tests/unit/v2/test_metadefs_resource_types.py b/glanceclient/tests/unit/v2/test_metadefs_resource_types.py
new file mode 100644
index 0000000..de3f9c2
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_metadefs_resource_types.py
@@ -0,0 +1,186 @@
+# Copyright 2012 OpenStack Foundation.
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient.tests import utils
+from glanceclient.v2 import metadefs
+
+NAMESPACE1 = 'Namespace1'
+RESOURCE_TYPE1 = 'ResourceType1'
+RESOURCE_TYPE2 = 'ResourceType2'
+RESOURCE_TYPE3 = 'ResourceType3'
+RESOURCE_TYPE4 = 'ResourceType4'
+RESOURCE_TYPENEW = 'ResourceTypeNew'
+
+
+data_fixtures = {
+ "/v2/metadefs/namespaces/%s/resource_types" % NAMESPACE1: {
+ "GET": (
+ {},
+ {
+ "resource_type_associations": [
+ {
+ "name": RESOURCE_TYPE3,
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ },
+ {
+ "name": RESOURCE_TYPE4,
+ "prefix": "PREFIX:",
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+ ]
+ }
+ ),
+ "POST": (
+ {},
+ {
+ "name": RESOURCE_TYPENEW,
+ "prefix": "PREFIX:",
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+ ),
+ },
+ "/v2/metadefs/namespaces/%s/resource_types/%s" % (NAMESPACE1,
+ RESOURCE_TYPE1):
+ {
+ "DELETE": (
+ {},
+ {}
+ ),
+ },
+ "/v2/metadefs/resource_types": {
+ "GET": (
+ {},
+ {
+ "resource_types": [
+ {
+ "name": RESOURCE_TYPE1,
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ },
+ {
+ "name": RESOURCE_TYPE2,
+ "created_at": "2014-08-14T09:07:06Z",
+ "updated_at": "2014-08-14T09:07:06Z",
+ }
+ ]
+ }
+ )
+ }
+}
+
+schema_fixtures = {
+ "metadefs/resource_type": {
+ "GET": (
+ {},
+ {
+ "name": "resource_type",
+ "properties": {
+ "prefix": {
+ "type": "string",
+ "description": "Specifies the prefix to use for the "
+ "given resource type. Any properties "
+ "in the namespace should be prefixed "
+ "with this prefix when being applied "
+ "to the specified resource type. Must "
+ "include prefix separator (e.g. a "
+ "colon :).",
+ "maxLength": 80
+ },
+ "properties_target": {
+ "type": "string",
+ "description": "Some resource types allow more than "
+ "one key / value pair per instance. "
+ "For example, Cinder allows user and "
+ "image metadata on volumes. Only the "
+ "image properties metadata is "
+ "evaluated by Nova (scheduling or "
+ "drivers). This property allows a "
+ "namespace target to remove the "
+ "ambiguity.",
+ "maxLength": 80
+ },
+ "name": {
+ "type": "string",
+ "description": "Resource type names should be "
+ "aligned with Heat resource types "
+ "whenever possible: http://docs."
+ "openstack.org/developer/heat/"
+ "template_guide/openstack.html",
+ "maxLength": 80
+ },
+ "created_at": {
+ "type": "string",
+ "description": "Date and time of resource type "
+ "association (READ-ONLY)",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "description": "Date and time of the last resource "
+ "type association modification "
+ "(READ-ONLY)",
+ "format": "date-time"
+ },
+ }
+ }
+ )
+ }
+}
+
+
+class TestResoureTypeController(testtools.TestCase):
+ def setUp(self):
+ super(TestResoureTypeController, self).setUp()
+ self.api = utils.FakeAPI(data_fixtures)
+ self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
+ self.controller = metadefs.ResourceTypeController(self.api,
+ self.schema_api)
+
+ def test_list_resource_types(self):
+ resource_types = list(self.controller.list())
+ names = [rt.name for rt in resource_types]
+ self.assertEqual([RESOURCE_TYPE1, RESOURCE_TYPE2], names)
+
+ def test_get_resource_types(self):
+ resource_types = list(self.controller.get(NAMESPACE1))
+ names = [rt.name for rt in resource_types]
+ self.assertEqual([RESOURCE_TYPE3, RESOURCE_TYPE4], names)
+
+ def test_associate_resource_types(self):
+ resource_types = self.controller.associate(NAMESPACE1,
+ name=RESOURCE_TYPENEW)
+
+ self.assertEqual(RESOURCE_TYPENEW, resource_types['name'])
+
+ def test_associate_resource_types_invalid_property(self):
+ longer = '1234' * 50
+ properties = {'name': RESOURCE_TYPENEW, 'prefix': longer}
+ self.assertRaises(TypeError, self.controller.associate, NAMESPACE1,
+ **properties)
+
+ def test_deassociate_resource_types(self):
+ self.controller.deassociate(NAMESPACE1, RESOURCE_TYPE1)
+ expect = [
+ ('DELETE',
+ '/v2/metadefs/namespaces/%s/resource_types/%s' % (NAMESPACE1,
+ RESOURCE_TYPE1),
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
diff --git a/glanceclient/tests/unit/v2/test_schemas.py b/glanceclient/tests/unit/v2/test_schemas.py
new file mode 100644
index 0000000..35788cd
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_schemas.py
@@ -0,0 +1,231 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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.
+
+from jsonpatch import JsonPatch
+import testtools
+import warlock
+
+from glanceclient.tests import utils
+from glanceclient.v2 import schemas
+
+
+fixtures = {
+ '/v2/schemas': {
+ 'GET': (
+ {},
+ {
+ 'image': '/v2/schemas/image',
+ 'access': '/v2/schemas/image/access',
+ },
+ ),
+ },
+ '/v2/schemas/image': {
+ 'GET': (
+ {},
+ {
+ 'name': 'image',
+ 'properties': {
+ 'name': {'type': 'string',
+ 'description': 'Name of image'},
+ 'tags': {'type': 'array'}
+ },
+
+ },
+ ),
+ },
+}
+
+
+_SCHEMA = schemas.Schema({
+ 'name': 'image',
+ 'properties': {
+ 'name': {'type': 'string'},
+ 'color': {'type': 'string'},
+ 'shape': {'type': 'string', 'is_base': False},
+ 'tags': {'type': 'array'}
+ },
+})
+
+
+def compare_json_patches(a, b):
+ """Return 0 if a and b describe the same JSON patch."""
+ return JsonPatch.from_string(a) == JsonPatch.from_string(b)
+
+
+class TestSchemaProperty(testtools.TestCase):
+ def test_property_minimum(self):
+ prop = schemas.SchemaProperty('size')
+ self.assertEqual('size', prop.name)
+
+ def test_property_description(self):
+ prop = schemas.SchemaProperty('size', description='some quantity')
+ self.assertEqual('size', prop.name)
+ self.assertEqual('some quantity', prop.description)
+
+ def test_property_is_base(self):
+ prop1 = schemas.SchemaProperty('name')
+ prop2 = schemas.SchemaProperty('foo', is_base=False)
+ prop3 = schemas.SchemaProperty('foo', is_base=True)
+ self.assertTrue(prop1.is_base)
+ self.assertFalse(prop2.is_base)
+ self.assertTrue(prop3.is_base)
+
+
+class TestSchema(testtools.TestCase):
+ def test_schema_minimum(self):
+ raw_schema = {'name': 'Country', 'properties': {}}
+ schema = schemas.Schema(raw_schema)
+ self.assertEqual('Country', schema.name)
+ self.assertEqual([], schema.properties)
+
+ def test_schema_with_property(self):
+ raw_schema = {'name': 'Country', 'properties': {'size': {}}}
+ schema = schemas.Schema(raw_schema)
+ self.assertEqual('Country', schema.name)
+ self.assertEqual(['size'], [p.name for p in schema.properties])
+
+ def test_raw(self):
+ raw_schema = {'name': 'Country', 'properties': {}}
+ schema = schemas.Schema(raw_schema)
+ self.assertEqual(raw_schema, schema.raw())
+
+ def test_property_is_base(self):
+ raw_schema = {'name': 'Country',
+ 'properties': {
+ 'size': {},
+ 'population': {'is_base': False}}}
+ schema = schemas.Schema(raw_schema)
+ self.assertTrue(schema.is_base_property('size'))
+ self.assertFalse(schema.is_base_property('population'))
+ self.assertFalse(schema.is_base_property('foo'))
+
+
+class TestController(testtools.TestCase):
+ def setUp(self):
+ super(TestController, self).setUp()
+ self.api = utils.FakeAPI(fixtures)
+ self.controller = schemas.Controller(self.api)
+
+ def test_get_schema(self):
+ schema = self.controller.get('image')
+ self.assertEqual('image', schema.name)
+ self.assertEqual(['name', 'tags'],
+ [p.name for p in schema.properties])
+
+
+class TestSchemaBasedModel(testtools.TestCase):
+ def setUp(self):
+ super(TestSchemaBasedModel, self).setUp()
+ self.model = warlock.model_factory(_SCHEMA.raw(),
+ schemas.SchemaBasedModel)
+
+ def test_patch_should_replace_missing_core_properties(self):
+ obj = {
+ 'name': 'fred'
+ }
+
+ original = self.model(obj)
+ original['color'] = 'red'
+
+ patch = original.patch
+ expected = '[{"path": "/color", "value": "red", "op": "replace"}]'
+ self.assertTrue(compare_json_patches(patch, expected))
+
+ def test_patch_should_add_extra_properties(self):
+ obj = {
+ 'name': 'fred',
+ }
+
+ original = self.model(obj)
+ original['weight'] = '10'
+
+ patch = original.patch
+ expected = '[{"path": "/weight", "value": "10", "op": "add"}]'
+ self.assertTrue(compare_json_patches(patch, expected))
+
+ def test_patch_should_replace_extra_properties(self):
+ obj = {
+ 'name': 'fred',
+ 'weight': '10'
+ }
+
+ original = self.model(obj)
+ original['weight'] = '22'
+
+ patch = original.patch
+ expected = '[{"path": "/weight", "value": "22", "op": "replace"}]'
+ self.assertTrue(compare_json_patches(patch, expected))
+
+ def test_patch_should_remove_extra_properties(self):
+ obj = {
+ 'name': 'fred',
+ 'weight': '10'
+ }
+
+ original = self.model(obj)
+ del original['weight']
+
+ patch = original.patch
+ expected = '[{"path": "/weight", "op": "remove"}]'
+ self.assertTrue(compare_json_patches(patch, expected))
+
+ def test_patch_should_remove_core_properties(self):
+ obj = {
+ 'name': 'fred',
+ 'color': 'red'
+ }
+
+ original = self.model(obj)
+ del original['color']
+
+ patch = original.patch
+ expected = '[{"path": "/color", "op": "remove"}]'
+ self.assertTrue(compare_json_patches(patch, expected))
+
+ def test_patch_should_add_missing_custom_properties(self):
+ obj = {
+ 'name': 'fred'
+ }
+
+ original = self.model(obj)
+ original['shape'] = 'circle'
+
+ patch = original.patch
+ expected = '[{"path": "/shape", "value": "circle", "op": "add"}]'
+ self.assertTrue(compare_json_patches(patch, expected))
+
+ def test_patch_should_replace_custom_properties(self):
+ obj = {
+ 'name': 'fred',
+ 'shape': 'circle'
+ }
+
+ original = self.model(obj)
+ original['shape'] = 'square'
+
+ patch = original.patch
+ expected = '[{"path": "/shape", "value": "square", "op": "replace"}]'
+ self.assertTrue(compare_json_patches(patch, expected))
+
+ def test_patch_should_replace_tags(self):
+ obj = {'name': 'fred', }
+
+ original = self.model(obj)
+ original['tags'] = ['tag1', 'tag2']
+
+ patch = original.patch
+ expected = '[{"path": "/tags", "value": ["tag1", "tag2"], ' \
+ '"op": "replace"}]'
+ self.assertTrue(compare_json_patches(patch, expected))
diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py
new file mode 100644
index 0000000..33985b9
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_shell_v2.py
@@ -0,0 +1,1094 @@
+# Copyright 2013 OpenStack Foundation
+# Copyright (C) 2013 Yahoo! Inc.
+# All Rights Reserved.
+#
+# 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 json
+import mock
+import os
+import tempfile
+import testtools
+
+from glanceclient.common import utils
+from glanceclient.v2 import shell as test_shell
+
+
+class ShellV2Test(testtools.TestCase):
+ def setUp(self):
+ super(ShellV2Test, self).setUp()
+ self._mock_utils()
+ self.gc = self._mock_glance_client()
+
+ def _make_args(self, args):
+ #NOTE(venkatesh): this conversion from a dict to an object
+ # is required because the test_shell.do_xxx(gc, args) methods
+ # expects the args to be attributes of an object. If passed as
+ # dict directly, it throws an AttributeError.
+ class Args():
+ def __init__(self, entries):
+ self.__dict__.update(entries)
+
+ return Args(args)
+
+ def _mock_glance_client(self):
+ my_mocked_gc = mock.Mock()
+ my_mocked_gc.schemas.return_value = 'test'
+ my_mocked_gc.get.return_value = {}
+ return my_mocked_gc
+
+ def _mock_utils(self):
+ utils.print_list = mock.Mock()
+ utils.print_dict = mock.Mock()
+ utils.save_image = mock.Mock()
+
+ def assert_exits_with_msg(self, func, func_args, err_msg):
+ with mock.patch.object(utils, 'exit') as mocked_utils_exit:
+ mocked_utils_exit.return_value = '%s' % err_msg
+
+ func(self.gc, func_args)
+
+ mocked_utils_exit.assert_called_once_with(err_msg)
+
+ def test_do_image_list(self):
+ input = {
+ 'limit': None,
+ 'page_size': 18,
+ 'visibility': True,
+ 'member_status': 'Fake',
+ 'owner': 'test',
+ 'checksum': 'fake_checksum',
+ 'tag': 'fake tag',
+ 'properties': [],
+ 'sort_key': ['name', 'id'],
+ 'sort_dir': ['desc', 'asc'],
+ 'sort': None
+ }
+ args = self._make_args(input)
+ with mock.patch.object(self.gc.images, 'list') as mocked_list:
+ mocked_list.return_value = {}
+
+ test_shell.do_image_list(self.gc, args)
+
+ exp_img_filters = {
+ 'owner': 'test',
+ 'member_status': 'Fake',
+ 'visibility': True,
+ 'checksum': 'fake_checksum',
+ 'tag': 'fake tag'
+ }
+ mocked_list.assert_called_once_with(page_size=18,
+ sort_key=['name', 'id'],
+ sort_dir=['desc', 'asc'],
+ filters=exp_img_filters)
+ utils.print_list.assert_called_once_with({}, ['ID', 'Name'])
+
+ def test_do_image_list_with_single_sort_key(self):
+ input = {
+ 'limit': None,
+ 'page_size': 18,
+ 'visibility': True,
+ 'member_status': 'Fake',
+ 'owner': 'test',
+ 'checksum': 'fake_checksum',
+ 'tag': 'fake tag',
+ 'properties': [],
+ 'sort_key': ['name'],
+ 'sort_dir': ['desc'],
+ 'sort': None
+ }
+ args = self._make_args(input)
+ with mock.patch.object(self.gc.images, 'list') as mocked_list:
+ mocked_list.return_value = {}
+
+ test_shell.do_image_list(self.gc, args)
+
+ exp_img_filters = {
+ 'owner': 'test',
+ 'member_status': 'Fake',
+ 'visibility': True,
+ 'checksum': 'fake_checksum',
+ 'tag': 'fake tag'
+ }
+ mocked_list.assert_called_once_with(page_size=18,
+ sort_key=['name'],
+ sort_dir=['desc'],
+ filters=exp_img_filters)
+ utils.print_list.assert_called_once_with({}, ['ID', 'Name'])
+
+ def test_do_image_list_new_sorting_syntax(self):
+ input = {
+ 'limit': None,
+ 'page_size': 18,
+ 'visibility': True,
+ 'member_status': 'Fake',
+ 'owner': 'test',
+ 'checksum': 'fake_checksum',
+ 'tag': 'fake tag',
+ 'properties': [],
+ 'sort': 'name:desc,size:asc',
+ 'sort_key': [],
+ 'sort_dir': []
+ }
+ args = self._make_args(input)
+ with mock.patch.object(self.gc.images, 'list') as mocked_list:
+ mocked_list.return_value = {}
+
+ test_shell.do_image_list(self.gc, args)
+
+ exp_img_filters = {
+ 'owner': 'test',
+ 'member_status': 'Fake',
+ 'visibility': True,
+ 'checksum': 'fake_checksum',
+ 'tag': 'fake tag'
+ }
+ mocked_list.assert_called_once_with(
+ page_size=18,
+ sort='name:desc,size:asc',
+ filters=exp_img_filters)
+ utils.print_list.assert_called_once_with({}, ['ID', 'Name'])
+
+ def test_do_image_list_with_property_filter(self):
+ input = {
+ 'limit': None,
+ 'page_size': 1,
+ 'visibility': True,
+ 'member_status': 'Fake',
+ 'owner': 'test',
+ 'checksum': 'fake_checksum',
+ 'tag': 'fake tag',
+ 'properties': ['os_distro=NixOS', 'architecture=x86_64'],
+ 'sort_key': ['name'],
+ 'sort_dir': ['desc'],
+ 'sort': None
+ }
+ args = self._make_args(input)
+ with mock.patch.object(self.gc.images, 'list') as mocked_list:
+ mocked_list.return_value = {}
+
+ test_shell.do_image_list(self.gc, args)
+
+ exp_img_filters = {
+ 'owner': 'test',
+ 'member_status': 'Fake',
+ 'visibility': True,
+ 'checksum': 'fake_checksum',
+ 'tag': 'fake tag',
+ 'os_distro': 'NixOS',
+ 'architecture': 'x86_64'
+ }
+
+ mocked_list.assert_called_once_with(page_size=1,
+ sort_key=['name'],
+ sort_dir=['desc'],
+ filters=exp_img_filters)
+ utils.print_list.assert_called_once_with({}, ['ID', 'Name'])
+
+ def test_do_image_show_human_readable(self):
+ args = self._make_args({'id': 'pass', 'page_size': 18,
+ 'human_readable': True,
+ 'max_column_width': 120})
+ with mock.patch.object(self.gc.images, 'get') as mocked_list:
+ ignore_fields = ['self', 'access', 'file', 'schema']
+ expect_image = dict([(field, field) for field in ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['size'] = 1024
+ mocked_list.return_value = expect_image
+
+ test_shell.do_image_show(self.gc, args)
+
+ mocked_list.assert_called_once_with('pass')
+ utils.print_dict.assert_called_once_with({'id': 'pass',
+ 'size': '1kB'},
+ max_column_width=120)
+
+ def test_do_image_show(self):
+ args = self._make_args({'id': 'pass', 'page_size': 18,
+ 'human_readable': False,
+ 'max_column_width': 120})
+ with mock.patch.object(self.gc.images, 'get') as mocked_list:
+ ignore_fields = ['self', 'access', 'file', 'schema']
+ expect_image = dict([(field, field) for field in ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['size'] = 1024
+ mocked_list.return_value = expect_image
+
+ test_shell.do_image_show(self.gc, args)
+
+ mocked_list.assert_called_once_with('pass')
+ utils.print_dict.assert_called_once_with({'id': 'pass',
+ 'size': 1024},
+ max_column_width=120)
+
+ @mock.patch('sys.stdin', autospec=True)
+ def test_do_image_create_no_user_props(self, mock_stdin):
+ args = self._make_args({'name': 'IMG-01', 'disk_format': 'vhd',
+ 'container_format': 'bare',
+ 'file': None})
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ ignore_fields = ['self', 'access', 'file', 'schema']
+ expect_image = dict([(field, field) for field in ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['name'] = 'IMG-01'
+ expect_image['disk_format'] = 'vhd'
+ expect_image['container_format'] = 'bare'
+ mocked_create.return_value = expect_image
+
+ # Ensure that the test stdin is not considered
+ # to be supplying image data
+ mock_stdin.isatty = lambda: True
+ test_shell.do_image_create(self.gc, args)
+
+ mocked_create.assert_called_once_with(name='IMG-01',
+ disk_format='vhd',
+ container_format='bare')
+ utils.print_dict.assert_called_once_with({
+ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd',
+ 'container_format': 'bare'})
+
+ def test_do_image_create_with_file(self):
+ try:
+ file_name = None
+ with open(tempfile.mktemp(), 'w+') as f:
+ f.write('Some data here')
+ f.flush()
+ f.seek(0)
+ file_name = f.name
+ temp_args = {'name': 'IMG-01',
+ 'disk_format': 'vhd',
+ 'container_format': 'bare',
+ 'file': file_name,
+ 'progress': False}
+ args = self._make_args(temp_args)
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['name'] = 'IMG-01'
+ expect_image['disk_format'] = 'vhd'
+ expect_image['container_format'] = 'bare'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+
+ test_shell.do_image_create(self.gc, args)
+
+ temp_args.pop('file', None)
+ mocked_create.assert_called_once_with(**temp_args)
+ mocked_get.assert_called_once_with('pass')
+ utils.print_dict.assert_called_once_with({
+ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd',
+ 'container_format': 'bare'})
+ finally:
+ try:
+ os.remove(f.name)
+ except Exception:
+ pass
+
+ @mock.patch('sys.stdin', autospec=True)
+ def test_do_image_create_with_user_props(self, mock_stdin):
+ args = self._make_args({'name': 'IMG-01',
+ 'property': ['myprop=myval'],
+ 'file': None})
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ ignore_fields = ['self', 'access', 'file', 'schema']
+ expect_image = dict([(field, field) for field in ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['name'] = 'IMG-01'
+ expect_image['myprop'] = 'myval'
+ mocked_create.return_value = expect_image
+
+ # Ensure that the test stdin is not considered
+ # to be supplying image data
+ mock_stdin.isatty = lambda: True
+ test_shell.do_image_create(self.gc, args)
+
+ mocked_create.assert_called_once_with(name='IMG-01',
+ myprop='myval')
+ utils.print_dict.assert_called_once_with({
+ 'id': 'pass', 'name': 'IMG-01', 'myprop': 'myval'})
+
+ def test_do_image_update_no_user_props(self):
+ args = self._make_args({'id': 'pass', 'name': 'IMG-01',
+ 'disk_format': 'vhd',
+ 'container_format': 'bare'})
+ with mock.patch.object(self.gc.images, 'update') as mocked_update:
+ ignore_fields = ['self', 'access', 'file', 'schema']
+ expect_image = dict([(field, field) for field in ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['name'] = 'IMG-01'
+ expect_image['disk_format'] = 'vhd'
+ expect_image['container_format'] = 'bare'
+ mocked_update.return_value = expect_image
+
+ test_shell.do_image_update(self.gc, args)
+
+ mocked_update.assert_called_once_with('pass',
+ None,
+ name='IMG-01',
+ disk_format='vhd',
+ container_format='bare')
+ utils.print_dict.assert_called_once_with({
+ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd',
+ 'container_format': 'bare'})
+
+ def test_do_image_update_with_user_props(self):
+ args = self._make_args({'id': 'pass', 'name': 'IMG-01',
+ 'property': ['myprop=myval']})
+ with mock.patch.object(self.gc.images, 'update') as mocked_update:
+ ignore_fields = ['self', 'access', 'file', 'schema']
+ expect_image = dict([(field, field) for field in ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['name'] = 'IMG-01'
+ expect_image['myprop'] = 'myval'
+ mocked_update.return_value = expect_image
+
+ test_shell.do_image_update(self.gc, args)
+
+ mocked_update.assert_called_once_with('pass',
+ None,
+ name='IMG-01',
+ myprop='myval')
+ utils.print_dict.assert_called_once_with({
+ 'id': 'pass', 'name': 'IMG-01', 'myprop': 'myval'})
+
+ def test_do_image_update_with_remove_props(self):
+ args = self._make_args({'id': 'pass', 'name': 'IMG-01',
+ 'disk_format': 'vhd',
+ 'remove-property': ['container_format']})
+ with mock.patch.object(self.gc.images, 'update') as mocked_update:
+ ignore_fields = ['self', 'access', 'file', 'schema']
+ expect_image = dict([(field, field) for field in ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['name'] = 'IMG-01'
+ expect_image['disk_format'] = 'vhd'
+
+ mocked_update.return_value = expect_image
+
+ test_shell.do_image_update(self.gc, args)
+
+ mocked_update.assert_called_once_with('pass',
+ ['container_format'],
+ name='IMG-01',
+ disk_format='vhd')
+ utils.print_dict.assert_called_once_with({
+ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd'})
+
+ def test_do_explain(self):
+ input = {
+ 'page_size': 18,
+ 'id': 'pass',
+ 'schemas': 'test',
+ 'model': 'test',
+ }
+ args = self._make_args(input)
+ with mock.patch.object(utils, 'print_list'):
+ test_shell.do_explain(self.gc, args)
+
+ self.gc.schemas.get.assert_called_once_with('test')
+
+ def test_do_location_add(self):
+ gc = self.gc
+ loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'bar'}}
+ args = self._make_args({'id': 'pass',
+ 'url': loc['url'],
+ 'metadata': json.dumps(loc['metadata'])})
+ with mock.patch.object(gc.images, 'add_location') as mocked_addloc:
+ expect_image = {'id': 'pass', 'locations': [loc]}
+ mocked_addloc.return_value = expect_image
+
+ test_shell.do_location_add(self.gc, args)
+ mocked_addloc.assert_called_once_with('pass',
+ loc['url'],
+ loc['metadata'])
+ utils.print_dict.assert_called_once_with(expect_image)
+
+ def test_do_location_delete(self):
+ gc = self.gc
+ loc_set = set(['http://foo/bar', 'http://spam/ham'])
+ args = self._make_args({'id': 'pass', 'url': loc_set})
+
+ with mock.patch.object(gc.images, 'delete_locations') as mocked_rmloc:
+ test_shell.do_location_delete(self.gc, args)
+ mocked_rmloc.assert_called_once_with('pass', loc_set)
+
+ def test_do_location_update(self):
+ gc = self.gc
+ loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'bar'}}
+ args = self._make_args({'id': 'pass',
+ 'url': loc['url'],
+ 'metadata': json.dumps(loc['metadata'])})
+ with mock.patch.object(gc.images, 'update_location') as mocked_modloc:
+ expect_image = {'id': 'pass', 'locations': [loc]}
+ mocked_modloc.return_value = expect_image
+
+ test_shell.do_location_update(self.gc, args)
+ mocked_modloc.assert_called_once_with('pass',
+ loc['url'],
+ loc['metadata'])
+ utils.print_dict.assert_called_once_with(expect_image)
+
+ def test_image_upload(self):
+ args = self._make_args(
+ {'id': 'IMG-01', 'file': 'test', 'size': 1024, 'progress': False})
+
+ with mock.patch.object(self.gc.images, 'upload') as mocked_upload:
+ utils.get_data_file = mock.Mock(return_value='testfile')
+ mocked_upload.return_value = None
+ test_shell.do_image_upload(self.gc, args)
+ mocked_upload.assert_called_once_with('IMG-01', 'testfile', 1024)
+
+ def test_image_download(self):
+ args = self._make_args(
+ {'id': 'IMG-01', 'file': 'test', 'progress': True})
+
+ with mock.patch.object(self.gc.images, 'data') as mocked_data:
+ def _data():
+ for c in 'abcedf':
+ yield c
+ mocked_data.return_value = utils.IterableWithLength(_data(), 5)
+
+ test_shell.do_image_download(self.gc, args)
+ mocked_data.assert_called_once_with('IMG-01')
+
+ def test_do_image_delete(self):
+ args = self._make_args({'id': 'pass', 'file': 'test'})
+ with mock.patch.object(self.gc.images, 'delete') as mocked_delete:
+ mocked_delete.return_value = 0
+
+ test_shell.do_image_delete(self.gc, args)
+
+ mocked_delete.assert_called_once_with('pass')
+
+ def test_do_image_delete_deleted(self):
+ image_id = 'deleted-img'
+ args = self._make_args({'id': image_id})
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ mocked_get.return_value = self._make_args({'id': image_id,
+ 'status': 'deleted'})
+
+ msg = "No image with an ID of '%s' exists." % image_id
+ self.assert_exits_with_msg(func=test_shell.do_image_delete,
+ func_args=args,
+ err_msg=msg)
+
+ def test_do_member_list(self):
+ args = self._make_args({'image_id': 'IMG-01'})
+ with mock.patch.object(self.gc.image_members, 'list') as mocked_list:
+ mocked_list.return_value = {}
+
+ test_shell.do_member_list(self.gc, args)
+
+ mocked_list.assert_called_once_with('IMG-01')
+ columns = ['Image ID', 'Member ID', 'Status']
+ utils.print_list.assert_called_once_with({}, columns)
+
+ def test_do_member_create(self):
+ args = self._make_args({'image_id': 'IMG-01', 'member_id': 'MEM-01'})
+ with mock.patch.object(self.gc.image_members, 'create') as mock_create:
+ mock_create.return_value = {}
+
+ test_shell.do_member_create(self.gc, args)
+
+ mock_create.assert_called_once_with('IMG-01', 'MEM-01')
+ columns = ['Image ID', 'Member ID', 'Status']
+ utils.print_list.assert_called_once_with([{}], columns)
+
+ def test_do_member_create_with_few_arguments(self):
+ args = self._make_args({'image_id': None, 'member_id': 'MEM-01'})
+ msg = 'Unable to create member. Specify image_id and member_id'
+
+ self.assert_exits_with_msg(func=test_shell.do_member_create,
+ func_args=args,
+ err_msg=msg)
+
+ def test_do_member_update(self):
+ input = {
+ 'image_id': 'IMG-01',
+ 'member_id': 'MEM-01',
+ 'member_status': 'status',
+ }
+ args = self._make_args(input)
+ with mock.patch.object(self.gc.image_members, 'update') as mock_update:
+ mock_update.return_value = {}
+
+ test_shell.do_member_update(self.gc, args)
+
+ mock_update.assert_called_once_with('IMG-01', 'MEM-01', 'status')
+ columns = ['Image ID', 'Member ID', 'Status']
+ utils.print_list.assert_called_once_with([{}], columns)
+
+ def test_do_member_update_with_few_arguments(self):
+ input = {
+ 'image_id': 'IMG-01',
+ 'member_id': 'MEM-01',
+ 'member_status': None,
+ }
+ args = self._make_args(input)
+ msg = 'Unable to update member. Specify image_id, member_id' \
+ ' and member_status'
+
+ self.assert_exits_with_msg(func=test_shell.do_member_update,
+ func_args=args,
+ err_msg=msg)
+
+ def test_do_member_delete(self):
+ args = self._make_args({'image_id': 'IMG-01', 'member_id': 'MEM-01'})
+ with mock.patch.object(self.gc.image_members, 'delete') as mock_delete:
+ test_shell.do_member_delete(self.gc, args)
+
+ mock_delete.assert_called_once_with('IMG-01', 'MEM-01')
+
+ def test_do_member_delete_with_few_arguments(self):
+ args = self._make_args({'image_id': None, 'member_id': 'MEM-01'})
+ msg = 'Unable to delete member. Specify image_id and member_id'
+
+ self.assert_exits_with_msg(func=test_shell.do_member_delete,
+ func_args=args,
+ err_msg=msg)
+
+ def test_image_tag_update(self):
+ args = self._make_args({'image_id': 'IMG-01', 'tag_value': 'tag01'})
+ with mock.patch.object(self.gc.image_tags, 'update') as mocked_update:
+ self.gc.images.get = mock.Mock(return_value={})
+ mocked_update.return_value = None
+
+ test_shell.do_image_tag_update(self.gc, args)
+
+ mocked_update.assert_called_once_with('IMG-01', 'tag01')
+
+ def test_image_tag_update_with_few_arguments(self):
+ args = self._make_args({'image_id': None, 'tag_value': 'tag01'})
+ msg = 'Unable to update tag. Specify image_id and tag_value'
+
+ self.assert_exits_with_msg(func=test_shell.do_image_tag_update,
+ func_args=args,
+ err_msg=msg)
+
+ def test_image_tag_delete(self):
+ args = self._make_args({'image_id': 'IMG-01', 'tag_value': 'tag01'})
+ with mock.patch.object(self.gc.image_tags, 'delete') as mocked_delete:
+ mocked_delete.return_value = None
+
+ test_shell.do_image_tag_delete(self.gc, args)
+
+ mocked_delete.assert_called_once_with('IMG-01', 'tag01')
+
+ def test_image_tag_delete_with_few_arguments(self):
+ args = self._make_args({'image_id': 'IMG-01', 'tag_value': None})
+ msg = 'Unable to delete tag. Specify image_id and tag_value'
+
+ self.assert_exits_with_msg(func=test_shell.do_image_tag_delete,
+ func_args=args,
+ err_msg=msg)
+
+ def test_do_md_namespace_create(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'protected': True})
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'create') as mocked_create:
+ expect_namespace = {}
+ expect_namespace['namespace'] = 'MyNamespace'
+ expect_namespace['protected'] = True
+
+ mocked_create.return_value = expect_namespace
+
+ test_shell.do_md_namespace_create(self.gc, args)
+
+ mocked_create.assert_called_once_with(namespace='MyNamespace',
+ protected=True)
+ utils.print_dict.assert_called_once_with(expect_namespace)
+
+ def test_do_md_namespace_import(self):
+ args = self._make_args({'file': 'test'})
+
+ expect_namespace = {}
+ expect_namespace['namespace'] = 'MyNamespace'
+ expect_namespace['protected'] = True
+
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'create') as mocked_create:
+ mock_read = mock.Mock(return_value=json.dumps(expect_namespace))
+ mock_file = mock.Mock(read=mock_read)
+ utils.get_data_file = mock.Mock(return_value=mock_file)
+ mocked_create.return_value = expect_namespace
+
+ test_shell.do_md_namespace_import(self.gc, args)
+
+ mocked_create.assert_called_once_with(**expect_namespace)
+ utils.print_dict.assert_called_once_with(expect_namespace)
+
+ def test_do_md_namespace_import_invalid_json(self):
+ args = self._make_args({'file': 'test'})
+ mock_read = mock.Mock(return_value='Invalid')
+ mock_file = mock.Mock(read=mock_read)
+ utils.get_data_file = mock.Mock(return_value=mock_file)
+
+ self.assertRaises(SystemExit, test_shell.do_md_namespace_import,
+ self.gc, args)
+
+ def test_do_md_namespace_import_no_input(self):
+ args = self._make_args({'file': None})
+ utils.get_data_file = mock.Mock(return_value=None)
+
+ self.assertRaises(SystemExit, test_shell.do_md_namespace_import,
+ self.gc, args)
+
+ def test_do_md_namespace_update(self):
+ args = self._make_args({'id': 'MyNamespace',
+ 'protected': True})
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'update') as mocked_update:
+ expect_namespace = {}
+ expect_namespace['namespace'] = 'MyNamespace'
+ expect_namespace['protected'] = True
+
+ mocked_update.return_value = expect_namespace
+
+ test_shell.do_md_namespace_update(self.gc, args)
+
+ mocked_update.assert_called_once_with('MyNamespace',
+ id='MyNamespace',
+ protected=True)
+ utils.print_dict.assert_called_once_with(expect_namespace)
+
+ def test_do_md_namespace_show(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'max_column_width': 80,
+ 'resource_type': None})
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'get') as mocked_get:
+ expect_namespace = {}
+ expect_namespace['namespace'] = 'MyNamespace'
+
+ mocked_get.return_value = expect_namespace
+
+ test_shell.do_md_namespace_show(self.gc, args)
+
+ mocked_get.assert_called_once_with('MyNamespace')
+ utils.print_dict.assert_called_once_with(expect_namespace, 80)
+
+ def test_do_md_namespace_show_resource_type(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'max_column_width': 80,
+ 'resource_type': 'RESOURCE'})
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'get') as mocked_get:
+ expect_namespace = {}
+ expect_namespace['namespace'] = 'MyNamespace'
+
+ mocked_get.return_value = expect_namespace
+
+ test_shell.do_md_namespace_show(self.gc, args)
+
+ mocked_get.assert_called_once_with('MyNamespace',
+ resource_type='RESOURCE')
+ utils.print_dict.assert_called_once_with(expect_namespace, 80)
+
+ def test_do_md_namespace_list(self):
+ args = self._make_args({'resource_type': None,
+ 'visibility': None,
+ 'page_size': None})
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'list') as mocked_list:
+ expect_namespaces = [{'namespace': 'MyNamespace'}]
+
+ mocked_list.return_value = expect_namespaces
+
+ test_shell.do_md_namespace_list(self.gc, args)
+
+ mocked_list.assert_called_once_with(filters={})
+ utils.print_list.assert_called_once_with(expect_namespaces,
+ ['namespace'])
+
+ def test_do_md_namespace_list_page_size(self):
+ args = self._make_args({'resource_type': None,
+ 'visibility': None,
+ 'page_size': 2})
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'list') as mocked_list:
+ expect_namespaces = [{'namespace': 'MyNamespace'}]
+
+ mocked_list.return_value = expect_namespaces
+
+ test_shell.do_md_namespace_list(self.gc, args)
+
+ mocked_list.assert_called_once_with(filters={}, page_size=2)
+ utils.print_list.assert_called_once_with(expect_namespaces,
+ ['namespace'])
+
+ def test_do_md_namespace_list_one_filter(self):
+ args = self._make_args({'resource_types': ['OS::Compute::Aggregate'],
+ 'visibility': None,
+ 'page_size': None})
+ with mock.patch.object(self.gc.metadefs_namespace, 'list') as \
+ mocked_list:
+ expect_namespaces = [{'namespace': 'MyNamespace'}]
+
+ mocked_list.return_value = expect_namespaces
+
+ test_shell.do_md_namespace_list(self.gc, args)
+
+ mocked_list.assert_called_once_with(filters={
+ 'resource_types': ['OS::Compute::Aggregate']})
+ utils.print_list.assert_called_once_with(expect_namespaces,
+ ['namespace'])
+
+ def test_do_md_namespace_list_all_filters(self):
+ args = self._make_args({'resource_types': ['OS::Compute::Aggregate'],
+ 'visibility': 'public',
+ 'page_size': None})
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'list') as mocked_list:
+ expect_namespaces = [{'namespace': 'MyNamespace'}]
+
+ mocked_list.return_value = expect_namespaces
+
+ test_shell.do_md_namespace_list(self.gc, args)
+
+ mocked_list.assert_called_once_with(filters={
+ 'resource_types': ['OS::Compute::Aggregate'],
+ 'visibility': 'public'})
+ utils.print_list.assert_called_once_with(expect_namespaces,
+ ['namespace'])
+
+ def test_do_md_namespace_list_unknown_filter(self):
+ args = self._make_args({'resource_type': None,
+ 'visibility': None,
+ 'some_arg': 'some_value',
+ 'page_size': None})
+ with mock.patch.object(self.gc.metadefs_namespace,
+ 'list') as mocked_list:
+ expect_namespaces = [{'namespace': 'MyNamespace'}]
+
+ mocked_list.return_value = expect_namespaces
+
+ test_shell.do_md_namespace_list(self.gc, args)
+
+ mocked_list.assert_called_once_with(filters={})
+ utils.print_list.assert_called_once_with(expect_namespaces,
+ ['namespace'])
+
+ def test_do_md_namespace_delete(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'content': False})
+ with mock.patch.object(self.gc.metadefs_namespace, 'delete') as \
+ mocked_delete:
+ test_shell.do_md_namespace_delete(self.gc, args)
+
+ mocked_delete.assert_called_once_with('MyNamespace')
+
+ def test_do_md_resource_type_associate(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'name': 'MyResourceType',
+ 'prefix': 'PREFIX:'})
+ with mock.patch.object(self.gc.metadefs_resource_type,
+ 'associate') as mocked_associate:
+ expect_rt = {}
+ expect_rt['namespace'] = 'MyNamespace'
+ expect_rt['name'] = 'MyResourceType'
+ expect_rt['prefix'] = 'PREFIX:'
+
+ mocked_associate.return_value = expect_rt
+
+ test_shell.do_md_resource_type_associate(self.gc, args)
+
+ mocked_associate.assert_called_once_with('MyNamespace',
+ **expect_rt)
+ utils.print_dict.assert_called_once_with(expect_rt)
+
+ def test_do_md_resource_type_deassociate(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'resource_type': 'MyResourceType'})
+ with mock.patch.object(self.gc.metadefs_resource_type,
+ 'deassociate') as mocked_deassociate:
+ test_shell.do_md_resource_type_deassociate(self.gc, args)
+
+ mocked_deassociate.assert_called_once_with('MyNamespace',
+ 'MyResourceType')
+
+ def test_do_md_resource_type_list(self):
+ args = self._make_args({})
+ with mock.patch.object(self.gc.metadefs_resource_type,
+ 'list') as mocked_list:
+ expect_objects = ['MyResourceType1', 'MyResourceType2']
+
+ mocked_list.return_value = expect_objects
+
+ test_shell.do_md_resource_type_list(self.gc, args)
+
+ mocked_list.assert_called_once()
+
+ def test_do_md_namespace_resource_type_list(self):
+ args = self._make_args({'namespace': 'MyNamespace'})
+ with mock.patch.object(self.gc.metadefs_resource_type,
+ 'get') as mocked_get:
+ expect_objects = [{'namespace': 'MyNamespace',
+ 'object': 'MyObject'}]
+
+ mocked_get.return_value = expect_objects
+
+ test_shell.do_md_namespace_resource_type_list(self.gc, args)
+
+ mocked_get.assert_called_once_with('MyNamespace')
+ utils.print_list.assert_called_once_with(expect_objects,
+ ['name', 'prefix',
+ 'properties_target'])
+
+ def test_do_md_property_create(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'name': "MyProperty",
+ 'title': "Title",
+ 'schema': '{}'})
+ with mock.patch.object(self.gc.metadefs_property,
+ 'create') as mocked_create:
+ expect_property = {}
+ expect_property['namespace'] = 'MyNamespace'
+ expect_property['name'] = 'MyProperty'
+ expect_property['title'] = 'Title'
+
+ mocked_create.return_value = expect_property
+
+ test_shell.do_md_property_create(self.gc, args)
+
+ mocked_create.assert_called_once_with('MyNamespace',
+ name='MyProperty',
+ title='Title')
+ utils.print_dict.assert_called_once_with(expect_property)
+
+ def test_do_md_property_create_invalid_schema(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'name': "MyProperty",
+ 'title': "Title",
+ 'schema': 'Invalid'})
+ self.assertRaises(SystemExit, test_shell.do_md_property_create,
+ self.gc, args)
+
+ def test_do_md_property_update(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'property': 'MyProperty',
+ 'name': 'NewName',
+ 'title': "Title",
+ 'schema': '{}'})
+ with mock.patch.object(self.gc.metadefs_property,
+ 'update') as mocked_update:
+ expect_property = {}
+ expect_property['namespace'] = 'MyNamespace'
+ expect_property['name'] = 'MyProperty'
+ expect_property['title'] = 'Title'
+
+ mocked_update.return_value = expect_property
+
+ test_shell.do_md_property_update(self.gc, args)
+
+ mocked_update.assert_called_once_with('MyNamespace', 'MyProperty',
+ name='NewName',
+ title='Title')
+ utils.print_dict.assert_called_once_with(expect_property)
+
+ def test_do_md_property_update_invalid_schema(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'property': 'MyProperty',
+ 'name': "MyObject",
+ 'title': "Title",
+ 'schema': 'Invalid'})
+ self.assertRaises(SystemExit, test_shell.do_md_property_update,
+ self.gc, args)
+
+ def test_do_md_property_show(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'property': 'MyProperty',
+ 'max_column_width': 80})
+ with mock.patch.object(self.gc.metadefs_property, 'get') as mocked_get:
+ expect_property = {}
+ expect_property['namespace'] = 'MyNamespace'
+ expect_property['property'] = 'MyProperty'
+ expect_property['title'] = 'Title'
+
+ mocked_get.return_value = expect_property
+
+ test_shell.do_md_property_show(self.gc, args)
+
+ mocked_get.assert_called_once_with('MyNamespace', 'MyProperty')
+ utils.print_dict.assert_called_once_with(expect_property, 80)
+
+ def test_do_md_property_delete(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'property': 'MyProperty'})
+ with mock.patch.object(self.gc.metadefs_property,
+ 'delete') as mocked_delete:
+ test_shell.do_md_property_delete(self.gc, args)
+
+ mocked_delete.assert_called_once_with('MyNamespace', 'MyProperty')
+
+ def test_do_md_namespace_property_delete(self):
+ args = self._make_args({'namespace': 'MyNamespace'})
+ with mock.patch.object(self.gc.metadefs_property,
+ 'delete_all') as mocked_delete_all:
+ test_shell.do_md_namespace_properties_delete(self.gc, args)
+
+ mocked_delete_all.assert_called_once_with('MyNamespace')
+
+ def test_do_md_property_list(self):
+ args = self._make_args({'namespace': 'MyNamespace'})
+ with mock.patch.object(self.gc.metadefs_property,
+ 'list') as mocked_list:
+ expect_objects = [{'namespace': 'MyNamespace',
+ 'property': 'MyProperty',
+ 'title': 'MyTitle'}]
+
+ mocked_list.return_value = expect_objects
+
+ test_shell.do_md_property_list(self.gc, args)
+
+ mocked_list.assert_called_once_with('MyNamespace')
+ utils.print_list.assert_called_once_with(expect_objects,
+ ['name', 'title', 'type'])
+
+ def test_do_md_object_create(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'name': "MyObject",
+ 'schema': '{}'})
+ with mock.patch.object(self.gc.metadefs_object,
+ 'create') as mocked_create:
+ expect_object = {}
+ expect_object['namespace'] = 'MyNamespace'
+ expect_object['name'] = 'MyObject'
+
+ mocked_create.return_value = expect_object
+
+ test_shell.do_md_object_create(self.gc, args)
+
+ mocked_create.assert_called_once_with('MyNamespace',
+ name='MyObject')
+ utils.print_dict.assert_called_once_with(expect_object)
+
+ def test_do_md_object_create_invalid_schema(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'name': "MyObject",
+ 'schema': 'Invalid'})
+ self.assertRaises(SystemExit, test_shell.do_md_object_create,
+ self.gc, args)
+
+ def test_do_md_object_update(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'object': 'MyObject',
+ 'name': 'NewName',
+ 'schema': '{}'})
+ with mock.patch.object(self.gc.metadefs_object,
+ 'update') as mocked_update:
+ expect_object = {}
+ expect_object['namespace'] = 'MyNamespace'
+ expect_object['name'] = 'MyObject'
+
+ mocked_update.return_value = expect_object
+
+ test_shell.do_md_object_update(self.gc, args)
+
+ mocked_update.assert_called_once_with('MyNamespace', 'MyObject',
+ name='NewName')
+ utils.print_dict.assert_called_once_with(expect_object)
+
+ def test_do_md_object_update_invalid_schema(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'object': 'MyObject',
+ 'name': "MyObject",
+ 'schema': 'Invalid'})
+ self.assertRaises(SystemExit, test_shell.do_md_object_update,
+ self.gc, args)
+
+ def test_do_md_object_show(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'object': 'MyObject',
+ 'max_column_width': 80})
+ with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get:
+ expect_object = {}
+ expect_object['namespace'] = 'MyNamespace'
+ expect_object['object'] = 'MyObject'
+
+ mocked_get.return_value = expect_object
+
+ test_shell.do_md_object_show(self.gc, args)
+
+ mocked_get.assert_called_once_with('MyNamespace', 'MyObject')
+ utils.print_dict.assert_called_once_with(expect_object, 80)
+
+ def test_do_md_object_property_show(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'object': 'MyObject',
+ 'property': 'MyProperty',
+ 'max_column_width': 80})
+ with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get:
+ expect_object = {'name': 'MyObject',
+ 'properties': {
+ 'MyProperty': {'type': 'string'}
+ }}
+
+ mocked_get.return_value = expect_object
+
+ test_shell.do_md_object_property_show(self.gc, args)
+
+ mocked_get.assert_called_once_with('MyNamespace', 'MyObject')
+ utils.print_dict.assert_called_once_with({'type': 'string',
+ 'name': 'MyProperty'},
+ 80)
+
+ def test_do_md_object_property_show_non_existing(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'object': 'MyObject',
+ 'property': 'MyProperty',
+ 'max_column_width': 80})
+ with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get:
+ expect_object = {'name': 'MyObject', 'properties': {}}
+ mocked_get.return_value = expect_object
+
+ self.assertRaises(SystemExit,
+ test_shell.do_md_object_property_show,
+ self.gc, args)
+ mocked_get.assert_called_once_with('MyNamespace', 'MyObject')
+
+ def test_do_md_object_delete(self):
+ args = self._make_args({'namespace': 'MyNamespace',
+ 'object': 'MyObject'})
+ with mock.patch.object(self.gc.metadefs_object,
+ 'delete') as mocked_delete:
+ test_shell.do_md_object_delete(self.gc, args)
+
+ mocked_delete.assert_called_once_with('MyNamespace', 'MyObject')
+
+ def test_do_md_namespace_objects_delete(self):
+ args = self._make_args({'namespace': 'MyNamespace'})
+ with mock.patch.object(self.gc.metadefs_object,
+ 'delete_all') as mocked_delete_all:
+ test_shell.do_md_namespace_objects_delete(self.gc, args)
+
+ mocked_delete_all.assert_called_once_with('MyNamespace')
+
+ def test_do_md_object_list(self):
+ args = self._make_args({'namespace': 'MyNamespace'})
+ with mock.patch.object(self.gc.metadefs_object, 'list') as mocked_list:
+ expect_objects = [{'namespace': 'MyNamespace',
+ 'object': 'MyObject'}]
+
+ mocked_list.return_value = expect_objects
+
+ test_shell.do_md_object_list(self.gc, args)
+
+ mocked_list.assert_called_once_with('MyNamespace')
+ utils.print_list.assert_called_once_with(
+ expect_objects,
+ ['name', 'description'],
+ field_settings={
+ 'description': {'align': 'l', 'max_width': 50}})
diff --git a/glanceclient/tests/unit/v2/test_tags.py b/glanceclient/tests/unit/v2/test_tags.py
new file mode 100644
index 0000000..790080e
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_tags.py
@@ -0,0 +1,81 @@
+# Copyright 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient.tests import utils
+from glanceclient.v2 import image_tags
+
+
+IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1'
+TAG = 'tag01'
+
+
+data_fixtures = {
+ '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG): {
+ 'DELETE': (
+ {},
+ None,
+ ),
+ 'PUT': (
+ {},
+ {
+ 'image_id': IMAGE,
+ 'tag_value': TAG
+ }
+ ),
+ }
+}
+
+schema_fixtures = {
+ 'tag': {
+ 'GET': (
+ {},
+ {'name': 'image', 'properties': {'image_id': {}, 'tags': {}}}
+ )
+ }
+}
+
+
+class TestController(testtools.TestCase):
+ def setUp(self):
+ super(TestController, self).setUp()
+ self.api = utils.FakeAPI(data_fixtures)
+ self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
+ self.controller = image_tags.Controller(self.api, self.schema_api)
+
+ def test_update_image_tag(self):
+ image_id = IMAGE
+ tag_value = TAG
+ self.controller.update(image_id, tag_value)
+ expect = [
+ ('PUT',
+ '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE,
+ tag_value=TAG),
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
+
+ def test_delete_image_tag(self):
+ image_id = IMAGE
+ tag_value = TAG
+ self.controller.delete(image_id, tag_value)
+ expect = [
+ ('DELETE',
+ '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE,
+ tag_value=TAG),
+ {},
+ None)]
+ self.assertEqual(expect, self.api.calls)
diff --git a/glanceclient/tests/unit/v2/test_tasks.py b/glanceclient/tests/unit/v2/test_tasks.py
new file mode 100644
index 0000000..ed63286
--- /dev/null
+++ b/glanceclient/tests/unit/v2/test_tasks.py
@@ -0,0 +1,285 @@
+# Copyright 2013 OpenStack Foundation.
+# Copyright 2013 IBM Corp.
+# All Rights Reserved.
+#
+# 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 testtools
+
+from glanceclient.tests import utils
+from glanceclient.v2 import tasks
+
+
+_OWNED_TASK_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf'
+_OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f'
+_FAKE_OWNER_ID = '63e7f218-29de-4477-abdc-8db7c9533188'
+
+
+fixtures = {
+ '/v2/tasks?limit=%d' % tasks.DEFAULT_PAGE_SIZE: {
+ 'GET': (
+ {},
+ {'tasks': [
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'type': 'import',
+ 'status': 'pending',
+ },
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'type': 'import',
+ 'status': 'processing',
+ },
+ ]},
+ ),
+ },
+ '/v2/tasks?limit=1': {
+ 'GET': (
+ {},
+ {
+ 'tasks': [
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'type': 'import',
+ 'status': 'pending',
+ },
+ ],
+ 'next': ('/v2/tasks?limit=1&'
+ 'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'),
+ },
+ ),
+ },
+ ('/v2/tasks?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): {
+ 'GET': (
+ {},
+ {'tasks': [
+ {
+ 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810',
+ 'type': 'import',
+ 'status': 'pending',
+ },
+ ]},
+ ),
+ },
+ '/v2/tasks/3a4560a1-e585-443e-9b39-553b46ec92d1': {
+ 'GET': (
+ {},
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'type': 'import',
+ 'status': 'pending',
+ },
+ ),
+ 'PATCH': (
+ {},
+ '',
+ ),
+ },
+ '/v2/tasks/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': {
+ 'GET': (
+ {},
+ {
+ 'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3',
+ 'type': 'import',
+ 'status': 'pending',
+ },
+ ),
+ 'PATCH': (
+ {},
+ '',
+ ),
+ },
+ '/v2/tasks': {
+ 'POST': (
+ {},
+ {
+ 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
+ 'type': 'import',
+ 'status': 'pending',
+ 'input': '{"import_from": "file:///", '
+ '"import_from_format": "qcow2"}'
+ },
+ ),
+ },
+ '/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _OWNER_ID): {
+ 'GET': (
+ {},
+ {'tasks': [
+ {
+ 'id': _OWNED_TASK_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/tasks?limit=%d&status=processing' % (tasks.DEFAULT_PAGE_SIZE): {
+ 'GET': (
+ {},
+ {'tasks': [
+ {
+ 'id': _OWNED_TASK_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): {
+ 'GET': (
+ {},
+ {'tasks': [
+ {
+ 'id': _OWNED_TASK_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/tasks?limit=%d&type=fake' % (tasks.DEFAULT_PAGE_SIZE): {
+ 'GET': (
+ {},
+ {'tasks': [
+ ]},
+ ),
+ },
+ '/v2/tasks?limit=%d&status=fake' % (tasks.DEFAULT_PAGE_SIZE): {
+ 'GET': (
+ {},
+ {'tasks': [
+ ]},
+ ),
+ },
+ '/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): {
+ 'GET': (
+ {},
+ {'tasks': [
+ {
+ 'id': _OWNED_TASK_ID,
+ },
+ ]},
+ ),
+ },
+ '/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _FAKE_OWNER_ID):
+ {
+ 'GET': ({},
+ {'tasks': []},
+ ),
+ }
+}
+
+schema_fixtures = {
+ 'task': {
+ 'GET': (
+ {},
+ {
+ 'name': 'task',
+ 'properties': {
+ 'id': {},
+ 'type': {},
+ 'status': {},
+ 'input': {},
+ 'result': {},
+ 'message': {},
+ },
+ 'additionalProperties': False,
+ }
+ )
+ }
+}
+
+
+class TestController(testtools.TestCase):
+ def setUp(self):
+ super(TestController, self).setUp()
+ self.api = utils.FakeAPI(fixtures)
+ self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
+ self.controller = tasks.Controller(self.api, self.schema_api)
+
+ def test_list_tasks(self):
+ #NOTE(flwang): cast to list since the controller returns a generator
+ tasks = list(self.controller.list())
+ self.assertEqual(tasks[0].id, '3a4560a1-e585-443e-9b39-553b46ec92d1')
+ self.assertEqual(tasks[0].type, 'import')
+ self.assertEqual(tasks[0].status, 'pending')
+ self.assertEqual(tasks[1].id, '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810')
+ self.assertEqual(tasks[1].type, 'import')
+ self.assertEqual(tasks[1].status, 'processing')
+
+ def test_list_tasks_paginated(self):
+ #NOTE(flwang): cast to list since the controller returns a generator
+ tasks = list(self.controller.list(page_size=1))
+ self.assertEqual(tasks[0].id, '3a4560a1-e585-443e-9b39-553b46ec92d1')
+ self.assertEqual(tasks[0].type, 'import')
+ self.assertEqual(tasks[1].id, '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810')
+ self.assertEqual(tasks[1].type, 'import')
+
+ def test_list_tasks_with_status(self):
+ filters = {'filters': {'status': 'processing'}}
+ tasks = list(self.controller.list(**filters))
+ self.assertEqual(tasks[0].id, _OWNED_TASK_ID)
+
+ def test_list_tasks_with_wrong_status(self):
+ filters = {'filters': {'status': 'fake'}}
+ tasks = list(self.controller.list(**filters))
+ self.assertEqual(len(tasks), 0)
+
+ def test_list_tasks_with_type(self):
+ filters = {'filters': {'type': 'import'}}
+ tasks = list(self.controller.list(**filters))
+ self.assertEqual(tasks[0].id, _OWNED_TASK_ID)
+
+ def test_list_tasks_with_wrong_type(self):
+ filters = {'filters': {'type': 'fake'}}
+ tasks = list(self.controller.list(**filters))
+ self.assertEqual(len(tasks), 0)
+
+ def test_list_tasks_for_owner(self):
+ filters = {'filters': {'owner': _OWNER_ID}}
+ tasks = list(self.controller.list(**filters))
+ self.assertEqual(tasks[0].id, _OWNED_TASK_ID)
+
+ def test_list_tasks_for_fake_owner(self):
+ filters = {'filters': {'owner': _FAKE_OWNER_ID}}
+ tasks = list(self.controller.list(**filters))
+ self.assertEqual(tasks, [])
+
+ def test_list_tasks_filters_encoding(self):
+ filters = {"owner": u"ni\xf1o"}
+ try:
+ list(self.controller.list(filters=filters))
+ except KeyError:
+ # NOTE(flaper87): It raises KeyError because there's
+ # no fixture supporting this query:
+ # /v2/tasks?owner=ni%C3%B1o&limit=20
+ # We just want to make sure filters are correctly encoded.
+ pass
+
+ self.assertEqual(b"ni\xc3\xb1o", filters["owner"])
+
+ def test_get_task(self):
+ task = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1')
+ self.assertEqual(task.id, '3a4560a1-e585-443e-9b39-553b46ec92d1')
+ self.assertEqual(task.type, 'import')
+
+ def test_create_task(self):
+ properties = {
+ 'type': 'import',
+ 'input': {'import_from_format': 'ovf', 'import_from':
+ 'swift://cloud.foo/myaccount/mycontainer/path'},
+ }
+ task = self.controller.create(**properties)
+ self.assertEqual(task.id, '3a4560a1-e585-443e-9b39-553b46ec92d1')
+ self.assertEqual(task.type, 'import')
+
+ def test_create_task_invalid_property(self):
+ properties = {
+ 'type': 'import',
+ 'bad_prop': 'value',
+ }
+ self.assertRaises(TypeError, self.controller.create, **properties)
diff --git a/glanceclient/tests/unit/var/ca.crt b/glanceclient/tests/unit/var/ca.crt
new file mode 100644
index 0000000..c149d8c
--- /dev/null
+++ b/glanceclient/tests/unit/var/ca.crt
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF7jCCA9YCCQDbl9qx7iIeJDANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQ
+T3BlbnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAh
+BgkqhkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0
+ZSBDQTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3Rh
+Y2sgVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE2MTI1MDE2WhcN
+NDAwNDAzMTI1MDE2WjCBuDEZMBcGA1UEChMQT3BlbnN0YWNrIENBIE9yZzEaMBgG
+A1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkqhkiG9w0BCQEWFGFkbWluQGNh
+LmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBDQTELMAkGA1UECBMCQ0ExCzAJ
+BgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sgVGVzdCBDZXJ0aWZpY2F0ZSBB
+dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC94cpBjwj2
+MD0w5j1Jlcy8Ljmk3r7CRaoV5vhWUrAWpT7Thxr/Ti0qAfZZRSIVpvBM0RlseH0Q
+toUJixuYMoNRPUQ74r/TRoO8HfjQDJfnXtWg2L7DRP8p4Zgj3vByBUCU+rKsbI/H
+Nssl/AronADbZXCoL5hJRN8euMYZGrt/Gh1ZotKE5gQlEjylDFlA3s3pn+ABLgzf
+7L7iufwV3zLdPRHCb6Ve8YvUmKfI6gy+WwTRhNhLz4Nj0uBthnj6QhnRXtxkNT7A
+aAStqKH6TtYRnk2Owh8ITFbtLQ0/MSV8jHAxMXx9AloBhEKxv3cIpgLH6lOCnj//
+Ql+H6/QWtmTUHzP1kBfMhTQnWTfR92QTcgEMiZ7a07VyVtLh+kp/G5IUqpM6Pyz/
+O6QDs7FF69bTpws7Ce916PPrGFZ9Gqvo/P0jXge8kYqO+a8QnTRldAxdUzPJCK9+
+Dyi2LWeHf8nPFYdwW9Ov6Jw1CKDYxjJg6KIwnrMPa2eUdPB6/OKkqr9/KemOoKQu
+4KSaYadFZbaJwt7JPZaHy6TpkGxW7Af8RqGrW6a6nWEFcfO2POuHcAHWL5LiRmni
+unm60DBF3b3itDTqCvER3mZE9pN8dqtxdpB8SUX8eq0UJJK2K8mJQS+oE9crbqYb
+1kQbYjhhPLlvOQru+/m/abqZrC04u2OtYQIDAQABMA0GCSqGSIb3DQEBBQUAA4IC
+AQA8wGVBbzfpQ3eYpchiHyHF9N5LIhr6Bt4jYDKLz8DIbElLtoOlgH/v7hLGJ7wu
+R9OteonwQ1qr9umMmnp61bKXOEBJLBJbGKEt0MNLmmX89+M/h3rdMVZEz/Hht/xK
+Xm4di8pjkHfmdhqsbiFW81lAt9W1r74lnH7wQHr9ueALGKDx0hi8pAZ27itgQVHL
+eA1erhw0kjr9BqWpDIskVwePcD7pFoZ48GQlST0uIEq5U+1AWq7AbOABsqODygKi
+Ri5pmTasNFT7nEX3ti4VN214MNy0JnPzTRNWR2rD0I30AebM3KkzTprbLVfnGkm4
+7hOPV+Wc8EjgbbrUAIp2YpOfO/9nbgljTOUsqfjqxzvHx/09XOo2M6NIE5UiHqIq
+TXN7CeGIhBoYbvBAH2QvtveFXv41IYL4zFFXo4wTBSzCCOUGeDDv0U4hhsNaCkDQ
+G2TcubNA4g/FAtqLvPj/6VbIIgFE/1/6acsT+W0O+kkVAb7ej2dpI7J+jKXDXuiA
+PDCMn9dVQ7oAcaQvVdvvRphLdIZ9wHgqKhxKsMwzIMExuDKL0lWe/3sueFyol6nv
+xRCSgzr5MqSObbO3EnWgcUocBvlPyYLnTM2T8C5wh3BGnJXqJSRETggNn8PXBVIm
++c5o+Ic0mYu4v8P1ZSozFdgf+HLriVPwzJU5dHvvTEu7sw==
+-----END CERTIFICATE-----
diff --git a/glanceclient/tests/unit/var/certificate.crt b/glanceclient/tests/unit/var/certificate.crt
new file mode 100644
index 0000000..06c02ab
--- /dev/null
+++ b/glanceclient/tests/unit/var/certificate.crt
@@ -0,0 +1,66 @@
+# Certificate:
+# Data:
+# Version: 3 (0x2)
+# Serial Number: 1 (0x1)
+# Signature Algorithm: sha1WithRSAEncryption
+# Issuer: O=Openstack CA Org, OU=Openstack Test CA/emailAddress=admin@ca.example.com,
+# L=State CA, ST=CA, C=AU, CN=Openstack Test Certificate Authority
+# Validity
+# Not Before: Nov 16 12:50:19 2012 GMT
+# Not After : Apr 3 12:50:19 2040 GMT
+# Subject: O=Openstack Test Org, OU=Openstack Test Unit/emailAddress=admin@example.com,
+# L=State1, ST=CA, C=US, CN=0.0.0.0
+# Subject Public Key Info:
+# Public Key Algorithm: rsaEncryption
+# RSA Public Key: (4096 bit)
+# Modulus (4096 bit):
+# 00:d4:bb:3a:c4:a0:06:54:31:23:5d:b0:78:5a:be:
+# 45:44:ae:a1:89:86:11:d8:ca:a8:33:b0:4f:f3:e1:
+# .
+# .
+# .
+# Exponent: 65537 (0x10001)
+# X509v3 extensions:
+# X509v3 Subject Alternative Name:
+# DNS:alt1.example.com, DNS:alt2.example.com
+# Signature Algorithm: sha1WithRSAEncryption
+# 2c:fc:5c:87:24:bd:4a:fa:40:d2:2e:35:a4:2a:f3:1c:b3:67:
+# b0:e4:8a:cd:67:6b:55:50:d4:cb:dd:2d:26:a5:15:62:90:a3:
+# .
+# .
+# .
+-----BEGIN CERTIFICATE-----
+MIIGADCCA+igAwIBAgIBATANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQT3Bl
+bnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkq
+hkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBD
+QTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sg
+VGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE2MTI1MDE5WhcNNDAw
+NDAzMTI1MDE5WjCBmjEbMBkGA1UEChMST3BlbnN0YWNrIFRlc3QgT3JnMRwwGgYD
+VQQLExNPcGVuc3RhY2sgVGVzdCBVbml0MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl
+eGFtcGxlLmNvbTEPMA0GA1UEBxMGU3RhdGUxMQswCQYDVQQIEwJDQTELMAkGA1UE
+BhMCVVMxEDAOBgNVBAMTBzAuMC4wLjAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+ggIKAoICAQDUuzrEoAZUMSNdsHhavkVErqGJhhHYyqgzsE/z4UYehaMqnKTgwhQ0
+T5Hf3GmlIBt4I96/3cxj0qSLrdR81fM+5Km8lIlVHwVn1y6LKcMlaUC4K+sgDLcj
+hZfbf9+fMkcur3WlNzKpAEaIosWwsu6YvYc+W/nPBpKxMbOZ4fZiPMEo8Pxmw7sl
+/6hnlBOJj7dpZOZpHhVPZgzYNVoyfKCZiwgdxH4JEYa+EQos87+2Nwhs7bCgrTLL
+ppCUvpobwZV5w4O0D6INpUfBmsr4IAuXeFWZa61vZYqhaVbAbTTlUzOLGh7Z2uz9
+gt75iSR2J0e2xntVaUIYLIAUNOO2edk8NMAuIOGr2EIyC7i2O/BTti2YjGNO7SsE
+ClxiIFKjYahylHmNrS1Q/oMAcJppmhz+oOCmKOMmAZXYAH1A3gs/sWphJpgv/MWt
+6Ji24VpFaJ+o4bHILlqIpuvL4GLIOkmxVP639khaumgKtgNIUTKJ/V6t/J31WARf
+xKxlBQTTzV/Be+84YJiiddx8eunU8AorPyAJFzsDPTJpFUB4Q5BwAeDGCySgxJpU
+qM2MTETBycdiVToM4SWkRsOZgZxQ+AVfkkqDct2Bat2lg9epcIez8PrsohQjQbmi
+qUUL2c3de4kLYzIWF8EN3P2Me/7b06jbn4c7Fly/AN6tJOG23BzhHQIDAQABozEw
+LzAtBgNVHREEJjAkghBhbHQxLmV4YW1wbGUuY29tghBhbHQyLmV4YW1wbGUuY29t
+MA0GCSqGSIb3DQEBBQUAA4ICAQAs/FyHJL1K+kDSLjWkKvMcs2ew5IrNZ2tVUNTL
+3S0mpRVikKOQbNLh5B6Q7eQIvilCdkuit7o2HrpxQHsRor5b4+LyjSLoltyE7dgr
+ioP5nkKH+ujw6PtMxJCiKvvI+6cVHh6EV2ZkddvbJLVBVVZmB4H64xocS3rrQj19
+SXFYVrEjqdLzdGPNIBR+XVnTCeofXg1rkMaU7JuY8nRztee8PRVcKYX6scPfZJb8
++Ea2dsTmtQP4H9mk+JiKGYhEeMLVmjiv3q7KIFownTKZ88K6QbpW2Nj66ItvphoT
+QqI3rs6E8N0BhftiCcxXtXg+o4utfcnp8jTXX5tVnv44FqtWx7Gzg8XTLPri+ZEB
+5IbgU4Q3qFicenBfjwZhH3+GNe52/wLVZLYjal5RPVSRdu9UEDeDAwTCMZSLF4lC
+rc9giQCMnJ4ISi6C7xH+lDZGFqcJd4oXg/ue9aOJJAFTwhd83fdCHhUu431iPrts
+NubfrHLMeUjluFgIWmhEZg+XTjB1SQeQzNaZiMODaAv4/40ZVKxvNpDFwIIsPUDf
++uC+fv1Q8+alqVMl2ouVyr8ut43HWNV6CJHXODvFp5irjxzVSgLtYDVUInkDFJEs
+tFpTY21/zVAHIvsj2n4F1231nILR6vBp/WbwBY7r7j0oRtbaO3B1Q6tsbCZQRkKU
+tdc5rw==
+-----END CERTIFICATE-----
diff --git a/glanceclient/tests/unit/var/expired-cert.crt b/glanceclient/tests/unit/var/expired-cert.crt
new file mode 100644
index 0000000..227d422
--- /dev/null
+++ b/glanceclient/tests/unit/var/expired-cert.crt
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGFTCCA/2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQT3Bl
+bnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkq
+hkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBD
+QTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sg
+VGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE1MTcwNjMzWhcNMTIx
+MTE2MTcwNjMzWjCBqDEbMBkGA1UEChMST3BlbnN0YWNrIFRlc3QgT3JnMRwwGgYD
+VQQLExNPcGVuc3RhY2sgVGVzdCBVbml0MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl
+eGFtcGxlLmNvbTEPMA0GA1UEBxMGU3RhdGUxMQswCQYDVQQIEwJDQTELMAkGA1UE
+BhMCVVMxHjAcBgNVBAMTFW9wZW5zdGFjay5leGFtcGxlLmNvbTCCAiIwDQYJKoZI
+hvcNAQEBBQADggIPADCCAgoCggIBANn9w82sGN+iALSlZ5/Odd5iJ3MAJ5BoalMG
+kfUECGMewd7lE5+6ok1+vqVbYjd+F56aSkIJFR/ck51EYG2diGM5E5zjdiLcyB9l
+dKB5PmaB2P9dHyomy+sMONqhw5uEsWKIfPbtjzGRhjJL0bIYwptGr4JPraZy8R3d
+HWbTO3SlnFkjHHtfoKuZtRJq5OD1hXM8J9IEsBC90zw7RWCTw1iKllLfKITPUi7O
+i8ITjUyTVKR2e56XRtmxGgGsGyZpcYrmhRuLo9jyL9m3VuNzsfwDvCqn7cnZIOQa
+VO4hNZdO+33PINCC+YVNOGYwqfBuKxYvHJSbMfOZ6JDK98v65pWLBN7PObYIjQFH
+uJyK5DuQMqvyRIcrtfLUalepD+PQaCn4ajgXjpqBz4t0pMte8jh0i4clLwvT0elT
+PtA+MMos3hIGjJgEHTvLdCff9qlkjHlW7lg45PYn7S0Z7dqtBWD7Ys2B+AWp/skt
+hRr7YZeegLfHVJVkMFL6Ojs98161W2FLmEA+5nejzjx7kWlJsg9aZPbBnN87m6iK
+RHI+VkqSpBHm10iMlp4Nn30RtOj0wQhxoZjtEouGeRobHN5ULwpAfNEpKMMZf5bt
+604JjOP9Pn+WzsvzGDeXjgxUP55PIR+EpHkvS5h1YQ+9RV5J669e2J9T4gnc0Abg
+t3jJvtp1AgMBAAGjODA2MDQGA1UdEQQtMCuCEGFsdDEuZXhhbXBsZS5jb22BDm9z
+QGV4YW1wbGUuY29tggcwLjAuMC4wMA0GCSqGSIb3DQEBBQUAA4ICAQBkKUA4lhsS
+zjcuh77wtAIP9SN5Se4CheTRDXKDeuwWB6VQDzdJdtqSnWNF6sVEA97vhNTSjaBD
+hfrtX9FZ+ImADlOf01t4Dakhsmje/DEPiQHaCy9P5fGtGIGRlWUyTmyQoV1LDLM5
+wgB1V5Oz2iDat2AdvUb0OFP0O1M887OgPpfUDQJEUTVAs5JS+6P/6RPyFh/dHWiX
+UGoM0nMvTwsLWT4CZ9NdIChecVwBFqXjNytPY53tKbCWp77d/oGUg5Pb6EBD3xSW
+AeMJ6PuafDRgm/He8nOtZnUd+53Ha59yzSGnSopu5WqrUa/xD+ZiK6dX7LsH/M8y
+Hz0rh7w22qNHUxNaC3hrhx1BxX4au6z4kpKXIlAWH7ViRzVZ8XkwqqrndqWPWOFk
+1emLLJ1dfT8FXdgpHenkUiktAf5qZhUWbF6nr9at+c4T7ZrLHSekux2r29kD9BJw
+O2gSSclxKlMPwirUC0P4J/2WP72kCbf6AEfKU2siT12E6/xOmgen9lVYKckBiLbb
+rJ97L1ieJI8GZTGExjtE9Lo+XVsv28D2XLU8vNCODs0xPZCr2TLNS/6YcnVy6594
+vpvU7fbNFAyxG4sjQC0wHoN6rn+kd1kzfprmBHKTx3W7y+hzjb+W7iS2EZn20k+N
+l3+dFHnWayuCdqcFwIl3m8i8FupFihz9+A==
+-----END CERTIFICATE-----
diff --git a/glanceclient/tests/unit/var/privatekey.key b/glanceclient/tests/unit/var/privatekey.key
new file mode 100644
index 0000000..5b47d44
--- /dev/null
+++ b/glanceclient/tests/unit/var/privatekey.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEA1Ls6xKAGVDEjXbB4Wr5FRK6hiYYR2MqoM7BP8+FGHoWjKpyk
+4MIUNE+R39xppSAbeCPev93MY9Kki63UfNXzPuSpvJSJVR8FZ9cuiynDJWlAuCvr
+IAy3I4WX23/fnzJHLq91pTcyqQBGiKLFsLLumL2HPlv5zwaSsTGzmeH2YjzBKPD8
+ZsO7Jf+oZ5QTiY+3aWTmaR4VT2YM2DVaMnygmYsIHcR+CRGGvhEKLPO/tjcIbO2w
+oK0yy6aQlL6aG8GVecODtA+iDaVHwZrK+CALl3hVmWutb2WKoWlWwG005VMzixoe
+2drs/YLe+YkkdidHtsZ7VWlCGCyAFDTjtnnZPDTALiDhq9hCMgu4tjvwU7YtmIxj
+Tu0rBApcYiBSo2GocpR5ja0tUP6DAHCaaZoc/qDgpijjJgGV2AB9QN4LP7FqYSaY
+L/zFreiYtuFaRWifqOGxyC5aiKbry+BiyDpJsVT+t/ZIWrpoCrYDSFEyif1erfyd
+9VgEX8SsZQUE081fwXvvOGCYonXcfHrp1PAKKz8gCRc7Az0yaRVAeEOQcAHgxgsk
+oMSaVKjNjExEwcnHYlU6DOElpEbDmYGcUPgFX5JKg3LdgWrdpYPXqXCHs/D67KIU
+I0G5oqlFC9nN3XuJC2MyFhfBDdz9jHv+29Oo25+HOxZcvwDerSThttwc4R0CAwEA
+AQKCAgEAqnwqSu4cZFjFCQ6mRcL67GIvn3FM2DsBtfr0+HRvp4JeE4ZaNK4VVx71
+vzx7hhRHL28/0vBEHzPvHun+wtUMDjlfNnyr2wXzZRb0fB7KAC9r6K15z8Og+dzU
+qNrAMmsu1OFVHUUxWnOYE2Svnj6oLMynmHhJqXqREWTNlOOce3pJKzCGdy0hzQAo
+zGnFhpcg3Fw6s7+iQHF+lb+cO53Zb3QW2xRgFZBwNd6eEwx9deCA5htPVFW5wbAJ
+asud4eSwkFb6M9Hbg6gT67rMMzIrWAbeQwgihIYSJe2v0qMyox6czjvuwZVMHJdH
+byBTkkVEmdxTd03V5F21f3wrik/4oWqytjmjvMIY1gGTMo7aBnvPoKpgc2fqJub9
+cdAfGiJnFqo4Ae55mL4sgJPUCP7UATaDNAOCgt0zStmHMH8ACwk0dh1pzjyjpSR3
+OQfFs8QCAl9cvzxwux1tzG/uYxOrr+Rj2JlZKW/ljbWOeE0Gnjca73F40uGkEIbZ
+5i6YEuiPE6XGH0TP62Sdu2t5OlaKnZT12Tf6E8xNDsdaLuvAIz5sXyhoxvOmVd9w
+V4+uN1bZ10c5k/4uGRsHiXjX6IyYZEj8rKz6ryNikCdi6OzxWE3pCXmfBlVaXtO6
+EIubzk6dgjWcsPoqOsIl5Ywz4RWu0YUk4ZxRts54jCn14bPQpoECggEBAPiLTN8Z
+I0GQXMQaq9sN8kVsM/6AG/vWbc+IukPDYEC6Prk79jzkxMpDP8qK9C71bh39U1ky
+Kz4gSsLi9v3rM1gZwNshkZJ/zdQJ1NiCkzJVJX48DGeyYqUBjVt8Si37V2vzblBN
+RvM7U3rDN0xGiannyWnBC/jed+ZFCo97E9yOxIAs2ekwsl+ED3j1cARv8pBTGWnw
+Zhh4AD/Osk5U038oYcWHaIzUuNhEpv46bFLjVT11mGHfUY51Db3jBn0HYRlOPEV/
+F0kE5F+6rRg2tt7n0PO3UbzSNFyDRwtknJ2Nh4EtZZe93domls8SMR/kEHXcPLiQ
+ytEFyIAzsxfUwrECggEBANsc54N/LPmX1XuC643ZsDobH5/ALKc8W7wE7e82oSTD
+7cKBgdgB71DupJ7m81LHaDgT2RIzjl+lR3VVYLR/ukMcW+47JWrHyrsinu6itOdt
+ruhw0UPksoJGsB4KxUdRioFVT7m45GpnseJL0tjYaTCW01swae4QL4skNjjphPrb
+b/heMz9n79TK2ePlw1BvJKH0fnOJRuh/v63pD9SymB8EPsazjloKZ5qTrqVi3Obs
+F8WTSdl8KB1JSgeppdvHRcZQY1J+UfdCAlGD/pP7/zCKkRYcetre7fGMKVyPIDzO
+GAWz0xA2jnrgg7UqIh74oRHe0lZVMdMQ7FoJbRa7KC0CggEAJreEbQh8bn0vhjjl
+ZoVApUHaw51vPobDql2RLncj6lFY7gACNrAoW52oNUP6D8qZscBBmJZxGAdtvfgf
+I6Tc5a91VG1hQOH5zTsO1f9ZMLEE2yo9gHXQWgXo4ER3RbxufNl56LZxA/jM40W/
+unkOftIllPzGgakeIlfE8l7o1CXFRHY4J9Q3JRvsURpirb5GmeboAZG6RbuDxmzL
+Z9pc6+T9fgi+55lHhiEDpnyxXSQepilIaI6iJL/lORxBaX6ZyJhgWS8YEH7bmHH6
+/tefGxAfg6ed6v0PvQ2SJpswrnZakmvg9IdWJOJ4AZ/C2UXsrn91Ugb0ISV2e0oS
+bvbssQKCAQBjstc04h0YxJmCxaNgu/iPt9+/1LV8st4awzNwcS8Jh40bv8nQ+7Bk
+5vFIzFVTCSDGw2E2Avd5Vb8aCGskNioOd0ztLURtPdNlKu+eLbKayzGW2h6eAeWn
+mXpxcP0q4lNfXe4U16g3Mk+iZFXgDThvv3EUQQcyJ3M6oJN7eeXkLwzXuiUfaK+b
+52EVbWpdovTMLG+NKp11FQummjF12n2VP11BFFplZe6WSzRgVIenGy4F3Grx5qhq
+CvsAWZT6V8XL4rAOzSOGmiZr6N9hfnwzHhm+Md9Ez8L88YWwc/97K1uK3LPg4LIb
+/yRuvmkgJolDlFuopMMzArRIk5lrimVRAoIBAQDZmXk/VMA7fsI1/2sgSME0xt1A
+jkJZMZSnVD0UDWFkbyK6E5jDnwVUyqBDYe+HJyT4UnPDNCj++BchCQcG0Jih04RM
+jwGqxkfTF9K7kfouINSSXPRw/BtHkqMhV/g324mWcifCFVkDQghuslfmey8BKumo
+2KPyGnF9Q8CvTSQ0VlK1ZAKRf/zish49PMm7vD1KGkjRPliS3tgAmXPEpwijPGse
+4dSUeTfw5wCKAoq9DHjyHdO5fnfkOvA5PMQ4JZAzOCzJak8ET+tw4wB/dBeYiLVi
+l00GHLYAr5Nv/WqVnl/VLMd9rOCnLck+pxBNSa6dTrp3FuY00son6hneIvkv
+-----END RSA PRIVATE KEY-----
diff --git a/glanceclient/tests/unit/var/wildcard-certificate.crt b/glanceclient/tests/unit/var/wildcard-certificate.crt
new file mode 100644
index 0000000..a5f0b62
--- /dev/null
+++ b/glanceclient/tests/unit/var/wildcard-certificate.crt
@@ -0,0 +1,61 @@
+#Certificate:
+# Data:
+# Version: 1 (0x0)
+# Serial Number: 13493453254446411258 (0xbb42603e589dedfa)
+# Signature Algorithm: sha1WithRSAEncryption
+# Issuer: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=*.pong.example.com/emailAddress=admin@example.com
+# Validity
+# Not Before: Aug 21 17:29:18 2013 GMT
+# Not After : Jul 28 17:29:18 2113 GMT
+# Subject: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=*.pong.example.com/emailAddress=admin@example.com
+# Subject Public Key Info:
+# Public Key Algorithm: rsaEncryption
+# Public-Key: (4096 bit)
+# Modulus:
+# 00:d4:bb:3a:c4:a0:06:54:31:23:5d:b0:78:5a:be:
+# 45:44:ae:a1:89:86:11:d8:ca:a8:33:b0:4f:f3:e1:
+# 46:1e:85:a3:2a:9c:a4:e0:c2:14:34:4f:91:df:dc:
+# .
+# .
+# .
+# Exponent: 65537 (0x10001)
+# Signature Algorithm: sha1WithRSAEncryption
+# 9f:cc:08:5d:19:ee:54:31:a3:57:d7:3c:89:89:c0:69:41:dd:
+# 46:f8:73:68:ec:46:b9:fa:f5:df:f6:d9:58:35:d8:53:94:88:
+# bd:36:a6:23:9e:0c:0d:89:62:35:91:49:b6:14:f4:43:69:3c:
+# .
+# .
+# .
+-----BEGIN CERTIFICATE-----
+MIIFyjCCA7ICCQC7QmA+WJ3t+jANBgkqhkiG9w0BAQUFADCBpTELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQHDAZTdGF0ZTExGzAZBgNVBAoMEk9wZW5z
+dGFjayBUZXN0IE9yZzEcMBoGA1UECwwTT3BlbnN0YWNrIFRlc3QgVW5pdDEbMBkG
+A1UEAwwSKi5wb25nLmV4YW1wbGUuY29tMSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl
+eGFtcGxlLmNvbTAgFw0xMzA4MjExNzI5MThaGA8yMTEzMDcyODE3MjkxOFowgaUx
+CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEPMA0GA1UEBwwGU3RhdGUxMRswGQYD
+VQQKDBJPcGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsME09wZW5zdGFjayBUZXN0
+IFVuaXQxGzAZBgNVBAMMEioucG9uZy5leGFtcGxlLmNvbTEgMB4GCSqGSIb3DQEJ
+ARYRYWRtaW5AZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQDUuzrEoAZUMSNdsHhavkVErqGJhhHYyqgzsE/z4UYehaMqnKTgwhQ0T5Hf
+3GmlIBt4I96/3cxj0qSLrdR81fM+5Km8lIlVHwVn1y6LKcMlaUC4K+sgDLcjhZfb
+f9+fMkcur3WlNzKpAEaIosWwsu6YvYc+W/nPBpKxMbOZ4fZiPMEo8Pxmw7sl/6hn
+lBOJj7dpZOZpHhVPZgzYNVoyfKCZiwgdxH4JEYa+EQos87+2Nwhs7bCgrTLLppCU
+vpobwZV5w4O0D6INpUfBmsr4IAuXeFWZa61vZYqhaVbAbTTlUzOLGh7Z2uz9gt75
+iSR2J0e2xntVaUIYLIAUNOO2edk8NMAuIOGr2EIyC7i2O/BTti2YjGNO7SsEClxi
+IFKjYahylHmNrS1Q/oMAcJppmhz+oOCmKOMmAZXYAH1A3gs/sWphJpgv/MWt6Ji2
+4VpFaJ+o4bHILlqIpuvL4GLIOkmxVP639khaumgKtgNIUTKJ/V6t/J31WARfxKxl
+BQTTzV/Be+84YJiiddx8eunU8AorPyAJFzsDPTJpFUB4Q5BwAeDGCySgxJpUqM2M
+TETBycdiVToM4SWkRsOZgZxQ+AVfkkqDct2Bat2lg9epcIez8PrsohQjQbmiqUUL
+2c3de4kLYzIWF8EN3P2Me/7b06jbn4c7Fly/AN6tJOG23BzhHQIDAQABMA0GCSqG
+SIb3DQEBBQUAA4ICAQCfzAhdGe5UMaNX1zyJicBpQd1G+HNo7Ea5+vXf9tlYNdhT
+lIi9NqYjngwNiWI1kUm2FPRDaTwC0kLxk5zBPzF7bcf0SwJCeDjmlUpY7YenS0DA
+XmIbg8FvgOlp69Ikrqz98Y4pB9H4O81WdjxNBBbHjrufAXxZYnh5rXrVsXeSJ8jN
+MYGWlSv4xwFGfRX53b8VwXFjGjAkH8SQGtRV2w9d0jF8OzFwBA4bKk4EplY0yBPR
+2d7Y3RVrDnOVfV13F8CZxJ5fu+6QamUwIaTjpyqflE1L52KTy+vWPYR47H2u2bhD
+IeZRufJ8adNIOtH32EcENkusQjLrb3cTXGW00TljhFXd22GqL5d740u+GEKHtWh+
+9OKPTMZK8yK7d5EyS2agTVWmXU6HfpAKz9+AEOnVYErpnggNZjkmJ9kD185rGlSZ
+Vvo429hXoUAHNbd+8zda3ufJnJf5q4ZEl8+hp8xsvraUy83XLroVZRsKceldmAM8
+swt6n6w5gRKg4xTH7KFrd+KNptaoY3SsVrnJuaSOPenrUXbZzaI2Q35CId93+8NP
+mXVIWdPO1msdZNiCYInRIGycK+oifUZPtAaJdErg8rt8NSpHzYKQ0jfjAGiVHBjK
+s0J2TjoKB3jtlrw2DAmFWKeMGNp//1Rm6kfQCCXWftn+TA7XEJhcjyDBVciugA==
+-----END CERTIFICATE-----
diff --git a/glanceclient/tests/unit/var/wildcard-san-certificate.crt b/glanceclient/tests/unit/var/wildcard-san-certificate.crt
new file mode 100644
index 0000000..6ce27bc
--- /dev/null
+++ b/glanceclient/tests/unit/var/wildcard-san-certificate.crt
@@ -0,0 +1,54 @@
+#Certificate:
+# Data:
+# Version: 3 (0x2)
+# Serial Number: 11990626514780340979 (0xa66743493fdcc2f3)
+# Signature Algorithm: sha1WithRSAEncryption
+# Issuer: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=0.0.0.0
+# Validity
+# Not Before: Dec 10 15:31:22 2013 GMT
+# Not After : Nov 16 15:31:22 2113 GMT
+# Subject: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=0.0.0.0
+# Subject Public Key Info:
+# Public Key Algorithm: rsaEncryption
+# Public-Key: (2048 bit)
+# Modulus:
+# 00:ca:6b:07:73:53:24:45:74:05:a5:2a:27:bd:3e:
+# .
+# .
+# .
+# Exponent: 65537 (0x10001)
+# X509v3 extensions:
+# X509v3 Key Usage:
+# Key Encipherment, Data Encipherment
+# X509v3 Extended Key Usage:
+# TLS Web Server Authentication
+# X509v3 Subject Alternative Name:
+# DNS:foo.example.net, DNS:*.example.com
+# Signature Algorithm: sha1WithRSAEncryption
+# 7e:41:69:da:f4:3c:06:d6:83:c6:f2:db:df:37:f1:ac:fa:f5:
+# .
+# .
+# .
+-----BEGIN CERTIFICATE-----
+MIIDxDCCAqygAwIBAgIJAKZnQ0k/3MLzMA0GCSqGSIb3DQEBBQUAMHgxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJDQTEPMA0GA1UEBxMGU3RhdGUxMRswGQYDVQQKExJP
+cGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsTE09wZW5zdGFjayBUZXN0IFVuaXQx
+EDAOBgNVBAMTBzAuMC4wLjAwIBcNMTMxMjEwMTUzMTIyWhgPMjExMzExMTYxNTMx
+MjJaMHgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEPMA0GA1UEBxMGU3RhdGUx
+MRswGQYDVQQKExJPcGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsTE09wZW5zdGFj
+ayBUZXN0IFVuaXQxEDAOBgNVBAMTBzAuMC4wLjAwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDKawdzUyRFdAWlKie9Pn10j7frffN+z1gEMluK2CtDEwv9
+kbD4uS/Kz4dujfTx03mdyNfiMVlOM+YJm/qeLLSdJyFyvZ9Y3WmJ+vT2RGlMMhLd
+/wEnMRrTYLL39pwI6z+gyw+4D78Pyv/OXy02IA6WtVEefYSx1vmVngb3pL+iBzhO
+8CZXNI6lqrFhh+Hr4iMkYMtY1vTnwezAL6p64E/ZAFNPYCEJlacESTLQ4VZYniHc
+QTgnE1czlI1vxlIk1KDXAzUGeeopZecRih9qlTxtOpklqEciQEE+sHtPcvyvdRE9
+Bdyx5rNSALLIcXs0ViJE1RPlw3fjdBoDIOygqvX1AgMBAAGjTzBNMAsGA1UdDwQE
+AwIEMDATBgNVHSUEDDAKBggrBgEFBQcDATApBgNVHREEIjAggg9mb28uZXhhbXBs
+ZS5uZXSCDSouZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADggEBAH5Badr0PAbW
+g8by29838az69Raul5IkpZQ5V3O1NaNNWxvmF1q8zFFqqGK5ktXJAwGiwnYEBb30
+Zfrr+eFIEERzBthSJkWlP8NG+2ooMyg50femp+asAvW+KYYefJW8KaXTsznMsAFy
+z1agcWVYVZ4H9PwunEYn/rM1krLEe4Cagsw5nmf8VqZg+hHtw930q8cRzgDsZdfA
+jVK6dWdmzmLCUTL1GKCeNriDw1jIeFvNufC+Q3orH7xBx4VL+NV5ORWdNY/B8q1b
+mFHdzbuZX6v39+2ww6aZqG2orfxUocc/5Ox6fXqenKPI3moeHS6Ktesq7sEQSJ6H
+QZFsTuT/124=
+-----END CERTIFICATE-----
diff --git a/glanceclient/tests/utils.py b/glanceclient/tests/utils.py
new file mode 100644
index 0000000..a47dcb3
--- /dev/null
+++ b/glanceclient/tests/utils.py
@@ -0,0 +1,215 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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 copy
+import json
+import six
+import six.moves.urllib.parse as urlparse
+import testtools
+
+from glanceclient.v2.schemas import Schema
+
+
+class FakeAPI(object):
+ def __init__(self, fixtures):
+ self.fixtures = fixtures
+ self.calls = []
+
+ def _request(self, method, url, headers=None, data=None,
+ content_length=None):
+ 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[sort_url_by_query_keys(url)][method]
+
+ data = fixture[1]
+ if isinstance(fixture[1], six.string_types):
+ try:
+ data = json.loads(fixture[1])
+ except ValueError:
+ data = six.StringIO(fixture[1])
+
+ return FakeResponse(fixture[0], fixture[1]), data
+
+ def get(self, *args, **kwargs):
+ return self._request('GET', *args, **kwargs)
+
+ def post(self, *args, **kwargs):
+ return self._request('POST', *args, **kwargs)
+
+ def put(self, *args, **kwargs):
+ return self._request('PUT', *args, **kwargs)
+
+ def patch(self, *args, **kwargs):
+ return self._request('PATCH', *args, **kwargs)
+
+ def delete(self, *args, **kwargs):
+ return self._request('DELETE', *args, **kwargs)
+
+ def head(self, *args, **kwargs):
+ return self._request('HEAD', *args, **kwargs)
+
+
+class FakeSchemaAPI(FakeAPI):
+ def get(self, *args, **kwargs):
+ _, raw_schema = self._request('GET', *args, **kwargs)
+ return Schema(raw_schema)
+
+
+class RawRequest(object):
+ def __init__(self, headers, body=None,
+ version=1.0, status=200, reason="Ok"):
+ """
+ :param headers: dict representing HTTP response headers
+ :param body: file-like object
+ :param version: HTTP Version
+ :param status: Response status code
+ :param reason: Status code related message.
+ """
+ self.body = body
+ self.status = status
+ self.reason = reason
+ self.version = version
+ self.headers = headers
+
+ def getheaders(self):
+ return copy.deepcopy(self.headers).items()
+
+ def getheader(self, key, default):
+ return self.headers.get(key, default)
+
+ def read(self, amt):
+ return self.body.read(amt)
+
+
+class FakeResponse(object):
+ def __init__(self, headers=None, body=None,
+ version=1.0, status_code=200, reason="Ok"):
+ """
+ :param headers: dict representing HTTP response headers
+ :param body: file-like object
+ :param version: HTTP Version
+ :param status: Response status code
+ :param reason: Status code related message.
+ """
+ self.body = body
+ self.reason = reason
+ self.version = version
+ self.headers = headers
+ self.status_code = status_code
+ self.raw = RawRequest(headers, body=body, reason=reason,
+ version=version, status=status_code)
+
+ @property
+ def ok(self):
+ return (self.status_code < 400 or
+ self.status_code >= 600)
+
+ def read(self, amt):
+ return self.body.read(amt)
+
+ def close(self):
+ pass
+
+ @property
+ def content(self):
+ if hasattr(self.body, "read"):
+ return self.body.read()
+ return self.body
+
+ @property
+ def text(self):
+ if isinstance(self.content, six.binary_type):
+ return self.content.decode('utf-8')
+
+ return self.content
+
+ def json(self, **kwargs):
+ return self.body and json.loads(self.text) or ""
+
+ def iter_content(self, chunk_size=1, decode_unicode=False):
+ while True:
+ chunk = self.raw.read(chunk_size)
+ if not chunk:
+ break
+ yield chunk
+
+
+class TestCase(testtools.TestCase):
+ TEST_REQUEST_BASE = {
+ 'config': {'danger_mode': False},
+ 'verify': True}
+
+
+class FakeTTYStdout(six.StringIO):
+ """A Fake stdout that try to emulate a TTY device as much as possible."""
+
+ def isatty(self):
+ return True
+
+ def write(self, data):
+ # When a CR (carriage return) is found reset file.
+ if data.startswith('\r'):
+ self.seek(0)
+ data = data[1:]
+ return six.StringIO.write(self, data)
+
+
+class FakeNoTTYStdout(FakeTTYStdout):
+ """A Fake stdout that is not a TTY device."""
+
+ 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)