diff options
author | Alessio Ababilov <aababilov@griddynamics.com> | 2013-05-22 11:46:07 +0300 |
---|---|---|
committer | Alessio Ababilov <aababilo@yahoo-inc.com> | 2013-07-22 20:29:03 +0300 |
commit | 062cc24f5dfc8e1652a589f6a2f45ce8f3a1b89c (patch) | |
tree | fe04d2c1f4f6aadcc8416801a83dab472ea7ec52 /tests | |
parent | cd78a6dbd48c346aabbc5554386d42ac5a4a5771 (diff) | |
download | oslo-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.py | 182 | ||||
-rw-r--r-- | tests/unit/apiclient/test_base.py | 240 | ||||
-rw-r--r-- | tests/unit/apiclient/test_client.py | 138 | ||||
-rw-r--r-- | tests/unit/apiclient/test_exceptions.py | 2 |
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) |