summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorAlessio Ababilov <aababilov@griddynamics.com>2013-05-22 11:46:07 +0300
committerAlessio Ababilov <aababilo@yahoo-inc.com>2013-07-22 20:29:03 +0300
commit062cc24f5dfc8e1652a589f6a2f45ce8f3a1b89c (patch)
treefe04d2c1f4f6aadcc8416801a83dab472ea7ec52 /tests
parentcd78a6dbd48c346aabbc5554386d42ac5a4a5771 (diff)
downloadoslo-062cc24f5dfc8e1652a589f6a2f45ce8f3a1b89c.tar.gz
oslo-062cc24f5dfc8e1652a589f6a2f45ce8f3a1b89c.tar.xz
oslo-062cc24f5dfc8e1652a589f6a2f45ce8f3a1b89c.zip
Implement apiclient library
This library can be used in novaclient, keystoneclient, glanceclient, and other client projects. The library contains common code and uses python-requests for HTTP communication. Features: * reissue authentication request for expired tokens; * pluggable authentication; * rich exceptions hierarchy. This code partially comes from: * python-keystoneclient/keystoneclient/base.py; * python-novaclient/novaclient/auth_plugin.py; * python-novaclient/novaclient/extension.py; * python-novaclient/tests/fakes.py. Partially implements: blueprint common-client-library Change-Id: Ic8b466a57554018092c31c6d6b3ea62f181d7cef
Diffstat (limited to 'tests')
-rw-r--r--tests/unit/apiclient/test_auth.py182
-rw-r--r--tests/unit/apiclient/test_base.py240
-rw-r--r--tests/unit/apiclient/test_client.py138
-rw-r--r--tests/unit/apiclient/test_exceptions.py2
4 files changed, 561 insertions, 1 deletions
diff --git a/tests/unit/apiclient/test_auth.py b/tests/unit/apiclient/test_auth.py
new file mode 100644
index 0000000..b3c432c
--- /dev/null
+++ b/tests/unit/apiclient/test_auth.py
@@ -0,0 +1,182 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 argparse
+
+import fixtures
+import mock
+import requests
+
+from stevedore import extension
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+from openstack.common.apiclient import auth
+from openstack.common.apiclient import client
+from openstack.common.apiclient import fake_client
+
+from tests import utils
+
+
+TEST_REQUEST_BASE = {
+ 'verify': True,
+}
+
+
+def mock_http_request(resp=None):
+ """Mock an HTTP Request."""
+ if not resp:
+ resp = {
+ "access": {
+ "token": {
+ "expires": "12345",
+ "id": "FAKE_ID",
+ "tenant": {
+ "id": "FAKE_TENANT_ID",
+ }
+ },
+ "serviceCatalog": [
+ {
+ "type": "compute",
+ "endpoints": [
+ {
+ "region": "RegionOne",
+ "adminURL": "http://localhost:8774/v1.1",
+ "internalURL": "http://localhost:8774/v1.1",
+ "publicURL": "http://localhost:8774/v1.1/",
+ },
+ ],
+ },
+ ],
+ },
+ }
+
+ auth_response = fake_client.TestResponse({
+ "status_code": 200,
+ "text": json.dumps(resp),
+ })
+ return mock.Mock(return_value=(auth_response))
+
+
+def requested_headers(cs):
+ """Return requested passed headers."""
+ return {
+ 'User-Agent': cs.user_agent,
+ 'Content-Type': 'application/json',
+ }
+
+
+class BaseFakePlugin(auth.BaseAuthPlugin):
+ def _do_authenticate(self, http_client):
+ pass
+
+ def token_and_endpoint(self, endpoint_type, service_type):
+ pass
+
+
+class GlobalFunctionsTest(utils.BaseTestCase):
+
+ def test_load_auth_system_opts(self):
+ self.useFixture(fixtures.MonkeyPatch(
+ "os.environ",
+ {"OS_TENANT_NAME": "fake-project",
+ "OS_USERNAME": "fake-username"}))
+ parser = argparse.ArgumentParser()
+ auth.load_auth_system_opts(parser)
+ options = parser.parse_args(
+ ["--os-auth-url=fake-url", "--os_auth_system=fake-system"])
+ self.assertTrue(options.os_tenant_name, "fake-project")
+ self.assertTrue(options.os_username, "fake-username")
+ self.assertTrue(options.os_auth_url, "fake-url")
+ self.assertTrue(options.os_auth_system, "fake-system")
+
+
+class MockEntrypoint(object):
+ def __init__(self, name, plugin):
+ self.name = name
+ self.plugin = plugin
+
+
+class AuthPluginTest(utils.BaseTestCase):
+ @mock.patch.object(requests.Session, "request")
+ @mock.patch.object(extension.ExtensionManager, "map")
+ def test_auth_system_success(self, mock_mgr_map, mock_request):
+ """Test that we can authenticate using the auth system."""
+ class FakePlugin(BaseFakePlugin):
+ def authenticate(self, cls):
+ cls.request(
+ "POST", "http://auth/tokens",
+ json={"fake": "me"}, allow_redirects=True)
+
+ mock_mgr_map.side_effect = (
+ lambda func: func(MockEntrypoint("fake", FakePlugin)))
+
+ mock_request.side_effect = mock_http_request()
+
+ auth.discover_auth_systems()
+ plugin = auth.load_plugin("fake")
+ cs = client.HTTPClient(auth_plugin=plugin)
+ cs.authenticate()
+
+ headers = requested_headers(cs)
+
+ mock_request.assert_called_with(
+ "POST",
+ "http://auth/tokens",
+ headers=headers,
+ data='{"fake": "me"}',
+ allow_redirects=True,
+ **TEST_REQUEST_BASE)
+
+ @mock.patch.object(extension.ExtensionManager, "map")
+ def test_discover_auth_system_options(self, mock_mgr_map):
+ """Test that we can load the auth system options."""
+ class FakePlugin(BaseFakePlugin):
+ @classmethod
+ def add_opts(cls, parser):
+ parser.add_argument('--auth_system_opt',
+ default=False,
+ action='store_true',
+ help="Fake option")
+
+ mock_mgr_map.side_effect = (
+ lambda func: func(MockEntrypoint("fake", FakePlugin)))
+
+ parser = argparse.ArgumentParser()
+ auth.discover_auth_systems()
+ auth.load_auth_system_opts(parser)
+ opts, _args = parser.parse_known_args(['--auth_system_opt'])
+
+ self.assertTrue(opts.auth_system_opt)
+
+ @mock.patch.object(extension.ExtensionManager, "map")
+ def test_parse_auth_system_options(self, mock_mgr_map):
+ """Test that we can parse the auth system options."""
+ class FakePlugin(BaseFakePlugin):
+ opt_names = ["fake_argument"]
+
+ mock_mgr_map.side_effect = (
+ lambda func: func(MockEntrypoint("fake", FakePlugin)))
+
+ auth.discover_auth_systems()
+ plugin = auth.load_plugin("fake")
+
+ plugin.parse_opts([])
+ self.assertIn("fake_argument", plugin.opts)
diff --git a/tests/unit/apiclient/test_base.py b/tests/unit/apiclient/test_base.py
new file mode 100644
index 0000000..460ee2c
--- /dev/null
+++ b/tests/unit/apiclient/test_base.py
@@ -0,0 +1,240 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.
+
+from openstack.common.apiclient import base
+from openstack.common.apiclient import client
+from openstack.common.apiclient import exceptions
+from openstack.common.apiclient import fake_client
+
+from tests import utils
+
+
+class HumanResource(base.Resource):
+ HUMAN_ID = True
+
+
+class HumanResourceManager(base.ManagerWithFind):
+ resource_class = HumanResource
+
+ def list(self):
+ return self._list("/human_resources", "human_resources")
+
+ def get(self, human_resource):
+ return self._get(
+ "/human_resources/%s" % base.getid(human_resource),
+ "human_resource")
+
+ def update(self, human_resource, name):
+ body = {
+ "human_resource": {
+ "name": name,
+ },
+ }
+ return self._put(
+ "/human_resources/%s" % base.getid(human_resource),
+ body,
+ "human_resource")
+
+
+class CrudResource(base.Resource):
+ pass
+
+
+class CrudResourceManager(base.CrudManager):
+ """Manager class for manipulating Identity crud_resources."""
+ resource_class = CrudResource
+ collection_key = 'crud_resources'
+ key = 'crud_resource'
+
+ def get(self, crud_resource):
+ return super(CrudResourceManager, self).get(
+ crud_resource_id=base.getid(crud_resource))
+
+
+class FakeHTTPClient(fake_client.FakeHTTPClient):
+ crud_resource_json = {"id": "1", "domain_id": "my-domain"}
+
+ def get_human_resources(self, **kw):
+ return (200, {}, {'human_resources': [
+ {'id': 1, 'name': '256 MB Server'},
+ {'id': 2, 'name': '512 MB Server'},
+ {'id': 'aa1', 'name': '128 MB Server'}
+ ]})
+
+ def get_human_resources_1(self, **kw):
+ res = self.get_human_resources()[2]['human_resources'][0]
+ return (200, {}, {'human_resource': res})
+
+ def put_human_resources_1(self, **kw):
+ kw = kw["json"]["human_resource"].copy()
+ kw["id"] = "1"
+ return (200, {}, {'human_resource': kw})
+
+ def post_crud_resources(self, **kw):
+ return (200, {}, {"crud_resource": {"id": "1"}})
+
+ def get_crud_resources(self, **kw):
+ crud_resources = []
+ if kw.get("domain_id") == self.crud_resource_json["domain_id"]:
+ crud_resources = [self.crud_resource_json]
+ else:
+ crud_resources = []
+ return (200, {}, {"crud_resources": crud_resources})
+
+ def get_crud_resources_1(self, **kw):
+ return (200, {}, {"crud_resource": self.crud_resource_json})
+
+ def head_crud_resources_1(self, **kw):
+ return (204, {}, None)
+
+ def patch_crud_resources_1(self, **kw):
+ self.crud_resource_json.update(kw)
+ return (200, {}, {"crud_resource": self.crud_resource_json})
+
+ def delete_crud_resources_1(self, **kw):
+ return (202, {}, None)
+
+
+class TestClient(client.BaseClient):
+
+ service_type = "test"
+
+ def __init__(self, http_client, extensions=None):
+ super(TestClient, self).__init__(
+ http_client, extensions=extensions)
+
+ self.human_resources = HumanResourceManager(self)
+ self.crud_resources = CrudResourceManager(self)
+
+
+class ResourceTest(utils.BaseTestCase):
+
+ def test_resource_repr(self):
+ r = base.Resource(None, dict(foo="bar", baz="spam"))
+ self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>")
+
+ def test_getid(self):
+ class TmpObject(base.Resource):
+ id = "4"
+ self.assertEqual(base.getid(TmpObject(None, {})), "4")
+
+ def test_human_id(self):
+ r = base.Resource(None, {"name": "1"})
+ self.assertEqual(r.human_id, None)
+ r = HumanResource(None, {"name": "1"})
+ self.assertEqual(r.human_id, "1")
+
+
+class BaseManagerTest(utils.BaseTestCase):
+
+ def setUp(self):
+ super(BaseManagerTest, self).setUp()
+ self.http_client = FakeHTTPClient()
+ self.tc = TestClient(self.http_client)
+
+ def test_resource_lazy_getattr(self):
+ f = HumanResource(self.tc.human_resources, {'id': 1})
+ self.assertEqual(f.name, '256 MB Server')
+ self.http_client.assert_called('GET', '/human_resources/1')
+
+ # Missing stuff still fails after a second get
+ self.assertRaises(AttributeError, getattr, f, 'blahblah')
+
+ def test_eq(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)
+
+ # Two resources of different types: never equal
+ r1 = base.Resource(None, {'id': 1})
+ r2 = HumanResource(None, {'id': 1})
+ self.assertNotEqual(r1, r2)
+
+ # 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_findall_invalid_attribute(self):
+ # Make sure findall with an invalid attribute doesn't cause errors.
+ # The following should not raise an exception.
+ self.tc.human_resources.findall(vegetable='carrot')
+
+ # However, find() should raise an error
+ self.assertRaises(exceptions.NotFound,
+ self.tc.human_resources.find,
+ vegetable='carrot')
+
+ def test_update(self):
+ name = "new-name"
+ human_resource = self.tc.human_resources.update("1", name)
+ self.assertEqual(human_resource.id, "1")
+ self.assertEqual(human_resource.name, name)
+
+
+class CrudManagerTest(utils.BaseTestCase):
+
+ domain_id = "my-domain"
+ crud_resource_id = "1"
+
+ def setUp(self):
+ super(CrudManagerTest, self).setUp()
+ self.http_client = FakeHTTPClient()
+ self.tc = TestClient(self.http_client)
+
+ def test_create(self):
+ crud_resource = self.tc.crud_resources.create()
+ self.assertEqual(crud_resource.id, self.crud_resource_id)
+
+ def test_list(self, domain=None, user=None):
+ crud_resources = self.tc.crud_resources.list(
+ base_url=None,
+ domain_id=self.domain_id)
+ self.assertEqual(len(crud_resources), 1)
+ self.assertEqual(crud_resources[0].id, self.crud_resource_id)
+ self.assertEqual(crud_resources[0].domain_id, self.domain_id)
+ crud_resources = self.tc.crud_resources.list(
+ base_url=None,
+ domain_id="another-domain",
+ another_attr=None)
+ self.assertEqual(len(crud_resources), 0)
+
+ def test_get(self):
+ crud_resource = self.tc.crud_resources.get(self.crud_resource_id)
+ self.assertEqual(crud_resource.id, self.crud_resource_id)
+ fake_client.assert_has_keys(
+ crud_resource._info,
+ required=["id", "domain_id"],
+ optional=["missing-attr"])
+
+ def test_update(self):
+ crud_resource = self.tc.crud_resources.update(
+ crud_resource_id=self.crud_resource_id,
+ domain_id=self.domain_id)
+ self.assertEqual(crud_resource.id, self.crud_resource_id)
+ self.assertEqual(crud_resource.domain_id, self.domain_id)
+
+ def test_delete(self):
+ resp = self.tc.crud_resources.delete(
+ crud_resource_id=self.crud_resource_id)
+ self.assertEqual(resp.status_code, 202)
+
+ def test_head(self):
+ ret = self.tc.crud_resources.head(
+ crud_resource_id=self.crud_resource_id)
+ self.assertTrue(ret)
diff --git a/tests/unit/apiclient/test_client.py b/tests/unit/apiclient/test_client.py
new file mode 100644
index 0000000..4594a6a
--- /dev/null
+++ b/tests/unit/apiclient/test_client.py
@@ -0,0 +1,138 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 requests
+
+from openstack.common.apiclient import auth
+from openstack.common.apiclient import client
+from openstack.common.apiclient import exceptions
+
+from tests import utils
+
+
+class TestClient(client.BaseClient):
+ service_type = "test"
+
+
+class FakeAuthPlugin(auth.BaseAuthPlugin):
+ auth_system = "fake"
+ attempt = -1
+
+ def _do_authenticate(self, http_client):
+ pass
+
+ def token_and_endpoint(self, endpoint_type, service_type):
+ self.attempt = self.attempt + 1
+ return ("token-%s" % self.attempt, "/endpoint-%s" % self.attempt)
+
+
+class ClientTest(utils.BaseTestCase):
+
+ def test_client_with_timeout(self):
+ http_client = client.HTTPClient(None, timeout=2)
+ self.assertEqual(http_client.timeout, 2)
+ mock_request = mock.Mock()
+ mock_request.return_value = requests.Response()
+ mock_request.return_value.status_code = 200
+ with mock.patch("requests.Session.request", mock_request):
+ http_client.request("GET", "/", json={"1": "2"})
+ requests.Session.request.assert_called_with(
+ "GET",
+ "/",
+ timeout=2,
+ headers=mock.ANY,
+ verify=mock.ANY,
+ data=mock.ANY)
+
+ def test_concat_url(self):
+ self.assertEqual(client.HTTPClient.concat_url("/a", "/b"), "/a/b")
+ self.assertEqual(client.HTTPClient.concat_url("/a", "b"), "/a/b")
+ self.assertEqual(client.HTTPClient.concat_url("/a/", "/b"), "/a/b")
+
+ def test_client_request(self):
+ http_client = client.HTTPClient(FakeAuthPlugin())
+ mock_request = mock.Mock()
+ mock_request.return_value = requests.Response()
+ mock_request.return_value.status_code = 200
+ with mock.patch("requests.Session.request", mock_request):
+ http_client.client_request(
+ TestClient(http_client), "GET", "/resource", json={"1": "2"})
+ requests.Session.request.assert_called_with(
+ "GET",
+ "/endpoint-0/resource",
+ headers={
+ "User-Agent": http_client.user_agent,
+ "Content-Type": "application/json",
+ "X-Auth-Token": "token-0"
+ },
+ data='{"1": "2"}',
+ verify=True)
+
+ def test_client_request_reissue(self):
+ reject_token = None
+
+ def fake_request(method, url, **kwargs):
+ if kwargs["headers"]["X-Auth-Token"] == reject_token:
+ raise exceptions.Unauthorized(method=method, url=url)
+ return "%s %s" % (method, url)
+
+ http_client = client.HTTPClient(FakeAuthPlugin())
+ test_client = TestClient(http_client)
+ http_client.request = fake_request
+
+ self.assertEqual(
+ http_client.client_request(
+ test_client, "GET", "/resource"),
+ "GET /endpoint-0/resource")
+ reject_token = "token-0"
+ self.assertEqual(
+ http_client.client_request(
+ test_client, "GET", "/resource"),
+ "GET /endpoint-1/resource")
+
+
+class FakeClient1(object):
+ pass
+
+
+class FakeClient21(object):
+ pass
+
+
+class GetClientClassTestCase(utils.BaseTestCase):
+ version_map = {
+ "1": "%s.FakeClient1" % __name__,
+ "2.1": "%s.FakeClient21" % __name__,
+ }
+
+ def test_get_int(self):
+ self.assertEqual(
+ client.BaseClient.get_class("fake", 1, self.version_map),
+ FakeClient1)
+
+ def test_get_str(self):
+ self.assertEqual(
+ client.BaseClient.get_class("fake", "2.1", self.version_map),
+ FakeClient21)
+
+ def test_unsupported_version(self):
+ self.assertRaises(
+ exceptions.UnsupportedVersion,
+ client.BaseClient.get_class,
+ "fake", "7", self.version_map)
diff --git a/tests/unit/apiclient/test_exceptions.py b/tests/unit/apiclient/test_exceptions.py
index 34cae73..bfbd2b0 100644
--- a/tests/unit/apiclient/test_exceptions.py
+++ b/tests/unit/apiclient/test_exceptions.py
@@ -61,7 +61,7 @@ class ExceptionsArgsTest(utils.BaseTestCase):
json_data = {"error": {"message": "fake unknown message",
"details": "fake unknown details"}}
self.assert_exception(
- exceptions.HttpClientError, method, url, status_code, json_data)
+ exceptions.HTTPClientError, method, url, status_code, json_data)
status_code = 600
self.assert_exception(
exceptions.HttpError, method, url, status_code, json_data)