commit 691ec36670fe9fb4895054b84ac6020846351948 Author: Jamie Lennox Date: Mon Sep 23 12:07:40 2013 +1000 Move tests in keystoneclient This is the suggested location for tests and is adopted by most projects. As part of this change relative imports to package imports. Fix all the test running and coverage code to point to the new location. Change-Id: I01264aed14f396ab9a7242e3e72b71e1bc332675 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apiclient/test_exceptions.py b/apiclient/test_exceptions.py new file mode 100644 index 0000000..d41ac02 --- /dev/null +++ b/apiclient/test_exceptions.py @@ -0,0 +1,66 @@ +# Copyright 2012 OpenStack LLC. +# 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 keystoneclient.apiclient import exceptions +from keystoneclient.tests import utils + + +class FakeResponse(object): + json_data = {} + + def __init__(self, **kwargs): + for key, value in kwargs.iteritems(): + setattr(self, key, value) + + def json(self): + return self.json_data + + +class ExceptionsArgsTest(utils.TestCase): + + def assert_exception(self, ex_cls, method, url, status_code, json_data): + ex = exceptions.from_response( + FakeResponse(status_code=status_code, + headers={"Content-Type": "application/json"}, + json_data=json_data), + method, + url) + self.assertTrue(isinstance(ex, ex_cls)) + self.assertEqual(ex.message, json_data["error"]["message"]) + self.assertEqual(ex.details, json_data["error"]["details"]) + self.assertEqual(ex.method, method) + self.assertEqual(ex.url, url) + self.assertEqual(ex.http_status, status_code) + + def test_from_response_known(self): + method = "GET" + url = "/fake" + status_code = 400 + json_data = {"error": {"message": "fake message", + "details": "fake details"}} + self.assert_exception( + exceptions.BadRequest, method, url, status_code, json_data) + + def test_from_response_unknown(self): + method = "POST" + url = "/fake-unknown" + status_code = 499 + json_data = {"error": {"message": "fake unknown message", + "details": "fake unknown details"}} + self.assert_exception( + exceptions.HTTPClientError, method, url, status_code, json_data) + status_code = 600 + self.assert_exception( + exceptions.HTTPError, method, url, status_code, json_data) diff --git a/client_fixtures.py b/client_fixtures.py new file mode 100644 index 0000000..dd1c95f --- /dev/null +++ b/client_fixtures.py @@ -0,0 +1,298 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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 keystoneclient.common import cms +from keystoneclient.openstack.common import jsonutils +from keystoneclient.openstack.common import timeutils +from keystoneclient import utils + + +CLIENTDIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +ROOTDIR = os.path.dirname(CLIENTDIR) +CERTDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'certs') +CMSDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'cms') + + +# @TODO(mordred) This should become a testresources resource attached to the +# class +# The data for these tests are signed using openssl and are stored in files +# in the signing subdirectory. In order to keep the values consistent between +# the tests and the signed documents, we read them in for use in the tests. +with open(os.path.join(CMSDIR, 'auth_token_scoped.pem')) as f: + SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read()) +with open(os.path.join(CMSDIR, 'auth_token_unscoped.pem')) as f: + SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read()) +with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pem')) as f: + SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read()) +with open(os.path.join(CMSDIR, 'auth_token_revoked.pem')) as f: + REVOKED_TOKEN = cms.cms_to_token(f.read()) +with open(os.path.join(CMSDIR, 'auth_token_scoped_expired.pem')) as f: + SIGNED_TOKEN_SCOPED_EXPIRED = cms.cms_to_token(f.read()) +with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pem')) as f: + REVOKED_v3_TOKEN = cms.cms_to_token(f.read()) +with open(os.path.join(CMSDIR, 'revocation_list.json')) as f: + REVOCATION_LIST = jsonutils.loads(f.read()) +with open(os.path.join(CMSDIR, 'revocation_list.pem')) as f: + SIGNED_REVOCATION_LIST = jsonutils.dumps({'signed': f.read()}) +with open(os.path.join(CERTDIR, 'signing_cert.pem')) as f: + SIGNING_CERT = f.read() +with open(os.path.join(CERTDIR, 'cacert.pem')) as f: + SIGNING_CA = f.read() + +UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d" +UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df' +UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776' +VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726' +v3_UUID_TOKEN_DEFAULT = '5603457654b346fdbb93437bfe76f2f1' +v3_UUID_TOKEN_UNSCOPED = 'd34835fdaec447e695a0a024d84f8d79' +v3_UUID_TOKEN_DOMAIN_SCOPED = 'e8a7b63aaa4449f38f0c5c05c3581792' + +REVOKED_TOKEN_HASH = utils.hash_signed_token(REVOKED_TOKEN) +REVOKED_TOKEN_LIST = {'revoked': [{'id': REVOKED_TOKEN_HASH, + 'expires': timeutils.utcnow()}]} +REVOKED_TOKEN_LIST_JSON = jsonutils.dumps(REVOKED_TOKEN_LIST) + +REVOKED_v3_TOKEN_HASH = utils.hash_signed_token(REVOKED_v3_TOKEN) +REVOKED_v3_TOKEN_LIST = {'revoked': [{'id': REVOKED_v3_TOKEN_HASH, + 'expires': timeutils.utcnow()}]} +REVOKED_v3_TOKEN_LIST_JSON = jsonutils.dumps(REVOKED_v3_TOKEN_LIST) + +SIGNED_TOKEN_SCOPED_KEY = cms.cms_hash_token(SIGNED_TOKEN_SCOPED) +SIGNED_TOKEN_UNSCOPED_KEY = cms.cms_hash_token(SIGNED_TOKEN_UNSCOPED) +SIGNED_v3_TOKEN_SCOPED_KEY = cms.cms_hash_token(SIGNED_v3_TOKEN_SCOPED) + +INVALID_SIGNED_TOKEN = \ + "MIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" \ + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" \ + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" \ + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" \ + "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" \ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" \ + "0000000000000000000000000000000000000000000000000000000000000000" \ + "1111111111111111111111111111111111111111111111111111111111111111" \ + "2222222222222222222222222222222222222222222222222222222222222222" \ + "3333333333333333333333333333333333333333333333333333333333333333" \ + "4444444444444444444444444444444444444444444444444444444444444444" \ + "5555555555555555555555555555555555555555555555555555555555555555" \ + "6666666666666666666666666666666666666666666666666666666666666666" \ + "7777777777777777777777777777777777777777777777777777777777777777" \ + "8888888888888888888888888888888888888888888888888888888888888888" \ + "9999999999999999999999999999999999999999999999999999999999999999" \ + "0000000000000000000000000000000000000000000000000000000000000000" \ + + +# JSON responses keyed by token ID +TOKEN_RESPONSES = { + UUID_TOKEN_DEFAULT: { + 'access': { + 'token': { + 'id': UUID_TOKEN_DEFAULT, + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + 'serviceCatalog': {} + }, + }, + VALID_DIABLO_TOKEN: { + 'access': { + 'token': { + 'id': VALID_DIABLO_TOKEN, + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenantId': 'tenant_id1', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + UUID_TOKEN_UNSCOPED: { + 'access': { + 'token': { + 'id': UUID_TOKEN_UNSCOPED, + 'expires': '2020-01-01T00:00:10.000123Z', + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + UUID_TOKEN_NO_SERVICE_CATALOG: { + 'access': { + 'token': { + 'id': 'valid-token', + 'expires': '2020-01-01T00:00:10.000123Z', + 'tenant': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + }, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + } + }, + }, + v3_UUID_TOKEN_DEFAULT: { + 'token': { + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + v3_UUID_TOKEN_UNSCOPED: { + 'token': { + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + } + } + }, + v3_UUID_TOKEN_DOMAIN_SCOPED: { + 'token': { + 'expires_at': '2020-01-01T00:00:10.000123Z', + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1', + }, + 'roles': [ + {'name': 'role1', 'id': 'Role1'}, + {'name': 'role2', 'id': 'Role2'}, + ], + 'catalog': {} + } + }, + SIGNED_TOKEN_SCOPED_KEY: { + 'access': { + 'token': { + 'id': SIGNED_TOKEN_SCOPED_KEY, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'tenantId': 'tenant_id1', + 'tenantName': 'tenant_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + SIGNED_TOKEN_UNSCOPED_KEY: { + 'access': { + 'token': { + 'id': SIGNED_TOKEN_UNSCOPED_KEY, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], + }, + }, + }, + SIGNED_v3_TOKEN_SCOPED_KEY: { + 'token': { + 'expires': '2020-01-01T00:00:10.000123Z', + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'project': { + 'id': 'tenant_id1', + 'name': 'tenant_name1', + 'domain': { + 'id': 'domain_id1', + 'name': 'domain_name1' + } + }, + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'} + ], + 'catalog': {} + } + }, +} + + +JSON_TOKEN_RESPONSES = dict([(k, jsonutils.dumps(v)) for k, v in + TOKEN_RESPONSES.iteritems()]) diff --git a/fakes.py b/fakes.py new file mode 100644 index 0000000..941f9e4 --- /dev/null +++ b/fakes.py @@ -0,0 +1,120 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" +A fake server that "responds" to API methods with pre-canned responses. + +All of these responses come from the spec, so if for some reason the spec's +wrong the tests might raise AssertionError. I've indicated in comments the +places where actual behavior differs from the spec. +""" + +from keystoneclient import access + + +def assert_has_keys(dict, required=[], optional=[]): + keys = dict.keys() + for k in required: + try: + assert k in keys + except AssertionError: + extra_keys = set(keys).difference(set(required + optional)) + raise AssertionError("found unexpected keys: %s" % + list(extra_keys)) + + +class FakeClient(object): + + def assert_called(self, method, url, body=None, pos=-1): + """Assert than an API method was just called.""" + expected = (method, url) + called = self.callstack[pos][0:2] + + assert self.callstack, ("Expected %s %s but no calls were made." % + expected) + assert expected == called, ("Expected %s %s; got %s %s" % + (expected + called)) + + if body is not None: + assert self.callstack[pos][2] == body + + def assert_called_anytime(self, method, url, body=None): + """Assert than an API method was called anytime in the test.""" + expected = (method, url) + + assert self.callstack, ("Expected %s %s but no calls were made." % + expected) + + found = False + for entry in self.callstack: + if expected == entry[0:2]: + found = True + break + + assert found, ('Expected %s; got %s' % + (expected, self.callstack)) + if body is not None: + if entry[2] != body: + raise AssertionError('%s != %s' % (entry[2], body)) + self.callstack = [] + + def clear_callstack(self): + self.callstack = [] + + def authenticate(self, cl_obj): + cl_obj.user_id = '1' + cl_obj.auth_user_id = '1' + cl_obj.project_id = '1' + cl_obj.auth_tenant_id = '1' + cl_obj.auth_ref = access.AccessInfo.factory(None, { + "access": { + "token": { + "expires": "2012-02-05T00:00:00", + "id": "887665443383838", + "tenant": { + "id": "1", + "name": "customer-x" + } + }, + "serviceCatalog": [{ + "endpoints": [{ + "adminURL": "http://swift.admin-nets.local:8080/", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8080/v1/AUTH_1", + "publicURL": + "http://swift.publicinternets.com/v1/AUTH_1" + }], + "type": "object-store", + "name": "swift" + }, { + "endpoints": [{ + "adminURL": "http://cdn.admin-nets.local/v1.1/1", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:7777/v1.1/1", + "publicURL": "http://cdn.publicinternets.com/v1.1/1" + }], + "type": "object-store", + "name": "cdn" + }], + "user": { + "id": "1", + "roles": [{ + "tenantId": "1", + "id": "3", + "name": "Member" + }], + "name": "joeuser" + } + } + }) diff --git a/test_auth_token_middleware.py b/test_auth_token_middleware.py new file mode 100644 index 0000000..06e7609 --- /dev/null +++ b/test_auth_token_middleware.py @@ -0,0 +1,1220 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 datetime +import iso8601 +import os +import shutil +import stat +import sys +import tempfile +import testtools +import uuid + +import fixtures +import httpretty +import mock +import webob + +from keystoneclient.common import cms +from keystoneclient.middleware import auth_token +from keystoneclient.openstack.common import jsonutils +from keystoneclient.openstack.common import memorycache +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests import client_fixtures + + +EXPECTED_V2_DEFAULT_ENV_RESPONSE = { + 'HTTP_X_IDENTITY_STATUS': 'Confirmed', + 'HTTP_X_TENANT_ID': 'tenant_id1', + 'HTTP_X_TENANT_NAME': 'tenant_name1', + 'HTTP_X_USER_ID': 'user_id1', + 'HTTP_X_USER_NAME': 'user_name1', + 'HTTP_X_ROLES': 'role1,role2', + 'HTTP_X_USER': 'user_name1', # deprecated (diablo-compat) + 'HTTP_X_TENANT': 'tenant_name1', # deprecated (diablo-compat) + 'HTTP_X_ROLE': 'role1,role2', # deprecated (diablo-compat) +} + + +BASE_HOST = 'https://keystone.example.com:1234' +BASE_URI = '%s/testadmin' % BASE_HOST +FAKE_ADMIN_TOKEN_ID = 'admin_token2' +FAKE_ADMIN_TOKEN = jsonutils.dumps( + {'access': {'token': {'id': FAKE_ADMIN_TOKEN_ID, + 'expires': '2022-10-03T16:58:01Z'}}}) + + +VERSION_LIST_v3 = jsonutils.dumps({ + "versions": { + "values": [ + { + "id": "v3.0", + "status": "stable", + "updated": "2013-03-06T00:00:00Z", + "links": [{'href': '%s/v3' % BASE_URI, 'rel': 'self'}] + }, + { + "id": "v2.0", + "status": "stable", + "updated": "2011-11-19T00:00:00Z", + "links": [{'href': '%s/v2.0' % BASE_URI, 'rel': 'self'}] + } + ] + } +}) + +VERSION_LIST_v2 = jsonutils.dumps({ + "versions": { + "values": [ + { + "id": "v2.0", + "status": "stable", + "updated": "2011-11-19T00:00:00Z", + "links": [] + } + ] + } +}) + +ERROR_TOKEN = '7ae290c2a06244c4b41692eb4e9225f2' + + +class NoModuleFinder(object): + """Disallow further imports of 'module'.""" + + def __init__(self, module): + self.module = module + + def find_module(self, fullname, path): + if fullname == self.module or fullname.startswith(self.module + '.'): + raise ImportError + + +class DisableModuleFixture(fixtures.Fixture): + """A fixture to provide support for unloading/disabling modules.""" + + def __init__(self, module, *args, **kw): + super(DisableModuleFixture, self).__init__(*args, **kw) + self.module = module + self._finders = [] + self._cleared_modules = {} + + def tearDown(self): + super(DisableModuleFixture, self).tearDown() + for finder in self._finders: + sys.meta_path.remove(finder) + sys.modules.update(self._cleared_modules) + + def clear_module(self): + cleared_modules = {} + for fullname in sys.modules.keys(): + if (fullname == self.module or + fullname.startswith(self.module + '.')): + cleared_modules[fullname] = sys.modules.pop(fullname) + return cleared_modules + + def setUp(self): + """Ensure ImportError for the specified module.""" + + super(DisableModuleFixture, self).setUp() + + # Clear 'module' references in sys.modules + self._cleared_modules.update(self.clear_module()) + + finder = NoModuleFinder(self.module) + self._finders.append(finder) + sys.meta_path.insert(0, finder) + + +class FakeSwiftOldMemcacheClient(memorycache.Client): + # NOTE(vish,chmou): old swift memcache uses param timeout instead of time + def set(self, key, value, timeout=0, min_compress_len=0): + sup = super(FakeSwiftOldMemcacheClient, self) + sup.set(key, value, timeout, min_compress_len) + + +class FakeApp(object): + """This represents a WSGI app protected by the auth_token middleware.""" + + def __init__(self, expected_env=None): + self.expected_env = dict(EXPECTED_V2_DEFAULT_ENV_RESPONSE) + + if expected_env: + self.expected_env.update(expected_env) + + def __call__(self, env, start_response): + for k, v in self.expected_env.items(): + assert env[k] == v, '%s != %s' % (env[k], v) + + resp = webob.Response() + resp.body = 'SUCCESS' + return resp(env, start_response) + + +class v3FakeApp(FakeApp): + """This represents a v3 WSGI app protected by the auth_token middleware.""" + + def __init__(self, expected_env=None): + + # with v3 additions, these are for the DEFAULT TOKEN + v3_default_env_additions = { + 'HTTP_X_PROJECT_ID': 'tenant_id1', + 'HTTP_X_PROJECT_NAME': 'tenant_name1', + 'HTTP_X_PROJECT_DOMAIN_ID': 'domain_id1', + 'HTTP_X_PROJECT_DOMAIN_NAME': 'domain_name1', + 'HTTP_X_USER_DOMAIN_ID': 'domain_id1', + 'HTTP_X_USER_DOMAIN_NAME': 'domain_name1' + } + + if expected_env: + v3_default_env_additions.update(expected_env) + + super(v3FakeApp, self).__init__(v3_default_env_additions) + + +class BaseAuthTokenMiddlewareTest(testtools.TestCase): + """Base test class for auth_token middleware. + + All the tests allow for running with auth_token + configured for receiving v2 or v3 tokens, with the + choice being made by passing configuration data into + Setup(). + + The base class will, by default, run all the tests + expecting v2 token formats. Child classes can override + this to specify, for instance, v3 format. + + """ + def setUp(self, expected_env=None, auth_version=None, fake_app=None): + testtools.TestCase.setUp(self) + + self.expected_env = expected_env or dict() + self.fake_app = fake_app or FakeApp + self.middleware = None + + self.conf = { + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'auth_protocol': 'https', + 'auth_admin_prefix': '/testadmin', + 'signing_dir': client_fixtures.CERTDIR, + 'auth_version': auth_version + } + + self.response_status = None + self.response_headers = None + + def set_middleware(self, fake_app=None, expected_env=None, conf=None): + """Configure the class ready to call the auth_token middleware. + + Set up the various fake items needed to run the middleware. + Individual tests that need to further refine these can call this + function to override the class defaults. + + """ + if conf: + self.conf.update(conf) + + if not fake_app: + fake_app = self.fake_app + + if expected_env: + self.expected_env.update(expected_env) + + self.middleware = auth_token.AuthProtocol(fake_app(self.expected_env), + self.conf) + self.middleware._iso8601 = iso8601 + self.middleware.revoked_file_name = tempfile.mkstemp()[1] + self.middleware.token_revocation_list = jsonutils.dumps( + {"revoked": [], "extra": "success"}) + + def tearDown(self): + testtools.TestCase.tearDown(self) + if self.middleware: + try: + os.remove(self.middleware.revoked_file_name) + except OSError: + pass + + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + + def assertLastPath(self, path): + if path: + self.assertEqual(path, httpretty.httpretty.last_request.path) + else: + self.assertIsInstance(httpretty.httpretty.last_request, + httpretty.core.HTTPrettyRequestEmpty) + +if tuple(sys.version_info)[0:2] < (2, 7): + + # 2.6 doesn't have the assert dict equals so make sure that it exists + class AdjustedBaseAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): + def assertIsInstance(self, obj, cls, msg=None): + """Same as self.assertTrue(isinstance(obj, cls)), with a nicer + default message. + """ + if not isinstance(obj, cls): + standardMsg = '%s is not an instance of %r' % (obj, cls) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertDictEqual(self, d1, d2, msg=None): + # Simple version taken from 2.7 + self.assertIsInstance(d1, dict, + 'First argument is not a dictionary') + self.assertIsInstance(d2, dict, + 'Second argument is not a dictionary') + if d1 != d2: + if msg: + self.fail(msg) + else: + standardMsg = '%r != %r' % (d1, d2) + self.fail(standardMsg) + + BaseAuthTokenMiddlewareTest = AdjustedBaseAuthTokenMiddlewareTest + + +class MultiStepAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): + + @httpretty.activate + def test_fetch_revocation_list_with_expire(self): + self.set_middleware() + + # Get a token, then try to retrieve revocation list and get a 401. + # Get a new token, try to retrieve revocation list and return 200. + httpretty.register_uri(httpretty.POST, "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + responses = [httpretty.Response(body='', status=401), + httpretty.Response( + body=client_fixtures.SIGNED_REVOCATION_LIST)] + + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/revoked" % BASE_URI, + responses=responses) + + fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) + self.assertEqual(fetched_list, client_fixtures.REVOCATION_LIST) + + # Check that 4 requests have been made + self.assertEqual(len(httpretty.httpretty.latest_requests), 4) + + +class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): + """Auth Token middleware should understand Diablo keystone responses.""" + def setUp(self): + # pre-diablo only had Tenant ID, which was also the Name + expected_env = { + 'HTTP_X_TENANT_ID': 'tenant_id1', + 'HTTP_X_TENANT_NAME': 'tenant_id1', + # now deprecated (diablo-compat) + 'HTTP_X_TENANT': 'tenant_id1', + } + + super(DiabloAuthTokenMiddlewareTest, self).setUp( + expected_env=expected_env) + + httpretty.httpretty.reset() + httpretty.enable() + + httpretty.register_uri(httpretty.GET, + "%s/" % BASE_URI, + body=VERSION_LIST_v2, + status=300) + + httpretty.register_uri(httpretty.POST, + "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + self.token_id = client_fixtures.VALID_DIABLO_TOKEN + token_response = client_fixtures.JSON_TOKEN_RESPONSES[self.token_id] + + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/%s" % (BASE_URI, self.token_id), + body=token_response) + + self.set_middleware() + + def tearDown(self): + httpretty.disable() + super(DiabloAuthTokenMiddlewareTest, self).tearDown() + + def test_valid_diablo_response(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.token_id + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertTrue('keystone.token_info' in req.environ) + + +class NoMemcacheAuthToken(BaseAuthTokenMiddlewareTest): + + def setUp(self): + super(NoMemcacheAuthToken, self).setUp() + self.useFixture(DisableModuleFixture('memcache')) + + def test_nomemcache(self): + conf = { + 'admin_token': 'admin_token1', + 'auth_host': 'keystone.example.com', + 'auth_port': 1234, + 'memcached_servers': 'localhost:11211', + } + + auth_token.AuthProtocol(FakeApp(), conf) + + def test_not_use_cache_from_env(self): + env = {'swift.cache': 'CACHE_TEST'} + conf = { + 'memcached_servers': 'localhost:11211' + } + self.set_middleware(conf=conf) + self.middleware._init_cache(env) + self.assertNotEqual(self.middleware._cache, 'CACHE_TEST') + + +class CommonAuthTokenMiddlewareTest(object): + + def test_init_does_not_call_http(self): + conf = { + 'revocation_cache_time': 1 + } + self.set_middleware(conf=conf) + self.assertLastPath(None) + + def test_init_by_ipv6Addr_auth_host(self): + conf = { + 'auth_host': '2001:2013:1:f101::1', + 'auth_port': 1234, + 'auth_protocol': 'http', + } + self.set_middleware(conf=conf) + expected_auth_uri = 'http://[2001:2013:1:f101::1]:1234' + self.assertEqual(expected_auth_uri, self.middleware.auth_uri) + + def assert_valid_request_200(self, token, with_catalog=True): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + if with_catalog: + self.assertTrue(req.headers.get('X-Service-Catalog')) + self.assertEqual(body, ['SUCCESS']) + self.assertTrue('keystone.token_info' in req.environ) + + def test_valid_uuid_request(self): + self.assert_valid_request_200(self.token_dict['uuid_token_default']) + self.assert_valid_last_url(self.token_dict['uuid_token_default']) + + def test_valid_signed_request(self): + self.assert_valid_request_200( + self.token_dict['signed_token_scoped']) + self.assertEqual(self.middleware.conf['auth_admin_prefix'], + "/testadmin") + #ensure that signed requests do not generate HTTP traffic + self.assertLastPath(None) + + def test_revoked_token_receives_401(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = self.token_dict['revoked_token'] + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + + def get_revocation_list_json(self, token_ids=None): + if token_ids is None: + token_ids = [self.token_dict['revoked_token_hash']] + revocation_list = {'revoked': [{'id': x, 'expires': timeutils.utcnow()} + for x in token_ids]} + return jsonutils.dumps(revocation_list) + + def test_is_signed_token_revoked_returns_false(self): + #explicitly setting an empty revocation list here to document intent + self.middleware.token_revocation_list = jsonutils.dumps( + {"revoked": [], "extra": "success"}) + result = self.middleware.is_signed_token_revoked( + self.token_dict['revoked_token']) + self.assertFalse(result) + + def test_is_signed_token_revoked_returns_true(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + result = self.middleware.is_signed_token_revoked( + self.token_dict['revoked_token']) + self.assertTrue(result) + + def test_verify_signed_token_raises_exception_for_revoked_token(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + self.assertRaises(auth_token.InvalidUserToken, + self.middleware.verify_signed_token, + self.token_dict['revoked_token']) + + def test_verify_signed_token_succeeds_for_unrevoked_token(self): + self.middleware.token_revocation_list = self.get_revocation_list_json() + self.middleware.verify_signed_token( + self.token_dict['signed_token_scoped']) + + def test_verify_signing_dir_create_while_missing(self): + tmp_name = uuid.uuid4().hex + test_parent_signing_dir = "/tmp/%s" % tmp_name + self.middleware.signing_dirname = "/tmp/%s/%s" % ((tmp_name,) * 2) + self.middleware.signing_cert_file_name = "%s/test.pem" %\ + self.middleware.signing_dirname + self.middleware.verify_signing_dir() + # NOTE(wu_wenxiang): Verify if the signing dir was created as expected. + self.assertTrue(os.path.isdir(self.middleware.signing_dirname)) + self.assertTrue(os.access(self.middleware.signing_dirname, os.W_OK)) + self.assertEqual(os.stat(self.middleware.signing_dirname).st_uid, + os.getuid()) + self.assertEqual( + stat.S_IMODE(os.stat(self.middleware.signing_dirname).st_mode), + stat.S_IRWXU) + shutil.rmtree(test_parent_signing_dir) + + def test_cert_file_missing(self): + self.assertFalse(self.middleware.cert_file_missing( + "openstack: /tmp/haystack: No such file or directory", + "/tmp/needle")) + self.assertTrue(self.middleware.cert_file_missing( + "openstack: /not/exist: No such file or directory", + "/not/exist")) + + def test_get_token_revocation_list_fetched_time_returns_min(self): + self.middleware.token_revocation_list_fetched_time = None + self.middleware.revoked_file_name = '' + self.assertEqual(self.middleware.token_revocation_list_fetched_time, + datetime.datetime.min) + + def test_get_token_revocation_list_fetched_time_returns_mtime(self): + self.middleware.token_revocation_list_fetched_time = None + mtime = os.path.getmtime(self.middleware.revoked_file_name) + fetched_time = datetime.datetime.fromtimestamp(mtime) + self.assertEqual(self.middleware.token_revocation_list_fetched_time, + fetched_time) + + def test_get_token_revocation_list_fetched_time_returns_value(self): + expected = self.middleware._token_revocation_list_fetched_time + self.assertEqual(self.middleware.token_revocation_list_fetched_time, + expected) + + def test_get_revocation_list_returns_fetched_list(self): + # auth_token uses v2 to fetch this, so don't allow the v3 + # tests to override the fake http connection + self.middleware.token_revocation_list_fetched_time = None + os.remove(self.middleware.revoked_file_name) + self.assertEqual(self.middleware.token_revocation_list, + client_fixtures.REVOCATION_LIST) + + def test_get_revocation_list_returns_current_list_from_memory(self): + self.assertEqual(self.middleware.token_revocation_list, + self.middleware._token_revocation_list) + + def test_get_revocation_list_returns_current_list_from_disk(self): + in_memory_list = self.middleware.token_revocation_list + self.middleware._token_revocation_list = None + self.assertEqual(self.middleware.token_revocation_list, in_memory_list) + + def test_invalid_revocation_list_raises_service_error(self): + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/revoked" % BASE_URI, + body="{}", + status=200) + + self.assertRaises(auth_token.ServiceError, + self.middleware.fetch_revocation_list) + + def test_fetch_revocation_list(self): + # auth_token uses v2 to fetch this, so don't allow the v3 + # tests to override the fake http connection + fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list()) + self.assertEqual(fetched_list, client_fixtures.REVOCATION_LIST) + + def test_request_invalid_uuid_token(self): + # remember because we are testing the middleware we stub the connection + # to the keystone server, but this is not what gets returned + invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI + httpretty.register_uri(httpretty.GET, invalid_uri, body="", status=404) + + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = 'invalid-token' + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def test_request_invalid_signed_token(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = client_fixtures.INVALID_SIGNED_TOKEN + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def test_request_no_token(self): + req = webob.Request.blank('/') + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def test_request_no_token_log_message(self): + class FakeLog(object): + def __init__(self): + self.msg = None + self.debugmsg = None + + def warn(self, msg=None, *args, **kwargs): + self.msg = msg + + def debug(self, msg=None, *args, **kwargs): + self.debugmsg = msg + + self.middleware.LOG = FakeLog() + self.middleware.delay_auth_decision = False + self.assertRaises(auth_token.InvalidUserToken, + self.middleware._get_user_token_from_header, {}) + self.assertIsNotNone(self.middleware.LOG.msg) + self.assertIsNotNone(self.middleware.LOG.debugmsg) + + def test_request_no_token_http(self): + req = webob.Request.blank('/', environ={'REQUEST_METHOD': 'HEAD'}) + self.set_middleware() + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + self.assertEqual(body, ['']) + + def test_request_blank_token(self): + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = '' + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def _get_cached_token(self, token): + token_id = cms.cms_hash_token(token) + # NOTE(vish): example tokens are expired so skip the expiration check. + return self.middleware._cache_get(token_id, ignore_expires=True) + + def test_memcache(self): + # NOTE(jamielennox): it appears that httpretty can mess with the + # memcache socket. Just disable it as it's not required here anyway. + httpretty.disable() + req = webob.Request.blank('/') + token = self.token_dict['signed_token_scoped'] + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertNotEqual(self._get_cached_token(token), None) + + def test_expired(self): + httpretty.disable() + req = webob.Request.blank('/') + token = self.token_dict['signed_token_scoped_expired'] + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + + def test_memcache_set_invalid_uuid(self): + invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI + httpretty.register_uri(httpretty.GET, invalid_uri, body="", status=404) + + req = webob.Request.blank('/') + token = 'invalid-token' + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertRaises(auth_token.InvalidUserToken, + self._get_cached_token, token) + + def test_memcache_set_invalid_signed(self): + req = webob.Request.blank('/') + token = self.token_dict['signed_token_scoped_expired'] + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertRaises(auth_token.InvalidUserToken, + self._get_cached_token, token) + + def test_memcache_set_expired(self, extra_conf={}, extra_environ={}): + httpretty.disable() + token_cache_time = 10 + conf = { + 'token_cache_time': token_cache_time, + 'signing_dir': client_fixtures.CERTDIR, + } + conf.update(extra_conf) + self.set_middleware(conf=conf) + req = webob.Request.blank('/') + token = self.token_dict['signed_token_scoped'] + req.headers['X-Auth-Token'] = token + req.environ.update(extra_environ) + try: + now = datetime.datetime.utcnow() + timeutils.set_time_override(now) + self.middleware(req.environ, self.start_fake_response) + self.assertNotEqual(self._get_cached_token(token), None) + expired = now + datetime.timedelta(seconds=token_cache_time) + timeutils.set_time_override(expired) + self.assertEqual(self._get_cached_token(token), None) + finally: + timeutils.clear_time_override() + + def test_old_swift_memcache_set_expired(self): + extra_conf = {'cache': 'swift.cache'} + extra_environ = {'swift.cache': FakeSwiftOldMemcacheClient()} + self.test_memcache_set_expired(extra_conf, extra_environ) + + def test_swift_memcache_set_expired(self): + extra_conf = {'cache': 'swift.cache'} + extra_environ = {'swift.cache': memorycache.Client()} + self.test_memcache_set_expired(extra_conf, extra_environ) + + def test_use_cache_from_env(self): + env = {'swift.cache': 'CACHE_TEST'} + conf = { + 'cache': 'swift.cache', + 'memcached_servers': ['localhost:11211'] + } + self.set_middleware(conf=conf) + self.middleware._init_cache(env) + self.assertEqual(self.middleware._cache, 'CACHE_TEST') + + def test_will_expire_soon(self): + tenseconds = datetime.datetime.utcnow() + datetime.timedelta( + seconds=10) + self.assertTrue(auth_token.will_expire_soon(tenseconds)) + fortyseconds = datetime.datetime.utcnow() + datetime.timedelta( + seconds=40) + self.assertFalse(auth_token.will_expire_soon(fortyseconds)) + + def test_encrypt_cache_data(self): + httpretty.disable() + conf = { + 'memcached_servers': ['localhost:11211'], + 'memcache_security_strategy': 'encrypt', + 'memcache_secret_key': 'mysecret' + } + self.set_middleware(conf=conf) + token = 'my_token' + data = ('this_data', 10e100) + self.middleware._init_cache({}) + self.middleware._cache_store(token, data) + self.assertEqual(self.middleware._cache_get(token), data[0]) + + def test_sign_cache_data(self): + httpretty.disable() + conf = { + 'memcached_servers': ['localhost:11211'], + 'memcache_security_strategy': 'mac', + 'memcache_secret_key': 'mysecret' + } + self.set_middleware(conf=conf) + token = 'my_token' + data = ('this_data', 10e100) + self.middleware._init_cache({}) + self.middleware._cache_store(token, data) + self.assertEqual(self.middleware._cache_get(token), data[0]) + + def test_no_memcache_protection(self): + httpretty.disable() + conf = { + 'memcached_servers': ['localhost:11211'], + 'memcache_secret_key': 'mysecret' + } + self.set_middleware(conf=conf) + token = 'my_token' + data = ('this_data', 10e100) + self.middleware._init_cache({}) + self.middleware._cache_store(token, data) + self.assertEqual(self.middleware._cache_get(token), data[0]) + + def test_assert_valid_memcache_protection_config(self): + # test missing memcache_secret_key + conf = { + 'memcached_servers': ['localhost:11211'], + 'memcache_security_strategy': 'Encrypt' + } + self.assertRaises(Exception, self.set_middleware, conf=conf) + # test invalue memcache_security_strategy + conf = { + 'memcached_servers': ['localhost:11211'], + 'memcache_security_strategy': 'whatever' + } + self.assertRaises(Exception, self.set_middleware, conf=conf) + # test missing memcache_secret_key + conf = { + 'memcached_servers': ['localhost:11211'], + 'memcache_security_strategy': 'mac' + } + self.assertRaises(Exception, self.set_middleware, conf=conf) + conf = { + 'memcached_servers': ['localhost:11211'], + 'memcache_security_strategy': 'Encrypt', + 'memcache_secret_key': '' + } + self.assertRaises(Exception, self.set_middleware, conf=conf) + conf = { + 'memcached_servers': ['localhost:11211'], + 'memcache_security_strategy': 'mAc', + 'memcache_secret_key': '' + } + self.assertRaises(Exception, self.set_middleware, conf=conf) + + def test_config_revocation_cache_timeout(self): + conf = { + 'revocation_cache_time': 24 + } + middleware = auth_token.AuthProtocol(self.fake_app, conf) + self.assertEquals(middleware.token_revocation_list_cache_timeout, + datetime.timedelta(seconds=24)) + + def test_http_error_not_cached_token(self): + """Test to don't cache token as invalid on network errors. + + We use UUID tokens since they are the easiest one to reach + get_http_connection. + """ + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = ERROR_TOKEN + self.middleware.http_request_max_retries = 0 + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self._get_cached_token(ERROR_TOKEN), None) + self.assert_valid_last_url(ERROR_TOKEN) + + def test_http_request_max_retries(self): + times_retry = 10 + + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = ERROR_TOKEN + + conf = {'http_request_max_retries': times_retry} + self.set_middleware(conf=conf) + + with mock.patch('time.sleep') as mock_obj: + self.middleware(req.environ, self.start_fake_response) + + self.assertEqual(mock_obj.call_count, times_retry) + + +class CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest): + def setUp(self): + super(CertDownloadMiddlewareTest, self).setUp() + self.base_dir = tempfile.mkdtemp() + self.cert_dir = os.path.join(self.base_dir, 'certs') + os.mkdir(self.cert_dir) + conf = { + 'signing_dir': self.cert_dir, + } + self.set_middleware(conf=conf) + + httpretty.enable() + + def tearDown(self): + httpretty.disable() + shutil.rmtree(self.base_dir) + super(CertDownloadMiddlewareTest, self).tearDown() + + # Usually we supply a signed_dir with pre-installed certificates, + # so invocation of /usr/bin/openssl succeeds. This time we give it + # an empty directory, so it fails. + def test_request_no_token_dummy(self): + cms._ensure_subprocess() + + httpretty.register_uri(httpretty.GET, + "%s/v2.0/certificates/ca" % BASE_URI, + status=404) + httpretty.register_uri(httpretty.GET, + "%s/v2.0/certificates/signing" % BASE_URI, + status=404) + self.assertRaises(cms.subprocess.CalledProcessError, + self.middleware.verify_signed_token, + client_fixtures.SIGNED_TOKEN_SCOPED) + + def test_fetch_signing_cert(self): + data = 'FAKE CERT' + httpretty.register_uri(httpretty.GET, + "%s/v2.0/certificates/signing" % BASE_URI, + body=data) + self.middleware.fetch_signing_cert() + + with open(self.middleware.signing_cert_file_name, 'r') as f: + self.assertEqual(f.read(), data) + + self.assertEqual("/testadmin/v2.0/certificates/signing", + httpretty.httpretty.last_request.path) + + def test_fetch_signing_ca(self): + data = 'FAKE CA' + httpretty.register_uri(httpretty.GET, + "%s/v2.0/certificates/ca" % BASE_URI, + body=data) + self.middleware.fetch_ca_cert() + + with open(self.middleware.signing_ca_file_name, 'r') as f: + self.assertEqual(f.read(), data) + + self.assertEqual("/testadmin/v2.0/certificates/ca", + httpretty.httpretty.last_request.path) + + def test_prefix_trailing_slash(self): + self.conf['auth_admin_prefix'] = '/newadmin/' + + httpretty.register_uri(httpretty.GET, + "%s/newadmin/v2.0/certificates/ca" % BASE_HOST, + body='FAKECA') + httpretty.register_uri(httpretty.GET, + "%s/newadmin/v2.0/certificates/signing" % + BASE_HOST, body='FAKECERT') + + self.set_middleware(conf=self.conf) + + self.middleware.fetch_ca_cert() + + self.assertEqual('/newadmin/v2.0/certificates/ca', + httpretty.httpretty.last_request.path) + + self.middleware.fetch_signing_cert() + + self.assertEqual('/newadmin/v2.0/certificates/signing', + httpretty.httpretty.last_request.path) + + def test_without_prefix(self): + self.conf['auth_admin_prefix'] = '' + + httpretty.register_uri(httpretty.GET, + "%s/v2.0/certificates/ca" % BASE_HOST, + body='FAKECA') + httpretty.register_uri(httpretty.GET, + "%s/v2.0/certificates/signing" % BASE_HOST, + body='FAKECERT') + + self.set_middleware(conf=self.conf) + + self.middleware.fetch_ca_cert() + + self.assertEqual('/v2.0/certificates/ca', + httpretty.httpretty.last_request.path) + + self.middleware.fetch_signing_cert() + + self.assertEqual('/v2.0/certificates/signing', + httpretty.httpretty.last_request.path) + + +def network_error_response(method, uri, headers): + raise auth_token.NetworkError("Network connection error.") + + +class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + CommonAuthTokenMiddlewareTest): + """v2 token specific tests. + + There are some differences between how the auth-token middleware handles + v2 and v3 tokens over and above the token formats, namely: + + - A v3 keystone server will auto scope a token to a user's default project + if no scope is specified. A v2 server assumes that the auth-token + middleware will do that. + - A v2 keystone server may issue a token without a catalog, even with a + tenant + + The tests below were originally part of the generic AuthTokenMiddlewareTest + class, but now, since they really are v2 specifc, they are included here. + + """ + + def setUp(self): + super(v2AuthTokenMiddlewareTest, self).setUp() + + self.token_dict = { + 'uuid_token_default': client_fixtures.UUID_TOKEN_DEFAULT, + 'uuid_token_unscoped': client_fixtures.UUID_TOKEN_UNSCOPED, + 'signed_token_scoped': client_fixtures.SIGNED_TOKEN_SCOPED, + 'signed_token_scoped_expired': + client_fixtures.SIGNED_TOKEN_SCOPED_EXPIRED, + 'revoked_token': client_fixtures.REVOKED_TOKEN, + 'revoked_token_hash': client_fixtures.REVOKED_TOKEN_HASH + } + + httpretty.httpretty.reset() + httpretty.enable() + + httpretty.register_uri(httpretty.GET, + "%s/" % BASE_URI, + body=VERSION_LIST_v2, + status=300) + + httpretty.register_uri(httpretty.POST, + "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/revoked" % BASE_URI, + body=client_fixtures.SIGNED_REVOCATION_LIST, + status=200) + + for token in (client_fixtures.UUID_TOKEN_DEFAULT, + client_fixtures.UUID_TOKEN_UNSCOPED, + client_fixtures.UUID_TOKEN_NO_SERVICE_CATALOG): + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/%s" % (BASE_URI, token), + body= + client_fixtures.JSON_TOKEN_RESPONSES[token]) + + httpretty.register_uri(httpretty.GET, + '%s/v2.0/tokens/%s' % (BASE_URI, ERROR_TOKEN), + body=network_error_response) + + self.set_middleware() + + def tearDown(self): + httpretty.disable() + super(v2AuthTokenMiddlewareTest, self).tearDown() + + def assert_unscoped_default_tenant_auto_scopes(self, token): + """Unscoped v2 requests with a default tenant should "auto-scope." + + The implied scope is the user's tenant ID. + + """ + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertEqual(body, ['SUCCESS']) + self.assertTrue('keystone.token_info' in req.environ) + + def assert_valid_last_url(self, token_id): + self.assertLastPath("/testadmin/v2.0/tokens/%s" % token_id) + + def test_default_tenant_uuid_token(self): + self.assert_unscoped_default_tenant_auto_scopes( + client_fixtures.UUID_TOKEN_DEFAULT) + + def test_default_tenant_signed_token(self): + self.assert_unscoped_default_tenant_auto_scopes( + client_fixtures.SIGNED_TOKEN_SCOPED) + + def assert_unscoped_token_receives_401(self, token): + """Unscoped requests with no default tenant ID should be rejected.""" + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = token + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 401) + self.assertEqual(self.response_headers['WWW-Authenticate'], + "Keystone uri='https://keystone.example.com:1234'") + + def test_unscoped_uuid_token_receives_401(self): + self.assert_unscoped_token_receives_401( + client_fixtures.UUID_TOKEN_UNSCOPED) + + def test_unscoped_pki_token_receives_401(self): + self.assert_unscoped_token_receives_401( + client_fixtures.SIGNED_TOKEN_UNSCOPED) + + def test_request_prevent_service_catalog_injection(self): + req = webob.Request.blank('/') + req.headers['X-Service-Catalog'] = '[]' + req.headers['X-Auth-Token'] = \ + client_fixtures.UUID_TOKEN_NO_SERVICE_CATALOG + body = self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertFalse(req.headers.get('X-Service-Catalog')) + self.assertEqual(body, ['SUCCESS']) + + +class CrossVersionAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): + + @httpretty.activate + def test_valid_uuid_request_forced_to_2_0(self): + """Test forcing auth_token to use lower api version. + + By installing the v3 http hander, auth_token will be get + a version list that looks like a v3 server - from which it + would normally chose v3.0 as the auth version. However, here + we specify v2.0 in the configuration - which should force + auth_token to use that version instead. + + """ + conf = { + 'signing_dir': client_fixtures.CERTDIR, + 'auth_version': 'v2.0' + } + + httpretty.register_uri(httpretty.GET, + "%s/" % BASE_URI, + body=VERSION_LIST_v3, + status=300) + + httpretty.register_uri(httpretty.POST, + "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + token = client_fixtures.UUID_TOKEN_DEFAULT + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/%s" % (BASE_URI, token), + body= + client_fixtures.JSON_TOKEN_RESPONSES[token]) + + self.set_middleware(conf=conf) + + # This tests will only work is auth_token has chosen to use the + # lower, v2, api version + req = webob.Request.blank('/') + req.headers['X-Auth-Token'] = client_fixtures.UUID_TOKEN_DEFAULT + self.middleware(req.environ, self.start_fake_response) + self.assertEqual(self.response_status, 200) + self.assertEqual("/testadmin/v2.0/tokens/%s" % + client_fixtures.UUID_TOKEN_DEFAULT, + httpretty.httpretty.last_request.path) + + +class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, + CommonAuthTokenMiddlewareTest): + """Test auth_token middleware with v3 tokens. + + Re-execute the AuthTokenMiddlewareTest class tests, but with the + the auth_token middleware configured to expect v3 tokens back from + a keystone server. + + This is done by configuring the AuthTokenMiddlewareTest class via + its Setup(), passing in v3 style data that will then be used by + the tests themselves. This approach has been used to ensure we + really are running the same tests for both v2 and v3 tokens. + + There a few additional specific test for v3 only: + + - We allow an unscoped token to be validated (as unscoped), where + as for v2 tokens, the auth_token middleware is expected to try and + auto-scope it (and fail if there is no default tenant) + - Domain scoped tokens + + Since we don't specify an auth version for auth_token to use, by + definition we are thefore implicitely testing that it will use + the highest available auth version, i.e. v3.0 + + """ + def setUp(self): + super(v3AuthTokenMiddlewareTest, self).setUp( + auth_version='v3.0', + fake_app=v3FakeApp) + + self.token_dict = { + 'uuid_token_default': client_fixtures.v3_UUID_TOKEN_DEFAULT, + 'uuid_token_unscoped': client_fixtures.v3_UUID_TOKEN_UNSCOPED, + 'signed_token_scoped': client_fixtures.SIGNED_v3_TOKEN_SCOPED, + 'signed_token_scoped_expired': + client_fixtures.SIGNED_TOKEN_SCOPED_EXPIRED, + 'revoked_token': client_fixtures.REVOKED_v3_TOKEN, + 'revoked_token_hash': client_fixtures.REVOKED_v3_TOKEN_HASH + } + + httpretty.httpretty.reset() + httpretty.enable() + + httpretty.register_uri(httpretty.GET, + "%s" % BASE_URI, + body=VERSION_LIST_v3, + status=300) + + # TODO(jamielennox): auth_token middleware uses a v2 admin token + # regardless of the auth_version that is set. + httpretty.register_uri(httpretty.POST, + "%s/v2.0/tokens" % BASE_URI, + body=FAKE_ADMIN_TOKEN) + + # TODO(jamielennox): there is no v3 revocation url yet, it uses v2 + httpretty.register_uri(httpretty.GET, + "%s/v2.0/tokens/revoked" % BASE_URI, + body=client_fixtures.SIGNED_REVOCATION_LIST, + status=200) + + httpretty.register_uri(httpretty.GET, + "%s/v3/auth/tokens" % BASE_URI, + body=self.token_response) + + self.set_middleware() + + def tearDown(self): + httpretty.disable() + super(v3AuthTokenMiddlewareTest, self).tearDown() + + def token_response(self, request, uri, headers): + auth_id = request.headers.get('X-Auth-Token') + token_id = request.headers.get('X-Subject-Token') + self.assertEqual(auth_id, FAKE_ADMIN_TOKEN_ID) + headers.pop('status') + + status = 200 + response = "" + + if token_id == ERROR_TOKEN: + raise auth_token.NetworkError("Network connection error.") + + try: + response = client_fixtures.JSON_TOKEN_RESPONSES[token_id] + except KeyError: + status = 404 + + return status, headers, response + + def assert_valid_last_url(self, token_id): + self.assertLastPath('/testadmin/v3/auth/tokens') + + def test_valid_unscoped_uuid_request(self): + # Remove items that won't be in an unscoped token + delta_expected_env = { + 'HTTP_X_PROJECT_ID': None, + 'HTTP_X_PROJECT_NAME': None, + 'HTTP_X_PROJECT_DOMAIN_ID': None, + 'HTTP_X_PROJECT_DOMAIN_NAME': None, + 'HTTP_X_TENANT_ID': None, + 'HTTP_X_TENANT_NAME': None, + 'HTTP_X_ROLES': '', + 'HTTP_X_TENANT': None, + 'HTTP_X_ROLE': '', + } + self.set_middleware(expected_env=delta_expected_env) + self.assert_valid_request_200(client_fixtures.v3_UUID_TOKEN_UNSCOPED, + with_catalog=False) + self.assertLastPath('/testadmin/v3/auth/tokens') + + def test_domain_scoped_uuid_request(self): + # Modify items compared to default token for a domain scope + delta_expected_env = { + 'HTTP_X_DOMAIN_ID': 'domain_id1', + 'HTTP_X_DOMAIN_NAME': 'domain_name1', + 'HTTP_X_PROJECT_ID': None, + 'HTTP_X_PROJECT_NAME': None, + 'HTTP_X_PROJECT_DOMAIN_ID': None, + 'HTTP_X_PROJECT_DOMAIN_NAME': None, + 'HTTP_X_TENANT_ID': None, + 'HTTP_X_TENANT_NAME': None, + 'HTTP_X_TENANT': None + } + self.set_middleware(expected_env=delta_expected_env) + self.assert_valid_request_200( + client_fixtures.v3_UUID_TOKEN_DOMAIN_SCOPED) + self.assertLastPath('/testadmin/v3/auth/tokens') + + +class TokenEncodingTest(testtools.TestCase): + def test_unquoted_token(self): + self.assertEqual('foo%20bar', auth_token.safe_quote('foo bar')) + + def test_quoted_token(self): + self.assertEqual('foo%20bar', auth_token.safe_quote('foo%20bar')) diff --git a/test_base.py b/test_base.py new file mode 100644 index 0000000..46e3dd2 --- /dev/null +++ b/test_base.py @@ -0,0 +1,163 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 keystoneclient import base +from keystoneclient.tests import utils +from keystoneclient.v2_0 import client +from keystoneclient.v2_0 import roles + + +class HumanReadable(base.Resource): + HUMAN_ID = True + + +class BaseTest(utils.TestCase): + + def test_resource_repr(self): + r = base.Resource(None, dict(foo="bar", baz="spam")) + self.assertEqual(repr(r), "") + + def test_getid(self): + self.assertEqual(base.getid(4), 4) + + class TmpObject(object): + id = 4 + self.assertEqual(base.getid(TmpObject), 4) + + def test_resource_lazy_getattr(self): + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url='http://127.0.0.1:5000', + endpoint='http://127.0.0.1:5000') + + self.client.get = self.mox.CreateMockAnything() + self.client.get('/OS-KSADM/roles/1').AndRaise(AttributeError) + self.mox.ReplayAll() + + f = roles.Role(self.client.roles, {'id': 1, 'name': 'Member'}) + self.assertEqual(f.name, 'Member') + + # 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 resoruces of different types: never equal + r1 = base.Resource(None, {'id': 1}) + r2 = roles.Role(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) + + r1 = base.Resource(None, {'id': 1}) + self.assertNotEqual(r1, object()) + self.assertNotEqual(r1, {'id': 1}) + + def test_human_id(self): + r = base.Resource(None, {"name": "1 of !"}) + self.assertEqual(r.human_id, None) + r = HumanReadable(None, {"name": "1 of !"}) + self.assertEqual(r.human_id, "1-of") + + +class ManagerTest(utils.TestCase): + body = {"hello": {"hi": 1}} + url = "/test-url" + + def setUp(self): + super(ManagerTest, self).setUp() + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url='http://127.0.0.1:5000', + endpoint='http://127.0.0.1:5000') + self.mgr = base.Manager(self.client) + self.mgr.resource_class = base.Resource + + def test_api(self): + self.assertEqual(self.mgr.api, self.client) + + def test_get(self): + self.client.get = self.mox.CreateMockAnything() + self.client.get(self.url).AndReturn((None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._get(self.url, "hello") + self.assertEqual(rsrc.hi, 1) + + def test_post(self): + self.client.post = self.mox.CreateMockAnything() + self.client.post(self.url, body=self.body).AndReturn((None, self.body)) + self.client.post(self.url, body=self.body).AndReturn((None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._post(self.url, self.body, "hello") + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._post(self.url, self.body, "hello", return_raw=True) + self.assertEqual(rsrc["hi"], 1) + + def test_put(self): + self.client.put = self.mox.CreateMockAnything() + self.client.put(self.url, body=self.body).AndReturn((None, self.body)) + self.client.put(self.url, body=self.body).AndReturn((None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._put(self.url, self.body, "hello") + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._put(self.url, self.body) + self.assertEqual(rsrc.hello["hi"], 1) + + def test_patch(self): + self.client.patch = self.mox.CreateMockAnything() + self.client.patch(self.url, body=self.body).AndReturn( + (None, self.body)) + self.client.patch(self.url, body=self.body).AndReturn( + (None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._patch(self.url, self.body, "hello") + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._patch(self.url, self.body) + self.assertEqual(rsrc.hello["hi"], 1) + + def test_update(self): + self.client.patch = self.mox.CreateMockAnything() + self.client.put = self.mox.CreateMockAnything() + self.client.patch( + self.url, body=self.body, management=False).AndReturn( + (None, self.body)) + self.client.put(self.url, body=None, management=True).AndReturn( + (None, self.body)) + self.mox.ReplayAll() + + rsrc = self.mgr._update( + self.url, body=self.body, response_key="hello", method="PATCH", + management=False) + self.assertEqual(rsrc.hi, 1) + + rsrc = self.mgr._update( + self.url, body=None, response_key="hello", method="PUT", + management=True) + self.assertEqual(rsrc.hi, 1) diff --git a/test_ec2utils.py b/test_ec2utils.py new file mode 100644 index 0000000..361396d --- /dev/null +++ b/test_ec2utils.py @@ -0,0 +1,257 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 __future__ import unicode_literals + +import testtools + +from keystoneclient.contrib.ec2 import utils + + +class Ec2SignerTest(testtools.TestCase): + + def setUp(self): + super(Ec2SignerTest, self).setUp() + self.access = '966afbde20b84200ae4e62e09acf46b2' + self.secret = '89cdf9e94e2643cab35b8b8ac5a51f83' + self.signer = utils.Ec2Signer(self.secret) + + def tearDown(self): + super(Ec2SignerTest, self).tearDown() + + def test_v4_creds_header(self): + auth_str = 'AWS4-HMAC-SHA256 blah' + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {}, + 'headers': {'Authorization': auth_str}} + self.assertTrue(self.signer._v4_creds(credentials)) + + def test_v4_creds_param(self): + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'X-Amz-Algorithm': 'AWS4-HMAC-SHA256'}, + 'headers': {}} + self.assertTrue(self.signer._v4_creds(credentials)) + + def test_v4_creds_false(self): + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '0', + 'AWSAccessKeyId': self.access, + 'Timestamp': '2012-11-27T11:47:02Z', + 'Action': 'Foo'}} + self.assertFalse(self.signer._v4_creds(credentials)) + + def test_generate_0(self): + """Test generate function for v0 signature.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '0', + 'AWSAccessKeyId': self.access, + 'Timestamp': '2012-11-27T11:47:02Z', + 'Action': 'Foo'}} + signature = self.signer.generate(credentials) + expected = 'SmXQEZAUdQw5glv5mX8mmixBtas=' + self.assertEqual(signature, expected) + + def test_generate_1(self): + """Test generate function for v1 signature.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '1', + 'AWSAccessKeyId': self.access}} + signature = self.signer.generate(credentials) + expected = 'VRnoQH/EhVTTLhwRLfuL7jmFW9c=' + self.assertEqual(signature, expected) + + def test_generate_v2_SHA256(self): + """Test generate function for v2 signature, SHA256.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '2', + 'AWSAccessKeyId': self.access}} + signature = self.signer.generate(credentials) + expected = 'odsGmT811GffUO0Eu13Pq+xTzKNIjJ6NhgZU74tYX/w=' + self.assertEqual(signature, expected) + + def test_generate_v2_SHA1(self): + """Test generate function for v2 signature, SHA1.""" + credentials = {'host': '127.0.0.1', + 'verb': 'GET', + 'path': '/v1/', + 'params': {'SignatureVersion': '2', + 'AWSAccessKeyId': self.access}} + self.signer.hmac_256 = None + signature = self.signer.generate(credentials) + expected = 'ZqCxMI4ZtTXWI175743mJ0hy/Gc=' + self.assertEqual(signature, expected) + + def test_generate_v4(self): + """Test v4 generator with data from AWS docs example. + + see: + http://docs.aws.amazon.com/general/latest/gr/ + sigv4-create-canonical-request.html + and + http://docs.aws.amazon.com/general/latest/gr/ + sigv4-signed-request-examples.html + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'iam.amazonaws.com', + 'Authorization': auth_str} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'iam.amazonaws.com', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + expected = ('ced6826de92d2bdeed8f846f0bf508e8' + '559e98e4b0199114b84c54174deb456c') + self.assertEqual(signature, expected) + + def test_generate_v4_port(self): + """Test v4 generator with host:port format.""" + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(signature, expected) + + def test_generate_v4_port_strip(self): + """Test v4 generator with host:port format, but for an old + (<2.9.3) version of boto, where the port should be stripped + to match boto behavior. + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.9.2 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('9a4b2276a5039ada3b90f72ea8ec1745' + '14b92b909fb106b22ad910c5d75a54f4') + self.assertEqual(expected, signature) + + def test_generate_v4_port_nostrip(self): + """Test v4 generator with host:port format, but for an new + (>=2.9.3) version of boto, where the port should not be stripped. + """ + # Create a new signer object with the AWS example key + secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' + signer = utils.Ec2Signer(secret) + + body_hash = ('b6359072c78d70ebee1e81adcbab4f0' + '1bf2c23245fa365ef83fe8f1f955085e2') + auth_str = ('AWS4-HMAC-SHA256 ' + 'Credential=AKIAIOSFODNN7EXAMPLE/20110909/' + 'us-east-1/iam/aws4_request,' + 'SignedHeaders=content-type;host;x-amz-date,') + headers = {'Content-type': + 'application/x-www-form-urlencoded; charset=utf-8', + 'X-Amz-Date': '20110909T233600Z', + 'Host': 'foo:8000', + 'Authorization': auth_str, + 'User-Agent': 'Boto/2.9.3 (linux2)'} + # Note the example in the AWS docs is inconsistent, previous + # examples specify no query string, but the final POST example + # does, apparently incorrectly since an empty parameter list + # aligns all steps and the final signature with the examples + params = {} + credentials = {'host': 'foo:8000', + 'verb': 'POST', + 'path': '/', + 'params': params, + 'headers': headers, + 'body_hash': body_hash} + signature = signer.generate(credentials) + + expected = ('26dd92ea79aaa49f533d13b1055acdc' + 'd7d7321460d64621f96cc79c4f4d4ab2b') + self.assertEqual(expected, signature) diff --git a/test_http.py b/test_http.py new file mode 100644 index 0000000..a4b8e8f --- /dev/null +++ b/test_http.py @@ -0,0 +1,209 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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 httpretty +import testtools +from testtools import matchers + +from keystoneclient import exceptions +from keystoneclient import httpclient +from keystoneclient.tests import utils + +RESPONSE_BODY = '{"hi": "there"}' + + +def get_client(): + cl = httpclient.HTTPClient(username="username", password="password", + tenant_id="tenant", auth_url="auth_test") + return cl + + +def get_authed_client(): + cl = get_client() + cl.management_url = "http://127.0.0.1:5000" + cl.auth_token = "token" + return cl + + +class FakeLog(object): + def __init__(self): + self.warn_log = str() + self.debug_log = str() + + def warn(self, msg=None, *args, **kwargs): + self.warn_log = "%s\n%s" % (self.warn_log, (msg % args)) + + def debug(self, msg=None, *args, **kwargs): + self.debug_log = "%s\n%s" % (self.debug_log, (msg % args)) + + +class ClientTest(utils.TestCase): + + TEST_URL = 'http://127.0.0.1:5000/hi' + + def test_unauthorized_client_requests(self): + cl = get_client() + self.assertRaises(exceptions.AuthorizationFailure, cl.get, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.post, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.put, '/hi') + self.assertRaises(exceptions.AuthorizationFailure, cl.delete, '/hi') + + @httpretty.activate + def test_get(self): + cl = get_authed_client() + + self.stub_url(httpretty.GET, body=RESPONSE_BODY) + + resp, body = cl.get("/hi") + self.assertEqual(httpretty.last_request().method, 'GET') + self.assertEqual(httpretty.last_request().path, '/hi') + + req_headers = httpretty.last_request().headers + + self.assertEqual(req_headers.getheader('X-Auth-Token'), 'token') + self.assertEqual(req_headers.getheader('User-Agent'), + httpclient.USER_AGENT) + + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + @httpretty.activate + def test_get_error_with_plaintext_resp(self): + cl = get_authed_client() + self.stub_url(httpretty.GET, status=400, + body='Some evil plaintext string') + + self.assertRaises(exceptions.BadRequest, cl.get, '/hi') + + @httpretty.activate + def test_get_error_with_json_resp(self): + cl = get_authed_client() + err_response = { + "error": { + "code": 400, + "title": "Error title", + "message": "Error message string" + } + } + self.stub_url(httpretty.GET, status=400, json=err_response) + exc_raised = False + try: + cl.get('/hi') + except exceptions.BadRequest as exc: + exc_raised = True + self.assertEqual(exc.message, "Error message string") + self.assertTrue(exc_raised, 'Exception not raised.') + + @httpretty.activate + def test_post(self): + cl = get_authed_client() + + self.stub_url(httpretty.POST) + cl.post("/hi", body=[1, 2, 3]) + + self.assertEqual(httpretty.last_request().method, 'POST') + self.assertEqual(httpretty.last_request().body, '[1, 2, 3]') + + req_headers = httpretty.last_request().headers + + self.assertEqual(req_headers.getheader('X-Auth-Token'), 'token') + self.assertEqual(req_headers.getheader('Content-Type'), + 'application/json') + self.assertEqual(req_headers.getheader('User-Agent'), + httpclient.USER_AGENT) + + @httpretty.activate + def test_forwarded_for(self): + ORIGINAL_IP = "10.100.100.1" + cl = httpclient.HTTPClient(username="username", password="password", + tenant_id="tenant", auth_url="auth_test", + original_ip=ORIGINAL_IP) + + self.stub_url(httpretty.GET) + + cl.request(self.TEST_URL, 'GET') + self.assertEqual(httpretty.last_request().headers['Forwarded'], + "for=%s;by=%s" % (ORIGINAL_IP, httpclient.USER_AGENT)) + + def test_client_deprecated(self): + # Can resolve symbols from the keystoneclient.client module. + # keystoneclient.client was deprecated and renamed to + # keystoneclient.httpclient. This tests that keystoneclient.client + # can still be used. + + from keystoneclient import client + + # These statements will raise an AttributeError if the symbol isn't + # defined in the module. + + client.HTTPClient + + +class BasicRequestTests(testtools.TestCase): + + url = 'http://keystone.test.com/' + + def setUp(self): + super(BasicRequestTests, self).setUp() + self.logger = FakeLog() + + def request(self, method='GET', response='Test Response', status=200, + url=None, **kwargs): + if not url: + url = self.url + + httpretty.register_uri(method, url, body=response, status=status) + + return httpclient.request(url, method, debug=True, + logger=self.logger, **kwargs) + + @httpretty.activate + def test_basic_params(self): + method = 'GET' + response = 'Test Response' + status = 200 + + self.request(method=method, status=status, response=response) + + self.assertEqual(httpretty.last_request().method, method) + + self.assertThat(self.logger.debug_log, matchers.Contains('curl')) + self.assertThat(self.logger.debug_log, matchers.Contains('-X %s' % + method)) + self.assertThat(self.logger.debug_log, matchers.Contains(self.url)) + + self.assertThat(self.logger.debug_log, matchers.Contains(str(status))) + self.assertThat(self.logger.debug_log, matchers.Contains(response)) + + @httpretty.activate + def test_headers(self): + headers = {'key': 'val', 'test': 'other'} + + self.request(headers=headers) + + for k, v in headers.iteritems(): + self.assertEqual(httpretty.last_request().headers[k], v) + + for header in headers.iteritems(): + self.assertThat(self.logger.debug_log, + matchers.Contains('-H "%s: %s"' % header)) + + @httpretty.activate + def test_body(self): + data = "BODY DATA" + self.request(response=data) + self.assertThat(self.logger.debug_log, matchers.Contains('BODY:')) + self.assertThat(self.logger.debug_log, matchers.Contains(data)) diff --git a/test_https.py b/test_https.py new file mode 100644 index 0000000..1477720 --- /dev/null +++ b/test_https.py @@ -0,0 +1,110 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 keystoneclient import httpclient +from keystoneclient.tests import utils + +FAKE_RESPONSE = utils.TestResponse({ + "status_code": 200, + "text": '{"hi": "there"}', +}) +MOCK_REQUEST = mock.Mock(return_value=(FAKE_RESPONSE)) + +REQUEST_URL = 'https://127.0.0.1:5000/hi' +RESPONSE_BODY = '{"hi": "there"}' + + +def get_client(): + cl = httpclient.HTTPClient(username="username", password="password", + tenant_id="tenant", auth_url="auth_test", + cacert="ca.pem", key="key.pem", cert="cert.pem") + return cl + + +def get_authed_client(): + cl = get_client() + cl.management_url = "https://127.0.0.1:5000" + cl.auth_token = "token" + return cl + + +class ClientTest(utils.TestCase): + + def setUp(self): + super(ClientTest, self).setUp() + self.request_patcher = mock.patch.object(requests, 'request', + self.mox.CreateMockAnything()) + self.request_patcher.start() + + def tearDown(self): + self.request_patcher.stop() + super(ClientTest, self).tearDown() + + def test_get(self): + cl = get_authed_client() + + with mock.patch.object(requests, "request", MOCK_REQUEST): + resp, body = cl.get("/hi") + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'GET') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') + + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + def test_post(self): + cl = get_authed_client() + + with mock.patch.object(requests, "request", MOCK_REQUEST): + cl.post("/hi", body=[1, 2, 3]) + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') + + def test_post_auth(self): + with mock.patch.object(requests, "request", MOCK_REQUEST): + cl = httpclient.HTTPClient( + username="username", password="password", tenant_id="tenant", + auth_url="auth_test", cacert="ca.pem", key="key.pem", + cert="cert.pem") + cl.management_url = "https://127.0.0.1:5000" + cl.auth_token = "token" + cl.post("/hi", body=[1, 2, 3]) + + # this may become too tightly couple later + mock_args, mock_kwargs = MOCK_REQUEST.call_args + + self.assertEqual(mock_args[0], 'POST') + self.assertEqual(mock_args[1], REQUEST_URL) + self.assertEqual(mock_kwargs['data'], '[1, 2, 3]') + self.assertEqual(mock_kwargs['headers']['X-Auth-Token'], 'token') + self.assertEqual(mock_kwargs['cert'], ('cert.pem', 'key.pem')) + self.assertEqual(mock_kwargs['verify'], 'ca.pem') diff --git a/test_keyring.py b/test_keyring.py new file mode 100644 index 0000000..a44d432 --- /dev/null +++ b/test_keyring.py @@ -0,0 +1,188 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 datetime + +import mock + +from keystoneclient import access +from keystoneclient import httpclient +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests import utils +from keystoneclient.tests.v2_0 import client_fixtures + +try: + import keyring # noqa + import pickle # noqa +except ImportError: + keyring = None + +PROJECT_SCOPED_TOKEN = client_fixtures.PROJECT_SCOPED_TOKEN + +# These mirror values from PROJECT_SCOPED_TOKEN +USERNAME = 'exampleuser' +AUTH_URL = 'http://public.com:5000/v2.0' +TOKEN = '04c7d5ffaeef485f9dc69c06db285bdb' + +PASSWORD = 'password' +TENANT = 'tenant' +TENANT_ID = 'tenant_id' + + +class KeyringTest(utils.TestCase): + + def setUp(self): + if keyring is None: + self.skipTest( + 'optional package keyring or pickle is not installed') + + class MemoryKeyring(keyring.backend.KeyringBackend): + """A Simple testing keyring. + + This class supports stubbing an initial password to be returned by + setting password, and allows easy password and key retrieval. Also + records if a password was retrieved. + """ + def __init__(self): + self.key = None + self.password = None + self.fetched = False + self.get_password_called = False + self.set_password_called = False + + def supported(self): + return 1 + + def get_password(self, service, username): + self.get_password_called = True + key = username + '@' + service + # make sure we don't get passwords crossed if one is enforced. + if self.key and self.key != key: + return None + if self.password: + self.fetched = True + return self.password + + def set_password(self, service, username, password): + self.set_password_called = True + self.key = username + '@' + service + self.password = password + + super(KeyringTest, self).setUp() + self.memory_keyring = MemoryKeyring() + keyring.set_keyring(self.memory_keyring) + + def test_no_keyring_key(self): + """Ensure that if we don't have use_keyring set in the client that + the keyring is never accessed. + """ + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL) + + # stub and check that a new token is received + with mock.patch.object(cl, 'get_raw_token_from_identity_service') \ + as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + meth.assert_called_once() + + # make sure that we never touched the keyring + self.assertFalse(self.memory_keyring.get_password_called) + self.assertFalse(self.memory_keyring.set_password_called) + + def test_build_keyring_key(self): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL) + + keyring_key = cl._build_keyring_key(auth_url=AUTH_URL, + username=USERNAME, + tenant_name=TENANT, + tenant_id=TENANT_ID, + token=TOKEN) + + self.assertEqual(keyring_key, + '%s/%s/%s/%s/%s' % + (AUTH_URL, TENANT_ID, TENANT, TOKEN, USERNAME)) + + def test_set_and_get_keyring_expired(self): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # set an expired token into the keyring + auth_ref = access.AccessInfo.factory(body=PROJECT_SCOPED_TOKEN) + expired = timeutils.utcnow() - datetime.timedelta(minutes=30) + auth_ref['token']['expires'] = timeutils.isotime(expired) + self.memory_keyring.password = pickle.dumps(auth_ref) + + # stub and check that a new token is received, so not using expired + with mock.patch.object(cl, 'get_raw_token_from_identity_service') \ + as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + meth.assert_called_once() + + # check that a value was returned from the keyring + self.assertTrue(self.memory_keyring.fetched) + + # check that the new token has been loaded into the keyring + new_auth_ref = pickle.loads(self.memory_keyring.password) + self.assertEqual(new_auth_ref['token']['expires'], + PROJECT_SCOPED_TOKEN['access']['token']['expires']) + + def test_get_keyring(self): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # set an token into the keyring + auth_ref = access.AccessInfo.factory(body=PROJECT_SCOPED_TOKEN) + future = timeutils.utcnow() + datetime.timedelta(minutes=30) + auth_ref['token']['expires'] = timeutils.isotime(future) + self.memory_keyring.password = pickle.dumps(auth_ref) + + # don't stub get_raw_token so will fail if authenticate happens + + self.assertTrue(cl.authenticate()) + self.assertTrue(self.memory_keyring.fetched) + + def test_set_keyring(self): + cl = httpclient.HTTPClient(username=USERNAME, password=PASSWORD, + tenant_id=TENANT_ID, auth_url=AUTH_URL, + use_keyring=True) + + # stub and check that a new token is received + with mock.patch.object(cl, 'get_raw_token_from_identity_service') \ + as meth: + meth.return_value = (True, PROJECT_SCOPED_TOKEN) + + self.assertTrue(cl.authenticate()) + + meth.assert_called_once() + + # we checked the keyring, but we didn't find anything + self.assertTrue(self.memory_keyring.get_password_called) + self.assertFalse(self.memory_keyring.fetched) + + # check that the new token has been loaded into the keyring + self.assertTrue(self.memory_keyring.set_password_called) + new_auth_ref = pickle.loads(self.memory_keyring.password) + self.assertEqual(new_auth_ref.auth_token, TOKEN) + self.assertEqual(new_auth_ref['token'], + PROJECT_SCOPED_TOKEN['access']['token']) + self.assertEqual(new_auth_ref.username, USERNAME) diff --git a/test_memcache_crypt.py b/test_memcache_crypt.py new file mode 100644 index 0000000..500a509 --- /dev/null +++ b/test_memcache_crypt.py @@ -0,0 +1,88 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 keystoneclient.middleware import memcache_crypt + + +class MemcacheCryptPositiveTests(testtools.TestCase): + def _setup_keys(self, strategy): + return memcache_crypt.derive_keys('token', 'secret', strategy) + + def test_constant_time_compare(self): + # make sure it works as a compare, the "constant time" aspect + # isn't appropriate to test in unittests + ctc = memcache_crypt.constant_time_compare + self.assertTrue(ctc('abcd', 'abcd')) + self.assertTrue(ctc('', '')) + self.assertFalse(ctc('abcd', 'efgh')) + self.assertFalse(ctc('abc', 'abcd')) + self.assertFalse(ctc('abc', 'abc\x00')) + self.assertFalse(ctc('', 'abc')) + + def test_derive_keys(self): + keys = memcache_crypt.derive_keys('token', 'secret', 'strategy') + self.assertEqual(len(keys['ENCRYPTION']), + len(keys['CACHE_KEY'])) + self.assertEqual(len(keys['CACHE_KEY']), + len(keys['MAC'])) + self.assertNotEqual(keys['ENCRYPTION'], + keys['MAC']) + self.assertIn('strategy', keys.keys()) + + def test_key_strategy_diff(self): + k1 = self._setup_keys('MAC') + k2 = self._setup_keys('ENCRYPT') + self.assertNotEqual(k1, k2) + + def test_sign_data(self): + keys = self._setup_keys('MAC') + sig = memcache_crypt.sign_data(keys['MAC'], 'data') + self.assertEqual(len(sig), memcache_crypt.DIGEST_LENGTH_B64) + + def test_encryption(self): + keys = self._setup_keys('ENCRYPT') + # what you put in is what you get out + for data in ['data', '1234567890123456', '\x00\xFF' * 13 + ] + [chr(x % 256) * x for x in range(768)]: + crypt = memcache_crypt.encrypt_data(keys['ENCRYPTION'], data) + decrypt = memcache_crypt.decrypt_data(keys['ENCRYPTION'], crypt) + self.assertEqual(data, decrypt) + self.assertRaises(memcache_crypt.DecryptError, + memcache_crypt.decrypt_data, + keys['ENCRYPTION'], crypt[:-1]) + + def test_protect_wrappers(self): + data = 'My Pretty Little Data' + for strategy in ['MAC', 'ENCRYPT']: + keys = self._setup_keys(strategy) + protected = memcache_crypt.protect_data(keys, data) + self.assertNotEqual(protected, data) + if strategy == 'ENCRYPT': + self.assertNotIn(data, protected) + unprotected = memcache_crypt.unprotect_data(keys, protected) + self.assertEqual(data, unprotected) + self.assertRaises(memcache_crypt.InvalidMacError, + memcache_crypt.unprotect_data, + keys, protected[:-1]) + self.assertIsNone(memcache_crypt.unprotect_data(keys, None)) + + def test_no_pycrypt(self): + aes = memcache_crypt.AES + memcache_crypt.AES = None + self.assertRaises(memcache_crypt.CryptoUnavailableError, + memcache_crypt.encrypt_data, 'token', 'secret', + 'data') + memcache_crypt.AES = aes diff --git a/test_shell.py b/test_shell.py new file mode 100644 index 0000000..8c9c06f --- /dev/null +++ b/test_shell.py @@ -0,0 +1,497 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 cStringIO +import json +import os +import sys +import uuid + +import fixtures +import mock +import testtools +from testtools import matchers + +from keystoneclient import exceptions +from keystoneclient import shell as openstack_shell +from keystoneclient.tests import utils +from keystoneclient.v2_0 import shell as shell_v2_0 + + +DEFAULT_USERNAME = 'username' +DEFAULT_PASSWORD = 'password' +DEFAULT_TENANT_ID = 'tenant_id' +DEFAULT_TENANT_NAME = 'tenant_name' +DEFAULT_AUTH_URL = 'http://127.0.0.1:5000/v2.0/' + + +class NoExitArgumentParser(argparse.ArgumentParser): + def error(self, message): + raise exceptions.CommandError(message) + + +class ShellTest(utils.TestCase): + + FAKE_ENV = { + 'OS_USERNAME': DEFAULT_USERNAME, + 'OS_PASSWORD': DEFAULT_PASSWORD, + 'OS_TENANT_ID': DEFAULT_TENANT_ID, + 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, + 'OS_AUTH_URL': DEFAULT_AUTH_URL, + } + + def _tolerant_shell(self, cmd): + t_shell = openstack_shell.OpenStackIdentityShell(NoExitArgumentParser) + t_shell.main(cmd.split()) + + # Patch os.environ to avoid required auth info. + def setUp(self): + + super(ShellTest, self).setUp() + for var in self.FAKE_ENV: + self.useFixture(fixtures.EnvironmentVariable(var, + self.FAKE_ENV[var])) + + # Make a fake shell object, a helping wrapper to call it, and a quick + # way of asserting that certain API calls were made. + global shell, _shell, assert_called, assert_called_anytime + _shell = openstack_shell.OpenStackIdentityShell() + shell = lambda cmd: _shell.main(cmd.split()) + + def test_help_unknown_command(self): + self.assertRaises(exceptions.CommandError, shell, 'help %s' + % uuid.uuid4().hex) + + def shell(self, argstr): + orig = sys.stdout + clean_env = {} + _old_env, os.environ = os.environ, clean_env.copy() + try: + sys.stdout = cStringIO.StringIO() + _shell = openstack_shell.OpenStackIdentityShell() + _shell.main(argstr.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertEqual(exc_value.code, 0) + finally: + out = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + os.environ = _old_env + return out + + def test_help_no_args(self): + do_tenant_mock = mock.MagicMock() + with mock.patch('keystoneclient.shell.OpenStackIdentityShell.do_help', + do_tenant_mock): + self.shell('') + assert do_tenant_mock.called + + def test_help(self): + required = 'usage:' + help_text = self.shell('help') + self.assertThat(help_text, + matchers.MatchesRegex(required)) + + def test_help_command(self): + required = 'usage: keystone user-create' + help_text = self.shell('help user-create') + self.assertThat(help_text, + matchers.MatchesRegex(required)) + + def test_auth_no_credentials(self): + with testtools.ExpectedException( + exceptions.CommandError, 'Expecting'): + self.shell('user-list') + + def test_auth_password_authurl_no_username(self): + with testtools.ExpectedException( + exceptions.CommandError, + 'Expecting a username provided via either'): + self.shell('--os-password=%s --os-auth-url=%s user-list' + % (uuid.uuid4().hex, uuid.uuid4().hex)) + + def test_auth_username_password_no_authurl(self): + with testtools.ExpectedException( + exceptions.CommandError, 'Expecting an auth URL via either'): + self.shell('--os-password=%s --os-username=%s user-list' + % (uuid.uuid4().hex, uuid.uuid4().hex)) + + def test_token_no_endpoint(self): + with testtools.ExpectedException( + exceptions.CommandError, 'Expecting an endpoint provided'): + self.shell('--token=%s user-list' % uuid.uuid4().hex) + + def test_endpoint_no_token(self): + with testtools.ExpectedException( + exceptions.CommandError, 'Expecting a token provided'): + self.shell('--endpoint=http://10.0.0.1:5000/v2.0/ user-list') + + def test_shell_args(self): + do_tenant_mock = mock.MagicMock() + with mock.patch('keystoneclient.v2_0.shell.do_user_list', + do_tenant_mock): + shell('user-list') + assert do_tenant_mock.called + ((a, b), c) = do_tenant_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Old_style options + shell('--os_auth_url http://0.0.0.0:5000/ --os_password xyzpdq ' + '--os_tenant_id 1234 --os_tenant_name fred ' + '--os_username barney ' + '--os_identity_api_version 2.0 user-list') + assert do_tenant_mock.called + ((a, b), c) = do_tenant_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = ('http://0.0.0.0:5000/', 'xyzpdq', '1234', + 'fred', 'barney', '2.0') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + shell('--os-auth-url http://1.1.1.1:5000/ --os-password xyzpdq ' + '--os-tenant-id 4321 --os-tenant-name wilma ' + '--os-username betty ' + '--os-identity-api-version 2.0 user-list') + assert do_tenant_mock.called + ((a, b), c) = do_tenant_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = ('http://1.1.1.1:5000/', 'xyzpdq', '4321', + 'wilma', 'betty', '2.0') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Test keyring options + shell('--os-auth-url http://1.1.1.1:5000/ --os-password xyzpdq ' + '--os-tenant-id 4321 --os-tenant-name wilma ' + '--os-username betty ' + '--os-identity-api-version 2.0 ' + '--os-cache ' + '--stale-duration 500 ' + '--force-new-token user-list') + assert do_tenant_mock.called + ((a, b), c) = do_tenant_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version, b.os_cache, + b.stale_duration, b.force_new_token) + expect = ('http://1.1.1.1:5000/', 'xyzpdq', '4321', + 'wilma', 'betty', '2.0', True, '500', True) + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + def test_shell_user_create_args(self): + """Test user-create args.""" + do_uc_mock = mock.MagicMock() + # grab the decorators for do_user_create + uc_func = getattr(shell_v2_0, 'do_user_create') + do_uc_mock.arguments = getattr(uc_func, 'arguments', []) + with mock.patch('keystoneclient.v2_0.shell.do_user_create', + do_uc_mock): + + # Old_style options + # Test case with one --tenant_id args present: ec2 creds + shell('user-create --name=FOO ' + '--pass=secrete --tenant_id=barrr --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.name, b.passwd, b.enabled) + expect = ('barrr', 'FOO', 'secrete', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with one --tenant args present: ec2 creds + shell('user-create --name=foo ' + '--pass=secrete --tenant=BARRR --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant, b.name, b.passwd, b.enabled) + expect = ('BARRR', 'foo', 'secrete', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with one --tenant-id args present: ec2 creds + shell('user-create --name=foo ' + '--pass=secrete --tenant-id=BARRR --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant, b.name, b.passwd, b.enabled) + expect = ('BARRR', 'foo', 'secrete', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Old_style options + # Test case with --os_tenant_id and --tenant_id args present + shell('--os_tenant_id=os-tenant user-create --name=FOO ' + '--pass=secrete --tenant_id=barrr --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'os-tenant', + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.name, b.passwd, b.enabled) + expect = ('barrr', 'FOO', 'secrete', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with --os-tenant-id and --tenant-id args present + shell('--os-tenant-id=ostenant user-create --name=foo ' + '--pass=secrete --tenant-id=BARRR --enabled=true') + assert do_uc_mock.called + ((a, b), c) = do_uc_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'ostenant', + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant, b.name, b.passwd, b.enabled) + expect = ('BARRR', 'foo', 'secrete', 'true') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + def test_do_tenant_create(self): + do_tenant_mock = mock.MagicMock() + with mock.patch('keystoneclient.v2_0.shell.do_tenant_create', + do_tenant_mock): + shell('tenant-create') + assert do_tenant_mock.called + # FIXME(dtroyer): how do you test the decorators? + #shell('tenant-create --tenant-name wilma ' + # '--description "fred\'s wife"') + #assert do_tenant_mock.called + + def test_do_tenant_list(self): + do_tenant_mock = mock.MagicMock() + with mock.patch('keystoneclient.v2_0.shell.do_tenant_list', + do_tenant_mock): + shell('tenant-list') + assert do_tenant_mock.called + + def test_shell_tenant_id_args(self): + """Test a corner case where --tenant_id appears on the + command-line twice. + """ + do_ec2_mock = mock.MagicMock() + # grab the decorators for do_ec2_create_credentials + ec2_func = getattr(shell_v2_0, 'do_ec2_credentials_create') + do_ec2_mock.arguments = getattr(ec2_func, 'arguments', []) + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_create', + do_ec2_mock): + + # Old_style options + # Test case with one --tenant_id args present: ec2 creds + shell('ec2-credentials-create ' + '--tenant_id=ec2-tenant --user_id=ec2-user') + assert do_ec2_mock.called + ((a, b), c) = do_ec2_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.user_id) + expect = ('ec2-tenant', 'ec2-user') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with one --tenant-id args present: ec2 creds + shell('ec2-credentials-create ' + '--tenant-id=dash-tenant --user-id=dash-user') + assert do_ec2_mock.called + ((a, b), c) = do_ec2_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.user_id) + expect = ('dash-tenant', 'dash-user') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # Old_style options + # Test case with two --tenant_id args present + shell('--os_tenant_id=os-tenant ec2-credentials-create ' + '--tenant_id=ec2-tenant --user_id=ec2-user') + assert do_ec2_mock.called + ((a, b), c) = do_ec2_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'os-tenant', + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.user_id) + expect = ('ec2-tenant', 'ec2-user') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test case with two --tenant-id args present + shell('--os-tenant-id=ostenant ec2-credentials-create ' + '--tenant-id=dash-tenant --user-id=dash-user') + assert do_ec2_mock.called + ((a, b), c) = do_ec2_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, 'ostenant', + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.tenant_id, b.user_id) + expect = ('dash-tenant', 'dash-user') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + def test_do_ec2_get(self): + do_shell_mock = mock.MagicMock() + + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_create', + do_shell_mock): + shell('ec2-credentials-create') + assert do_shell_mock.called + + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_get', + do_shell_mock): + shell('ec2-credentials-get') + assert do_shell_mock.called + + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_list', + do_shell_mock): + shell('ec2-credentials-list') + assert do_shell_mock.called + + with mock.patch('keystoneclient.v2_0.shell.do_ec2_credentials_delete', + do_shell_mock): + shell('ec2-credentials-delete') + assert do_shell_mock.called + + def test_timeout_parse_invalid_type(self): + for f in ['foobar', 'xyz']: + cmd = '--timeout %s endpoint-create' % (f) + self.assertRaises(exceptions.CommandError, + self._tolerant_shell, cmd) + + def test_timeout_parse_invalid_number(self): + for f in [-1, 0]: + cmd = '--timeout %s endpoint-create' % (f) + self.assertRaises(exceptions.CommandError, + self._tolerant_shell, cmd) + + def test_do_timeout(self): + response_mock = mock.MagicMock() + response_mock.status_code = 200 + response_mock.text = json.dumps({ + 'endpoints': [], + }) + request_mock = mock.MagicMock(return_value=response_mock) + with mock.patch('requests.request', request_mock): + shell(('--timeout 2 --os-token=blah --os-endpoint=blah' + ' --os-auth-url=blah.com endpoint-list')) + request_mock.assert_called_with(mock.ANY, mock.ANY, + timeout=2, + headers=mock.ANY, + verify=mock.ANY) + + def test_do_endpoints(self): + do_shell_mock = mock.MagicMock() + # grab the decorators for do_endpoint_create + shell_func = getattr(shell_v2_0, 'do_endpoint_create') + do_shell_mock.arguments = getattr(shell_func, 'arguments', []) + with mock.patch('keystoneclient.v2_0.shell.do_endpoint_create', + do_shell_mock): + + # Old_style options + # Test create args + shell('endpoint-create ' + '--service_id=2 --publicurl=http://example.com:1234/go ' + '--adminurl=http://example.com:9876/adm') + assert do_shell_mock.called + ((a, b), c) = do_shell_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.service, b.publicurl, b.adminurl) + expect = ('2', + 'http://example.com:1234/go', + 'http://example.com:9876/adm') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test create args + shell('endpoint-create ' + '--service-id=3 --publicurl=http://example.com:4321/go ' + '--adminurl=http://example.com:9876/adm') + assert do_shell_mock.called + ((a, b), c) = do_shell_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.service, b.publicurl, b.adminurl) + expect = ('3', + 'http://example.com:4321/go', + 'http://example.com:9876/adm') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + + # New-style options + # Test create args + shell('endpoint-create ' + '--service=3 --publicurl=http://example.com:4321/go ' + '--adminurl=http://example.com:9876/adm') + assert do_shell_mock.called + ((a, b), c) = do_shell_mock.call_args + actual = (b.os_auth_url, b.os_password, b.os_tenant_id, + b.os_tenant_name, b.os_username, + b.os_identity_api_version) + expect = (DEFAULT_AUTH_URL, DEFAULT_PASSWORD, DEFAULT_TENANT_ID, + DEFAULT_TENANT_NAME, DEFAULT_USERNAME, '') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) + actual = (b.service, b.publicurl, b.adminurl) + expect = ('3', + 'http://example.com:4321/go', + 'http://example.com:9876/adm') + self.assertTrue(all([x == y for x, y in zip(actual, expect)])) diff --git a/test_utils.py b/test_utils.py new file mode 100644 index 0000000..65c6a1a --- /dev/null +++ b/test_utils.py @@ -0,0 +1,93 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 keystoneclient import exceptions +from keystoneclient.tests import utils as test_utils +from keystoneclient import utils + + +class FakeResource(object): + pass + + +class FakeManager(object): + + resource_class = FakeResource + + resources = { + '1234': {'name': 'entity_one'}, + '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0': {'name': 'entity_two'}, + '\xe3\x82\xbdtest': {'name': u'\u30bdtest'}, + '5678': {'name': '9876'} + } + + def get(self, resource_id): + try: + return self.resources[str(resource_id)] + except KeyError: + raise exceptions.NotFound(resource_id) + + def find(self, name=None): + if name == '9999': + # NOTE(morganfainberg): special case that raises NoUniqueMatch. + raise exceptions.NoUniqueMatch() + for resource_id, resource in self.resources.items(): + if resource['name'] == str(name): + return resource + raise exceptions.NotFound(name) + + +class FindResourceTestCase(test_utils.TestCase): + + def setUp(self): + super(FindResourceTestCase, self).setUp() + self.manager = FakeManager() + + def test_find_none(self): + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + 'asdf') + + def test_find_by_integer_id(self): + output = utils.find_resource(self.manager, 1234) + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_str_id(self): + output = utils.find_resource(self.manager, '1234') + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_uuid(self): + uuid = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' + output = utils.find_resource(self.manager, uuid) + self.assertEqual(output, self.manager.resources[uuid]) + + def test_find_by_unicode(self): + name = '\xe3\x82\xbdtest' + output = utils.find_resource(self.manager, name) + self.assertEqual(output, self.manager.resources[name]) + + def test_find_by_str_name(self): + output = utils.find_resource(self.manager, 'entity_one') + self.assertEqual(output, self.manager.resources['1234']) + + def test_find_by_int_name(self): + output = utils.find_resource(self.manager, 9876) + self.assertEqual(output, self.manager.resources['5678']) + + def test_find_no_unique_match(self): + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + 9999) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..24e8bed --- /dev/null +++ b/utils.py @@ -0,0 +1,117 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 time + +import httpretty +import mock +from mox3 import mox +import requests +import testtools + +from keystoneclient.openstack.common import jsonutils + + +class TestCase(testtools.TestCase): + TEST_DOMAIN_ID = '1' + TEST_DOMAIN_NAME = 'aDomain' + TEST_TENANT_ID = '1' + TEST_TENANT_NAME = 'aTenant' + TEST_TOKEN = 'aToken' + TEST_USER = 'test' + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + + def setUp(self): + super(TestCase, self).setUp() + self.mox = mox.Mox() + self.time_patcher = mock.patch.object(time, 'time', lambda: 1234) + self.time_patcher.start() + + def tearDown(self): + self.time_patcher.stop() + self.mox.UnsetStubs() + self.mox.VerifyAll() + super(TestCase, self).tearDown() + + def stub_url(self, method, parts=None, base_url=None, json=None, **kwargs): + if not base_url: + base_url = self.TEST_URL + + if json: + kwargs['body'] = jsonutils.dumps(json) + kwargs['content_type'] = 'application/json' + + if parts: + url = '/'.join([p.strip('/') for p in [base_url] + parts]) + else: + url = base_url + + httpretty.register_uri(method, url, **kwargs) + + def assertRequestBodyIs(self, body=None, json=None): + if json: + val = jsonutils.loads(httpretty.last_request().body) + self.assertEqual(json, val) + elif body: + self.assertEqual(body, httpretty.last_request().body) + + def assertQueryStringIs(self, val): + self.assertEqual(httpretty.last_request().querystring, val) + + +if tuple(sys.version_info)[0:2] < (2, 7): + + def assertDictEqual(self, d1, d2, msg=None): + # Simple version taken from 2.7 + self.assertIsInstance(d1, dict, + 'First argument is not a dictionary') + self.assertIsInstance(d2, dict, + 'Second argument is not a dictionary') + if d1 != d2: + if msg: + self.fail(msg) + else: + standardMsg = '%r != %r' % (d1, d2) + self.fail(standardMsg) + + TestCase.assertDictEqual = assertDictEqual + + +class TestResponse(requests.Response): + """Class used to wrap requests.Response and provide some + convenience to initialize with a dict. + """ + + def __init__(self, data): + self._text = None + super(TestResponse, self).__init__() + if isinstance(data, dict): + self.status_code = data.get('status_code', None) + headers = data.get('headers') + if headers: + self.headers.update(headers) + # Fake the text attribute to streamline Response creation + # _content is defined by requests.Response + self._content = data.get('text', None) + else: + self.status_code = data + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @property + def text(self): + return self.content diff --git a/v2_0/__init__.py b/v2_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v2_0/client_fixtures.py b/v2_0/client_fixtures.py new file mode 100644 index 0000000..5735fd4 --- /dev/null +++ b/v2_0/client_fixtures.py @@ -0,0 +1,180 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 __future__ import unicode_literals + + +UNSCOPED_TOKEN = { + 'access': {'serviceCatalog': {}, + 'token': {'expires': '2012-10-03T16:58:01Z', + 'id': '3e2813b7ba0b4006840c3825860b86ed'}, + 'user': {'id': 'c4da488862bd435c9e6c0275a0d0e49a', + 'name': 'exampleuser', + 'roles': [], + 'roles_links': [], + 'username': 'exampleuser'} + } +} + +PROJECT_SCOPED_TOKEN = { + 'access': { + 'serviceCatalog': [{ + 'endpoints': [{ + 'adminURL': 'http://admin:8776/v1/225da22d3ce34b15877ea70b2a575f58', + 'internalURL': + 'http://internal:8776/v1/225da22d3ce34b15877ea70b2a575f58', + 'publicURL': + 'http://public.com:8776/v1/225da22d3ce34b15877ea70b2a575f58', + 'region': 'RegionOne' + }], + 'endpoints_links': [], + 'name': 'Volume Service', + 'type': 'volume'}, + {'endpoints': [{ + 'adminURL': 'http://admin:9292/v1', + 'internalURL': 'http://internal:9292/v1', + 'publicURL': 'http://public.com:9292/v1', + 'region': 'RegionOne'}], + 'endpoints_links': [], + 'name': 'Image Service', + 'type': 'image'}, + {'endpoints': [{ +'adminURL': 'http://admin:8774/v2/225da22d3ce34b15877ea70b2a575f58', +'internalURL': 'http://internal:8774/v2/225da22d3ce34b15877ea70b2a575f58', +'publicURL': 'http://public.com:8774/v2/225da22d3ce34b15877ea70b2a575f58', +'region': 'RegionOne'}], + 'endpoints_links': [], + 'name': 'Compute Service', + 'type': 'compute'}, + {'endpoints': [{ +'adminURL': 'http://admin:8773/services/Admin', +'internalURL': 'http://internal:8773/services/Cloud', +'publicURL': 'http://public.com:8773/services/Cloud', +'region': 'RegionOne'}], + 'endpoints_links': [], + 'name': 'EC2 Service', + 'type': 'ec2'}, + {'endpoints': [{ +'adminURL': 'http://admin:35357/v2.0', +'internalURL': 'http://internal:5000/v2.0', +'publicURL': 'http://public.com:5000/v2.0', +'region': 'RegionOne'}], + 'endpoints_links': [], + 'name': 'Identity Service', + 'type': 'identity'}], + 'token': {'expires': '2012-10-03T16:53:36Z', + 'id': '04c7d5ffaeef485f9dc69c06db285bdb', + 'tenant': {'description': '', + 'enabled': True, + 'id': '225da22d3ce34b15877ea70b2a575f58', + 'name': 'exampleproject'}}, + 'user': {'id': 'c4da488862bd435c9e6c0275a0d0e49a', + 'name': 'exampleuser', + 'roles': [{'id': 'edc12489faa74ee0aca0b8a0b4d74a74', + 'name': 'Member'}], + 'roles_links': [], + 'username': 'exampleuser'} + } +} + +AUTH_RESPONSE_BODY = { + 'access': { + 'token': { + 'id': 'ab48a9efdfedb23ty3494', + 'expires': '2010-11-01T03:32:15-05:00', + 'tenant': { + 'id': '345', + 'name': 'My Project' + } + }, + 'user': { + 'id': '123', + 'name': 'jqsmith', + 'roles': [{ + 'id': '234', + 'name': 'compute:admin' + }, { + 'id': '235', + 'name': 'object-store:admin', + 'tenantId': '1' + }], + 'roles_links': [] + }, + 'serviceCatalog': [{ + 'name': 'Cloud Servers', + 'type': 'compute', + 'endpoints': [{ + 'tenantId': '1', + 'publicURL': 'https://compute.north.host/v1/1234', + 'internalURL': 'https://compute.north.host/v1/1234', + 'region': 'North', + 'versionId': '1.0', + 'versionInfo': 'https://compute.north.host/v1.0/', + 'versionList': 'https://compute.north.host/' + }, { + 'tenantId': '2', + 'publicURL': 'https://compute.north.host/v1.1/3456', + 'internalURL': 'https://compute.north.host/v1.1/3456', + 'region': 'North', + 'versionId': '1.1', + 'versionInfo': 'https://compute.north.host/v1.1/', + 'versionList': 'https://compute.north.host/' + }], + 'endpoints_links': [] + }, { + 'name': 'Cloud Files', + 'type': 'object-store', + 'endpoints': [{ + 'tenantId': '11', + 'publicURL': 'https://swift.north.host/v1/blah', + 'internalURL': 'https://swift.north.host/v1/blah', + 'region': 'South', + 'versionId': '1.0', + 'versionInfo': 'uri', + 'versionList': 'uri' + }, { + 'tenantId': '2', + 'publicURL': 'https://swift.north.host/v1.1/blah', + 'internalURL': 'https://compute.north.host/v1.1/blah', + 'region': 'South', + 'versionId': '1.1', + 'versionInfo': 'https://swift.north.host/v1.1/', + 'versionList': 'https://swift.north.host/' + }], + 'endpoints_links': [{ + 'rel': 'next', + 'href': 'https://identity.north.host/v2.0/' + 'endpoints?marker=2' + }] + }, { + 'name': 'Image Servers', + 'type': 'image', + 'endpoints': [{ + 'publicURL': 'https://image.north.host/v1/', + 'internalURL': 'https://image-internal.north.host/v1/', + 'region': 'North' + }, { + 'publicURL': 'https://image.south.host/v1/', + 'internalURL': 'https://image-internal.south.host/v1/', + 'region': 'South' + }], + 'endpoints_links': [] + }], + 'serviceCatalog_links': [{ + 'rel': 'next', + 'href': ('https://identity.host/v2.0/endpoints?' + 'session=2hfh8Ar&marker=2') + }] + } +} diff --git a/v2_0/fakes.py b/v2_0/fakes.py new file mode 100644 index 0000000..943efe0 --- /dev/null +++ b/v2_0/fakes.py @@ -0,0 +1,494 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 OpenStack, LLC +# +# 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 urlparse + +from keystoneclient.tests import fakes +from keystoneclient.tests.v2_0 import utils + + +class FakeHTTPClient(fakes.FakeClient): + def __init__(self, **kwargs): + self.username = 'username' + self.password = 'password' + self.auth_url = 'auth_url' + self.callstack = [] + + def _cs_request(self, url, method, **kwargs): + # Check that certain things are called correctly + if method in ['GET', 'DELETE']: + assert 'body' not in kwargs + elif method == 'PUT': + kwargs.setdefault('body', None) + + # Call the method + args = urlparse.parse_qsl(urlparse.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') + munged_url = munged_url.replace('-', '_') + + callback = "%s_%s" % (method.lower(), munged_url) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + # Note the call + self.callstack.append((method, url, kwargs.get('body', None))) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + # Note the call + self.callstack.append((method, url, kwargs.get('body', None))) + + status, body = getattr(self, callback)(**kwargs) + r = utils.TestResponse({ + "status_code": status, + "text": body}) + return r, body + + # + # List all extensions + # + def post_tokens(self, **kw): + body = [ + {"access": + {"token": + {"expires": "2012-02-05T00:00:00", + "id": "887665443383838", + "tenant": + {"id": "1", + "name": "customer-x"}}, + "serviceCatalog": [ + {"endpoints": [ + {"adminURL": "http://swift.admin-nets.local:8080/", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8080/v1/AUTH_1", + "publicURL": + "http://swift.publicinternets.com/v1/AUTH_1"}], + "type": "object-store", + "name": "swift"}, + {"endpoints": [ + {"adminURL": "http://cdn.admin-nets.local/v1.1/1", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:7777/v1.1/1", + "publicURL": + "http://cdn.publicinternets.com/v1.1/1"}], + "type": "object-store", + "name": "cdn"}], + "user": + {"id": "1", + "roles": [ + {"tenantId": "1", + "id": "3", + "name": "Member"}], + "name": "joeuser"}} + } + ] + return (200, body) + + def get_tokens_887665443383838(self, **kw): + body = [ + {"access": + {"token": + {"expires": "2012-02-05T00:00:00", + "id": "887665443383838", + "tenant": {"id": "1", + "name": "customer-x"}}, + "user": + {"name": "joeuser", + "tenantName": "customer-x", + "id": "1", + "roles": [ + {"serviceId": "1", + "id": "3", + "name": "Member"}], + "tenantId": "1"}} + } + ] + return (200, body) + + def get_tokens_887665443383838_endpoints(self, **kw): + body = [ + {"endpoints_links": [ + {"href": + "http://127.0.0.1:35357/tokens/887665443383838" + "/endpoints?'marker=5&limit=10'", + "rel": "next"}], + "endpoints": [ + {"internalURL": "http://127.0.0.1:8080/v1/AUTH_1", + "name": "swift", + "adminURL": "http://swift.admin-nets.local:8080/", + "region": "RegionOne", + "tenantId": 1, + "type": "object-store", + "id": 1, + "publicURL": "http://swift.publicinternets.com/v1/AUTH_1"}, + {"internalURL": "http://localhost:8774/v1.0", + "name": "nova_compat", + "adminURL": "http://127.0.0.1:8774/v1.0", + "region": "RegionOne", + "tenantId": 1, + "type": "compute", + "id": 2, + "publicURL": "http://nova.publicinternets.com/v1.0/"}, + {"internalURL": "http://localhost:8774/v1.1", + "name": "nova", + "adminURL": "http://127.0.0.1:8774/v1.1", + "region": "RegionOne", + "tenantId": 1, + "type": "compute", + "id": 3, + "publicURL": "http://nova.publicinternets.com/v1.1/"}, + {"internalURL": "http://127.0.0.1:9292/v1.1/", + "name": "glance", + "adminURL": "http://nova.admin-nets.local/v1.1/", + "region": "RegionOne", + "tenantId": 1, + "type": "image", + "id": 4, + "publicURL": "http://glance.publicinternets.com/v1.1/"}, + {"internalURL": "http://127.0.0.1:7777/v1.1/1", + "name": "cdn", + "adminURL": "http://cdn.admin-nets.local/v1.1/1", + "region": "RegionOne", + "tenantId": 1, + "versionId": "1.1", + "versionList": "http://127.0.0.1:7777/", + "versionInfo": "http://127.0.0.1:7777/v1.1", + "type": "object-store", + "id": 5, + "publicURL": "http://cdn.publicinternets.com/v1.1/1"}] + } + ] + return (200, body) + + def get(self, **kw): + body = { + "version": { + "id": "v2.0", + "status": "beta", + "updated": "2011-11-19T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:35357/v2.0/"}, + {"rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/" + "api/openstack-identity-service/2.0/content/"}, + {"rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/identity-dev-guide-2.0.pdf"}, + {"rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://127.0.0.1:35357/v2.0/identity-admin.wadl"}], + "media-types": [ + {"base": "application/xml", + "type": "application/vnd.openstack.identity-v2.0+xml"}, + {"base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json"}] + } + } + return (200, body) + + def get_extensions(self, **kw): + body = { + "extensions": {"values": []} + } + return (200, body) + + def post_tenants(self, **kw): + body = {"tenant": + {"enabled": True, + "description": None, + "name": "new-tenant", + "id": "1"}} + return (200, body) + + def post_tenants_2(self, **kw): + body = {"tenant": + {"enabled": False, + "description": "desc", + "name": "new-tenant1", + "id": "2"}} + return (200, body) + + def get_tenants(self, **kw): + body = { + "tenants_links": [], + "tenants": [ + {"enabled": False, + "description": None, + "name": "project-y", + "id": "1"}, + {"enabled": True, + "description": None, + "name": "new-tenant", + "id": "2"}, + {"enabled": True, + "description": None, + "name": "customer-x", + "id": "1"}] + } + return (200, body) + + def get_tenants_1(self, **kw): + body = {"tenant": + {"enabled": True, + "description": None, + "name": "new-tenant", + "id": "1"}} + return (200, body) + + def get_tenants_2(self, **kw): + body = {"tenant": + {"enabled": True, + "description": None, + "name": "new-tenant", + "id": "2"}} + return (200, body) + + def delete_tenants_2(self, **kw): + body = {} + return (200, body) + + def get_tenants_1_users_1_roles(self, **kw): + body = { + "roles": [ + {"id": "1", + "name": "Admin"}, + {"id": "2", + "name": "Member"}, + {"id": "3", + "name": "new-role"}] + } + return (200, body) + + def put_users_1_roles_OS_KSADM_1(self, **kw): + body = { + "roles": + {"id": "1", + "name": "Admin"}} + return (200, body) + + def delete_users_1_roles_OS_KSADM_1(self, **kw): + body = {} + return (200, body) + + def put_tenants_1_users_1_roles_OS_KSADM_1(self, **kw): + body = { + "role": + {"id": "1", + "name": "Admin"}} + return (200, body) + + def get_users(self, **kw): + body = { + "users": [ + {"name": self.username, + "enabled": "true", + "email": "sdfsdf@sdfsd.sdf", + "id": "1", + "tenantId": "1"}, + {"name": "user2", + "enabled": "true", + "email": "sdfsdf@sdfsd.sdf", + "id": "2", + "tenantId": "1"}] + } + return (200, body) + + def get_users_1(self, **kw): + body = { + "user": { + "tenantId": "1", + "enabled": "true", + "id": "1", + "name": self.username} + } + return (200, body) + + def put_users_1(self, **kw): + body = { + "user": { + "tenantId": "1", + "enabled": "true", + "id": "1", + "name": "new-user1", + "email": "user@email.com"} + } + return (200, body) + + def put_users_1_OS_KSADM_password(self, **kw): + body = { + "user": { + "tenantId": "1", + "enabled": "true", + "id": "1", + "name": "new-user1", + "email": "user@email.com"} + } + return (200, body) + + def post_users(self, **kw): + body = { + "user": { + "tenantId": "1", + "enabled": "true", + "id": "1", + "name": self.username} + } + return (200, body) + + def delete_users_1(self, **kw): + body = [] + return (200, body) + + def get_users_1_roles(self, **kw): + body = [ + {"roles_links": [], + "roles":[ + {"id": "2", + "name": "KeystoneServiceAdmin"}] + } + ] + return (200, body) + + def post_OS_KSADM_roles(self, **kw): + body = {"role": + {"name": "new-role", + "id": "1"}} + return (200, body) + + def get_OS_KSADM_roles(self, **kw): + body = {"roles": [ + {"id": "10", "name": "admin"}, + {"id": "20", "name": "member"}, + {"id": "1", "name": "new-role"}] + } + return (200, body) + + def get_OS_KSADM_roles_1(self, **kw): + body = {"role": + {"name": "new-role", + "id": "1"} + } + return (200, body) + + def delete_OS_KSADM_roles_1(self, **kw): + body = {} + return (200, body) + + def post_OS_KSADM_services(self, **kw): + body = {"OS-KSADM:service": + {"id": "1", + "type": "compute", + "name": "service1", + "description": None} + } + return (200, body) + + def get_OS_KSADM_services_1(self, **kw): + body = {"OS-KSADM:service": + {"description": None, + "type": "compute", + "id": "1", + "name": "service1"} + } + return (200, body) + + def get_OS_KSADM_services(self, **kw): + body = { + "OS-KSADM:services": [ + {"description": None, + "type": "compute", + "id": "1", + "name": "service1"}, + {"description": None, + "type": "identity", + "id": "2", + "name": "service2"}] + } + return (200, body) + + def delete_OS_KSADM_services_1(self, **kw): + body = {} + return (200, body) + + def post_users_1_credentials_OS_EC2(self, **kw): + body = {"credential": + {"access": "1", + "tenant_id": "1", + "secret": "1", + "user_id": "1"} + } + return (200, body) + + def get_users_1_credentials_OS_EC2(self, **kw): + body = {"credentials": [ + {"access": "1", + "tenant_id": "1", + "secret": "1", + "user_id": "1"}] + } + return (200, body) + + def get_users_1_credentials_OS_EC2_2(self, **kw): + body = { + "credential": + {"access": "2", + "tenant_id": "1", + "secret": "1", + "user_id": "1"} + } + return (200, body) + + def delete_users_1_credentials_OS_EC2_2(self, **kw): + body = {} + return (200, body) + + def patch_OS_KSCRUD_users_1(self, **kw): + body = {} + return (200, body) + + def get_endpoints(self, **kw): + body = { + 'endpoints': [ + {'adminURL': 'http://cdn.admin-nets.local/v1.1/1', + 'region': 'RegionOne', + 'internalURL': 'http://127.0.0.1:7777/v1.1/1', + 'publicURL': 'http://cdn.publicinternets.com/v1.1/1'}], + 'type': 'compute', + 'name': 'nova-compute' + } + return (200, body) + + def post_endpoints(self, **kw): + body = { + "endpoint": + {"adminURL": "http://swift.admin-nets.local:8080/", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8080/v1/AUTH_1", + "publicURL": "http://swift.publicinternets.com/v1/AUTH_1"}, + "type": "compute", + "name": "nova-compute" + } + return (200, body) diff --git a/v2_0/test_access.py b/v2_0/test_access.py new file mode 100644 index 0000000..896803e --- /dev/null +++ b/v2_0/test_access.py @@ -0,0 +1,125 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 datetime + +from keystoneclient import access +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests import client_fixtures as token_data +from keystoneclient.tests.v2_0 import client_fixtures +from keystoneclient.tests.v2_0 import utils + +UNSCOPED_TOKEN = client_fixtures.UNSCOPED_TOKEN +PROJECT_SCOPED_TOKEN = client_fixtures.PROJECT_SCOPED_TOKEN +DIABLO_TOKEN = token_data.TOKEN_RESPONSES[token_data.VALID_DIABLO_TOKEN] +GRIZZLY_TOKEN = token_data.TOKEN_RESPONSES[token_data.SIGNED_TOKEN_SCOPED_KEY] + + +class AccessInfoTest(utils.TestCase): + def test_building_unscoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(body=UNSCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('token', auth_ref) + self.assertIn('serviceCatalog', auth_ref) + self.assertFalse(auth_ref['serviceCatalog']) + + self.assertEquals(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEquals(auth_ref.username, 'exampleuser') + self.assertEquals(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEquals(auth_ref.tenant_name, None) + self.assertEquals(auth_ref.tenant_id, None) + + self.assertEquals(auth_ref.auth_url, None) + self.assertEquals(auth_ref.management_url, None) + + self.assertFalse(auth_ref.scoped) + self.assertFalse(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + self.assertFalse(auth_ref.trust_scoped) + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + + self.assertEquals(auth_ref.expires, timeutils.parse_isotime( + UNSCOPED_TOKEN['access']['token']['expires'])) + + def test_will_expire_soon(self): + expires = timeutils.utcnow() + datetime.timedelta(minutes=5) + UNSCOPED_TOKEN['access']['token']['expires'] = expires.isoformat() + auth_ref = access.AccessInfo.factory(body=UNSCOPED_TOKEN) + self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) + self.assertTrue(auth_ref.will_expire_soon(stale_duration=300)) + self.assertFalse(auth_ref.will_expire_soon()) + + def test_building_scoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(body=PROJECT_SCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('token', auth_ref) + self.assertIn('serviceCatalog', auth_ref) + self.assertTrue(auth_ref['serviceCatalog']) + + self.assertEquals(auth_ref.auth_token, + '04c7d5ffaeef485f9dc69c06db285bdb') + self.assertEquals(auth_ref.username, 'exampleuser') + self.assertEquals(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEquals(auth_ref.tenant_name, 'exampleproject') + self.assertEquals(auth_ref.tenant_id, + '225da22d3ce34b15877ea70b2a575f58') + + self.assertEquals(auth_ref.tenant_name, auth_ref.project_name) + self.assertEquals(auth_ref.tenant_id, auth_ref.project_id) + + self.assertEquals(auth_ref.auth_url, + ('http://public.com:5000/v2.0',)) + self.assertEquals(auth_ref.management_url, + ('http://admin:35357/v2.0',)) + + self.assertEqual(auth_ref.project_domain_id, 'default') + self.assertEqual(auth_ref.project_domain_name, 'Default') + self.assertEqual(auth_ref.user_domain_id, 'default') + self.assertEqual(auth_ref.user_domain_name, 'Default') + + self.assertTrue(auth_ref.scoped) + self.assertTrue(auth_ref.project_scoped) + self.assertFalse(auth_ref.domain_scoped) + + def test_diablo_token(self): + auth_ref = access.AccessInfo.factory(body=DIABLO_TOKEN) + + self.assertTrue(auth_ref) + self.assertEquals(auth_ref.username, 'user_name1') + self.assertEquals(auth_ref.project_id, 'tenant_id1') + self.assertEquals(auth_ref.project_name, 'tenant_id1') + self.assertEquals(auth_ref.project_domain_id, 'default') + self.assertEquals(auth_ref.project_domain_name, 'Default') + self.assertEquals(auth_ref.user_domain_id, 'default') + self.assertEquals(auth_ref.user_domain_name, 'Default') + self.assertFalse(auth_ref.scoped) + + def test_grizzly_token(self): + auth_ref = access.AccessInfo.factory(body=GRIZZLY_TOKEN) + + self.assertEquals(auth_ref.project_id, 'tenant_id1') + self.assertEquals(auth_ref.project_name, 'tenant_name1') + self.assertEquals(auth_ref.project_domain_id, 'default') + self.assertEquals(auth_ref.project_domain_name, 'Default') + self.assertEquals(auth_ref.user_domain_id, 'default') + self.assertEquals(auth_ref.user_domain_name, 'Default') diff --git a/v2_0/test_auth.py b/v2_0/test_auth.py new file mode 100644 index 0000000..f25ff9f --- /dev/null +++ b/v2_0/test_auth.py @@ -0,0 +1,190 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 datetime +import json + +import httpretty + +from keystoneclient import exceptions +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import client + + +class AuthenticateAgainstKeystoneTests(utils.TestCase): + def setUp(self): + super(AuthenticateAgainstKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "access": { + "token": { + "expires": "2020-01-01T00:00:10.000123Z", + "id": self.TEST_TOKEN, + "tenant": { + "id": self.TEST_TENANT_ID + }, + }, + "user": { + "id": self.TEST_USER + }, + "serviceCatalog": self.TEST_SERVICE_CATALOG, + }, + } + self.TEST_REQUEST_BODY = { + "auth": { + "passwordCredentials": { + "username": self.TEST_USER, + "password": self.TEST_TOKEN, + }, + "tenantId": self.TEST_TENANT_ID, + }, + } + + @httpretty.activate + def test_authenticate_success_expired(self): + # Build an expired token + self.TEST_RESPONSE_DICT['access']['token']['expires'] = \ + (timeutils.utcnow() - datetime.timedelta(1)).isoformat() + + exp_resp = httpretty.Response(body=json.dumps(self.TEST_RESPONSE_DICT), + content_type='application/json') + + # Build a new response + TEST_TOKEN = "abcdef" + self.TEST_RESPONSE_DICT['access']['token']['expires'] = \ + '2020-01-01T00:00:10.000123Z' + self.TEST_RESPONSE_DICT['access']['token']['id'] = TEST_TOKEN + + new_resp = httpretty.Response(body=json.dumps(self.TEST_RESPONSE_DICT), + content_type='application/json') + + # return expired first, and then the new response + self.stub_auth(responses=[exp_resp, new_resp]) + + cs = client.Client(tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL, + username=self.TEST_USER, + password=self.TEST_TOKEN) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + + self.assertEqual(cs.auth_token, TEST_TOKEN) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_failure(self): + _auth = 'auth' + _cred = 'passwordCredentials' + _pass = 'password' + self.TEST_REQUEST_BODY[_auth][_cred][_pass] = 'bad_key' + error = {"unauthorized": {"message": "Unauthorized", + "code": "401"}} + + self.stub_auth(status=401, json=error) + + # Workaround for issue with assertRaises on python2.6 + # where with assertRaises(exceptions.Unauthorized): doesn't work + # right + def client_create_wrapper(): + client.Client(username=self.TEST_USER, + password="bad_key", + tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertRaises(exceptions.Unauthorized, client_create_wrapper) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_auth_redirect(self): + self.stub_auth(status=305, body='Use Proxy', + location=self.TEST_ADMIN_URL + "/tokens") + + self.stub_auth(base_url=self.TEST_ADMIN_URL, + json=self.TEST_RESPONSE_DICT) + + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_password_unscoped(self): + del self.TEST_RESPONSE_DICT['access']['serviceCatalog'] + del self.TEST_REQUEST_BODY['auth']['tenantId'] + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(username=self.TEST_USER, + password=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertFalse('serviceCatalog' in cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_token_scoped(self): + del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] + self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + tenant_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3] + ['endpoints'][0]["adminURL"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_token_unscoped(self): + del self.TEST_REQUEST_BODY['auth']['passwordCredentials'] + del self.TEST_REQUEST_BODY['auth']['tenantId'] + del self.TEST_RESPONSE_DICT['access']['serviceCatalog'] + self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN} + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_DICT["access"]["token"]["id"]) + self.assertFalse('serviceCatalog' in cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) diff --git a/v2_0/test_client.py b/v2_0/test_client.py new file mode 100644 index 0000000..3cf0ffc --- /dev/null +++ b/v2_0/test_client.py @@ -0,0 +1,106 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.v2_0 import client_fixtures +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import client + + +class KeystoneClientTest(utils.TestCase): + + @httpretty.activate + def test_unscoped_init(self): + self.stub_auth(json=client_fixtures.UNSCOPED_TOKEN) + + c = client.Client(username='exampleuser', + password='password', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.scoped) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertIsNone(c.auth_ref.trust_id) + self.assertFalse(c.auth_ref.trust_scoped) + + @httpretty.activate + def test_scoped_init(self): + self.stub_auth(json=client_fixtures.PROJECT_SCOPED_TOKEN) + + c = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertTrue(c.auth_ref.scoped) + self.assertTrue(c.auth_ref.project_scoped) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertIsNone(c.auth_ref.trust_id) + self.assertFalse(c.auth_ref.trust_scoped) + + @httpretty.activate + def test_auth_ref_load(self): + self.stub_auth(json=client_fixtures.PROJECT_SCOPED_TOKEN) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + cache = json.dumps(cl.auth_ref) + new_client = client.Client(auth_ref=json.loads(cache)) + self.assertIsNotNone(new_client.auth_ref) + self.assertTrue(new_client.auth_ref.scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertIsNone(new_client.auth_ref.trust_id) + self.assertFalse(new_client.auth_ref.trust_scoped) + self.assertEquals(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v2.0') + + @httpretty.activate + def test_auth_ref_load_with_overridden_arguments(self): + self.stub_auth(json=client_fixtures.PROJECT_SCOPED_TOKEN) + + cl = client.Client(username='exampleuser', + password='password', + tenant_name='exampleproject', + auth_url=self.TEST_URL) + cache = json.dumps(cl.auth_ref) + new_auth_url = "http://new-public:5000/v2.0" + new_client = client.Client(auth_ref=json.loads(cache), + auth_url=new_auth_url) + self.assertIsNotNone(new_client.auth_ref) + self.assertTrue(new_client.auth_ref.scoped) + self.assertTrue(new_client.auth_ref.scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertIsNone(new_client.auth_ref.trust_id) + self.assertFalse(new_client.auth_ref.trust_scoped) + self.assertEquals(new_client.auth_url, new_auth_url) + self.assertEquals(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v2.0') + + def test_init_err_no_auth_url(self): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + username='exampleuser', + password='password') diff --git a/v2_0/test_discovery.py b/v2_0/test_discovery.py new file mode 100644 index 0000000..fed61d4 --- /dev/null +++ b/v2_0/test_discovery.py @@ -0,0 +1,85 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.generic import client +from keystoneclient.tests.v2_0 import utils + + +class DiscoverKeystoneTests(utils.UnauthenticatedTestCase): + def setUp(self): + super(DiscoverKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "versions": { + "values": [{ + "id": "v2.0", + "status": "beta", + "updated": "2011-11-19T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:5000/v2.0/", }, + {"rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/content/", }, + {"rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/" + "identity-dev-guide-2.0.pdf", }, + {"rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://127.0.0.1:5000/v2.0/identity.wadl", } + ], + "media-types": [{ + "base": "application/xml", + "type": "application/vnd.openstack.identity-v2.0+xml", + }, { + "base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json", + }], + }], + }, + } + + @httpretty.activate + def test_get_versions(self): + self.stub_url(httpretty.GET, base_url=self.TEST_ROOT_URL, + json=self.TEST_RESPONSE_DICT) + + cs = client.Client() + versions = cs.discover(self.TEST_ROOT_URL) + self.assertIsInstance(versions, dict) + self.assertIn('message', versions) + self.assertIn('v2.0', versions) + self.assertEquals( + versions['v2.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] + ['href']) + + @httpretty.activate + def test_get_version_local(self): + self.stub_url(httpretty.GET, base_url="http://localhost:35357/", + json=self.TEST_RESPONSE_DICT) + + cs = client.Client() + versions = cs.discover() + self.assertIsInstance(versions, dict) + self.assertIn('message', versions) + self.assertIn('v2.0', versions) + self.assertEquals( + versions['v2.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] + ['href']) diff --git a/v2_0/test_ec2.py b/v2_0/test_ec2.py new file mode 100644 index 0000000..6153a8d --- /dev/null +++ b/v2_0/test_ec2.py @@ -0,0 +1,115 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import ec2 + + +class EC2Tests(utils.TestCase): + + @httpretty.activate + def test_create(self): + user_id = 'usr' + tenant_id = 'tnt' + req_body = { + "tenant_id": tenant_id, + } + resp_body = { + "credential": { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + } + } + self.stub_url(httpretty.POST, ['users', user_id, 'credentials', + 'OS-EC2'], json=resp_body) + + cred = self.client.ec2.create(user_id, tenant_id) + self.assertTrue(isinstance(cred, ec2.EC2)) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + self.assertRequestBodyIs(json=req_body) + + @httpretty.activate + def test_get(self): + user_id = 'usr' + tenant_id = 'tnt' + resp_body = { + "credential": { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + } + } + self.stub_url(httpretty.GET, ['users', user_id, 'credentials', + 'OS-EC2', 'access'], json=resp_body) + + cred = self.client.ec2.get(user_id, 'access') + self.assertTrue(isinstance(cred, ec2.EC2)) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + + @httpretty.activate + def test_list(self): + user_id = 'usr' + tenant_id = 'tnt' + resp_body = { + "credentials": { + "values": [ + { + "access": "access", + "secret": "secret", + "tenant_id": tenant_id, + "created": "12/12/12", + "enabled": True, + }, + { + "access": "another", + "secret": "key", + "tenant_id": tenant_id, + "created": "12/12/31", + "enabled": True, + } + ] + } + } + self.stub_url(httpretty.GET, ['users', user_id, 'credentials', + 'OS-EC2'], json=resp_body) + + creds = self.client.ec2.list(user_id) + self.assertTrue(len(creds), 2) + cred = creds[0] + self.assertTrue(isinstance(cred, ec2.EC2)) + self.assertEqual(cred.tenant_id, tenant_id) + self.assertEqual(cred.enabled, True) + self.assertEqual(cred.access, 'access') + self.assertEqual(cred.secret, 'secret') + + @httpretty.activate + def test_delete(self): + user_id = 'usr' + access = 'access' + self.stub_url(httpretty.DELETE, ['users', user_id, 'credentials', + 'OS-EC2', access], status=204) + self.client.ec2.delete(user_id, access) diff --git a/v2_0/test_endpoints.py b/v2_0/test_endpoints.py new file mode 100644 index 0000000..d0f85e7 --- /dev/null +++ b/v2_0/test_endpoints.py @@ -0,0 +1,88 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import endpoints + + +class EndpointTests(utils.TestCase): + def setUp(self): + super(EndpointTests, self).setUp() + self.TEST_ENDPOINTS = { + 'endpoints': [ + { + 'adminurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'id': '8f9531231e044e218824b0e58688d262', + 'internalurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'publicurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'region': 'RegionOne', + }, + { + 'adminurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'id': '8f9531231e044e218824b0e58688d263', + 'internalurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'publicurl': 'http://host-1:8774/v1.1/$(tenant_id)s', + 'region': 'RegionOne', + } + ] + } + + @httpretty.activate + def test_create(self): + req_body = { + "endpoint": { + "region": "RegionOne", + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "service_id": "e044e21", + } + } + + resp_body = { + "endpoint": { + "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "region": "RegionOne", + "id": "1fd485b2ffd54f409a5ecd42cba11401", + "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s", + "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s", + } + } + + self.stub_url(httpretty.POST, ['endpoints'], json=resp_body) + + endpoint = self.client.endpoints.create( + region=req_body['endpoint']['region'], + publicurl=req_body['endpoint']['publicurl'], + adminurl=req_body['endpoint']['adminurl'], + internalurl=req_body['endpoint']['internalurl'], + service_id=req_body['endpoint']['service_id'] + ) + self.assertTrue(isinstance(endpoint, endpoints.Endpoint)) + self.assertRequestBodyIs(json=req_body) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['endpoints', '8f953'], status=204) + self.client.endpoints.delete('8f953') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.GET, ['endpoints'], json=self.TEST_ENDPOINTS) + + endpoint_list = self.client.endpoints.list() + [self.assertTrue(isinstance(r, endpoints.Endpoint)) + for r in endpoint_list] diff --git a/v2_0/test_roles.py b/v2_0/test_roles.py new file mode 100644 index 0000000..72b4d44 --- /dev/null +++ b/v2_0/test_roles.py @@ -0,0 +1,124 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import roles + + +class RoleTests(utils.TestCase): + def setUp(self): + super(RoleTests, self).setUp() + self.TEST_ROLES = { + "roles": { + "values": [ + { + "name": "admin", + "id": 1, + }, + { + "name": "member", + "id": 2, + } + ], + }, + } + + @httpretty.activate + def test_create(self): + req_body = { + "role": { + "name": "sysadmin", + } + } + resp_body = { + "role": { + "name": "sysadmin", + "id": 3, + } + } + self.stub_url(httpretty.POST, ['OS-KSADM', 'roles'], json=resp_body) + + role = self.client.roles.create(req_body['role']['name']) + self.assertRequestBodyIs(json=req_body) + self.assertTrue(isinstance(role, roles.Role)) + self.assertEqual(role.id, 3) + self.assertEqual(role.name, req_body['role']['name']) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['OS-KSADM', 'roles', '1'], status=204) + self.client.roles.delete(1) + + @httpretty.activate + def test_get(self): + self.stub_url(httpretty.GET, ['OS-KSADM', 'roles', '1'], + json={'role': self.TEST_ROLES['roles']['values'][0]}) + + role = self.client.roles.get(1) + self.assertTrue(isinstance(role, roles.Role)) + self.assertEqual(role.id, 1) + self.assertEqual(role.name, 'admin') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.GET, ['OS-KSADM', 'roles'], + json=self.TEST_ROLES) + + role_list = self.client.roles.list() + [self.assertTrue(isinstance(r, roles.Role)) for r in role_list] + + @httpretty.activate + def test_roles_for_user(self): + self.stub_url(httpretty.GET, ['users', 'foo', 'roles'], + json=self.TEST_ROLES) + + role_list = self.client.roles.roles_for_user('foo') + [self.assertTrue(isinstance(r, roles.Role)) for r in role_list] + + @httpretty.activate + def test_roles_for_user_tenant(self): + self.stub_url(httpretty.GET, ['tenants', 'barrr', 'users', 'foo', + 'roles'], json=self.TEST_ROLES) + + role_list = self.client.roles.roles_for_user('foo', 'barrr') + [self.assertTrue(isinstance(r, roles.Role)) for r in role_list] + + @httpretty.activate + def test_add_user_role(self): + self.stub_url(httpretty.PUT, ['users', 'foo', 'roles', 'OS-KSADM', + 'barrr'], status=204) + + self.client.roles.add_user_role('foo', 'barrr') + + @httpretty.activate + def test_add_user_role_tenant(self): + self.stub_url(httpretty.PUT, ['tenants', '4', 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], status=204) + + self.client.roles.add_user_role('foo', 'barrr', '4') + + @httpretty.activate + def test_remove_user_role(self): + self.stub_url(httpretty.DELETE, ['users', 'foo', 'roles', 'OS-KSADM', + 'barrr'], status=204) + self.client.roles.remove_user_role('foo', 'barrr') + + @httpretty.activate + def test_remove_user_role_tenant(self): + self.stub_url(httpretty.DELETE, ['tenants', '4', 'users', 'foo', + 'roles', 'OS-KSADM', 'barrr'], + status=204) + self.client.roles.remove_user_role('foo', 'barrr', '4') diff --git a/v2_0/test_service_catalog.py b/v2_0/test_service_catalog.py new file mode 100644 index 0000000..07f3004 --- /dev/null +++ b/v2_0/test_service_catalog.py @@ -0,0 +1,79 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 + +from keystoneclient import access +from keystoneclient import exceptions +from keystoneclient.tests.v2_0 import client_fixtures +from keystoneclient.tests.v2_0 import utils + + +class ServiceCatalogTest(utils.TestCase): + def setUp(self): + super(ServiceCatalogTest, self).setUp() + self.AUTH_RESPONSE_BODY = client_fixtures.AUTH_RESPONSE_BODY + + def test_building_a_service_catalog(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + self.assertEquals(sc.url_for(service_type='compute'), + "https://compute.north.host/v1/1234") + self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'), + "https://compute.north.host/v1/1234") + self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'), + "https://compute.north.host/v1.1/3456") + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", + "South", service_type='compute') + + def test_service_catalog_endpoints(self): + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + public_ep = sc.get_endpoints(service_type='compute', + endpoint_type='publicURL') + self.assertEquals(public_ep['compute'][1]['tenantId'], '2') + self.assertEquals(public_ep['compute'][1]['versionId'], '1.1') + self.assertEquals(public_ep['compute'][1]['internalURL'], + "https://compute.north.host/v1.1/3456") + + def test_service_catalog_regions(self): + self.AUTH_RESPONSE_BODY['access']['region_name'] = "North" + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='publicURL') + self.assertEquals(url, "https://image.north.host/v1/") + + self.AUTH_RESPONSE_BODY['access']['region_name'] = "South" + auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='internalURL') + self.assertEquals(url, "https://image-internal.south.host/v1/") + + def test_service_catalog_empty(self): + # We need to do a copy.deepcopy here since + # dict(self.AUTH_RESPONSE_BODY) or self.AUTH_RESPONSE_BODY.copy() will + # only do a shadowcopy and sc_empty['token']['catalog'] will still be a + # reference to self.AUTH_RESPONSE_BODY so setting it to empty will fail + # the other tests that needs a service catalog. + sc_empty = copy.deepcopy(self.AUTH_RESPONSE_BODY) + sc_empty['access']['serviceCatalog'] = [] + auth_ref = access.AccessInfo.factory(None, sc_empty) + self.assertRaises(exceptions.EmptyCatalog, + auth_ref.service_catalog.url_for, + service_type='image', + endpoint_type='internalURL') diff --git a/v2_0/test_services.py b/v2_0/test_services.py new file mode 100644 index 0000000..0b539a9 --- /dev/null +++ b/v2_0/test_services.py @@ -0,0 +1,98 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import services + + +class ServiceTests(utils.TestCase): + def setUp(self): + super(ServiceTests, self).setUp() + self.TEST_SERVICES = { + "OS-KSADM:services": { + "values": [ + { + "name": "nova", + "type": "compute", + "description": "Nova-compatible service.", + "id": 1 + }, + { + "name": "keystone", + "type": "identity", + "description": "Keystone-compatible service.", + "id": 2 + }, + ], + }, + } + + @httpretty.activate + def test_create(self): + req_body = { + "OS-KSADM:service": { + "name": "swift", + "type": "object-store", + "description": "Swift-compatible service.", + } + } + resp_body = { + "OS-KSADM:service": { + "name": "swift", + "type": "object-store", + "description": "Swift-compatible service.", + "id": 3, + } + } + self.stub_url(httpretty.POST, ['OS-KSADM', 'services'], json=resp_body) + + service = self.client.services.create( + req_body['OS-KSADM:service']['name'], + req_body['OS-KSADM:service']['type'], + req_body['OS-KSADM:service']['description']) + self.assertTrue(isinstance(service, services.Service)) + self.assertEqual(service.id, 3) + self.assertEqual(service.name, req_body['OS-KSADM:service']['name']) + self.assertRequestBodyIs(json=req_body) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['OS-KSADM', 'services', '1'], + status=204) + + self.client.services.delete(1) + + @httpretty.activate + def test_get(self): + test_services = self.TEST_SERVICES['OS-KSADM:services']['values'][0] + + self.stub_url(httpretty.GET, ['OS-KSADM', 'services', '1'], + json={'OS-KSADM:service': test_services}) + + service = self.client.services.get(1) + self.assertTrue(isinstance(service, services.Service)) + self.assertEqual(service.id, 1) + self.assertEqual(service.name, 'nova') + self.assertEqual(service.type, 'compute') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.GET, ['OS-KSADM', 'services'], + json=self.TEST_SERVICES) + + service_list = self.client.services.list() + [self.assertTrue(isinstance(r, services.Service)) + for r in service_list] diff --git a/v2_0/test_shell.py b/v2_0/test_shell.py new file mode 100644 index 0000000..1b6db20 --- /dev/null +++ b/v2_0/test_shell.py @@ -0,0 +1,347 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 cStringIO +import os +import sys + +from mox3 import stubout +from testtools import matchers + +from keystoneclient import httpclient +from keystoneclient.tests.v2_0 import fakes +from keystoneclient.tests.v2_0 import utils + + +DEFAULT_USERNAME = 'username' +DEFAULT_PASSWORD = 'password' +DEFAULT_TENANT_ID = 'tenant_id' +DEFAULT_TENANT_NAME = 'tenant_name' +DEFAULT_AUTH_URL = 'http://127.0.0.1:5000/v2.0/' + + +class ShellTests(utils.TestCase): + + def setUp(self): + """Patch os.environ to avoid required auth info.""" + + super(ShellTests, self).setUp() + self.stubs = stubout.StubOutForTesting() + + self.fake_client = fakes.FakeHTTPClient() + self.stubs.Set( + httpclient.HTTPClient, "_cs_request", + lambda ign_self, *args, **kwargs: + self.fake_client._cs_request(*args, **kwargs)) + self.stubs.Set( + httpclient.HTTPClient, "authenticate", + lambda cl_obj: + self.fake_client.authenticate(cl_obj)) + self.old_environment = os.environ.copy() + os.environ = { + 'OS_USERNAME': DEFAULT_USERNAME, + 'OS_PASSWORD': DEFAULT_PASSWORD, + 'OS_TENANT_ID': DEFAULT_TENANT_ID, + 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, + 'OS_AUTH_URL': DEFAULT_AUTH_URL, + } + import keystoneclient.shell + self.shell = keystoneclient.shell.OpenStackIdentityShell() + + def tearDown(self): + self.stubs.UnsetAll() + self.stubs.SmartUnsetAll() + os.environ = self.old_environment + self.fake_client.clear_callstack() + super(ShellTests, self).tearDown() + + def run_command(self, cmd): + orig = sys.stdout + try: + sys.stdout = cStringIO.StringIO() + if isinstance(cmd, list): + self.shell.main(cmd) + else: + self.shell.main(cmd.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertEqual(exc_value.code, 0) + finally: + out = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + return out + + def assert_called(self, method, url, body=None, **kwargs): + return self.fake_client.assert_called(method, url, body, **kwargs) + + def assert_called_anytime(self, method, url, body=None): + return self.fake_client.assert_called_anytime(method, url, body) + + def test_user_list(self): + self.run_command('user-list') + self.fake_client.assert_called_anytime('GET', '/users') + + def test_user_create(self): + self.run_command('user-create --name new-user') + self.fake_client.assert_called_anytime( + 'POST', '/users', + {'user': + {'email': None, + 'password': None, + 'enabled': True, + 'name': 'new-user', + 'tenantId': None}}) + + def test_user_get(self): + self.run_command('user-get 1') + self.fake_client.assert_called_anytime('GET', '/users/1') + + def test_user_delete(self): + self.run_command('user-delete 1') + self.fake_client.assert_called_anytime('DELETE', '/users/1') + + def test_user_password_update(self): + self.run_command('user-password-update --pass newpass 1') + self.fake_client.assert_called_anytime( + 'PUT', '/users/1/OS-KSADM/password') + + def test_user_update(self): + self.run_command('user-update --name new-user1' + ' --email user@email.com --enabled true 1') + self.fake_client.assert_called_anytime( + 'PUT', '/users/1', + {'user': + {'id': '1', + 'email': 'user@email.com', + 'enabled': True, + 'name': 'new-user1'} + }) + required = 'User not updated, no arguments present.' + out = self.run_command('user-update 1') + self.assertThat(out, matchers.MatchesRegex(required)) + + self.run_command(['user-update', '--email', '', '1']) + self.fake_client.assert_called_anytime( + 'PUT', '/users/1', + {'user': + {'id': '1', + 'email': ''} + }) + + def test_role_create(self): + self.run_command('role-create --name new-role') + self.fake_client.assert_called_anytime( + 'POST', '/OS-KSADM/roles', + {"role": {"name": "new-role"}}) + + def test_role_get(self): + self.run_command('role-get 1') + self.fake_client.assert_called_anytime('GET', '/OS-KSADM/roles/1') + + def test_role_list(self): + self.run_command('role-list') + self.fake_client.assert_called_anytime('GET', '/OS-KSADM/roles') + + def test_role_delete(self): + self.run_command('role-delete 1') + self.fake_client.assert_called_anytime('DELETE', '/OS-KSADM/roles/1') + + def test_user_role_add(self): + self.run_command('user-role-add --user_id 1 --role_id 1') + self.fake_client.assert_called_anytime( + 'PUT', '/users/1/roles/OS-KSADM/1') + + def test_user_role_list(self): + self.run_command('user-role-list --user_id 1 --tenant-id 1') + self.fake_client.assert_called_anytime( + 'GET', '/tenants/1/users/1/roles') + self.run_command('user-role-list --user_id 1') + self.fake_client.assert_called_anytime( + 'GET', '/tenants/1/users/1/roles') + self.run_command('user-role-list') + self.fake_client.assert_called_anytime( + 'GET', '/tenants/1/users/1/roles') + + def test_user_role_remove(self): + self.run_command('user-role-remove --user_id 1 --role_id 1') + self.fake_client.assert_called_anytime( + 'DELETE', '/users/1/roles/OS-KSADM/1') + + def test_tenant_create(self): + self.run_command('tenant-create --name new-tenant') + self.fake_client.assert_called_anytime( + 'POST', '/tenants', + {"tenant": {"enabled": True, + "name": "new-tenant", + "description": None}}) + + def test_tenant_get(self): + self.run_command('tenant-get 2') + self.fake_client.assert_called_anytime('GET', '/tenants/2') + + def test_tenant_list(self): + self.run_command('tenant-list') + self.fake_client.assert_called_anytime('GET', '/tenants') + + def test_tenant_update(self): + self.run_command('tenant-update' + ' --name new-tenant1 --enabled false' + ' --description desc 2') + self.fake_client.assert_called_anytime( + 'POST', '/tenants/2', + {"tenant": + {"enabled": False, + "id": "2", + "description": "desc", + "name": "new-tenant1"}}) + + required = 'Tenant not updated, no arguments present.' + out = self.run_command('tenant-update 1') + self.assertThat(out, matchers.MatchesRegex(required)) + + def test_tenant_delete(self): + self.run_command('tenant-delete 2') + self.fake_client.assert_called_anytime('DELETE', '/tenants/2') + + def test_service_create(self): + self.run_command('service-create --name service1 --type compute') + self.fake_client.assert_called_anytime( + 'POST', '/OS-KSADM/services', + {"OS-KSADM:service": + {"type": "compute", + "name": "service1", + "description": None}}) + + def test_service_get(self): + self.run_command('service-get 1') + self.fake_client.assert_called_anytime('GET', '/OS-KSADM/services/1') + + def test_service_list(self): + self.run_command('service-list') + self.fake_client.assert_called_anytime('GET', '/OS-KSADM/services') + + def test_service_delete(self): + self.run_command('service-delete 1') + self.fake_client.assert_called_anytime( + 'DELETE', '/OS-KSADM/services/1') + + def test_catalog(self): + self.run_command('catalog') + self.run_command('catalog --service compute') + + def test_ec2_credentials_create(self): + self.run_command('ec2-credentials-create' + ' --tenant-id 1 --user-id 1') + self.fake_client.assert_called_anytime( + 'POST', '/users/1/credentials/OS-EC2', + {'tenant_id': '1'}) + + self.run_command('ec2-credentials-create --tenant-id 1') + self.fake_client.assert_called_anytime( + 'POST', '/users/1/credentials/OS-EC2', + {'tenant_id': '1'}) + + self.run_command('ec2-credentials-create') + self.fake_client.assert_called_anytime( + 'POST', '/users/1/credentials/OS-EC2', + {'tenant_id': '1'}) + + def test_ec2_credentials_delete(self): + self.run_command('ec2-credentials-delete --access 2 --user-id 1') + self.fake_client.assert_called_anytime( + 'DELETE', '/users/1/credentials/OS-EC2/2') + + self.run_command('ec2-credentials-delete --access 2') + self.fake_client.assert_called_anytime( + 'DELETE', '/users/1/credentials/OS-EC2/2') + + def test_ec2_credentials_list(self): + self.run_command('ec2-credentials-list --user-id 1') + self.fake_client.assert_called_anytime( + 'GET', '/users/1/credentials/OS-EC2') + + self.run_command('ec2-credentials-list') + self.fake_client.assert_called_anytime( + 'GET', '/users/1/credentials/OS-EC2') + + def test_ec2_credentials_get(self): + self.run_command('ec2-credentials-get --access 2 --user-id 1') + self.fake_client.assert_called_anytime( + 'GET', '/users/1/credentials/OS-EC2/2') + + def test_bootstrap(self): + self.run_command('bootstrap --user-name new-user' + ' --pass 1 --role-name admin' + ' --tenant-name new-tenant') + self.fake_client.assert_called_anytime( + 'POST', '/users', + {'user': + {'email': None, + 'password': '1', + 'enabled': True, + 'name': 'new-user', + 'tenantId': None}}) + self.run_command('bootstrap --user-name new-user' + ' --pass 1 --role-name admin' + ' --tenant-name new-tenant') + self.fake_client.assert_called_anytime( + 'POST', '/tenants', + {"tenant": {"enabled": True, + "name": "new-tenant", + "description": None}}) + self.run_command('bootstrap --user-name new-user' + ' --pass 1 --role-name new-role' + ' --tenant-name new-tenant') + self.fake_client.assert_called_anytime( + 'POST', '/OS-KSADM/roles', + {"role": {"name": "new-role"}}) + + self.run_command('bootstrap --user-name' + ' new-user --pass 1 --role-name admin' + ' --tenant-name new-tenant') + self.fake_client.assert_called_anytime( + 'PUT', '/tenants/1/users/1/roles/OS-KSADM/1') + + def test_bash_completion(self): + self.run_command('bash-completion') + + def test_help(self): + out = self.run_command('help') + required = 'usage: keystone' + self.assertThat(out, matchers.MatchesRegex(required)) + + def test_password_update(self): + self.run_command('password-update --current-password oldpass' + ' --new-password newpass') + self.fake_client.assert_called_anytime( + 'PATCH', '/OS-KSCRUD/users/1', + {'user': + {'original_password': 'oldpass', + 'password': 'newpass'}}) + + def test_endpoint_create(self): + self.run_command('endpoint-create --service-id 1') + self.fake_client.assert_called_anytime( + 'POST', '/endpoints', + {'endpoint': + {'adminurl': None, + 'service_id': '1', + 'region': 'regionOne', + 'internalurl': None, + 'publicurl': None}}) + + def test_endpoint_list(self): + self.run_command('endpoint-list') + self.fake_client.assert_called_anytime('GET', '/endpoints') diff --git a/v2_0/test_tenants.py b/v2_0/test_tenants.py new file mode 100644 index 0000000..95859d4 --- /dev/null +++ b/v2_0/test_tenants.py @@ -0,0 +1,287 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import tenants + + +class TenantTests(utils.TestCase): + def setUp(self): + super(TenantTests, self).setUp() + self.TEST_TENANTS = { + "tenants": { + "values": [ + { + "enabled": True, + "description": "A description change!", + "name": "invisible_to_admin", + "id": 3, + }, + { + "enabled": True, + "description": "None", + "name": "demo", + "id": 2, + }, + { + "enabled": True, + "description": "None", + "name": "admin", + "id": 1, + }, + { + "extravalue01": "metadata01", + "enabled": True, + "description": "For testing extras", + "name": "test_extras", + "id": 4, + } + ], + "links": [], + }, + } + + @httpretty.activate + def test_create(self): + req_body = { + "tenant": { + "name": "tenantX", + "description": "Like tenant 9, but better.", + "enabled": True, + "extravalue01": "metadata01", + }, + } + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": True, + "id": 4, + "description": "Like tenant 9, but better.", + "extravalue01": "metadata01", + } + } + self.stub_url(httpretty.POST, ['tenants'], json=resp_body) + + tenant = self.client.tenants.create( + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled'], + extravalue01=req_body['tenant']['extravalue01'], + name="dont overwrite priors") + self.assertTrue(isinstance(tenant, tenants.Tenant)) + self.assertEqual(tenant.id, 4) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "Like tenant 9, but better.") + self.assertEqual(tenant.extravalue01, "metadata01") + self.assertRequestBodyIs(json=req_body) + + @httpretty.activate + def test_duplicate_create(self): + req_body = { + "tenant": { + "name": "tenantX", + "description": "The duplicate tenant.", + "enabled": True + }, + } + resp_body = { + "error": { + "message": "Conflict occurred attempting to store project.", + "code": 409, + "title": "Conflict", + } + } + self.stub_url(httpretty.POST, ['tenants'], status=409, json=resp_body) + + def create_duplicate_tenant(): + self.client.tenants.create(req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled']) + + self.assertRaises(exceptions.Conflict, create_duplicate_tenant) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['tenants', '1'], status=204) + self.client.tenants.delete(1) + + @httpretty.activate + def test_get(self): + resp = {'tenant': self.TEST_TENANTS['tenants']['values'][2]} + self.stub_url(httpretty.GET, ['tenants', '1'], json=resp) + + t = self.client.tenants.get(1) + self.assertTrue(isinstance(t, tenants.Tenant)) + self.assertEqual(t.id, 1) + self.assertEqual(t.name, 'admin') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.GET, ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list() + [self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list] + + @httpretty.activate + def test_list_limit(self): + self.stub_url(httpretty.GET, ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(limit=1) + self.assertQueryStringIs({'limit': ['1']}) + [self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list] + + @httpretty.activate + def test_list_marker(self): + self.stub_url(httpretty.GET, ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(marker=1) + self.assertQueryStringIs({'marker': ['1']}) + [self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list] + + @httpretty.activate + def test_list_limit_marker(self): + self.stub_url(httpretty.GET, ['tenants'], json=self.TEST_TENANTS) + + tenant_list = self.client.tenants.list(limit=1, marker=1) + self.assertQueryStringIs({'marker': ['1'], 'limit': ['1']}) + [self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list] + + @httpretty.activate + def test_update(self): + req_body = { + "tenant": { + "id": 4, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + "extravalue01": "metadataChanged", + #"extraname": "dontoverwrite!", + }, + } + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": False, + "id": 4, + "description": "I changed you!", + "extravalue01": "metadataChanged", + }, + } + + self.stub_url(httpretty.POST, ['tenants', '4'], json=resp_body) + + tenant = self.client.tenants.update( + req_body['tenant']['id'], + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled'], + extravalue01=req_body['tenant']['extravalue01'], + name="dont overwrite priors") + self.assertTrue(isinstance(tenant, tenants.Tenant)) + self.assertRequestBodyIs(json=req_body) + self.assertEqual(tenant.id, 4) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "I changed you!") + self.assertFalse(tenant.enabled) + self.assertEqual(tenant.extravalue01, "metadataChanged") + + @httpretty.activate + def test_update_empty_description(self): + req_body = { + "tenant": { + "id": 4, + "name": "tenantX", + "description": "", + "enabled": False, + }, + } + resp_body = { + "tenant": { + "name": "tenantX", + "enabled": False, + "id": 4, + "description": "", + }, + } + self.stub_url(httpretty.POST, ['tenants', '4'], json=resp_body) + + tenant = self.client.tenants.update(req_body['tenant']['id'], + req_body['tenant']['name'], + req_body['tenant']['description'], + req_body['tenant']['enabled']) + self.assertTrue(isinstance(tenant, tenants.Tenant)) + self.assertRequestBodyIs(json=req_body) + self.assertEqual(tenant.id, 4) + self.assertEqual(tenant.name, "tenantX") + self.assertEqual(tenant.description, "") + self.assertFalse(tenant.enabled) + + @httpretty.activate + def test_add_user(self): + self.stub_url(httpretty.PUT, ['tenants', '4', 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], status=204) + + self.client.tenants.add_user('4', 'foo', 'barrr') + + @httpretty.activate + def test_remove_user(self): + self.stub_url(httpretty.DELETE, ['tenants', '4', 'users', 'foo', + 'roles', 'OS-KSADM', 'barrr'], + status=204) + + self.client.tenants.remove_user('4', 'foo', 'barrr') + + @httpretty.activate + def test_tenant_add_user(self): + self.stub_url(httpretty.PUT, ['tenants', '4', 'users', 'foo', 'roles', + 'OS-KSADM', 'barrr'], + status=204) + + req_body = { + "tenant": { + "id": 4, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + }, + } + # make tenant object with manager + tenant = self.client.tenants.resource_class(self.client.tenants, + req_body['tenant']) + tenant.add_user('foo', 'barrr') + self.assertTrue(isinstance(tenant, tenants.Tenant)) + + @httpretty.activate + def test_tenant_remove_user(self): + self.stub_url(httpretty.DELETE, ['tenants', '4', 'users', 'foo', + 'roles', 'OS-KSADM', 'barrr'], + status=204) + + req_body = { + "tenant": { + "id": 4, + "name": "tenantX", + "description": "I changed you!", + "enabled": False, + }, + } + + # make tenant object with manager + tenant = self.client.tenants.resource_class(self.client.tenants, + req_body['tenant']) + tenant.remove_user('foo', 'barrr') + self.assertTrue(isinstance(tenant, tenants.Tenant)) diff --git a/v2_0/test_tokens.py b/v2_0/test_tokens.py new file mode 100644 index 0000000..b623ada --- /dev/null +++ b/v2_0/test_tokens.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.tests.v2_0 import utils + + +class TokenTests(utils.TestCase): + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['tokens', '1'], status=204) + self.client.tokens.delete(1) diff --git a/v2_0/test_users.py b/v2_0/test_users.py new file mode 100644 index 0000000..1c9395c --- /dev/null +++ b/v2_0/test_users.py @@ -0,0 +1,191 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.tests.v2_0 import utils +from keystoneclient.v2_0 import users + + +class UserTests(utils.TestCase): + def setUp(self): + super(UserTests, self).setUp() + self.TEST_USERS = { + "users": { + "values": [ + { + "email": "None", + "enabled": True, + "id": 1, + "name": "admin", + }, + { + "email": "None", + "enabled": True, + "id": 2, + "name": "demo", + }, + ] + } + } + + @httpretty.activate + def test_create(self): + req_body = { + "user": { + "name": "gabriel", + "password": "test", + "tenantId": 2, + "email": "test@example.com", + "enabled": True, + } + } + + resp_body = { + "user": { + "name": "gabriel", + "enabled": True, + "tenantId": 2, + "id": 3, + "password": "test", + "email": "test@example.com", + } + } + + self.stub_url(httpretty.POST, ['users'], json=resp_body) + + user = self.client.users.create(req_body['user']['name'], + req_body['user']['password'], + req_body['user']['email'], + tenant_id=req_body['user']['tenantId'], + enabled=req_body['user']['enabled']) + self.assertTrue(isinstance(user, users.User)) + self.assertEqual(user.id, 3) + self.assertEqual(user.name, "gabriel") + self.assertEqual(user.email, "test@example.com") + self.assertRequestBodyIs(json=req_body) + + @httpretty.activate + def test_delete(self): + self.stub_url(httpretty.DELETE, ['users', '1'], status=204) + self.client.users.delete(1) + + @httpretty.activate + def test_get(self): + self.stub_url(httpretty.GET, ['users', '1'], + json={'user': self.TEST_USERS['users']['values'][0]}) + + u = self.client.users.get(1) + self.assertTrue(isinstance(u, users.User)) + self.assertEqual(u.id, 1) + self.assertEqual(u.name, 'admin') + + @httpretty.activate + def test_list(self): + self.stub_url(httpretty.GET, ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list() + [self.assertTrue(isinstance(u, users.User)) for u in user_list] + + @httpretty.activate + def test_list_limit(self): + self.stub_url(httpretty.GET, ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(limit=1) + self.assertEqual(httpretty.last_request().querystring, + {'limit': ['1']}) + [self.assertTrue(isinstance(u, users.User)) for u in user_list] + + @httpretty.activate + def test_list_marker(self): + self.stub_url(httpretty.GET, ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(marker='foo') + self.assertDictEqual(httpretty.last_request().querystring, + {'marker': ['foo']}) + [self.assertTrue(isinstance(u, users.User)) for u in user_list] + + @httpretty.activate + def test_list_limit_marker(self): + self.stub_url(httpretty.GET, ['users'], json=self.TEST_USERS) + + user_list = self.client.users.list(limit=1, marker='foo') + + self.assertDictEqual(httpretty.last_request().querystring, + {'marker': ['foo'], 'limit': ['1']}) + [self.assertTrue(isinstance(u, users.User)) for u in user_list] + + @httpretty.activate + def test_update(self): + req_1 = { + "user": { + "id": 2, + "email": "gabriel@example.com", + "name": "gabriel", + } + } + req_2 = { + "user": { + "id": 2, + "password": "swordfish", + } + } + req_3 = { + "user": { + "id": 2, + "tenantId": 1, + } + } + req_4 = { + "user": { + "id": 2, + "enabled": False, + } + } + + self.stub_url(httpretty.PUT, ['users', '2'], json=req_1) + self.stub_url(httpretty.PUT, ['users', '2', 'OS-KSADM', 'password'], + json=req_2) + self.stub_url(httpretty.PUT, ['users', '2', 'OS-KSADM', 'tenant'], + json=req_3) + self.stub_url(httpretty.PUT, ['users', '2', 'OS-KSADM', 'enabled'], + json=req_4) + + self.client.users.update(2, + name='gabriel', + email='gabriel@example.com') + self.assertRequestBodyIs(json=req_1) + self.client.users.update_password(2, 'swordfish') + self.assertRequestBodyIs(json=req_2) + self.client.users.update_tenant(2, 1) + self.assertRequestBodyIs(json=req_3) + self.client.users.update_enabled(2, False) + self.assertRequestBodyIs(json=req_4) + + @httpretty.activate + def test_update_own_password(self): + req_body = { + 'user': { + 'password': 'ABCD', 'original_password': 'DCBA' + } + } + resp_body = { + 'access': {} + } + self.stub_url(httpretty.PATCH, ['OS-KSCRUD', 'users', '123'], + json=resp_body) + + self.client.user_id = '123' + self.client.users.update_own_password('DCBA', 'ABCD') + self.assertRequestBodyIs(json=req_body) diff --git a/v2_0/utils.py b/v2_0/utils.py new file mode 100644 index 0000000..6716527 --- /dev/null +++ b/v2_0/utils.py @@ -0,0 +1,90 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.tests import utils +from keystoneclient.v2_0 import client + +TestResponse = utils.TestResponse + + +class UnauthenticatedTestCase(utils.TestCase): + """Class used as base for unauthenticated calls.""" + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v2.0') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v2.0') + + +class TestCase(UnauthenticatedTestCase): + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "adminURL": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:8774/v1.0", + "publicURL": "http://cdn.admin-nets.local:8774/v1.0/" + }], + "type": "nova_compat", + "name": "nova_compat" + }, { + "endpoints": [{ + "adminURL": "http://nova/novapi/admin", + "region": "RegionOne", + "internalURL": "http://nova/novapi/internal", + "publicURL": "http://nova/novapi/public" + }], + "type": "compute", + "name": "nova" + }, { + "endpoints": [{ + "adminURL": "http://glance/glanceapi/admin", + "region": "RegionOne", + "internalURL": "http://glance/glanceapi/internal", + "publicURL": "http://glance/glanceapi/public" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "adminURL": "http://127.0.0.1:35357/v2.0", + "region": "RegionOne", + "internalURL": "http://127.0.0.1:5000/v2.0", + "publicURL": "http://127.0.0.1:5000/v2.0" + }], + "type": "identity", + "name": "keystone" + }, { + "endpoints": [{ + "adminURL": "http://swift/swiftapi/admin", + "region": "RegionOne", + "internalURL": "http://swift/swiftapi/internal", + "publicURL": "http://swift/swiftapi/public" + }], + "type": "object-store", + "name": "swift" + }] + + def setUp(self): + super(TestCase, self).setUp() + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url=self.TEST_URL, + endpoint=self.TEST_URL) + + def stub_auth(self, **kwargs): + self.stub_url(httpretty.POST, ['tokens'], **kwargs) diff --git a/v3/__init__.py b/v3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v3/client_fixtures.py b/v3/client_fixtures.py new file mode 100644 index 0000000..179916b --- /dev/null +++ b/v3/client_fixtures.py @@ -0,0 +1,290 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 __future__ import unicode_literals + + +UNSCOPED_TOKEN = { + 'token': { + 'methods': [ + 'password' + ], + 'catalog': {}, + 'expires_at': '2010-11-01T03:32:15-05:00', + 'user': { + 'domain': { + 'id': '4e6893b7ba0b4006840c3845660b86ed', + 'name': 'exampledomain' + }, + 'id': 'c4da488862bd435c9e6c0275a0d0e49a', + 'name': 'exampleuser', + } + } +} + +DOMAIN_SCOPED_TOKEN = { + 'token': { + 'methods': [ + 'password' + ], + 'catalog': {}, + 'expires_at': '2010-11-01T03:32:15-05:00', + 'user': { + 'domain': { + 'id': '4e6893b7ba0b4006840c3845660b86ed', + 'name': 'exampledomain' + }, + 'id': 'c4da488862bd435c9e6c0275a0d0e49a', + 'name': 'exampleuser', + }, + 'domain': { + 'id': '8e9283b7ba0b1038840c3842058b86ab', + 'name': 'anotherdomain' + }, + } +} + +PROJECT_SCOPED_TOKEN = { + 'token': { + 'methods': [ + 'password' + ], + 'catalog': [{ + 'endpoints': [{ + 'url': + 'http://public.com:8776/v1/225da22d3ce34b15877ea70b2a575f58', + 'region': 'RegionOne', + 'interface': 'public' + }, { + 'url': + 'http://internal:8776/v1/225da22d3ce34b15877ea70b2a575f58', + 'region': 'RegionOne', + 'interface': 'internal' + }, { + 'url': + 'http://admin:8776/v1/225da22d3ce34b15877ea70b2a575f58', + 'region': 'RegionOne', + 'interface': 'admin' + }], + 'type': 'volume' + }, { + 'endpoints': [{ + 'url': 'http://public.com:9292/v1', + 'region': 'RegionOne', + 'interface': 'public' + }, { + 'url': 'http://internal:9292/v1', + 'region': 'RegionOne', + 'interface': 'internal' + }, { + 'url': 'http://admin:9292/v1', + 'region': 'RegionOne', + 'interface': 'admin' + }], + 'type': 'image' + }, { + 'endpoints': [{ + 'url': + 'http://public.com:8774/v2/225da22d3ce34b15877ea70b2a575f58', + 'region': 'RegionOne', + 'interface': 'public' + }, { + 'url': + 'http://internal:8774/v2/225da22d3ce34b15877ea70b2a575f58', + 'region': 'RegionOne', + 'interface': 'internal' + }, { + 'url': + 'http://admin:8774/v2/225da22d3ce34b15877ea70b2a575f58', + 'region': 'RegionOne', + 'interface': 'admin' + }], + 'type': 'compute' + }, { + 'endpoints': [{ + 'url': 'http://public.com:8773/services/Cloud', + 'region': 'RegionOne', + 'interface': 'public' + }, { + 'url': 'http://internal:8773/services/Cloud', + 'region': 'RegionOne', + 'interface': 'internal' + }, { + 'url': 'http://admin:8773/services/Admin', + 'region': 'RegionOne', + 'interface': 'admin' + }], + 'type': 'ec2' + }, { + 'endpoints': [{ + 'url': 'http://public.com:5000/v3', + 'region': 'RegionOne', + 'interface': 'public' + }, { + 'url': 'http://internal:5000/v3', + 'region': 'RegionOne', + 'interface': 'internal' + }, { + 'url': 'http://admin:35357/v3', + 'region': 'RegionOne', + 'interface': 'admin' + }], + 'type': 'identity' + }], + 'expires_at': '2010-11-01T03:32:15-05:00', + 'user': { + 'domain': { + 'id': '4e6893b7ba0b4006840c3845660b86ed', + 'name': 'exampledomain' + }, + 'id': 'c4da488862bd435c9e6c0275a0d0e49a', + 'name': 'exampleuser', + }, + 'project': { + 'domain': { + 'id': '4e6893b7ba0b4006840c3845660b86ed', + 'name': 'exampledomain' + }, + 'id': '225da22d3ce34b15877ea70b2a575f58', + 'name': 'exampleproject', + }, + } +} + +AUTH_SUBJECT_TOKEN = '3e2813b7ba0b4006840c3825860b86ed' + +AUTH_RESPONSE_HEADERS = { + 'X-Subject-Token': AUTH_SUBJECT_TOKEN +} + +AUTH_RESPONSE_BODY = { + 'token': { + 'methods': [ + 'password' + ], + 'expires_at': '2010-11-01T03:32:15-05:00', + 'project': { + 'domain': { + 'id': '123', + 'name': 'aDomain' + }, + 'id': '345', + 'name': 'aTenant' + }, + 'user': { + 'domain': { + 'id': '1', + 'name': 'aDomain' + }, + 'id': '567', + 'name': 'test' + }, + 'issued_at': '2010-10-31T03:32:15-05:00', + 'catalog': [{ + 'endpoints': [{ + 'url': 'https://compute.north.host/novapi/public', + 'region': 'North', + 'interface': 'public' + }, { + 'url': 'https://compute.north.host/novapi/internal', + 'region': 'North', + 'interface': 'internal' + }, { + 'url': 'https://compute.north.host/novapi/admin', + 'region': 'North', + 'interface': 'admin' + }], + 'type': 'compute' + }, { + 'endpoints': [{ + 'url': 'http://swift.north.host/swiftapi/public', + 'region': 'South', + 'interface': 'public' + }, { + 'url': 'http://swift.north.host/swiftapi/internal', + 'region': 'South', + 'interface': 'internal' + }, { + 'url': 'http://swift.north.host/swiftapi/admin', + 'region': 'South', + 'interface': 'admin' + }], + 'type': 'object-store' + }, { + 'endpoints': [{ + 'url': 'http://glance.north.host/glanceapi/public', + 'region': 'North', + 'interface': 'public' + }, { + 'url': 'http://glance.north.host/glanceapi/internal', + 'region': 'North', + 'interface': 'internal' + }, { + 'url': 'http://glance.north.host/glanceapi/admin', + 'region': 'North', + 'interface': 'admin' + }, { + 'url': 'http://glance.south.host/glanceapi/public', + 'region': 'South', + 'interface': 'public' + }, { + 'url': 'http://glance.south.host/glanceapi/internal', + 'region': 'South', + 'interface': 'internal' + }, { + 'url': 'http://glance.south.host/glanceapi/admin', + 'region': 'South', + 'interface': 'admin' + }], + 'type': 'image' + }] + } +} + +TRUST_TOKEN = { + 'token': { + 'methods': [ + 'password' + ], + 'catalog': {}, + 'expires_at': '2010-11-01T03:32:15-05:00', + "OS-TRUST:trust": { + "id": "fe0aef", + "impersonation": False, + "links": { + "self": "http://identity:35357/v3/trusts/fe0aef" + }, + "trustee_user": { + "id": "0ca8f6", + "links": { + "self": "http://identity:35357/v3/users/0ca8f6" + } + }, + "trustor_user": { + "id": "bd263c", + "links": { + "self": "http://identity:35357/v3/users/bd263c" + } + } + }, + 'user': { + 'domain': { + 'id': '4e6893b7ba0b4006840c3845660b86ed', + 'name': 'exampledomain' + }, + 'id': '0ca8f6', + 'name': 'exampleuser', + } + } +} diff --git a/v3/test_access.py b/v3/test_access.py new file mode 100644 index 0000000..4bc3db6 --- /dev/null +++ b/v3/test_access.py @@ -0,0 +1,142 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 datetime + +from keystoneclient import access +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests.v3 import client_fixtures +from keystoneclient.tests.v3 import utils + +TOKEN_RESPONSE = utils.TestResponse({ + "headers": client_fixtures.AUTH_RESPONSE_HEADERS +}) +UNSCOPED_TOKEN = client_fixtures.UNSCOPED_TOKEN +DOMAIN_SCOPED_TOKEN = client_fixtures.DOMAIN_SCOPED_TOKEN +PROJECT_SCOPED_TOKEN = client_fixtures.PROJECT_SCOPED_TOKEN + + +class AccessInfoTest(utils.TestCase): + def test_building_unscoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=UNSCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertIn('catalog', auth_ref) + self.assertFalse(auth_ref['catalog']) + + self.assertEquals(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEquals(auth_ref.username, 'exampleuser') + self.assertEquals(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEquals(auth_ref.project_name, None) + self.assertEquals(auth_ref.project_id, None) + + self.assertEquals(auth_ref.auth_url, None) + self.assertEquals(auth_ref.management_url, None) + + self.assertFalse(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + + self.assertEquals(auth_ref.user_domain_id, + '4e6893b7ba0b4006840c3845660b86ed') + self.assertEquals(auth_ref.user_domain_name, 'exampledomain') + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + + self.assertEquals(auth_ref.expires, timeutils.parse_isotime( + UNSCOPED_TOKEN['token']['expires_at'])) + + def test_will_expire_soon(self): + expires = timeutils.utcnow() + datetime.timedelta(minutes=5) + UNSCOPED_TOKEN['token']['expires_at'] = expires.isoformat() + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=UNSCOPED_TOKEN) + self.assertFalse(auth_ref.will_expire_soon(stale_duration=120)) + self.assertTrue(auth_ref.will_expire_soon(stale_duration=300)) + self.assertFalse(auth_ref.will_expire_soon()) + + def test_building_domain_scoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=DOMAIN_SCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertIn('catalog', auth_ref) + self.assertFalse(auth_ref['catalog']) + + self.assertEquals(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEquals(auth_ref.username, 'exampleuser') + self.assertEquals(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEquals(auth_ref.domain_name, 'anotherdomain') + self.assertEquals(auth_ref.domain_id, + '8e9283b7ba0b1038840c3842058b86ab') + + self.assertEquals(auth_ref.project_name, None) + self.assertEquals(auth_ref.project_id, None) + + self.assertEquals(auth_ref.user_domain_id, + '4e6893b7ba0b4006840c3845660b86ed') + self.assertEquals(auth_ref.user_domain_name, 'exampledomain') + + self.assertIsNone(auth_ref.project_domain_id) + self.assertIsNone(auth_ref.project_domain_name) + + self.assertTrue(auth_ref.domain_scoped) + self.assertFalse(auth_ref.project_scoped) + + def test_building_project_scoped_accessinfo(self): + auth_ref = access.AccessInfo.factory(resp=TOKEN_RESPONSE, + body=PROJECT_SCOPED_TOKEN) + + self.assertTrue(auth_ref) + self.assertIn('methods', auth_ref) + self.assertIn('catalog', auth_ref) + self.assertTrue(auth_ref['catalog']) + + self.assertEquals(auth_ref.auth_token, + '3e2813b7ba0b4006840c3825860b86ed') + self.assertEquals(auth_ref.username, 'exampleuser') + self.assertEquals(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a') + + self.assertEquals(auth_ref.domain_name, None) + self.assertEquals(auth_ref.domain_id, None) + + self.assertEquals(auth_ref.project_name, 'exampleproject') + self.assertEquals(auth_ref.project_id, + '225da22d3ce34b15877ea70b2a575f58') + + self.assertEquals(auth_ref.tenant_name, auth_ref.project_name) + self.assertEquals(auth_ref.tenant_id, auth_ref.project_id) + + self.assertEquals(auth_ref.auth_url, + ('http://public.com:5000/v3',)) + self.assertEquals(auth_ref.management_url, + ('http://admin:35357/v3',)) + + self.assertEquals(auth_ref.project_domain_id, + '4e6893b7ba0b4006840c3845660b86ed') + self.assertEquals(auth_ref.project_domain_name, 'exampledomain') + + self.assertEquals(auth_ref.user_domain_id, + '4e6893b7ba0b4006840c3845660b86ed') + self.assertEquals(auth_ref.user_domain_name, 'exampledomain') + + self.assertFalse(auth_ref.domain_scoped) + self.assertTrue(auth_ref.project_scoped) diff --git a/v3/test_auth.py b/v3/test_auth.py new file mode 100644 index 0000000..c7149a8 --- /dev/null +++ b/v3/test_auth.py @@ -0,0 +1,303 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import client + + +class AuthenticateAgainstKeystoneTests(utils.TestCase): + def setUp(self): + super(AuthenticateAgainstKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "token": { + "methods": [ + "token", + "password" + ], + + "expires_at": "2020-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_TENANT_ID, + "name": self.TEST_TENANT_NAME + }, + "user": { + "domain": { + "id": self.TEST_DOMAIN_ID, + "name": self.TEST_DOMAIN_NAME + }, + "id": self.TEST_USER, + "name": self.TEST_USER + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + "catalog": self.TEST_SERVICE_CATALOG + }, + } + self.TEST_REQUEST_BODY = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": { + "name": self.TEST_DOMAIN_NAME + }, + "name": self.TEST_USER, + "password": self.TEST_TOKEN + } + } + }, + "scope": { + "project": { + "id": self.TEST_TENANT_ID + }, + } + } + } + self.TEST_REQUEST_HEADERS = { + 'Content-Type': 'application/json', + 'User-Agent': 'python-keystoneclient' + } + self.TEST_RESPONSE_HEADERS = { + 'X-Subject-Token': self.TEST_TOKEN + } + + @httpretty.activate + def test_authenticate_success(self): + TEST_TOKEN = "abcdef" + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + self.stub_auth(json=self.TEST_RESPONSE_DICT, subject_token=TEST_TOKEN) + + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, TEST_TOKEN) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_failure(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + ident['password']['user']['password'] = 'bad_key' + error = {"unauthorized": {"message": "Unauthorized", + "code": "401"}} + + self.stub_auth(status=401, json=error) + + # Workaround for issue with assertRaises on python2.6 + # where with assertRaises(exceptions.Unauthorized): doesn't work + # right + def client_create_wrapper(): + client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password="bad_key", + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertRaises(exceptions.Unauthorized, client_create_wrapper) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_auth_redirect(self): + self.stub_auth(status=305, body='Use proxy', + location=self.TEST_ADMIN_URL + '/auth/tokens') + + self.stub_auth(json=self.TEST_RESPONSE_DICT, + base_url=self.TEST_ADMIN_URL) + + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + + @httpretty.activate + def test_authenticate_success_domain_username_password_scoped(self): + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + + @httpretty.activate + def test_authenticate_success_userid_password_domain_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + scope = self.TEST_REQUEST_BODY['auth']['scope'] + del scope['project'] + scope['domain'] = {} + scope['domain']['id'] = self.TEST_DOMAIN_ID + + token = self.TEST_RESPONSE_DICT['token'] + del token['project'] + token['domain'] = {} + token['domain']['id'] = self.TEST_DOMAIN_ID + token['domain']['name'] = self.TEST_DOMAIN_NAME + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + domain_id=self.TEST_DOMAIN_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_domain_id, + self.TEST_DOMAIN_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_userid_password_project_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password']['user']['domain'] + del ident['password']['user']['name'] + ident['password']['user']['id'] = self.TEST_USER + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(user_id=self.TEST_USER, + password=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_tenant_id, + self.TEST_TENANT_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_password_unscoped(self): + del self.TEST_RESPONSE_DICT['token']['catalog'] + del self.TEST_REQUEST_BODY['auth']['scope'] + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(user_domain_name=self.TEST_DOMAIN_NAME, + username=self.TEST_USER, + password=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertFalse('catalog' in cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_token_domain_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + + scope = self.TEST_REQUEST_BODY['auth']['scope'] + del scope['project'] + scope['domain'] = {} + scope['domain']['id'] = self.TEST_DOMAIN_ID + + token = self.TEST_RESPONSE_DICT['token'] + del token['project'] + token['domain'] = {} + token['domain']['id'] = self.TEST_DOMAIN_ID + token['domain']['name'] = self.TEST_DOMAIN_NAME + + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + domain_id=self.TEST_DOMAIN_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_domain_id, + self.TEST_DOMAIN_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_token_project_scoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + project_id=self.TEST_TENANT_ID, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_tenant_id, + self.TEST_TENANT_ID) + self.assertEqual(cs.management_url, + self.TEST_RESPONSE_DICT["token"]["catalog"][3] + ['endpoints'][2]["url"]) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) + + @httpretty.activate + def test_authenticate_success_token_unscoped(self): + ident = self.TEST_REQUEST_BODY['auth']['identity'] + del ident['password'] + ident['methods'] = ['token'] + ident['token'] = {} + ident['token']['id'] = self.TEST_TOKEN + del self.TEST_REQUEST_BODY['auth']['scope'] + del self.TEST_RESPONSE_DICT['token']['catalog'] + self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN + + self.stub_auth(json=self.TEST_RESPONSE_DICT) + + cs = client.Client(token=self.TEST_TOKEN, + auth_url=self.TEST_URL) + self.assertEqual(cs.auth_token, + self.TEST_RESPONSE_HEADERS["X-Subject-Token"]) + self.assertFalse('catalog' in cs.service_catalog.catalog) + self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY) diff --git a/v3/test_client.py b/v3/test_client.py new file mode 100644 index 0000000..f479ef7 --- /dev/null +++ b/v3/test_client.py @@ -0,0 +1,136 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.v3 import client_fixtures +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import client + + +class KeystoneClientTest(utils.TestCase): + + @httpretty.activate + def test_unscoped_init(self): + self.stub_auth(json=client_fixtures.UNSCOPED_TOKEN) + + c = client.Client(user_domain_name='exampledomain', + username='exampleuser', + password='password', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEquals(c.auth_user_id, + 'c4da488862bd435c9e6c0275a0d0e49a') + + @httpretty.activate + def test_domain_scoped_init(self): + self.stub_auth(json=client_fixtures.DOMAIN_SCOPED_TOKEN) + + c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', + password='password', + domain_name='exampledomain', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertTrue(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEquals(c.auth_user_id, + 'c4da488862bd435c9e6c0275a0d0e49a') + self.assertEquals(c.auth_domain_id, + '8e9283b7ba0b1038840c3842058b86ab') + + @httpretty.activate + def test_project_scoped_init(self): + self.stub_auth(json=client_fixtures.PROJECT_SCOPED_TOKEN), + + c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', + password='password', + user_domain_name='exampledomain', + project_name='exampleproject', + auth_url=self.TEST_URL) + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertTrue(c.auth_ref.project_scoped) + self.assertEquals(c.auth_user_id, + 'c4da488862bd435c9e6c0275a0d0e49a') + self.assertEquals(c.auth_tenant_id, + '225da22d3ce34b15877ea70b2a575f58') + + @httpretty.activate + def test_auth_ref_load(self): + self.stub_auth(json=client_fixtures.PROJECT_SCOPED_TOKEN) + + c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', + password='password', + project_id='225da22d3ce34b15877ea70b2a575f58', + auth_url=self.TEST_URL) + cache = json.dumps(c.auth_ref) + new_client = client.Client(auth_ref=json.loads(cache)) + self.assertIsNotNone(new_client.auth_ref) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertEquals(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v3') + + @httpretty.activate + def test_auth_ref_load_with_overridden_arguments(self): + new_auth_url = 'https://newkeystone.com/v3' + + self.stub_auth(json=client_fixtures.PROJECT_SCOPED_TOKEN) + self.stub_auth(json=client_fixtures.PROJECT_SCOPED_TOKEN, + base_url=new_auth_url) + + c = client.Client(user_id='c4da488862bd435c9e6c0275a0d0e49a', + password='password', + project_id='225da22d3ce34b15877ea70b2a575f58', + auth_url=self.TEST_URL) + cache = json.dumps(c.auth_ref) + new_client = client.Client(auth_ref=json.loads(cache), + auth_url=new_auth_url) + self.assertIsNotNone(new_client.auth_ref) + self.assertFalse(new_client.auth_ref.domain_scoped) + self.assertTrue(new_client.auth_ref.project_scoped) + self.assertEquals(new_client.auth_url, new_auth_url) + self.assertEquals(new_client.username, 'exampleuser') + self.assertIsNone(new_client.password) + self.assertEqual(new_client.management_url, + 'http://admin:35357/v3') + + @httpretty.activate + def test_trust_init(self): + self.stub_auth(json=client_fixtures.TRUST_TOKEN) + + c = client.Client(user_domain_name='exampledomain', + username='exampleuser', + password='password', + auth_url=self.TEST_URL, + trust_id='fe0aef') + self.assertIsNotNone(c.auth_ref) + self.assertFalse(c.auth_ref.domain_scoped) + self.assertFalse(c.auth_ref.project_scoped) + self.assertEqual(c.auth_ref.trust_id, 'fe0aef') + self.assertTrue(c.auth_ref.trust_scoped) + self.assertEquals(c.auth_user_id, '0ca8f6') + + def test_init_err_no_auth_url(self): + self.assertRaises(exceptions.AuthorizationFailure, + client.Client, + username='exampleuser', + password='password') diff --git a/v3/test_credentials.py b/v3/test_credentials.py new file mode 100644 index 0000000..50b4a93 --- /dev/null +++ b/v3/test_credentials.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 uuid + +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import credentials + + +class CredentialTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(CredentialTests, self).setUp() + self.key = 'credential' + self.collection_key = 'credentials' + self.model = credentials.Credential + self.manager = self.client.credentials + + def new_ref(self, **kwargs): + kwargs = super(CredentialTests, self).new_ref(**kwargs) + kwargs.setdefault('data', uuid.uuid4().hex) + kwargs.setdefault('project_id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('user_id', uuid.uuid4().hex) + return kwargs diff --git a/v3/test_discover.py b/v3/test_discover.py new file mode 100644 index 0000000..994f959 --- /dev/null +++ b/v3/test_discover.py @@ -0,0 +1,87 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 httpretty + +from keystoneclient.generic import client +from keystoneclient.tests.v3 import utils + + +class DiscoverKeystoneTests(utils.UnauthenticatedTestCase): + def setUp(self): + super(DiscoverKeystoneTests, self).setUp() + self.TEST_RESPONSE_DICT = { + "versions": { + "values": [{"id": "v3.0", + "status": "beta", + "updated": "2013-03-06T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:5000/v3.0/", }, + {"rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/3/" + "content/", }, + {"rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/3/" + "identity-dev-guide-3.pdf", }, + ]}, + {"id": "v2.0", + "status": "beta", + "updated": "2013-03-06T00:00:00Z", + "links": [ + {"rel": "self", + "href": "http://127.0.0.1:5000/v2.0/", }, + {"rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/" + "content/", }, + {"rel": "describedby", + "type": "application/pdf", + "href": "http://docs.openstack.org/api/" + "openstack-identity-service/2.0/" + "identity-dev-guide-2.0.pdf", } + ]}], + }, + } + self.TEST_REQUEST_HEADERS = { + 'User-Agent': 'python-keystoneclient', + 'Accept': 'application/json', + } + + @httpretty.activate + def test_get_version_local(self): + httpretty.register_uri(httpretty.GET, "http://localhost:35357/", + status=300, + body=json.dumps(self.TEST_RESPONSE_DICT)) + + cs = client.Client() + versions = cs.discover() + self.assertIsInstance(versions, dict) + self.assertIn('message', versions) + self.assertIn('v3.0', versions) + self.assertEquals( + versions['v3.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0] + ['href']) + self.assertEquals( + versions['v2.0']['url'], + self.TEST_RESPONSE_DICT['versions']['values'][1]['links'][0] + ['href']) diff --git a/v3/test_domains.py b/v3/test_domains.py new file mode 100644 index 0000000..f70c67d --- /dev/null +++ b/v3/test_domains.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 uuid + +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import domains + + +class DomainTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(DomainTests, self).setUp() + self.key = 'domain' + self.collection_key = 'domains' + self.model = domains.Domain + self.manager = self.client.domains + + def new_ref(self, **kwargs): + kwargs = super(DomainTests, self).new_ref(**kwargs) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs diff --git a/v3/test_endpoints.py b/v3/test_endpoints.py new file mode 100644 index 0000000..6734523 --- /dev/null +++ b/v3/test_endpoints.py @@ -0,0 +1,91 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 uuid + +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import endpoints + + +class EndpointTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(EndpointTests, self).setUp() + self.key = 'endpoint' + self.collection_key = 'endpoints' + self.model = endpoints.Endpoint + self.manager = self.client.endpoints + + def new_ref(self, **kwargs): + kwargs = super(EndpointTests, self).new_ref(**kwargs) + kwargs.setdefault('interface', 'public') + kwargs.setdefault('region', uuid.uuid4().hex) + kwargs.setdefault('service_id', uuid.uuid4().hex) + kwargs.setdefault('url', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def test_create_public_interface(self): + ref = self.new_ref(interface='public') + self.test_create(ref) + + def test_create_admin_interface(self): + ref = self.new_ref(interface='admin') + self.test_create(ref) + + def test_create_internal_interface(self): + ref = self.new_ref(interface='internal') + self.test_create(ref) + + def test_create_invalid_interface(self): + ref = self.new_ref(interface=uuid.uuid4().hex) + self.assertRaises(Exception, self.manager.create, + **utils.parameterize(ref)) + + def test_update_public_interface(self): + ref = self.new_ref(interface='public') + self.test_update(ref) + + def test_update_admin_interface(self): + ref = self.new_ref(interface='admin') + self.test_update(ref) + + def test_update_internal_interface(self): + ref = self.new_ref(interface='internal') + self.test_update(ref) + + def test_update_invalid_interface(self): + ref = self.new_ref(interface=uuid.uuid4().hex) + self.assertRaises(Exception, self.manager.update, + **utils.parameterize(ref)) + + def test_list_public_interface(self): + interface = 'public' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_admin_interface(self): + interface = 'admin' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_internal_interface(self): + interface = 'admin' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_invalid_interface(self): + interface = uuid.uuid4().hex + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.assertRaises(Exception, self.manager.list, + expected_path=expected_path, interface=interface) diff --git a/v3/test_groups.py b/v3/test_groups.py new file mode 100644 index 0000000..c728267 --- /dev/null +++ b/v3/test_groups.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 uuid + +import httpretty + +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import groups + + +class GroupTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(GroupTests, self).setUp() + self.key = 'group' + self.collection_key = 'groups' + self.model = groups.Group + self.manager = self.client.groups + + def new_ref(self, **kwargs): + kwargs = super(GroupTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + @httpretty.activate + def test_list_groups_for_user(self): + user_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity(httpretty.GET, + ['users', user_id, self.collection_key], + status=200, entity=ref_list) + + returned_list = self.manager.list(user=user_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + @httpretty.activate + def test_list_groups_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + domain_id = uuid.uuid4().hex + + self.stub_entity(httpretty.GET, + [self.collection_key], + status=200, entity=ref_list) + + returned_list = self.manager.list(domain=domain_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + self.assertEqual(httpretty.last_request().querystring, + {'domain_id': [domain_id]}) diff --git a/v3/test_policies.py b/v3/test_policies.py new file mode 100644 index 0000000..a692d1c --- /dev/null +++ b/v3/test_policies.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 uuid + +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import policies + + +class PolicyTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(PolicyTests, self).setUp() + self.key = 'policy' + self.collection_key = 'policies' + self.model = policies.Policy + self.manager = self.client.policies + + def new_ref(self, **kwargs): + kwargs = super(PolicyTests, self).new_ref(**kwargs) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('blob', uuid.uuid4().hex) + return kwargs diff --git a/v3/test_projects.py b/v3/test_projects.py new file mode 100644 index 0000000..6aa7383 --- /dev/null +++ b/v3/test_projects.py @@ -0,0 +1,64 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 uuid + +import httpretty + +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import projects + + +class ProjectTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ProjectTests, self).setUp() + self.key = 'project' + self.collection_key = 'projects' + self.model = projects.Project + self.manager = self.client.projects + + def new_ref(self, **kwargs): + kwargs = super(ProjectTests, self).new_ref(**kwargs) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + @httpretty.activate + def test_list_projects_for_user(self): + ref_list = [self.new_ref(), self.new_ref()] + user_id = uuid.uuid4().hex + + self.stub_entity(httpretty.GET, + ['users', user_id, self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(user=user_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + @httpretty.activate + def test_list_projects_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + domain_id = uuid.uuid4().hex + + self.stub_entity(httpretty.GET, [self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(domain=domain_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + self.assertEqual(httpretty.last_request().querystring, + {'domain_id': [domain_id]}) diff --git a/v3/test_roles.py b/v3/test_roles.py new file mode 100644 index 0000000..8abe885 --- /dev/null +++ b/v3/test_roles.py @@ -0,0 +1,352 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 uuid + +import httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import roles + + +class RoleTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(RoleTests, self).setUp() + self.key = 'role' + self.collection_key = 'roles' + self.model = roles.Role + self.manager = self.client.roles + + def new_ref(self, **kwargs): + kwargs = super(RoleTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + @httpretty.activate + def test_domain_role_grant(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.PUT, + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status=201) + + self.manager.grant(role=ref['id'], domain=domain_id, user=user_id) + + @httpretty.activate + def test_domain_group_role_grant(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.PUT, + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status=201) + + self.manager.grant(role=ref['id'], domain=domain_id, group=group_id) + + @httpretty.activate + def test_domain_role_list(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity(httpretty.GET, + ['domains', domain_id, 'users', user_id, + self.collection_key], entity=ref_list) + + self.manager.list(domain=domain_id, user=user_id) + + @httpretty.activate + def test_domain_group_role_list(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity(httpretty.GET, + ['domains', domain_id, 'groups', group_id, + self.collection_key], entity=ref_list) + + self.manager.list(domain=domain_id, group=group_id) + + @httpretty.activate + def test_domain_role_check(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status=204) + + self.manager.check(role=ref['id'], domain=domain_id, + user=user_id) + + @httpretty.activate + def test_domain_group_role_check(self): + return + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status=204) + + self.manager.check(role=ref['id'], domain=domain_id, group=group_id) + + @httpretty.activate + def test_domain_role_revoke(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['domains', domain_id, 'users', user_id, + self.collection_key, ref['id']], + status=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, user=user_id) + + @httpretty.activate + def test_domain_group_role_revoke(self): + group_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['domains', domain_id, 'groups', group_id, + self.collection_key, ref['id']], + status=204) + + self.manager.revoke(role=ref['id'], domain=domain_id, group=group_id) + + @httpretty.activate + def test_project_role_grant(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.PUT, + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status=201) + + self.manager.grant(role=ref['id'], project=project_id, user=user_id) + + @httpretty.activate + def test_project_group_role_grant(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.PUT, + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status=201) + + self.manager.grant(role=ref['id'], project=project_id, group=group_id) + + @httpretty.activate + def test_project_role_list(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity(httpretty.GET, + ['projects', project_id, 'users', user_id, + self.collection_key], entity=ref_list) + + self.manager.list(project=project_id, user=user_id) + + @httpretty.activate + def test_project_group_role_list(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity(httpretty.GET, + ['projects', project_id, 'groups', group_id, + self.collection_key], entity=ref_list) + + self.manager.list(project=project_id, group=group_id) + + @httpretty.activate + def test_project_role_check(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status=200) + + self.manager.check(role=ref['id'], project=project_id, user=user_id) + + @httpretty.activate + def test_project_group_role_check(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status=200) + + self.manager.check(role=ref['id'], project=project_id, group=group_id) + + @httpretty.activate + def test_project_role_revoke(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['projects', project_id, 'users', user_id, + self.collection_key, ref['id']], + status=204) + + self.manager.revoke(role=ref['id'], project=project_id, user=user_id) + + @httpretty.activate + def test_project_group_role_revoke(self): + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['projects', project_id, 'groups', group_id, + self.collection_key, ref['id']], + status=204) + + self.manager.revoke(role=ref['id'], project=project_id, group=group_id) + + @httpretty.activate + def test_domain_project_role_grant_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.grant, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_list_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + + self.assertRaises( + exceptions.ValidationError, + self.manager.list, + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_check_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.check, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_revoke_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.revoke, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_user_group_role_grant_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.grant, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_list_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + + self.assertRaises( + exceptions.ValidationError, + self.manager.list, + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_check_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.check, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) + + def test_user_group_role_revoke_fails(self): + user_id = uuid.uuid4().hex + group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.revoke, + role=ref['id'], + project=project_id, + group=group_id, + user=user_id) diff --git a/v3/test_service_catalog.py b/v3/test_service_catalog.py new file mode 100644 index 0000000..18d8109 --- /dev/null +++ b/v3/test_service_catalog.py @@ -0,0 +1,84 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 + +from keystoneclient import access +from keystoneclient import exceptions +from keystoneclient.tests.v3 import client_fixtures +from keystoneclient.tests.v3 import utils + + +class ServiceCatalogTest(utils.TestCase): + def setUp(self): + super(ServiceCatalogTest, self).setUp() + self.AUTH_RESPONSE_BODY = client_fixtures.AUTH_RESPONSE_BODY + self.RESPONSE = utils.TestResponse({ + "headers": client_fixtures.AUTH_RESPONSE_HEADERS + }) + + def test_building_a_service_catalog(self): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + self.assertEquals(sc.url_for(service_type='compute'), + "https://compute.north.host/novapi/public") + self.assertEquals(sc.url_for(service_type='compute', + endpoint_type='internal'), + "https://compute.north.host/novapi/internal") + + self.assertRaises(exceptions.EndpointNotFound, sc.url_for, "region", + "South", service_type='compute') + + def test_service_catalog_endpoints(self): + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + public_ep = sc.get_endpoints(service_type='compute', + endpoint_type='public') + self.assertEquals(public_ep['compute'][0]['region'], 'North') + self.assertEquals(public_ep['compute'][0]['url'], + "https://compute.north.host/novapi/public") + + def test_service_catalog_regions(self): + self.AUTH_RESPONSE_BODY['token']['region_name'] = "North" + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + + url = sc.url_for(service_type='image', endpoint_type='public') + self.assertEquals(url, "http://glance.north.host/glanceapi/public") + + self.AUTH_RESPONSE_BODY['token']['region_name'] = "South" + auth_ref = access.AccessInfo.factory(self.RESPONSE, + self.AUTH_RESPONSE_BODY) + sc = auth_ref.service_catalog + url = sc.url_for(service_type='image', endpoint_type='internal') + self.assertEquals(url, "http://glance.south.host/glanceapi/internal") + + def test_service_catalog_empty(self): + # We need to do a copy.deepcopy here since + # dict(self.AUTH_RESPONSE_BODY) or self.AUTH_RESPONSE_BODY.copy() will + # only do a shadowcopy and sc_empty['token']['catalog'] will still be a + # reference to self.AUTH_RESPONSE_BODY so setting it to empty will fail + # the other tests that needs a service catalog. + sc_empty = copy.deepcopy(self.AUTH_RESPONSE_BODY) + sc_empty['token']['catalog'] = [] + auth_ref = access.AccessInfo.factory(self.RESPONSE, sc_empty) + self.assertRaises(exceptions.EmptyCatalog, + auth_ref.service_catalog.url_for, + service_type='image', + endpoint_type='internalURL') diff --git a/v3/test_services.py b/v3/test_services.py new file mode 100644 index 0000000..5b0e91d --- /dev/null +++ b/v3/test_services.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 uuid + +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import services + + +class ServiceTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ServiceTests, self).setUp() + self.key = 'service' + self.collection_key = 'services' + self.model = services.Service + self.manager = self.client.services + + def new_ref(self, **kwargs): + kwargs = super(ServiceTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs diff --git a/v3/test_trusts.py b/v3/test_trusts.py new file mode 100644 index 0000000..9c3d953 --- /dev/null +++ b/v3/test_trusts.py @@ -0,0 +1,102 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# 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 uuid + +from keystoneclient import exceptions +from keystoneclient.openstack.common import timeutils +from keystoneclient.tests.v3 import utils +from keystoneclient.v3.contrib import trusts + + +class TrustTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(TrustTests, self).setUp() + self.key = 'trust' + self.collection_key = 'trusts' + self.model = trusts.Trust + self.manager = self.client.trusts + self.path_prefix = 'OS-TRUST' + + def new_ref(self, **kwargs): + kwargs = super(TrustTests, self).new_ref(**kwargs) + kwargs.setdefault('project_id', uuid.uuid4().hex) + return kwargs + + def test_create(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + super(TrustTests, self).test_create(ref=ref) + + def test_create_roles(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + req_ref = ref.copy() + + # Note the TrustManager takes a list of role_names, and converts + # internally to the slightly odd list-of-dict API format, so we + # have to pass the expected request data to allow correct stubbing + ref['role_names'] = ['atestrole'] + req_ref['roles'] = [{'name': 'atestrole'}] + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_create_expires(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = False + ref['expires_at'] = timeutils.parse_isotime( + '2013-03-04T12:00:01.000000Z') + req_ref = ref.copy() + + # Note the TrustManager takes a datetime.datetime object for + # expires_at, and converts it internally into an iso format datestamp + req_ref['expires_at'] = '2013-03-04T12:00:01.000000Z' + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_create_imp(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = True + super(TrustTests, self).test_create(ref=ref) + + def test_create_roles_imp(self): + ref = self.new_ref() + ref['trustor_user_id'] = uuid.uuid4().hex + ref['trustee_user_id'] = uuid.uuid4().hex + ref['impersonation'] = True + req_ref = ref.copy() + ref['role_names'] = ['atestrole'] + req_ref['roles'] = [{'name': 'atestrole'}] + super(TrustTests, self).test_create(ref=ref, req_ref=req_ref) + + def test_list_filter_trustor(self): + ep = 'v3/OS-TRUST/trusts?trustor_user_id=12345' + super(TrustTests, self).test_list(expected_path=ep, + trustor_user='12345') + + def test_list_filter_trustee(self): + ep = 'v3/OS-TRUST/trusts?trustee_user_id=12345' + super(TrustTests, self).test_list(expected_path=ep, + trustee_user='12345') + + def test_update(self): + # Update not supported for the OS-TRUST API + self.assertRaises(exceptions.HTTPNotImplemented, self.manager.update) diff --git a/v3/test_users.py b/v3/test_users.py new file mode 100644 index 0000000..92135dd --- /dev/null +++ b/v3/test_users.py @@ -0,0 +1,201 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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 uuid + +import httpretty + +from keystoneclient import exceptions +from keystoneclient.tests.v3 import utils +from keystoneclient.v3 import users + + +class UserTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(UserTests, self).setUp() + self.key = 'user' + self.collection_key = 'users' + self.model = users.User + self.manager = self.client.users + + def new_ref(self, **kwargs): + kwargs = super(UserTests, self).new_ref(**kwargs) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('default_project_id', uuid.uuid4().hex) + return kwargs + + @httpretty.activate + def test_add_user_to_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + self.stub_url(httpretty.PUT, + ['groups', group_id, self.collection_key, ref['id']], + status=204) + + self.manager.add_to_group(user=ref['id'], group=group_id) + self.assertRaises(exceptions.ValidationError, + self.manager.remove_from_group, + user=ref['id'], + group=None) + + @httpretty.activate + def test_list_users_in_group(self): + group_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + + self.stub_entity(httpretty.GET, + ['groups', group_id, self.collection_key], + entity=ref_list) + + returned_list = self.manager.list(group=group_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + @httpretty.activate + def test_check_user_in_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.HEAD, + ['groups', group_id, self.collection_key, ref['id']], + status=204) + + self.manager.check_in_group(user=ref['id'], group=group_id) + + self.assertRaises(exceptions.ValidationError, + self.manager.check_in_group, + user=ref['id'], + group=None) + + @httpretty.activate + def test_remove_user_from_group(self): + group_id = uuid.uuid4().hex + ref = self.new_ref() + + self.stub_url(httpretty.DELETE, + ['groups', group_id, self.collection_key, ref['id']], + status=204) + + self.manager.remove_from_group(user=ref['id'], group=group_id) + self.assertRaises(exceptions.ValidationError, + self.manager.remove_from_group, + user=ref['id'], + group=None) + + @httpretty.activate + def test_create_with_project(self): + # Can create a user with the deprecated project option rather than + # default_project_id. + ref = self.new_ref() + + self.stub_entity(httpretty.POST, [self.collection_key], + status=201, entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + # Use deprecated project_id rather than new default_project_id. + param_ref['project_id'] = param_ref.pop('default_project_id') + params = utils.parameterize(param_ref) + + returned = self.manager.create(**params) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + @httpretty.activate + def test_create_with_project_and_default_project(self): + # Can create a user with the deprecated project and default_project_id. + # The backend call should only pass the default_project_id. + ref = self.new_ref() + + self.stub_entity(httpretty.POST, + [self.collection_key], + status=201, entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + # Add the deprecated project_id in the call, the value will be ignored. + param_ref['project_id'] = 'project' + params = utils.parameterize(param_ref) + + returned = self.manager.create(**params) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + @httpretty.activate + def test_update_with_project(self): + # Can update a user with the deprecated project option rather than + # default_project_id. + ref = self.new_ref() + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + self.stub_entity(httpretty.PATCH, + [self.collection_key, ref['id']], + status=200, entity=ref) + + # Use deprecated project_id rather than new default_project_id. + param_ref['project_id'] = param_ref.pop('default_project_id') + params = utils.parameterize(param_ref) + + returned = self.manager.update(ref['id'], **params) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + @httpretty.activate + def test_update_with_project_and_default_project(self, ref=None): + ref = self.new_ref() + req_ref = ref.copy() + req_ref.pop('id') + param_ref = req_ref.copy() + + self.stub_entity(httpretty.PATCH, + [self.collection_key, ref['id']], + status=200, entity=ref) + + # Add the deprecated project_id in the call, the value will be ignored. + param_ref['project_id'] = 'project' + params = utils.parameterize(param_ref) + + returned = self.manager.update(ref['id'], **params) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) diff --git a/v3/utils.py b/v3/utils.py new file mode 100644 index 0000000..00a2c2b --- /dev/null +++ b/v3/utils.py @@ -0,0 +1,286 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 urlparse +import uuid + +import httpretty + +from keystoneclient.openstack.common import jsonutils +from keystoneclient.tests import utils +from keystoneclient.v3 import client + +TestResponse = utils.TestResponse + + +def parameterize(ref): + """Rewrites attributes to match the kwarg naming convention in client. + + >>> parameterize({'project_id': 0}) + {'project': 0} + + """ + params = ref.copy() + for key in ref: + if key[-3:] == '_id': + params.setdefault(key[:-3], params.pop(key)) + return params + + +class UnauthenticatedTestCase(utils.TestCase): + """Class used as base for unauthenticated calls.""" + + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + +class TestCase(UnauthenticatedTestCase): + + TEST_SERVICE_CATALOG = [{ + "endpoints": [{ + "url": "http://cdn.admin-nets.local:8774/v1.0/", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:8774/v1.0", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://cdn.admin-nets.local:8774/v1.0", + "region": "RegionOne", + "interface": "admin" + }], + "type": "nova_compat" + }, { + "endpoints": [{ + "url": "http://nova/novapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://nova/novapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://nova/novapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "compute" + }, { + "endpoints": [{ + "url": "http://glance/glanceapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://glance/glanceapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://glance/glanceapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "image", + "name": "glance" + }, { + "endpoints": [{ + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://127.0.0.1:5000/v3", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://127.0.0.1:35357/v3", + "region": "RegionOne", + "interface": "admin" + }], + "type": "identity" + }, { + "endpoints": [{ + "url": "http://swift/swiftapi/public", + "region": "RegionOne", + "interface": "public" + }, { + "url": "http://swift/swiftapi/internal", + "region": "RegionOne", + "interface": "internal" + }, { + "url": "http://swift/swiftapi/admin", + "region": "RegionOne", + "interface": "admin" + }], + "type": "object-store" + }] + + def setUp(self): + super(TestCase, self).setUp() + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url=self.TEST_URL, + endpoint=self.TEST_URL) + + def stub_auth(self, subject_token=None, **kwargs): + if not subject_token: + subject_token = self.TEST_TOKEN + + self.stub_url(httpretty.POST, ['auth', 'tokens'], + X_Subject_Token=subject_token, **kwargs) + + +class CrudTests(object): + key = None + collection_key = None + model = None + manager = None + path_prefix = None + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + return kwargs + + def encode(self, entity): + if isinstance(entity, dict): + return {self.key: entity} + if isinstance(entity, list): + return {self.collection_key: entity} + raise NotImplementedError('Are you sure you want to encode that?') + + def stub_entity(self, method, parts=None, entity=None, id=None, **kwargs): + if entity: + entity = self.encode(entity) + kwargs['json'] = entity + + if not parts: + parts = [self.collection_key] + + if self.path_prefix: + parts.insert(0, self.path_prefix) + + if id: + if not parts: + parts = [] + + parts.append(id) + + self.stub_url(method, parts=parts, **kwargs) + + def assertEntityRequestBodyIs(self, entity): + self.assertRequestBodyIs(json=self.encode(entity)) + + @httpretty.activate + def test_create(self, ref=None, req_ref=None): + ref = ref or self.new_ref() + manager_ref = ref.copy() + manager_ref.pop('id') + + # req_ref argument allows you to specify a different + # signature for the request when the manager does some + # conversion before doing the request (e.g converting + # from datetime object to timestamp string) + req_ref = req_ref or ref.copy() + req_ref.pop('id') + + self.stub_entity(httpretty.POST, entity=req_ref, status=201) + + returned = self.manager.create(**parameterize(manager_ref)) + self.assertTrue(isinstance(returned, self.model)) + for attr in req_ref: + self.assertEqual( + getattr(returned, attr), + req_ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + @httpretty.activate + def test_get(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity(httpretty.GET, id=ref['id'], entity=ref) + + returned = self.manager.get(ref['id']) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + @httpretty.activate + def test_list(self, ref_list=None, expected_path=None, **filter_kwargs): + ref_list = ref_list or [self.new_ref(), self.new_ref()] + + if not expected_path: + if self.path_prefix: + expected_path = 'v3/%s/%s' % (self.path_prefix, + self.collection_key) + else: + expected_path = 'v3/%s' % self.collection_key + + httpretty.register_uri(httpretty.GET, + urlparse.urljoin(self.TEST_URL, expected_path), + body=jsonutils.dumps(self.encode(ref_list))) + + returned_list = self.manager.list(**filter_kwargs) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + @httpretty.activate + def test_find(self, ref=None): + ref = ref or self.new_ref() + ref_list = [ref] + + self.stub_entity(httpretty.GET, entity=ref_list) + + returned = self.manager.find(name=getattr(ref, 'name', None)) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + if hasattr(ref, 'name'): + self.assertQueryStringIs({'name': ref['name']}) + else: + self.assertQueryStringIs({}) + + @httpretty.activate + def test_update(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity(httpretty.PATCH, id=ref['id'], entity=ref) + + req_ref = ref.copy() + req_ref.pop('id') + + returned = self.manager.update(ref['id'], **parameterize(req_ref)) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + self.assertEntityRequestBodyIs(req_ref) + + @httpretty.activate + def test_delete(self, ref=None): + ref = ref or self.new_ref() + + self.stub_entity(httpretty.DELETE, id=ref['id'], status=204) + self.manager.delete(ref['id'])