summaryrefslogtreecommitdiff
path: root/keystoneclient/tests/unit
diff options
context:
space:
mode:
authorJamie Lennox <jamielennox@redhat.com>2015-02-11 19:03:25 +1100
committerJamie Lennox <jamielennox@redhat.com>2015-02-11 19:03:25 +1100
commit6bd93179a2966f2b5c67e297628510ac73689fb3 (patch)
treefaf3a93a16fb49b4a742f74b6fcdd20f8a0ebd0e /keystoneclient/tests/unit
parent58ac2de5d4a6b58e8bd5d430a04199a4d40427a8 (diff)
downloadpython-keystoneclient-6bd93179a2966f2b5c67e297628510ac73689fb3.tar.gz
Move tests to the unit subdirectory
Move all the existing tests to the unit/ subdirectory. This gives us some room to add a functional/ directory later with other tests. Change-Id: I0fb8d5b628eb8ee1f35f05f42d0c0ac9f285e8c3 Implements: functional-testing
Diffstat (limited to 'keystoneclient/tests/unit')
-rw-r--r--keystoneclient/tests/unit/__init__.py0
-rw-r--r--keystoneclient/tests/unit/apiclient/__init__.py0
-rw-r--r--keystoneclient/tests/unit/apiclient/test_exceptions.py68
-rw-r--r--keystoneclient/tests/unit/auth/__init__.py0
-rw-r--r--keystoneclient/tests/unit/auth/test_access.py61
-rw-r--r--keystoneclient/tests/unit/auth/test_cli.py196
-rw-r--r--keystoneclient/tests/unit/auth/test_conf.py177
-rw-r--r--keystoneclient/tests/unit/auth/test_identity_common.py422
-rw-r--r--keystoneclient/tests/unit/auth/test_identity_v2.py295
-rw-r--r--keystoneclient/tests/unit/auth/test_identity_v3.py490
-rw-r--r--keystoneclient/tests/unit/auth/test_password.py63
-rw-r--r--keystoneclient/tests/unit/auth/test_token.py47
-rw-r--r--keystoneclient/tests/unit/auth/test_token_endpoint.py63
-rw-r--r--keystoneclient/tests/unit/auth/utils.py200
-rw-r--r--keystoneclient/tests/unit/client_fixtures.py597
-rw-r--r--keystoneclient/tests/unit/generic/__init__.py0
-rw-r--r--keystoneclient/tests/unit/generic/test_client.py64
-rw-r--r--keystoneclient/tests/unit/generic/test_shell.py129
-rw-r--r--keystoneclient/tests/unit/test_auth_token_middleware.py1939
-rw-r--r--keystoneclient/tests/unit/test_base.py161
-rw-r--r--keystoneclient/tests/unit/test_cms.py160
-rw-r--r--keystoneclient/tests/unit/test_discovery.py802
-rw-r--r--keystoneclient/tests/unit/test_ec2utils.py262
-rw-r--r--keystoneclient/tests/unit/test_fixtures.py237
-rw-r--r--keystoneclient/tests/unit/test_hacking_checks.py47
-rw-r--r--keystoneclient/tests/unit/test_http.py208
-rw-r--r--keystoneclient/tests/unit/test_https.py107
-rw-r--r--keystoneclient/tests/unit/test_keyring.py187
-rw-r--r--keystoneclient/tests/unit/test_memcache_crypt.py97
-rw-r--r--keystoneclient/tests/unit/test_s3_token_middleware.py233
-rw-r--r--keystoneclient/tests/unit/test_session.py866
-rw-r--r--keystoneclient/tests/unit/test_shell.py532
-rw-r--r--keystoneclient/tests/unit/test_utils.py240
-rw-r--r--keystoneclient/tests/unit/utils.py209
-rw-r--r--keystoneclient/tests/unit/v2_0/__init__.py0
-rw-r--r--keystoneclient/tests/unit/v2_0/client_fixtures.py124
-rw-r--r--keystoneclient/tests/unit/v2_0/test_access.py201
-rw-r--r--keystoneclient/tests/unit/v2_0/test_auth.py258
-rw-r--r--keystoneclient/tests/unit/v2_0/test_certificates.py40
-rw-r--r--keystoneclient/tests/unit/v2_0/test_client.py184
-rw-r--r--keystoneclient/tests/unit/v2_0/test_discovery.py80
-rw-r--r--keystoneclient/tests/unit/v2_0/test_ec2.py107
-rw-r--r--keystoneclient/tests/unit/v2_0/test_endpoints.py147
-rw-r--r--keystoneclient/tests/unit/v2_0/test_extensions.py63
-rw-r--r--keystoneclient/tests/unit/v2_0/test_roles.py121
-rw-r--r--keystoneclient/tests/unit/v2_0/test_service_catalog.py175
-rw-r--r--keystoneclient/tests/unit/v2_0/test_services.py98
-rw-r--r--keystoneclient/tests/unit/v2_0/test_shell.py464
-rw-r--r--keystoneclient/tests/unit/v2_0/test_tenants.py363
-rw-r--r--keystoneclient/tests/unit/v2_0/test_tokens.py208
-rw-r--r--keystoneclient/tests/unit/v2_0/test_users.py305
-rw-r--r--keystoneclient/tests/unit/v2_0/utils.py88
-rw-r--r--keystoneclient/tests/unit/v3/__init__.py0
-rw-r--r--keystoneclient/tests/unit/v3/client_fixtures.py182
-rw-r--r--keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml132
-rw-r--r--keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml19
-rw-r--r--keystoneclient/tests/unit/v3/saml2_fixtures.py171
-rw-r--r--keystoneclient/tests/unit/v3/test_access.py191
-rw-r--r--keystoneclient/tests/unit/v3/test_auth.py352
-rw-r--r--keystoneclient/tests/unit/v3/test_auth_saml2.py623
-rw-r--r--keystoneclient/tests/unit/v3/test_client.py230
-rw-r--r--keystoneclient/tests/unit/v3/test_credentials.py54
-rw-r--r--keystoneclient/tests/unit/v3/test_discover.py80
-rw-r--r--keystoneclient/tests/unit/v3/test_domains.py47
-rw-r--r--keystoneclient/tests/unit/v3/test_endpoint_filter.py149
-rw-r--r--keystoneclient/tests/unit/v3/test_endpoint_policy.py242
-rw-r--r--keystoneclient/tests/unit/v3/test_endpoints.py91
-rw-r--r--keystoneclient/tests/unit/v3/test_federation.py409
-rw-r--r--keystoneclient/tests/unit/v3/test_groups.py58
-rw-r--r--keystoneclient/tests/unit/v3/test_oauth1.py298
-rw-r--r--keystoneclient/tests/unit/v3/test_policies.py31
-rw-r--r--keystoneclient/tests/unit/v3/test_projects.py227
-rw-r--r--keystoneclient/tests/unit/v3/test_regions.py37
-rw-r--r--keystoneclient/tests/unit/v3/test_role_assignments.py210
-rw-r--r--keystoneclient/tests/unit/v3/test_roles.py331
-rw-r--r--keystoneclient/tests/unit/v3/test_service_catalog.py218
-rw-r--r--keystoneclient/tests/unit/v3/test_services.py44
-rw-r--r--keystoneclient/tests/unit/v3/test_tokens.py110
-rw-r--r--keystoneclient/tests/unit/v3/test_trusts.py112
-rw-r--r--keystoneclient/tests/unit/v3/test_users.py279
-rw-r--r--keystoneclient/tests/unit/v3/utils.py332
81 files changed, 17444 insertions, 0 deletions
diff --git a/keystoneclient/tests/unit/__init__.py b/keystoneclient/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/keystoneclient/tests/unit/__init__.py
diff --git a/keystoneclient/tests/unit/apiclient/__init__.py b/keystoneclient/tests/unit/apiclient/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/keystoneclient/tests/unit/apiclient/__init__.py
diff --git a/keystoneclient/tests/unit/apiclient/test_exceptions.py b/keystoneclient/tests/unit/apiclient/test_exceptions.py
new file mode 100644
index 0000000..4a803c7
--- /dev/null
+++ b/keystoneclient/tests/unit/apiclient/test_exceptions.py
@@ -0,0 +1,68 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import six
+
+from keystoneclient import exceptions
+from keystoneclient.tests.unit import utils
+
+
+class FakeResponse(object):
+ json_data = {}
+
+ def __init__(self, **kwargs):
+ for key, value in six.iteritems(kwargs):
+ 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.assertIsInstance(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/keystoneclient/tests/unit/auth/__init__.py b/keystoneclient/tests/unit/auth/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/__init__.py
diff --git a/keystoneclient/tests/unit/auth/test_access.py b/keystoneclient/tests/unit/auth/test_access.py
new file mode 100644
index 0000000..405fb8b
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_access.py
@@ -0,0 +1,61 @@
+# 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 access
+from keystoneclient import auth
+from keystoneclient.auth.identity import access as access_plugin
+from keystoneclient import fixture
+from keystoneclient import session
+from keystoneclient.tests.unit import utils
+
+
+class AccessInfoPluginTests(utils.TestCase):
+
+ def setUp(self):
+ super(AccessInfoPluginTests, self).setUp()
+ self.session = session.Session()
+ self.auth_token = uuid.uuid4().hex
+
+ def _plugin(self, **kwargs):
+ token = fixture.V3Token()
+ s = token.add_service('identity')
+ s.add_standard_endpoints(public=self.TEST_ROOT_URL)
+
+ auth_ref = access.AccessInfo.factory(body=token,
+ auth_token=self.auth_token)
+ return access_plugin.AccessInfoPlugin(auth_ref, **kwargs)
+
+ def test_auth_ref(self):
+ plugin = self._plugin()
+ self.assertEqual(self.TEST_ROOT_URL,
+ plugin.get_endpoint(self.session,
+ service_type='identity',
+ interface='public'))
+ self.assertEqual(self.auth_token, plugin.get_token(session))
+
+ def test_auth_url(self):
+ auth_url = 'http://keystone.test.url'
+ plugin = self._plugin(auth_url=auth_url)
+
+ self.assertEqual(auth_url,
+ plugin.get_endpoint(self.session,
+ interface=auth.AUTH_INTERFACE))
+
+ def test_invalidate(self):
+ plugin = self._plugin()
+ auth_ref = plugin.auth_ref
+
+ self.assertIsInstance(auth_ref, access.AccessInfo)
+ self.assertFalse(plugin.invalidate())
+ self.assertIs(auth_ref, plugin.auth_ref)
diff --git a/keystoneclient/tests/unit/auth/test_cli.py b/keystoneclient/tests/unit/auth/test_cli.py
new file mode 100644
index 0000000..d65de73
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_cli.py
@@ -0,0 +1,196 @@
+# 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 uuid
+
+import fixtures
+import mock
+from oslo_config import cfg
+
+from keystoneclient.auth import base
+from keystoneclient.auth import cli
+from keystoneclient.tests.unit.auth import utils
+
+
+class TesterPlugin(base.BaseAuthPlugin):
+
+ def get_token(self, *args, **kwargs):
+ return None
+
+ @classmethod
+ def get_options(cls):
+ # NOTE(jamielennox): this is kind of horrible. If you specify this as
+ # a deprecated_name= value it will convert - to _ which is not what we
+ # want for a CLI option.
+ deprecated = [cfg.DeprecatedOpt('test-other')]
+ return [
+ cfg.StrOpt('test-opt', help='tester', deprecated_opts=deprecated)
+ ]
+
+
+class CliTests(utils.TestCase):
+
+ def setUp(self):
+ super(CliTests, self).setUp()
+ self.p = argparse.ArgumentParser()
+
+ def env(self, name, value=None):
+ if value is not None:
+ # environment variables are always strings
+ value = str(value)
+
+ return self.useFixture(fixtures.EnvironmentVariable(name, value))
+
+ def test_creating_with_no_args(self):
+ ret = cli.register_argparse_arguments(self.p, [])
+ self.assertIsNone(ret)
+ self.assertIn('--os-auth-plugin', self.p.format_usage())
+
+ def test_load_with_nothing(self):
+ cli.register_argparse_arguments(self.p, [])
+ opts = self.p.parse_args([])
+ self.assertIsNone(cli.load_from_argparse_arguments(opts))
+
+ @utils.mock_plugin
+ def test_basic_params_added(self, m):
+ name = uuid.uuid4().hex
+ argv = ['--os-auth-plugin', name]
+ ret = cli.register_argparse_arguments(self.p, argv)
+ self.assertIs(utils.MockPlugin, ret)
+
+ for n in ('--os-a-int', '--os-a-bool', '--os-a-float'):
+ self.assertIn(n, self.p.format_usage())
+
+ m.assert_called_once_with(name)
+
+ @utils.mock_plugin
+ def test_param_loading(self, m):
+ name = uuid.uuid4().hex
+ argv = ['--os-auth-plugin', name,
+ '--os-a-int', str(self.a_int),
+ '--os-a-float', str(self.a_float),
+ '--os-a-bool', str(self.a_bool)]
+
+ klass = cli.register_argparse_arguments(self.p, argv)
+ self.assertIs(utils.MockPlugin, klass)
+
+ opts = self.p.parse_args(argv)
+ self.assertEqual(name, opts.os_auth_plugin)
+
+ a = cli.load_from_argparse_arguments(opts)
+ self.assertTestVals(a)
+
+ self.assertEqual(name, opts.os_auth_plugin)
+ self.assertEqual(str(self.a_int), opts.os_a_int)
+ self.assertEqual(str(self.a_float), opts.os_a_float)
+ self.assertEqual(str(self.a_bool), opts.os_a_bool)
+
+ @utils.mock_plugin
+ def test_default_options(self, m):
+ name = uuid.uuid4().hex
+ argv = ['--os-auth-plugin', name,
+ '--os-a-float', str(self.a_float)]
+
+ klass = cli.register_argparse_arguments(self.p, argv)
+ self.assertIs(utils.MockPlugin, klass)
+
+ opts = self.p.parse_args(argv)
+ self.assertEqual(name, opts.os_auth_plugin)
+
+ a = cli.load_from_argparse_arguments(opts)
+
+ self.assertEqual(self.a_float, a['a_float'])
+ self.assertEqual(3, a['a_int'])
+
+ @utils.mock_plugin
+ def test_with_default_string_value(self, m):
+ name = uuid.uuid4().hex
+ klass = cli.register_argparse_arguments(self.p, [], default=name)
+ self.assertIs(utils.MockPlugin, klass)
+ m.assert_called_once_with(name)
+
+ @utils.mock_plugin
+ def test_overrides_default_string_value(self, m):
+ name = uuid.uuid4().hex
+ default = uuid.uuid4().hex
+ argv = ['--os-auth-plugin', name]
+ klass = cli.register_argparse_arguments(self.p, argv, default=default)
+ self.assertIs(utils.MockPlugin, klass)
+ m.assert_called_once_with(name)
+
+ @utils.mock_plugin
+ def test_with_default_type_value(self, m):
+ klass = cli.register_argparse_arguments(self.p, [],
+ default=utils.MockPlugin)
+ self.assertIs(utils.MockPlugin, klass)
+ self.assertEqual(0, m.call_count)
+
+ @utils.mock_plugin
+ def test_overrides_default_type_value(self, m):
+ # using this test plugin would fail if called because there
+ # is no get_options() function
+ class TestPlugin(object):
+ pass
+ name = uuid.uuid4().hex
+ argv = ['--os-auth-plugin', name]
+ klass = cli.register_argparse_arguments(self.p, argv,
+ default=TestPlugin)
+ self.assertIs(utils.MockPlugin, klass)
+ m.assert_called_once_with(name)
+
+ @utils.mock_plugin
+ def test_env_overrides_default_opt(self, m):
+ name = uuid.uuid4().hex
+ val = uuid.uuid4().hex
+ self.env('OS_A_STR', val)
+
+ klass = cli.register_argparse_arguments(self.p, [], default=name)
+ opts = self.p.parse_args([])
+ a = klass.load_from_argparse_arguments(opts)
+
+ self.assertEqual(val, a['a_str'])
+
+ def test_deprecated_cli_options(self):
+ TesterPlugin.register_argparse_arguments(self.p)
+ val = uuid.uuid4().hex
+ opts = self.p.parse_args(['--os-test-other', val])
+ self.assertEqual(val, opts.os_test_opt)
+
+ def test_deprecated_multi_cli_options(self):
+ TesterPlugin.register_argparse_arguments(self.p)
+ val1 = uuid.uuid4().hex
+ val2 = uuid.uuid4().hex
+ # argarse rules say that the last specified wins.
+ opts = self.p.parse_args(['--os-test-other', val2,
+ '--os-test-opt', val1])
+ self.assertEqual(val1, opts.os_test_opt)
+
+ def test_deprecated_env_options(self):
+ val = uuid.uuid4().hex
+
+ with mock.patch.dict('os.environ', {'OS_TEST_OTHER': val}):
+ TesterPlugin.register_argparse_arguments(self.p)
+
+ opts = self.p.parse_args([])
+ self.assertEqual(val, opts.os_test_opt)
+
+ def test_deprecated_env_multi_options(self):
+ val1 = uuid.uuid4().hex
+ val2 = uuid.uuid4().hex
+
+ with mock.patch.dict('os.environ', {'OS_TEST_OPT': val1,
+ 'OS_TEST_OTHER': val2}):
+ TesterPlugin.register_argparse_arguments(self.p)
+
+ opts = self.p.parse_args([])
+ self.assertEqual(val1, opts.os_test_opt)
diff --git a/keystoneclient/tests/unit/auth/test_conf.py b/keystoneclient/tests/unit/auth/test_conf.py
new file mode 100644
index 0000000..c3ce8eb
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_conf.py
@@ -0,0 +1,177 @@
+# 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 mock
+from oslo_config import cfg
+from oslo_config import fixture as config
+import stevedore
+
+from keystoneclient.auth import base
+from keystoneclient.auth import conf
+from keystoneclient.auth.identity import v2 as v2_auth
+from keystoneclient.auth.identity import v3 as v3_auth
+from keystoneclient import exceptions
+from keystoneclient.tests.unit.auth import utils
+
+
+class ConfTests(utils.TestCase):
+
+ def setUp(self):
+ super(ConfTests, self).setUp()
+ self.conf_fixture = self.useFixture(config.Config())
+
+ # NOTE(jamielennox): we register the basic config options first because
+ # we need them in place before we can stub them. We will need to run
+ # the register again after we stub the auth section and auth plugin so
+ # it can load the plugin specific options.
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+
+ def test_loading_v2(self):
+ section = uuid.uuid4().hex
+ username = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+ trust_id = uuid.uuid4().hex
+ tenant_id = uuid.uuid4().hex
+
+ self.conf_fixture.config(auth_section=section, group=self.GROUP)
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+
+ self.conf_fixture.register_opts(v2_auth.Password.get_options(),
+ group=section)
+
+ self.conf_fixture.config(auth_plugin=self.V2PASS,
+ username=username,
+ password=password,
+ trust_id=trust_id,
+ tenant_id=tenant_id,
+ group=section)
+
+ a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
+
+ self.assertEqual(username, a.username)
+ self.assertEqual(password, a.password)
+ self.assertEqual(trust_id, a.trust_id)
+ self.assertEqual(tenant_id, a.tenant_id)
+
+ def test_loading_v3(self):
+ section = uuid.uuid4().hex
+ token = uuid.uuid4().hex
+ trust_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+ project_domain_name = uuid.uuid4().hex
+
+ self.conf_fixture.config(auth_section=section, group=self.GROUP)
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+
+ self.conf_fixture.register_opts(v3_auth.Token.get_options(),
+ group=section)
+
+ self.conf_fixture.config(auth_plugin=self.V3TOKEN,
+ token=token,
+ trust_id=trust_id,
+ project_id=project_id,
+ project_domain_name=project_domain_name,
+ group=section)
+
+ a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
+
+ self.assertEqual(token, a.auth_methods[0].token)
+ self.assertEqual(trust_id, a.trust_id)
+ self.assertEqual(project_id, a.project_id)
+ self.assertEqual(project_domain_name, a.project_domain_name)
+
+ def test_loading_invalid_plugin(self):
+ auth_plugin = uuid.uuid4().hex
+ self.conf_fixture.config(auth_plugin=auth_plugin,
+ group=self.GROUP)
+
+ e = self.assertRaises(exceptions.NoMatchingPlugin,
+ conf.load_from_conf_options,
+ self.conf_fixture.conf,
+ self.GROUP)
+
+ self.assertEqual(auth_plugin, e.name)
+
+ def test_loading_with_no_data(self):
+ self.assertIsNone(conf.load_from_conf_options(self.conf_fixture.conf,
+ self.GROUP))
+
+ @mock.patch('stevedore.DriverManager')
+ def test_other_params(self, m):
+ m.return_value = utils.MockManager(utils.MockPlugin)
+ driver_name = uuid.uuid4().hex
+
+ self.conf_fixture.register_opts(utils.MockPlugin.get_options(),
+ group=self.GROUP)
+ self.conf_fixture.config(auth_plugin=driver_name,
+ group=self.GROUP,
+ **self.TEST_VALS)
+
+ a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
+ self.assertTestVals(a)
+
+ m.assert_called_once_with(namespace=base.PLUGIN_NAMESPACE,
+ name=driver_name,
+ invoke_on_load=False)
+
+ @utils.mock_plugin
+ def test_same_section(self, m):
+ self.conf_fixture.register_opts(utils.MockPlugin.get_options(),
+ group=self.GROUP)
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+ self.conf_fixture.config(auth_plugin=uuid.uuid4().hex,
+ group=self.GROUP,
+ **self.TEST_VALS)
+
+ a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
+ self.assertTestVals(a)
+
+ @utils.mock_plugin
+ def test_diff_section(self, m):
+ section = uuid.uuid4().hex
+
+ self.conf_fixture.config(auth_section=section, group=self.GROUP)
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+
+ self.conf_fixture.register_opts(utils.MockPlugin.get_options(),
+ group=section)
+ self.conf_fixture.config(group=section,
+ auth_plugin=uuid.uuid4().hex,
+ **self.TEST_VALS)
+
+ a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
+ self.assertTestVals(a)
+
+ def test_plugins_are_all_opts(self):
+ manager = stevedore.ExtensionManager(base.PLUGIN_NAMESPACE,
+ invoke_on_load=False,
+ propagate_map_exceptions=True)
+
+ def inner(driver):
+ for p in driver.plugin.get_options():
+ self.assertIsInstance(p, cfg.Opt)
+
+ manager.map(inner)
+
+ def test_get_common(self):
+ opts = conf.get_common_conf_options()
+ for opt in opts:
+ self.assertIsInstance(opt, cfg.Opt)
+ self.assertEqual(2, len(opts))
+
+ def test_get_named(self):
+ loaded_opts = conf.get_plugin_options('v2password')
+ plugin_opts = v2_auth.Password.get_options()
+
+ self.assertEqual(plugin_opts, loaded_opts)
diff --git a/keystoneclient/tests/unit/auth/test_identity_common.py b/keystoneclient/tests/unit/auth/test_identity_common.py
new file mode 100644
index 0000000..db30bea
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_identity_common.py
@@ -0,0 +1,422 @@
+# 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 abc
+import datetime
+import uuid
+
+from oslo_utils import timeutils
+import six
+
+from keystoneclient import access
+from keystoneclient.auth import base
+from keystoneclient.auth import identity
+from keystoneclient import fixture
+from keystoneclient import session
+from keystoneclient.tests.unit import utils
+
+
+@six.add_metaclass(abc.ABCMeta)
+class CommonIdentityTests(object):
+
+ TEST_ROOT_URL = 'http://127.0.0.1:5000/'
+ TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/'
+
+ TEST_COMPUTE_PUBLIC = 'http://nova/novapi/public'
+ TEST_COMPUTE_INTERNAL = 'http://nova/novapi/internal'
+ TEST_COMPUTE_ADMIN = 'http://nova/novapi/admin'
+
+ TEST_PASS = uuid.uuid4().hex
+
+ def setUp(self):
+ super(CommonIdentityTests, self).setUp()
+
+ self.TEST_URL = '%s%s' % (self.TEST_ROOT_URL, self.version)
+ self.TEST_ADMIN_URL = '%s%s' % (self.TEST_ROOT_ADMIN_URL, self.version)
+ self.TEST_DISCOVERY = fixture.DiscoveryList(href=self.TEST_ROOT_URL)
+
+ self.stub_auth_data()
+
+ @abc.abstractmethod
+ def create_auth_plugin(self, **kwargs):
+ """Create an auth plugin that makes sense for the auth data.
+
+ It doesn't really matter what auth mechanism is used but it should be
+ appropriate to the API version.
+ """
+
+ @abc.abstractmethod
+ def get_auth_data(self, **kwargs):
+ """Return fake authentication data.
+
+ This should register a valid token response and ensure that the compute
+ endpoints are set to TEST_COMPUTE_PUBLIC, _INTERNAL and _ADMIN.
+ """
+
+ def stub_auth_data(self, **kwargs):
+ token = self.get_auth_data(**kwargs)
+ self.user_id = token.user_id
+
+ try:
+ self.project_id = token.project_id
+ except AttributeError:
+ self.project_id = token.tenant_id
+
+ self.stub_auth(json=token)
+
+ @abc.abstractproperty
+ def version(self):
+ """The API version being tested."""
+
+ def test_discovering(self):
+ self.stub_url('GET', [],
+ base_url=self.TEST_COMPUTE_ADMIN,
+ json=self.TEST_DISCOVERY)
+
+ body = 'SUCCESS'
+
+ # which gives our sample values
+ self.stub_url('GET', ['path'], text=body)
+
+ a = self.create_auth_plugin()
+ s = session.Session(auth=a)
+
+ resp = s.get('/path', endpoint_filter={'service_type': 'compute',
+ 'interface': 'admin',
+ 'version': self.version})
+
+ self.assertEqual(200, resp.status_code)
+ self.assertEqual(body, resp.text)
+
+ new_body = 'SC SUCCESS'
+ # if we don't specify a version, we use the URL from the SC
+ self.stub_url('GET', ['path'],
+ base_url=self.TEST_COMPUTE_ADMIN,
+ text=new_body)
+
+ resp = s.get('/path', endpoint_filter={'service_type': 'compute',
+ 'interface': 'admin'})
+
+ self.assertEqual(200, resp.status_code)
+ self.assertEqual(new_body, resp.text)
+
+ def test_discovery_uses_session_cache(self):
+ # register responses such that if the discovery URL is hit more than
+ # once then the response will be invalid and not point to COMPUTE_ADMIN
+ resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}]
+ self.requests.get(self.TEST_COMPUTE_ADMIN, resps)
+
+ body = 'SUCCESS'
+ self.stub_url('GET', ['path'], text=body)
+
+ # now either of the two plugins I use, it should not cause a second
+ # request to the discovery url.
+ s = session.Session()
+ a = self.create_auth_plugin()
+ b = self.create_auth_plugin()
+
+ for auth in (a, b):
+ resp = s.get('/path',
+ auth=auth,
+ endpoint_filter={'service_type': 'compute',
+ 'interface': 'admin',
+ 'version': self.version})
+
+ self.assertEqual(200, resp.status_code)
+ self.assertEqual(body, resp.text)
+
+ def test_discovery_uses_plugin_cache(self):
+ # register responses such that if the discovery URL is hit more than
+ # once then the response will be invalid and not point to COMPUTE_ADMIN
+ resps = [{'json': self.TEST_DISCOVERY}, {'status_code': 500}]
+ self.requests.get(self.TEST_COMPUTE_ADMIN, resps)
+
+ body = 'SUCCESS'
+ self.stub_url('GET', ['path'], text=body)
+
+ # now either of the two sessions I use, it should not cause a second
+ # request to the discovery url.
+ sa = session.Session()
+ sb = session.Session()
+ auth = self.create_auth_plugin()
+
+ for sess in (sa, sb):
+ resp = sess.get('/path',
+ auth=auth,
+ endpoint_filter={'service_type': 'compute',
+ 'interface': 'admin',
+ 'version': self.version})
+
+ self.assertEqual(200, resp.status_code)
+ self.assertEqual(body, resp.text)
+
+ def test_discovering_with_no_data(self):
+ # which returns discovery information pointing to TEST_URL but there is
+ # no data there.
+ self.stub_url('GET', [],
+ base_url=self.TEST_COMPUTE_ADMIN,
+ status_code=400)
+
+ # so the url that will be used is the same TEST_COMPUTE_ADMIN
+ body = 'SUCCESS'
+ self.stub_url('GET', ['path'], base_url=self.TEST_COMPUTE_ADMIN,
+ text=body, status_code=200)
+
+ a = self.create_auth_plugin()
+ s = session.Session(auth=a)
+
+ resp = s.get('/path', endpoint_filter={'service_type': 'compute',
+ 'interface': 'admin',
+ 'version': self.version})
+
+ self.assertEqual(200, resp.status_code)
+ self.assertEqual(body, resp.text)
+
+ def test_asking_for_auth_endpoint_ignores_checks(self):
+ a = self.create_auth_plugin()
+ s = session.Session(auth=a)
+
+ auth_url = s.get_endpoint(service_type='compute',
+ interface=base.AUTH_INTERFACE)
+
+ self.assertEqual(self.TEST_URL, auth_url)
+
+ def _create_expired_auth_plugin(self, **kwargs):
+ expires = timeutils.utcnow() - datetime.timedelta(minutes=20)
+ expired_token = self.get_auth_data(expires=expires)
+ expired_auth_ref = access.AccessInfo.factory(body=expired_token)
+
+ body = 'SUCCESS'
+ self.stub_url('GET', ['path'],
+ base_url=self.TEST_COMPUTE_ADMIN, text=body)
+
+ a = self.create_auth_plugin(**kwargs)
+ a.auth_ref = expired_auth_ref
+ return a
+
+ def test_reauthenticate(self):
+ a = self._create_expired_auth_plugin()
+ expired_auth_ref = a.auth_ref
+ s = session.Session(auth=a)
+ self.assertIsNot(expired_auth_ref, a.get_access(s))
+
+ def test_no_reauthenticate(self):
+ a = self._create_expired_auth_plugin(reauthenticate=False)
+ expired_auth_ref = a.auth_ref
+ s = session.Session(auth=a)
+ self.assertIs(expired_auth_ref, a.get_access(s))
+
+ def test_invalidate(self):
+ a = self.create_auth_plugin()
+ s = session.Session(auth=a)
+
+ # trigger token fetching
+ s.get_auth_headers()
+
+ self.assertTrue(a.auth_ref)
+ self.assertTrue(a.invalidate())
+ self.assertIsNone(a.auth_ref)
+ self.assertFalse(a.invalidate())
+
+ def test_get_auth_properties(self):
+ a = self.create_auth_plugin()
+ s = session.Session()
+
+ self.assertEqual(self.user_id, a.get_user_id(s))
+ self.assertEqual(self.project_id, a.get_project_id(s))
+
+
+class V3(CommonIdentityTests, utils.TestCase):
+
+ @property
+ def version(self):
+ return 'v3'
+
+ def get_auth_data(self, **kwargs):
+ token = fixture.V3Token(**kwargs)
+ region = 'RegionOne'
+
+ svc = token.add_service('identity')
+ svc.add_standard_endpoints(admin=self.TEST_ADMIN_URL, region=region)
+
+ svc = token.add_service('compute')
+ svc.add_standard_endpoints(admin=self.TEST_COMPUTE_ADMIN,
+ public=self.TEST_COMPUTE_PUBLIC,
+ internal=self.TEST_COMPUTE_INTERNAL,
+ region=region)
+
+ return token
+
+ def stub_auth(self, subject_token=None, **kwargs):
+ if not subject_token:
+ subject_token = self.TEST_TOKEN
+
+ kwargs.setdefault('headers', {})['X-Subject-Token'] = subject_token
+ self.stub_url('POST', ['auth', 'tokens'], **kwargs)
+
+ def create_auth_plugin(self, **kwargs):
+ kwargs.setdefault('auth_url', self.TEST_URL)
+ kwargs.setdefault('username', self.TEST_USER)
+ kwargs.setdefault('password', self.TEST_PASS)
+ return identity.V3Password(**kwargs)
+
+
+class V2(CommonIdentityTests, utils.TestCase):
+
+ @property
+ def version(self):
+ return 'v2.0'
+
+ def create_auth_plugin(self, **kwargs):
+ kwargs.setdefault('auth_url', self.TEST_URL)
+ kwargs.setdefault('username', self.TEST_USER)
+ kwargs.setdefault('password', self.TEST_PASS)
+ return identity.V2Password(**kwargs)
+
+ def get_auth_data(self, **kwargs):
+ token = fixture.V2Token(**kwargs)
+ region = 'RegionOne'
+
+ svc = token.add_service('identity')
+ svc.add_endpoint(self.TEST_ADMIN_URL, region=region)
+
+ svc = token.add_service('compute')
+ svc.add_endpoint(public=self.TEST_COMPUTE_PUBLIC,
+ internal=self.TEST_COMPUTE_INTERNAL,
+ admin=self.TEST_COMPUTE_ADMIN,
+ region=region)
+
+ return token
+
+ def stub_auth(self, **kwargs):
+ self.stub_url('POST', ['tokens'], **kwargs)
+
+
+class CatalogHackTests(utils.TestCase):
+
+ TEST_URL = 'http://keystone.server:5000/v2.0'
+ OTHER_URL = 'http://other.server:5000/path'
+
+ IDENTITY = 'identity'
+
+ BASE_URL = 'http://keystone.server:5000/'
+ V2_URL = BASE_URL + 'v2.0'
+ V3_URL = BASE_URL + 'v3'
+
+ def test_getting_endpoints(self):
+ disc = fixture.DiscoveryList(href=self.BASE_URL)
+ self.stub_url('GET',
+ ['/'],
+ base_url=self.BASE_URL,
+ json=disc)
+
+ token = fixture.V2Token()
+ service = token.add_service(self.IDENTITY)
+ service.add_endpoint(public=self.V2_URL,
+ admin=self.V2_URL,
+ internal=self.V2_URL)
+
+ self.stub_url('POST',
+ ['tokens'],
+ base_url=self.V2_URL,
+ json=token)
+
+ v2_auth = identity.V2Password(self.V2_URL,
+ username=uuid.uuid4().hex,
+ password=uuid.uuid4().hex)
+
+ sess = session.Session(auth=v2_auth)
+
+ endpoint = sess.get_endpoint(service_type=self.IDENTITY,
+ interface='public',
+ version=(3, 0))
+
+ self.assertEqual(self.V3_URL, endpoint)
+
+ def test_returns_original_when_discover_fails(self):
+ token = fixture.V2Token()
+ service = token.add_service(self.IDENTITY)
+ service.add_endpoint(public=self.V2_URL,
+ admin=self.V2_URL,
+ internal=self.V2_URL)
+
+ self.stub_url('POST',
+ ['tokens'],
+ base_url=self.V2_URL,
+ json=token)
+
+ self.stub_url('GET', [], base_url=self.BASE_URL, status_code=404)
+
+ v2_auth = identity.V2Password(self.V2_URL,
+ username=uuid.uuid4().hex,
+ password=uuid.uuid4().hex)
+
+ sess = session.Session(auth=v2_auth)
+
+ endpoint = sess.get_endpoint(service_type=self.IDENTITY,
+ interface='public',
+ version=(3, 0))
+
+ self.assertEqual(self.V2_URL, endpoint)
+
+
+class GenericPlugin(base.BaseAuthPlugin):
+
+ BAD_TOKEN = uuid.uuid4().hex
+
+ def __init__(self):
+ super(GenericPlugin, self).__init__()
+
+ self.endpoint = 'http://keystone.host:5000'
+
+ self.headers = {'headerA': 'valueA',
+ 'headerB': 'valueB'}
+
+ def url(self, prefix):
+ return '%s/%s' % (self.endpoint, prefix)
+
+ def get_token(self, session, **kwargs):
+ # NOTE(jamielennox): by specifying get_headers this should not be used
+ return self.BAD_TOKEN
+
+ def get_headers(self, session, **kwargs):
+ return self.headers
+
+ def get_endpoint(self, session, **kwargs):
+ return self.endpoint
+
+
+class GenericAuthPluginTests(utils.TestCase):
+
+ # filter doesn't matter to GenericPlugin, but we have to specify one
+ ENDPOINT_FILTER = {uuid.uuid4().hex: uuid.uuid4().hex}
+
+ def setUp(self):
+ super(GenericAuthPluginTests, self).setUp()
+ self.auth = GenericPlugin()
+ self.session = session.Session(auth=self.auth)
+
+ def test_setting_headers(self):
+ text = uuid.uuid4().hex
+ self.stub_url('GET', base_url=self.auth.url('prefix'), text=text)
+
+ resp = self.session.get('prefix', endpoint_filter=self.ENDPOINT_FILTER)
+
+ self.assertEqual(text, resp.text)
+
+ for k, v in six.iteritems(self.auth.headers):
+ self.assertRequestHeaderEqual(k, v)
+
+ self.assertIsNone(self.session.get_token())
+ self.assertEqual(self.auth.headers,
+ self.session.get_auth_headers())
+ self.assertNotIn('X-Auth-Token', self.requests.last_request.headers)
diff --git a/keystoneclient/tests/unit/auth/test_identity_v2.py b/keystoneclient/tests/unit/auth/test_identity_v2.py
new file mode 100644
index 0000000..6d432a7
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_identity_v2.py
@@ -0,0 +1,295 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import uuid
+
+from keystoneclient.auth.identity import v2
+from keystoneclient import exceptions
+from keystoneclient import session
+from keystoneclient.tests.unit import utils
+
+
+class V2IdentityPlugin(utils.TestCase):
+
+ 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')
+
+ TEST_PASS = 'password'
+
+ 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": TEST_ADMIN_URL,
+ "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(V2IdentityPlugin, 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,
+ },
+ }
+
+ def stub_auth(self, **kwargs):
+ self.stub_url('POST', ['tokens'], **kwargs)
+
+ def test_authenticate_with_username_password(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ self.assertIsNone(a.user_id)
+ s = session.Session(a)
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
+ 'password': self.TEST_PASS}}}
+ self.assertRequestBodyIs(json=req)
+ self.assertRequestHeaderEqual('Content-Type', 'application/json')
+ self.assertRequestHeaderEqual('Accept', 'application/json')
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_authenticate_with_user_id_password(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v2.Password(self.TEST_URL, user_id=self.TEST_USER,
+ password=self.TEST_PASS)
+ self.assertIsNone(a.username)
+ s = session.Session(a)
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER,
+ 'password': self.TEST_PASS}}}
+ self.assertRequestBodyIs(json=req)
+ self.assertRequestHeaderEqual('Content-Type', 'application/json')
+ self.assertRequestHeaderEqual('Accept', 'application/json')
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_authenticate_with_username_password_scoped(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID)
+ self.assertIsNone(a.user_id)
+ s = session.Session(a)
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
+ 'password': self.TEST_PASS},
+ 'tenantId': self.TEST_TENANT_ID}}
+ self.assertRequestBodyIs(json=req)
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_authenticate_with_user_id_password_scoped(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v2.Password(self.TEST_URL, user_id=self.TEST_USER,
+ password=self.TEST_PASS, tenant_id=self.TEST_TENANT_ID)
+ self.assertIsNone(a.username)
+ s = session.Session(a)
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'passwordCredentials': {'userId': self.TEST_USER,
+ 'password': self.TEST_PASS},
+ 'tenantId': self.TEST_TENANT_ID}}
+ self.assertRequestBodyIs(json=req)
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_authenticate_with_token(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v2.Token(self.TEST_URL, 'foo')
+ s = session.Session(a)
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'token': {'id': 'foo'}}}
+ self.assertRequestBodyIs(json=req)
+ self.assertRequestHeaderEqual('x-Auth-Token', 'foo')
+ self.assertRequestHeaderEqual('Content-Type', 'application/json')
+ self.assertRequestHeaderEqual('Accept', 'application/json')
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_with_trust_id(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS, trust_id='trust')
+ s = session.Session(a)
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'passwordCredentials': {'username': self.TEST_USER,
+ 'password': self.TEST_PASS},
+ 'trust_id': 'trust'}}
+
+ self.assertRequestBodyIs(json=req)
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def _do_service_url_test(self, base_url, endpoint_filter):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url('GET', ['path'],
+ base_url=base_url,
+ text='SUCCESS', status_code=200)
+
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ resp = s.get('/path', endpoint_filter=endpoint_filter)
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(self.requests.last_request.url, base_url + '/path')
+
+ def test_service_url(self):
+ endpoint_filter = {'service_type': 'compute',
+ 'interface': 'admin',
+ 'service_name': 'nova'}
+ self._do_service_url_test('http://nova/novapi/admin', endpoint_filter)
+
+ def test_service_url_defaults_to_public(self):
+ endpoint_filter = {'service_type': 'compute'}
+ self._do_service_url_test('http://nova/novapi/public', endpoint_filter)
+
+ def test_endpoint_filter_without_service_type_fails(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertRaises(exceptions.EndpointNotFound, s.get, '/path',
+ endpoint_filter={'interface': 'admin'})
+
+ def test_full_url_overrides_endpoint_filter(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url('GET', [],
+ base_url='http://testurl/',
+ text='SUCCESS', status_code=200)
+
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ resp = s.get('http://testurl/',
+ endpoint_filter={'service_type': 'compute'})
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.text, 'SUCCESS')
+
+ def test_invalid_auth_response_dict(self):
+ self.stub_auth(json={'hello': 'world'})
+
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
+ authenticated=True)
+
+ def test_invalid_auth_response_type(self):
+ self.stub_url('POST', ['tokens'], text='testdata')
+
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
+ authenticated=True)
+
+ def test_invalidate_response(self):
+ resp_data1 = copy.deepcopy(self.TEST_RESPONSE_DICT)
+ resp_data2 = copy.deepcopy(self.TEST_RESPONSE_DICT)
+
+ resp_data1['access']['token']['id'] = 'token1'
+ resp_data2['access']['token']['id'] = 'token2'
+
+ auth_responses = [{'json': resp_data1}, {'json': resp_data2}]
+ self.stub_auth(response_list=auth_responses)
+
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertEqual('token1', s.get_token())
+ self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers())
+
+ a.invalidate()
+ self.assertEqual('token2', s.get_token())
+ self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers())
+
+ def test_doesnt_log_password(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ password = uuid.uuid4().hex
+
+ a = v2.Password(self.TEST_URL, username=self.TEST_USER,
+ password=password)
+ s = session.Session(auth=a)
+ self.assertEqual(self.TEST_TOKEN, s.get_token())
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+ self.assertNotIn(password, self.logger.output)
+
+ def test_password_with_no_user_id_or_name(self):
+ self.assertRaises(TypeError,
+ v2.Password, self.TEST_URL, password=self.TEST_PASS)
diff --git a/keystoneclient/tests/unit/auth/test_identity_v3.py b/keystoneclient/tests/unit/auth/test_identity_v3.py
new file mode 100644
index 0000000..29cbb0e
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_identity_v3.py
@@ -0,0 +1,490 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import uuid
+
+from keystoneclient import access
+from keystoneclient.auth.identity import v3
+from keystoneclient import client
+from keystoneclient import exceptions
+from keystoneclient import fixture
+from keystoneclient import session
+from keystoneclient.tests.unit import utils
+
+
+class V3IdentityPlugin(utils.TestCase):
+
+ 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')
+
+ TEST_PASS = 'password'
+
+ 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",
+ "name": "nova",
+ }, {
+ "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": TEST_ADMIN_URL,
+ "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(V3IdentityPlugin, self).setUp()
+
+ V3_URL = "%sv3" % self.TEST_URL
+ self.TEST_DISCOVERY_RESPONSE = {
+ 'versions': {'values': [fixture.V3Discovery(V3_URL)]}}
+
+ 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_PROJECTS_RESPONSE = {
+ "projects": [
+ {
+ "domain_id": "1789d1",
+ "enabled": "True",
+ "id": "263fd9",
+ "links": {
+ "self": "https://identity:5000/v3/projects/263fd9"
+ },
+ "name": "Dev Group A"
+ },
+ {
+ "domain_id": "1789d1",
+ "enabled": "True",
+ "id": "e56ad3",
+ "links": {
+ "self": "https://identity:5000/v3/projects/e56ad3"
+ },
+ "name": "Dev Group B"
+ }
+ ],
+ "links": {
+ "self": "https://identity:5000/v3/projects",
+ }
+ }
+
+ def stub_auth(self, subject_token=None, **kwargs):
+ if not subject_token:
+ subject_token = self.TEST_TOKEN
+
+ self.stub_url('POST', ['auth', 'tokens'],
+ headers={'X-Subject-Token': subject_token}, **kwargs)
+
+ def test_authenticate_with_username_password(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v3.Password(self.TEST_URL,
+ username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'identity':
+ {'methods': ['password'],
+ 'password': {'user': {'name': self.TEST_USER,
+ 'password': self.TEST_PASS}}}}}
+
+ self.assertRequestBodyIs(json=req)
+ self.assertRequestHeaderEqual('Content-Type', 'application/json')
+ self.assertRequestHeaderEqual('Accept', 'application/json')
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_authenticate_with_username_password_unscoped(self):
+ del self.TEST_RESPONSE_DICT['token']['catalog']
+ del self.TEST_RESPONSE_DICT['token']['project']
+
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url(method="GET", json=self.TEST_DISCOVERY_RESPONSE)
+ test_user_id = self.TEST_RESPONSE_DICT['token']['user']['id']
+ self.stub_url(method="GET",
+ json=self.TEST_PROJECTS_RESPONSE,
+ parts=['users', test_user_id, 'projects'])
+
+ a = v3.Password(self.TEST_URL,
+ username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+ cs = client.Client(session=s, auth_url=self.TEST_URL)
+
+ # As a sanity check on the auth_ref, make sure client has the
+ # proper user id, that it fetches the right project response
+ self.assertEqual(test_user_id, a.auth_ref.user_id)
+ t = cs.projects.list(user=a.auth_ref.user_id)
+ self.assertEqual(2, len(t))
+
+ def test_authenticate_with_username_password_domain_scoped(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS, domain_id=self.TEST_DOMAIN_ID)
+ s = session.Session(a)
+
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'identity':
+ {'methods': ['password'],
+ 'password': {'user': {'name': self.TEST_USER,
+ 'password': self.TEST_PASS}}},
+ 'scope': {'domain': {'id': self.TEST_DOMAIN_ID}}}}
+ self.assertRequestBodyIs(json=req)
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_authenticate_with_username_password_project_scoped(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS,
+ project_id=self.TEST_DOMAIN_ID)
+ s = session.Session(a)
+
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'identity':
+ {'methods': ['password'],
+ 'password': {'user': {'name': self.TEST_USER,
+ 'password': self.TEST_PASS}}},
+ 'scope': {'project': {'id': self.TEST_DOMAIN_ID}}}}
+ self.assertRequestBodyIs(json=req)
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+ self.assertEqual(s.auth.auth_ref.project_id, self.TEST_DOMAIN_ID)
+
+ def test_authenticate_with_token(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v3.Token(self.TEST_URL, self.TEST_TOKEN)
+ s = session.Session(auth=a)
+
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'identity':
+ {'methods': ['token'],
+ 'token': {'id': self.TEST_TOKEN}}}}
+
+ self.assertRequestBodyIs(json=req)
+
+ self.assertRequestHeaderEqual('Content-Type', 'application/json')
+ self.assertRequestHeaderEqual('Accept', 'application/json')
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_with_expired(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+
+ d = copy.deepcopy(self.TEST_RESPONSE_DICT)
+ d['token']['expires_at'] = '2000-01-01T00:00:10.000123Z'
+
+ a = v3.Password(self.TEST_URL, username='username',
+ password='password')
+ a.auth_ref = access.AccessInfo.factory(body=d)
+ s = session.Session(auth=a)
+
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ self.assertEqual(a.auth_ref['expires_at'],
+ self.TEST_RESPONSE_DICT['token']['expires_at'])
+
+ def test_with_domain_and_project_scoping(self):
+ a = v3.Password(self.TEST_URL, username='username',
+ password='password', project_id='project',
+ domain_id='domain')
+
+ self.assertRaises(exceptions.AuthorizationFailure,
+ a.get_token, None)
+ self.assertRaises(exceptions.AuthorizationFailure,
+ a.get_headers, None)
+
+ def test_with_trust_id(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS, trust_id='trust')
+ s = session.Session(a)
+
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'identity':
+ {'methods': ['password'],
+ 'password': {'user': {'name': self.TEST_USER,
+ 'password': self.TEST_PASS}}},
+ 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}}
+ self.assertRequestBodyIs(json=req)
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_with_multiple_mechanisms_factory(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ p = v3.PasswordMethod(username=self.TEST_USER, password=self.TEST_PASS)
+ t = v3.TokenMethod(token='foo')
+ a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust')
+ s = session.Session(a)
+
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'identity':
+ {'methods': ['password', 'token'],
+ 'password': {'user': {'name': self.TEST_USER,
+ 'password': self.TEST_PASS}},
+ 'token': {'id': 'foo'}},
+ 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}}
+ self.assertRequestBodyIs(json=req)
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_with_multiple_mechanisms(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ p = v3.PasswordMethod(username=self.TEST_USER,
+ password=self.TEST_PASS)
+ t = v3.TokenMethod(token='foo')
+ a = v3.Auth(self.TEST_URL, [p, t], trust_id='trust')
+ s = session.Session(auth=a)
+
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ req = {'auth': {'identity':
+ {'methods': ['password', 'token'],
+ 'password': {'user': {'name': self.TEST_USER,
+ 'password': self.TEST_PASS}},
+ 'token': {'id': 'foo'}},
+ 'scope': {'OS-TRUST:trust': {'id': 'trust'}}}}
+ self.assertRequestBodyIs(json=req)
+ self.assertEqual(s.auth.auth_ref.auth_token, self.TEST_TOKEN)
+
+ def test_with_multiple_scopes(self):
+ s = session.Session()
+
+ a = v3.Password(self.TEST_URL,
+ username=self.TEST_USER, password=self.TEST_PASS,
+ domain_id='x', project_id='x')
+ self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s)
+
+ a = v3.Password(self.TEST_URL,
+ username=self.TEST_USER, password=self.TEST_PASS,
+ domain_id='x', trust_id='x')
+ self.assertRaises(exceptions.AuthorizationFailure, a.get_auth_ref, s)
+
+ def _do_service_url_test(self, base_url, endpoint_filter):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url('GET', ['path'],
+ base_url=base_url,
+ text='SUCCESS', status_code=200)
+
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ resp = s.get('/path', endpoint_filter=endpoint_filter)
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(self.requests.last_request.url, base_url + '/path')
+
+ def test_service_url(self):
+ endpoint_filter = {'service_type': 'compute',
+ 'interface': 'admin',
+ 'service_name': 'nova'}
+ self._do_service_url_test('http://nova/novapi/admin', endpoint_filter)
+
+ def test_service_url_defaults_to_public(self):
+ endpoint_filter = {'service_type': 'compute'}
+ self._do_service_url_test('http://nova/novapi/public', endpoint_filter)
+
+ def test_endpoint_filter_without_service_type_fails(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertRaises(exceptions.EndpointNotFound, s.get, '/path',
+ endpoint_filter={'interface': 'admin'})
+
+ def test_full_url_overrides_endpoint_filter(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url('GET', [],
+ base_url='http://testurl/',
+ text='SUCCESS', status_code=200)
+
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ resp = s.get('http://testurl/',
+ endpoint_filter={'service_type': 'compute'})
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.text, 'SUCCESS')
+
+ def test_invalid_auth_response_dict(self):
+ self.stub_auth(json={'hello': 'world'})
+
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
+ authenticated=True)
+
+ def test_invalid_auth_response_type(self):
+ self.stub_url('POST', ['auth', 'tokens'], text='testdata')
+
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertRaises(exceptions.InvalidResponse, s.get, 'http://any',
+ authenticated=True)
+
+ def test_invalidate_response(self):
+ auth_responses = [{'status_code': 200, 'json': self.TEST_RESPONSE_DICT,
+ 'headers': {'X-Subject-Token': 'token1'}},
+ {'status_code': 200, 'json': self.TEST_RESPONSE_DICT,
+ 'headers': {'X-Subject-Token': 'token2'}}]
+
+ self.requests.post('%s/auth/tokens' % self.TEST_URL, auth_responses)
+
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=self.TEST_PASS)
+ s = session.Session(auth=a)
+
+ self.assertEqual('token1', s.get_token())
+ self.assertEqual({'X-Auth-Token': 'token1'}, s.get_auth_headers())
+ a.invalidate()
+ self.assertEqual('token2', s.get_token())
+ self.assertEqual({'X-Auth-Token': 'token2'}, s.get_auth_headers())
+
+ def test_doesnt_log_password(self):
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+
+ password = uuid.uuid4().hex
+ a = v3.Password(self.TEST_URL, username=self.TEST_USER,
+ password=password)
+ s = session.Session(a)
+ self.assertEqual(self.TEST_TOKEN, s.get_token())
+ self.assertEqual({'X-Auth-Token': self.TEST_TOKEN},
+ s.get_auth_headers())
+
+ self.assertNotIn(password, self.logger.output)
+
+ def test_sends_nocatalog(self):
+ del self.TEST_RESPONSE_DICT['token']['catalog']
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+
+ a = v3.Password(self.TEST_URL,
+ username=self.TEST_USER,
+ password=self.TEST_PASS,
+ include_catalog=False)
+ s = session.Session(auth=a)
+
+ s.get_token()
+
+ auth_url = self.TEST_URL + '/auth/tokens'
+ self.assertEqual(auth_url, a.token_url)
+ self.assertEqual(auth_url + '?nocatalog',
+ self.requests.last_request.url)
diff --git a/keystoneclient/tests/unit/auth/test_password.py b/keystoneclient/tests/unit/auth/test_password.py
new file mode 100644
index 0000000..c5067c0
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_password.py
@@ -0,0 +1,63 @@
+# 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.auth.identity.generic import password
+from keystoneclient.auth.identity import v2
+from keystoneclient.auth.identity import v3
+from keystoneclient.tests.unit.auth import utils
+
+
+class PasswordTests(utils.GenericPluginTestCase):
+
+ PLUGIN_CLASS = password.Password
+ V2_PLUGIN_CLASS = v2.Password
+ V3_PLUGIN_CLASS = v3.Password
+
+ def new_plugin(self, **kwargs):
+ kwargs.setdefault('username', uuid.uuid4().hex)
+ kwargs.setdefault('password', uuid.uuid4().hex)
+ return super(PasswordTests, self).new_plugin(**kwargs)
+
+ def test_with_user_domain_params(self):
+ self.stub_discovery()
+
+ self.assertCreateV3(domain_id=uuid.uuid4().hex,
+ user_domain_id=uuid.uuid4().hex)
+
+ def test_v3_user_params_v2_url(self):
+ self.stub_discovery(v3=False)
+ self.assertDiscoveryFailure(user_domain_id=uuid.uuid4().hex)
+
+ def test_options(self):
+ opts = [o.name for o in self.PLUGIN_CLASS.get_options()]
+
+ allowed_opts = ['user-name',
+ 'user-domain-id',
+ 'user-domain-name',
+ 'user-id',
+ 'password',
+
+ 'domain-id',
+ 'domain-name',
+ 'tenant-id',
+ 'tenant-name',
+ 'project-id',
+ 'project-name',
+ 'project-domain-id',
+ 'project-domain-name',
+ 'trust-id',
+ 'auth-url']
+
+ self.assertEqual(set(allowed_opts), set(opts))
+ self.assertEqual(len(allowed_opts), len(opts))
diff --git a/keystoneclient/tests/unit/auth/test_token.py b/keystoneclient/tests/unit/auth/test_token.py
new file mode 100644
index 0000000..928e2b2
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_token.py
@@ -0,0 +1,47 @@
+# 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.auth.identity.generic import token
+from keystoneclient.auth.identity import v2
+from keystoneclient.auth.identity import v3
+from keystoneclient.tests.unit.auth import utils
+
+
+class TokenTests(utils.GenericPluginTestCase):
+
+ PLUGIN_CLASS = token.Token
+ V2_PLUGIN_CLASS = v2.Token
+ V3_PLUGIN_CLASS = v3.Token
+
+ def new_plugin(self, **kwargs):
+ kwargs.setdefault('token', uuid.uuid4().hex)
+ return super(TokenTests, self).new_plugin(**kwargs)
+
+ def test_options(self):
+ opts = [o.name for o in self.PLUGIN_CLASS.get_options()]
+
+ allowed_opts = ['token',
+ 'domain-id',
+ 'domain-name',
+ 'tenant-id',
+ 'tenant-name',
+ 'project-id',
+ 'project-name',
+ 'project-domain-id',
+ 'project-domain-name',
+ 'trust-id',
+ 'auth-url']
+
+ self.assertEqual(set(allowed_opts), set(opts))
+ self.assertEqual(len(allowed_opts), len(opts))
diff --git a/keystoneclient/tests/unit/auth/test_token_endpoint.py b/keystoneclient/tests/unit/auth/test_token_endpoint.py
new file mode 100644
index 0000000..4b5f82c
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/test_token_endpoint.py
@@ -0,0 +1,63 @@
+# 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 testtools import matchers
+
+from keystoneclient.auth import token_endpoint
+from keystoneclient import session
+from keystoneclient.tests.unit import utils
+
+
+class TokenEndpointTest(utils.TestCase):
+
+ TEST_TOKEN = 'aToken'
+ TEST_URL = 'http://server/prefix'
+
+ def test_basic_case(self):
+ self.requests.get(self.TEST_URL, text='body')
+
+ a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN)
+ s = session.Session(auth=a)
+
+ data = s.get(self.TEST_URL, authenticated=True)
+
+ self.assertEqual(data.text, 'body')
+ self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN)
+
+ def test_basic_endpoint_case(self):
+ self.stub_url('GET', ['p'], text='body')
+ a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN)
+ s = session.Session(auth=a)
+
+ data = s.get('/p',
+ authenticated=True,
+ endpoint_filter={'service': 'identity'})
+
+ self.assertEqual(self.TEST_URL, a.get_endpoint(s))
+ self.assertEqual('body', data.text)
+ self.assertRequestHeaderEqual('X-Auth-Token', self.TEST_TOKEN)
+
+ def test_token_endpoint_options(self):
+ opt_names = [opt.name for opt in token_endpoint.Token.get_options()]
+
+ self.assertThat(opt_names, matchers.HasLength(2))
+
+ self.assertIn('token', opt_names)
+ self.assertIn('endpoint', opt_names)
+
+ def test_token_endpoint_user_id(self):
+ a = token_endpoint.Token(self.TEST_URL, self.TEST_TOKEN)
+ s = session.Session()
+
+ # we can't know this information about this sort of plugin
+ self.assertIsNone(a.get_user_id(s))
+ self.assertIsNone(a.get_project_id(s))
diff --git a/keystoneclient/tests/unit/auth/utils.py b/keystoneclient/tests/unit/auth/utils.py
new file mode 100644
index 0000000..6580c73
--- /dev/null
+++ b/keystoneclient/tests/unit/auth/utils.py
@@ -0,0 +1,200 @@
+# 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 functools
+import uuid
+
+import mock
+from oslo_config import cfg
+import six
+
+from keystoneclient import access
+from keystoneclient.auth import base
+from keystoneclient import exceptions
+from keystoneclient import fixture
+from keystoneclient import session
+from keystoneclient.tests.unit import utils
+
+
+class MockPlugin(base.BaseAuthPlugin):
+
+ INT_DESC = 'test int'
+ FLOAT_DESC = 'test float'
+ BOOL_DESC = 'test bool'
+ STR_DESC = 'test str'
+ STR_DEFAULT = uuid.uuid4().hex
+
+ def __init__(self, **kwargs):
+ self._data = kwargs
+
+ def __getitem__(self, key):
+ return self._data[key]
+
+ def get_token(self, *args, **kwargs):
+ return 'aToken'
+
+ def get_endpoint(self, *args, **kwargs):
+ return 'http://test'
+
+ @classmethod
+ def get_options(cls):
+ return [
+ cfg.IntOpt('a-int', default='3', help=cls.INT_DESC),
+ cfg.BoolOpt('a-bool', help=cls.BOOL_DESC),
+ cfg.FloatOpt('a-float', help=cls.FLOAT_DESC),
+ cfg.StrOpt('a-str', help=cls.STR_DESC, default=cls.STR_DEFAULT),
+ ]
+
+
+class MockManager(object):
+
+ def __init__(self, driver):
+ self.driver = driver
+
+
+def mock_plugin(f):
+ @functools.wraps(f)
+ def inner(*args, **kwargs):
+ with mock.patch.object(base, 'get_plugin_class') as m:
+ m.return_value = MockPlugin
+ args = list(args) + [m]
+ return f(*args, **kwargs)
+
+ return inner
+
+
+class TestCase(utils.TestCase):
+
+ GROUP = 'auth'
+ V2PASS = 'v2password'
+ V3TOKEN = 'v3token'
+
+ a_int = 88
+ a_float = 88.8
+ a_bool = False
+
+ TEST_VALS = {'a_int': a_int,
+ 'a_float': a_float,
+ 'a_bool': a_bool}
+
+ def assertTestVals(self, plugin, vals=TEST_VALS):
+ for k, v in six.iteritems(vals):
+ self.assertEqual(v, plugin[k])
+
+
+class GenericPluginTestCase(utils.TestCase):
+
+ TEST_URL = 'http://keystone.host:5000/'
+
+ # OVERRIDE THESE IN SUB CLASSES
+ PLUGIN_CLASS = None
+ V2_PLUGIN_CLASS = None
+ V3_PLUGIN_CLASS = None
+
+ def setUp(self):
+ super(GenericPluginTestCase, self).setUp()
+
+ self.token_v2 = fixture.V2Token()
+ self.token_v3 = fixture.V3Token()
+ self.token_v3_id = uuid.uuid4().hex
+ self.session = session.Session()
+
+ self.stub_url('POST', ['v2.0', 'tokens'], json=self.token_v2)
+ self.stub_url('POST', ['v3', 'auth', 'tokens'],
+ headers={'X-Subject-Token': self.token_v3_id},
+ json=self.token_v3)
+
+ def new_plugin(self, **kwargs):
+ kwargs.setdefault('auth_url', self.TEST_URL)
+ return self.PLUGIN_CLASS(**kwargs)
+
+ def stub_discovery(self, base_url=None, **kwargs):
+ kwargs.setdefault('href', self.TEST_URL)
+ disc = fixture.DiscoveryList(**kwargs)
+ self.stub_url('GET', json=disc, base_url=base_url, status_code=300)
+ return disc
+
+ def assertCreateV3(self, **kwargs):
+ auth = self.new_plugin(**kwargs)
+ auth_ref = auth.get_auth_ref(self.session)
+ self.assertIsInstance(auth_ref, access.AccessInfoV3)
+ self.assertEqual(self.TEST_URL + 'v3/auth/tokens',
+ self.requests.last_request.url)
+ self.assertIsInstance(auth._plugin, self.V3_PLUGIN_CLASS)
+ return auth
+
+ def assertCreateV2(self, **kwargs):
+ auth = self.new_plugin(**kwargs)
+ auth_ref = auth.get_auth_ref(self.session)
+ self.assertIsInstance(auth_ref, access.AccessInfoV2)
+ self.assertEqual(self.TEST_URL + 'v2.0/tokens',
+ self.requests.last_request.url)
+ self.assertIsInstance(auth._plugin, self.V2_PLUGIN_CLASS)
+ return auth
+
+ def assertDiscoveryFailure(self, **kwargs):
+ plugin = self.new_plugin(**kwargs)
+ self.assertRaises(exceptions.DiscoveryFailure,
+ plugin.get_auth_ref,
+ self.session)
+
+ def test_create_v3_if_domain_params(self):
+ self.stub_discovery()
+
+ self.assertCreateV3(domain_id=uuid.uuid4().hex)
+ self.assertCreateV3(domain_name=uuid.uuid4().hex)
+ self.assertCreateV3(project_name=uuid.uuid4().hex,
+ project_domain_name=uuid.uuid4().hex)
+ self.assertCreateV3(project_name=uuid.uuid4().hex,
+ project_domain_id=uuid.uuid4().hex)
+
+ def test_create_v2_if_no_domain_params(self):
+ self.stub_discovery()
+ self.assertCreateV2()
+ self.assertCreateV2(project_id=uuid.uuid4().hex)
+ self.assertCreateV2(project_name=uuid.uuid4().hex)
+ self.assertCreateV2(tenant_id=uuid.uuid4().hex)
+ self.assertCreateV2(tenant_name=uuid.uuid4().hex)
+
+ def test_v3_params_v2_url(self):
+ self.stub_discovery(v3=False)
+ self.assertDiscoveryFailure(domain_name=uuid.uuid4().hex)
+
+ def test_v2_params_v3_url(self):
+ self.stub_discovery(v2=False)
+ self.assertCreateV3()
+
+ def test_no_urls(self):
+ self.stub_discovery(v2=False, v3=False)
+ self.assertDiscoveryFailure()
+
+ def test_path_based_url_v2(self):
+ self.stub_url('GET', ['v2.0'], status_code=403)
+ self.assertCreateV2(auth_url=self.TEST_URL + 'v2.0')
+
+ def test_path_based_url_v3(self):
+ self.stub_url('GET', ['v3'], status_code=403)
+ self.assertCreateV3(auth_url=self.TEST_URL + 'v3')
+
+ def test_disc_error_for_failure(self):
+ self.stub_url('GET', [], status_code=403)
+ self.assertDiscoveryFailure()
+
+ def test_v3_plugin_from_failure(self):
+ url = self.TEST_URL + 'v3'
+ self.stub_url('GET', [], base_url=url, status_code=403)
+ self.assertCreateV3(auth_url=url)
+
+ def test_unknown_discovery_version(self):
+ # make a v4 entry that's mostly the same as a v3
+ self.stub_discovery(v2=False, v3_id='v4.0')
+ self.assertDiscoveryFailure()
diff --git a/keystoneclient/tests/unit/client_fixtures.py b/keystoneclient/tests/unit/client_fixtures.py
new file mode 100644
index 0000000..b226e32
--- /dev/null
+++ b/keystoneclient/tests/unit/client_fixtures.py
@@ -0,0 +1,597 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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
+
+import fixtures
+from oslo_serialization import jsonutils
+from oslo_utils import timeutils
+import six
+import testresources
+
+from keystoneclient.common import cms
+from keystoneclient import utils
+
+
+TESTDIR = os.path.dirname(os.path.abspath(__file__))
+ROOTDIR = os.path.normpath(os.path.join(TESTDIR, '..', '..', '..'))
+CERTDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'certs')
+CMSDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'cms')
+KEYDIR = os.path.join(ROOTDIR, 'examples', 'pki', 'private')
+
+
+def _hash_signed_token_safe(signed_text, **kwargs):
+ if isinstance(signed_text, six.text_type):
+ signed_text = signed_text.encode('utf-8')
+ return utils.hash_signed_token(signed_text, **kwargs)
+
+
+class Examples(fixtures.Fixture):
+ """Example tokens and certs loaded from the examples directory.
+
+ To use this class correctly, the module needs to override the test suite
+ class to use testresources.OptimisingTestSuite (otherwise the files will
+ be read on every test). This is done by defining a load_tests function
+ in the module, like this:
+
+ def load_tests(loader, tests, pattern):
+ return testresources.OptimisingTestSuite(tests)
+
+ (see http://docs.python.org/2/library/unittest.html#load-tests-protocol )
+
+ """
+
+ def setUp(self):
+ super(Examples, self).setUp()
+
+ # The data for several 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.json')) as f:
+ self.TOKEN_SCOPED_DATA = cms.cms_to_token(f.read())
+
+ with open(os.path.join(CMSDIR, 'auth_token_scoped.pem')) as f:
+ self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read())
+ self.SIGNED_TOKEN_SCOPED_HASH = _hash_signed_token_safe(
+ self.SIGNED_TOKEN_SCOPED)
+ self.SIGNED_TOKEN_SCOPED_HASH_SHA256 = _hash_signed_token_safe(
+ self.SIGNED_TOKEN_SCOPED, mode='sha256')
+ with open(os.path.join(CMSDIR, 'auth_token_unscoped.pem')) as f:
+ self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pem')) as f:
+ self.SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read())
+ self.SIGNED_v3_TOKEN_SCOPED_HASH = _hash_signed_token_safe(
+ self.SIGNED_v3_TOKEN_SCOPED)
+ self.SIGNED_v3_TOKEN_SCOPED_HASH_SHA256 = _hash_signed_token_safe(
+ self.SIGNED_v3_TOKEN_SCOPED, mode='sha256')
+ with open(os.path.join(CMSDIR, 'auth_token_revoked.pem')) as f:
+ self.REVOKED_TOKEN = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'auth_token_scoped_expired.pem')) as f:
+ self.SIGNED_TOKEN_SCOPED_EXPIRED = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pem')) as f:
+ self.REVOKED_v3_TOKEN = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'auth_token_scoped.pkiz')) as f:
+ self.SIGNED_TOKEN_SCOPED_PKIZ = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'auth_token_unscoped.pkiz')) as f:
+ self.SIGNED_TOKEN_UNSCOPED_PKIZ = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'auth_v3_token_scoped.pkiz')) as f:
+ self.SIGNED_v3_TOKEN_SCOPED_PKIZ = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'auth_token_revoked.pkiz')) as f:
+ self.REVOKED_TOKEN_PKIZ = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR,
+ 'auth_token_scoped_expired.pkiz')) as f:
+ self.SIGNED_TOKEN_SCOPED_EXPIRED_PKIZ = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'auth_v3_token_revoked.pkiz')) as f:
+ self.REVOKED_v3_TOKEN_PKIZ = cms.cms_to_token(f.read())
+ with open(os.path.join(CMSDIR, 'revocation_list.json')) as f:
+ self.REVOCATION_LIST = jsonutils.loads(f.read())
+ with open(os.path.join(CMSDIR, 'revocation_list.pem')) as f:
+ self.SIGNED_REVOCATION_LIST = jsonutils.dumps({'signed': f.read()})
+
+ self.SIGNING_CERT_FILE = os.path.join(CERTDIR, 'signing_cert.pem')
+ with open(self.SIGNING_CERT_FILE) as f:
+ self.SIGNING_CERT = f.read()
+
+ self.KERBEROS_BIND = 'USER@REALM'
+
+ self.SIGNING_KEY_FILE = os.path.join(KEYDIR, 'signing_key.pem')
+ with open(self.SIGNING_KEY_FILE) as f:
+ self.SIGNING_KEY = f.read()
+
+ self.SIGNING_CA_FILE = os.path.join(CERTDIR, 'cacert.pem')
+ with open(self.SIGNING_CA_FILE) as f:
+ self.SIGNING_CA = f.read()
+
+ self.UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d"
+ self.UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df'
+ self.UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776'
+ self.UUID_TOKEN_BIND = '3fc54048ad64405c98225ce0897af7c5'
+ self.UUID_TOKEN_UNKNOWN_BIND = '8885fdf4d42e4fb9879e6379fa1eaf48'
+ self.VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726'
+ self.v3_UUID_TOKEN_DEFAULT = '5603457654b346fdbb93437bfe76f2f1'
+ self.v3_UUID_TOKEN_UNSCOPED = 'd34835fdaec447e695a0a024d84f8d79'
+ self.v3_UUID_TOKEN_DOMAIN_SCOPED = 'e8a7b63aaa4449f38f0c5c05c3581792'
+ self.v3_UUID_TOKEN_BIND = '2f61f73e1c854cbb9534c487f9bd63c2'
+ self.v3_UUID_TOKEN_UNKNOWN_BIND = '7ed9781b62cd4880b8d8c6788ab1d1e2'
+
+ revoked_token = self.REVOKED_TOKEN
+ if isinstance(revoked_token, six.text_type):
+ revoked_token = revoked_token.encode('utf-8')
+ self.REVOKED_TOKEN_HASH = utils.hash_signed_token(revoked_token)
+ self.REVOKED_TOKEN_HASH_SHA256 = utils.hash_signed_token(revoked_token,
+ mode='sha256')
+ self.REVOKED_TOKEN_LIST = (
+ {'revoked': [{'id': self.REVOKED_TOKEN_HASH,
+ 'expires': timeutils.utcnow()}]})
+ self.REVOKED_TOKEN_LIST_JSON = jsonutils.dumps(self.REVOKED_TOKEN_LIST)
+
+ revoked_v3_token = self.REVOKED_v3_TOKEN
+ if isinstance(revoked_v3_token, six.text_type):
+ revoked_v3_token = revoked_v3_token.encode('utf-8')
+ self.REVOKED_v3_TOKEN_HASH = utils.hash_signed_token(revoked_v3_token)
+ hash = utils.hash_signed_token(revoked_v3_token, mode='sha256')
+ self.REVOKED_v3_TOKEN_HASH_SHA256 = hash
+ self.REVOKED_v3_TOKEN_LIST = (
+ {'revoked': [{'id': self.REVOKED_v3_TOKEN_HASH,
+ 'expires': timeutils.utcnow()}]})
+ self.REVOKED_v3_TOKEN_LIST_JSON = jsonutils.dumps(
+ self.REVOKED_v3_TOKEN_LIST)
+
+ revoked_token_pkiz = self.REVOKED_TOKEN_PKIZ
+ if isinstance(revoked_token_pkiz, six.text_type):
+ revoked_token_pkiz = revoked_token_pkiz.encode('utf-8')
+ self.REVOKED_TOKEN_PKIZ_HASH = utils.hash_signed_token(
+ revoked_token_pkiz)
+ revoked_v3_token_pkiz = self.REVOKED_v3_TOKEN_PKIZ
+ if isinstance(revoked_v3_token_pkiz, six.text_type):
+ revoked_v3_token_pkiz = revoked_v3_token_pkiz.encode('utf-8')
+ self.REVOKED_v3_PKIZ_TOKEN_HASH = utils.hash_signed_token(
+ revoked_v3_token_pkiz)
+
+ self.REVOKED_TOKEN_PKIZ_LIST = (
+ {'revoked': [{'id': self.REVOKED_TOKEN_PKIZ_HASH,
+ 'expires': timeutils.utcnow()},
+ {'id': self.REVOKED_v3_PKIZ_TOKEN_HASH,
+ 'expires': timeutils.utcnow()},
+ ]})
+ self.REVOKED_TOKEN_PKIZ_LIST_JSON = jsonutils.dumps(
+ self.REVOKED_TOKEN_PKIZ_LIST)
+
+ self.SIGNED_TOKEN_SCOPED_KEY = cms.cms_hash_token(
+ self.SIGNED_TOKEN_SCOPED)
+ self.SIGNED_TOKEN_UNSCOPED_KEY = cms.cms_hash_token(
+ self.SIGNED_TOKEN_UNSCOPED)
+ self.SIGNED_v3_TOKEN_SCOPED_KEY = cms.cms_hash_token(
+ self.SIGNED_v3_TOKEN_SCOPED)
+
+ self.SIGNED_TOKEN_SCOPED_PKIZ_KEY = cms.cms_hash_token(
+ self.SIGNED_TOKEN_SCOPED_PKIZ)
+ self.SIGNED_TOKEN_UNSCOPED_PKIZ_KEY = cms.cms_hash_token(
+ self.SIGNED_TOKEN_UNSCOPED_PKIZ)
+ self.SIGNED_v3_TOKEN_SCOPED_PKIZ_KEY = cms.cms_hash_token(
+ self.SIGNED_v3_TOKEN_SCOPED_PKIZ)
+
+ self.INVALID_SIGNED_TOKEN = (
+ "MIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
+ "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
+ "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
+ "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "1111111111111111111111111111111111111111111111111111111111111111"
+ "2222222222222222222222222222222222222222222222222222222222222222"
+ "3333333333333333333333333333333333333333333333333333333333333333"
+ "4444444444444444444444444444444444444444444444444444444444444444"
+ "5555555555555555555555555555555555555555555555555555555555555555"
+ "6666666666666666666666666666666666666666666666666666666666666666"
+ "7777777777777777777777777777777777777777777777777777777777777777"
+ "8888888888888888888888888888888888888888888888888888888888888888"
+ "9999999999999999999999999999999999999999999999999999999999999999"
+ "0000000000000000000000000000000000000000000000000000000000000000")
+
+ self.INVALID_SIGNED_PKIZ_TOKEN = (
+ "PKIZ_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
+ "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
+ "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
+ "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "1111111111111111111111111111111111111111111111111111111111111111"
+ "2222222222222222222222222222222222222222222222222222222222222222"
+ "3333333333333333333333333333333333333333333333333333333333333333"
+ "4444444444444444444444444444444444444444444444444444444444444444"
+ "5555555555555555555555555555555555555555555555555555555555555555"
+ "6666666666666666666666666666666666666666666666666666666666666666"
+ "7777777777777777777777777777777777777777777777777777777777777777"
+ "8888888888888888888888888888888888888888888888888888888888888888"
+ "9999999999999999999999999999999999999999999999999999999999999999"
+ "0000000000000000000000000000000000000000000000000000000000000000")
+
+ # JSON responses keyed by token ID
+ self.TOKEN_RESPONSES = {
+ self.UUID_TOKEN_DEFAULT: {
+ 'access': {
+ 'token': {
+ 'id': self.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': {}
+ },
+ },
+ self.VALID_DIABLO_TOKEN: {
+ 'access': {
+ 'token': {
+ 'id': self.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'},
+ ],
+ },
+ },
+ },
+ self.UUID_TOKEN_UNSCOPED: {
+ 'access': {
+ 'token': {
+ 'id': self.UUID_TOKEN_UNSCOPED,
+ 'expires': '2020-01-01T00:00:10.000123Z',
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ },
+ },
+ self.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'},
+ ],
+ }
+ },
+ },
+ self.UUID_TOKEN_BIND: {
+ 'access': {
+ 'token': {
+ 'bind': {'kerberos': self.KERBEROS_BIND},
+ 'id': self.UUID_TOKEN_BIND,
+ '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': {}
+ },
+ },
+ self.UUID_TOKEN_UNKNOWN_BIND: {
+ 'access': {
+ 'token': {
+ 'bind': {'FOO': 'BAR'},
+ 'id': self.UUID_TOKEN_UNKNOWN_BIND,
+ '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': {}
+ },
+ },
+ self.v3_UUID_TOKEN_DEFAULT: {
+ 'token': {
+ 'expires_at': '2020-01-01T00:00:10.000123Z',
+ 'methods': ['password'],
+ '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': {}
+ }
+ },
+ self.v3_UUID_TOKEN_UNSCOPED: {
+ 'token': {
+ 'expires_at': '2020-01-01T00:00:10.000123Z',
+ 'methods': ['password'],
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'domain': {
+ 'id': 'domain_id1',
+ 'name': 'domain_name1'
+ }
+ }
+ }
+ },
+ self.v3_UUID_TOKEN_DOMAIN_SCOPED: {
+ 'token': {
+ 'expires_at': '2020-01-01T00:00:10.000123Z',
+ 'methods': ['password'],
+ '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': {}
+ }
+ },
+ self.SIGNED_TOKEN_SCOPED_KEY: {
+ 'access': {
+ 'token': {
+ 'id': self.SIGNED_TOKEN_SCOPED_KEY,
+ 'expires': '2020-01-01T00:00:10.000123Z',
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'tenantId': 'tenant_id1',
+ 'tenantName': 'tenant_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ },
+ },
+ self.SIGNED_TOKEN_UNSCOPED_KEY: {
+ 'access': {
+ 'token': {
+ 'id': self.SIGNED_TOKEN_UNSCOPED_KEY,
+ 'expires': '2020-01-01T00:00:10.000123Z',
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ },
+ },
+ self.SIGNED_v3_TOKEN_SCOPED_KEY: {
+ 'token': {
+ 'expires_at': '2020-01-01T00:00:10.000123Z',
+ 'methods': ['password'],
+ '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': {}
+ }
+ },
+ self.v3_UUID_TOKEN_BIND: {
+ 'token': {
+ 'bind': {'kerberos': self.KERBEROS_BIND},
+ 'methods': ['password'],
+ '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': {}
+ }
+ },
+ self.v3_UUID_TOKEN_UNKNOWN_BIND: {
+ 'token': {
+ 'bind': {'FOO': 'BAR'},
+ 'expires_at': '2020-01-01T00:00:10.000123Z',
+ 'methods': ['password'],
+ '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': {}
+ }
+ },
+ }
+ self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_PKIZ_KEY] = (
+ self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY])
+ self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_PKIZ_KEY] = (
+ self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED_KEY])
+ self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_PKIZ_KEY] = (
+ self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY])
+
+ self.JSON_TOKEN_RESPONSES = dict([(k, jsonutils.dumps(v)) for k, v in
+ six.iteritems(self.TOKEN_RESPONSES)])
+
+
+EXAMPLES_RESOURCE = testresources.FixtureResource(Examples())
+
+
+class HackingCode(fixtures.Fixture):
+ """A fixture to house the various code examples for the keystoneclient
+ hacking style checks.
+ """
+
+ oslo_namespace_imports = {
+ 'code': """
+ import oslo.utils
+ import oslo_utils
+ import oslo.utils.encodeutils
+ import oslo_utils.encodeutils
+ from oslo import utils
+ from oslo.utils import encodeutils
+ from oslo_utils import encodeutils
+
+ import oslo.serialization
+ import oslo_serialization
+ import oslo.serialization.jsonutils
+ import oslo_serialization.jsonutils
+ from oslo import serialization
+ from oslo.serialization import jsonutils
+ from oslo_serialization import jsonutils
+
+ import oslo.config
+ import oslo_config
+ import oslo.config.cfg
+ import oslo_config.cfg
+ from oslo import config
+ from oslo.config import cfg
+ from oslo_config import cfg
+
+ import oslo.i18n
+ import oslo_i18n
+ import oslo.i18n.log
+ import oslo_i18n.log
+ from oslo import i18n
+ from oslo.i18n import log
+ from oslo_i18n import log
+ """,
+ 'expected_errors': [
+ (1, 0, 'K333'),
+ (3, 0, 'K333'),
+ (5, 0, 'K333'),
+ (6, 0, 'K333'),
+ (9, 0, 'K333'),
+ (11, 0, 'K333'),
+ (13, 0, 'K333'),
+ (14, 0, 'K333'),
+ (17, 0, 'K333'),
+ (19, 0, 'K333'),
+ (21, 0, 'K333'),
+ (22, 0, 'K333'),
+ (25, 0, 'K333'),
+ (27, 0, 'K333'),
+ (29, 0, 'K333'),
+ (30, 0, 'K333'),
+ ],
+ }
diff --git a/keystoneclient/tests/unit/generic/__init__.py b/keystoneclient/tests/unit/generic/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/keystoneclient/tests/unit/generic/__init__.py
diff --git a/keystoneclient/tests/unit/generic/test_client.py b/keystoneclient/tests/unit/generic/test_client.py
new file mode 100644
index 0000000..e56e3df
--- /dev/null
+++ b/keystoneclient/tests/unit/generic/test_client.py
@@ -0,0 +1,64 @@
+# Copyright 2014 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils
+
+from keystoneclient.generic import client
+from keystoneclient.tests.unit import utils
+
+BASE_HOST = 'http://keystone.example.com'
+BASE_URL = "%s:5000/" % BASE_HOST
+V2_URL = "%sv2.0" % BASE_URL
+
+EXTENSION_NAMESPACE = "http://docs.openstack.org/identity/api/ext/OS-FAKE/v1.0"
+EXTENSION_DESCRIBED = {"href": "https://github.com/openstack/identity-api",
+ "rel": "describedby",
+ "type": "text/html"}
+
+EXTENSION_ALIAS_FOO = "OS-FAKE-FOO"
+EXTENSION_NAME_FOO = "OpenStack Keystone Fake Extension Foo"
+EXTENSION_FOO = {"alias": EXTENSION_ALIAS_FOO,
+ "description": "Fake Foo extension to V2.0 API.",
+ "links": [EXTENSION_DESCRIBED],
+ "name": EXTENSION_NAME_FOO,
+ "namespace": EXTENSION_NAMESPACE,
+ "updated": '2014-01-08T00:00:00Z'}
+
+EXTENSION_ALIAS_BAR = "OS-FAKE-BAR"
+EXTENSION_NAME_BAR = "OpenStack Keystone Fake Extension Bar"
+EXTENSION_BAR = {"alias": EXTENSION_ALIAS_BAR,
+ "description": "Fake Bar extension to V2.0 API.",
+ "links": [EXTENSION_DESCRIBED],
+ "name": EXTENSION_NAME_BAR,
+ "namespace": EXTENSION_NAMESPACE,
+ "updated": '2014-01-08T00:00:00Z'}
+
+
+def _create_extension_list(extensions):
+ return jsonutils.dumps({'extensions': {'values': extensions}})
+
+
+EXTENSION_LIST = _create_extension_list([EXTENSION_FOO, EXTENSION_BAR])
+
+
+class ClientDiscoveryTests(utils.TestCase):
+
+ def test_discover_extensions_v2(self):
+ self.requests.get("%s/extensions" % V2_URL, text=EXTENSION_LIST)
+ extensions = client.Client().discover_extensions(url=V2_URL)
+ self.assertIn(EXTENSION_ALIAS_FOO, extensions)
+ self.assertEqual(extensions[EXTENSION_ALIAS_FOO], EXTENSION_NAME_FOO)
+ self.assertIn(EXTENSION_ALIAS_BAR, extensions)
+ self.assertEqual(extensions[EXTENSION_ALIAS_BAR], EXTENSION_NAME_BAR)
diff --git a/keystoneclient/tests/unit/generic/test_shell.py b/keystoneclient/tests/unit/generic/test_shell.py
new file mode 100644
index 0000000..ad457aa
--- /dev/null
+++ b/keystoneclient/tests/unit/generic/test_shell.py
@@ -0,0 +1,129 @@
+# Copyright 2014 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import mock
+from six import moves
+
+from keystoneclient.generic import shell
+from keystoneclient.tests.unit import utils
+
+
+class DoDiscoverTest(utils.TestCase):
+ """Unit tests for do_discover function."""
+ foo_version = {
+ 'id': 'foo_id',
+ 'status': 'foo_status',
+ 'url': 'http://foo/url',
+ }
+ bar_version = {
+ 'id': 'bar_id',
+ 'status': 'bar_status',
+ 'url': 'http://bar/url',
+ }
+ foo_extension = {
+ 'foo': 'foo_extension',
+ 'message': 'extension_message',
+ 'bar': 'bar_extension',
+ }
+ stub_message = 'This is a stub message'
+
+ def setUp(self):
+ super(DoDiscoverTest, self).setUp()
+
+ self.client_mock = mock.Mock()
+ self.client_mock.discover.return_value = {}
+
+ def _execute_discover(self):
+ """Call do_discover function and capture output
+
+ :returns: captured output is returned
+ """
+ with mock.patch('sys.stdout',
+ new_callable=moves.StringIO) as mock_stdout:
+ shell.do_discover(self.client_mock, args=None)
+ output = mock_stdout.getvalue()
+ return output
+
+ def _check_version_print(self, output, version):
+ """Checks all api version's parameters are present in output."""
+ self.assertIn(version['id'], output)
+ self.assertIn(version['status'], output)
+ self.assertIn(version['url'], output)
+
+ def test_no_keystones(self):
+ # No servers configured for client,
+ # corresponding message should be printed
+ output = self._execute_discover()
+ self.assertIn('No Keystone-compatible endpoint found', output)
+
+ def test_endpoint(self):
+ # Endpoint is configured for client,
+ # client's discover method should be called with that value
+ self.client_mock.endpoint = 'Some non-empty value'
+ shell.do_discover(self.client_mock, args=None)
+ self.client_mock.discover.assert_called_with(self.client_mock.endpoint)
+
+ def test_auth_url(self):
+ # No endpoint provided for client, but there is an auth_url
+ # client's discover method should be called with auth_url value
+ self.client_mock.endpoint = False
+ self.client_mock.auth_url = 'Some non-empty value'
+ shell.do_discover(self.client_mock, args=None)
+ self.client_mock.discover.assert_called_with(self.client_mock.auth_url)
+
+ def test_empty(self):
+ # No endpoint or auth_url is configured for client.
+ # client.discover() should be called without parameters
+ self.client_mock.endpoint = False
+ self.client_mock.auth_url = False
+ shell.do_discover(self.client_mock, args=None)
+ self.client_mock.discover.assert_called_with()
+
+ def test_message(self):
+ # If client.discover() result contains message - it should be printed
+ self.client_mock.discover.return_value = {'message': self.stub_message}
+ output = self._execute_discover()
+ self.assertIn(self.stub_message, output)
+
+ def test_versions(self):
+ # Every version in client.discover() result should be printed
+ # and client.discover_extension() should be called on its url
+ self.client_mock.discover.return_value = {
+ 'foo': self.foo_version,
+ 'bar': self.bar_version,
+ }
+ self.client_mock.discover_extensions.return_value = {}
+ output = self._execute_discover()
+ self._check_version_print(output, self.foo_version)
+ self._check_version_print(output, self.bar_version)
+
+ discover_extension_calls = [
+ mock.call(self.foo_version['url']),
+ mock.call(self.bar_version['url']),
+ ]
+
+ self.client_mock.discover_extensions.assert_has_calls(
+ discover_extension_calls,
+ any_order=True)
+
+ def test_extensions(self):
+ # Every extension's parameters should be printed
+ # Extension's message should be omitted
+ self.client_mock.discover.return_value = {'foo': self.foo_version}
+ self.client_mock.discover_extensions.return_value = self.foo_extension
+ output = self._execute_discover()
+ self.assertIn(self.foo_extension['foo'], output)
+ self.assertIn(self.foo_extension['bar'], output)
+ self.assertNotIn(self.foo_extension['message'], output)
diff --git a/keystoneclient/tests/unit/test_auth_token_middleware.py b/keystoneclient/tests/unit/test_auth_token_middleware.py
new file mode 100644
index 0000000..f3b523d
--- /dev/null
+++ b/keystoneclient/tests/unit/test_auth_token_middleware.py
@@ -0,0 +1,1939 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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 calendar
+import datetime
+import json
+import os
+import shutil
+import stat
+import tempfile
+import time
+import uuid
+
+import fixtures
+import iso8601
+import mock
+from oslo_serialization import jsonutils
+from oslo_utils import timeutils
+from requests_mock.contrib import fixture as mock_fixture
+import six
+from six.moves.urllib import parse as urlparse
+import testresources
+import testtools
+from testtools import matchers
+import webob
+
+from keystoneclient import access
+from keystoneclient.common import cms
+from keystoneclient import exceptions
+from keystoneclient import fixture
+from keystoneclient.middleware import auth_token
+from keystoneclient.openstack.common import memorycache
+from keystoneclient.tests.unit import client_fixtures
+from keystoneclient.tests.unit import utils
+
+
+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_v2 = jsonutils.dumps(fixture.DiscoveryList(href=BASE_URI,
+ v3=False))
+VERSION_LIST_v3 = jsonutils.dumps(fixture.DiscoveryList(href=BASE_URI))
+
+ERROR_TOKEN = '7ae290c2a06244c4b41692eb4e9225f2'
+MEMCACHED_SERVERS = ['localhost:11211']
+MEMCACHED_AVAILABLE = None
+
+
+def memcached_available():
+ """Do a sanity check against memcached.
+
+ Returns ``True`` if the following conditions are met (otherwise, returns
+ ``False``):
+
+ - ``python-memcached`` is installed
+ - a usable ``memcached`` instance is available via ``MEMCACHED_SERVERS``
+ - the client is able to set and get a key/value pair
+
+ """
+ global MEMCACHED_AVAILABLE
+
+ if MEMCACHED_AVAILABLE is None:
+ try:
+ import memcache
+ c = memcache.Client(MEMCACHED_SERVERS)
+ c.set('ping', 'pong', time=1)
+ MEMCACHED_AVAILABLE = c.get('ping') == 'pong'
+ except ImportError:
+ MEMCACHED_AVAILABLE = False
+
+ return MEMCACHED_AVAILABLE
+
+
+def cleanup_revoked_file(filename):
+ try:
+ os.remove(filename)
+ except OSError:
+ pass
+
+
+class TimezoneFixture(fixtures.Fixture):
+ @staticmethod
+ def supported():
+ # tzset is only supported on Unix.
+ return hasattr(time, 'tzset')
+
+ def __init__(self, new_tz):
+ super(TimezoneFixture, self).__init__()
+ self.tz = new_tz
+ self.old_tz = os.environ.get('TZ')
+
+ def setUp(self):
+ super(TimezoneFixture, self).setUp()
+ if not self.supported():
+ raise NotImplementedError('timezone override is not supported.')
+ os.environ['TZ'] = self.tz
+ time.tzset()
+ self.addCleanup(self.cleanup)
+
+ def cleanup(self):
+ if self.old_tz is not None:
+ os.environ['TZ'] = self.old_tz
+ elif 'TZ' in os.environ:
+ del os.environ['TZ']
+ time.tzset()
+
+
+class TimeFixture(fixtures.Fixture):
+
+ def __init__(self, new_time, normalize=True):
+ super(TimeFixture, self).__init__()
+ if isinstance(new_time, six.string_types):
+ new_time = timeutils.parse_isotime(new_time)
+ if normalize:
+ new_time = timeutils.normalize_time(new_time)
+ self.new_time = new_time
+
+ def setUp(self):
+ super(TimeFixture, self).setUp()
+ timeutils.set_time_override(self.new_time)
+ self.addCleanup(timeutils.clear_time_override)
+
+
+class FakeApp(object):
+ """This represents a WSGI app protected by the auth_token middleware."""
+
+ SUCCESS = b'SUCCESS'
+
+ 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 = FakeApp.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 = {
+ 'identity_uri': 'https://keystone.example.com:1234/testadmin/',
+ 'signing_dir': client_fixtures.CERTDIR,
+ 'auth_version': auth_version,
+ 'auth_uri': 'https://keystone.example.com:1234',
+ }
+
+ self.auth_version = auth_version
+ self.response_status = None
+ self.response_headers = None
+
+ self.requests = self.useFixture(mock_fixture.Fixture())
+
+ def set_middleware(self, 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 expected_env:
+ self.expected_env.update(expected_env)
+
+ self.middleware = auth_token.AuthProtocol(
+ self.fake_app(self.expected_env), self.conf)
+ self.middleware._iso8601 = iso8601
+
+ with tempfile.NamedTemporaryFile(dir=self.middleware.signing_dirname,
+ delete=False) as f:
+ pass
+ self.middleware.revoked_file_name = f.name
+
+ self.addCleanup(cleanup_revoked_file,
+ self.middleware.revoked_file_name)
+
+ self.middleware.token_revocation_list = jsonutils.dumps(
+ {"revoked": [], "extra": "success"})
+
+ 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:
+ parts = urlparse.urlparse(self.requests.last_request.url)
+ self.assertEqual(path, parts.path)
+ else:
+ self.assertIsNone(self.requests.last_request)
+
+
+class MultiStepAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+ testresources.ResourcedTestCase):
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ 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.
+ self.requests.post("%s/v2.0/tokens" % BASE_URI, text=FAKE_ADMIN_TOKEN)
+
+ text = self.examples.SIGNED_REVOCATION_LIST
+ self.requests.get("%s/v2.0/tokens/revoked" % BASE_URI,
+ response_list=[{'status_code': 401}, {'text': text}])
+
+ fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list())
+ self.assertEqual(fetched_list, self.examples.REVOCATION_LIST)
+
+ # Check that 4 requests have been made
+ self.assertEqual(len(self.requests.request_history), 4)
+
+
+class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+ testresources.ResourcedTestCase):
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ """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)
+
+ self.requests.get("%s/" % BASE_URI,
+ text=VERSION_LIST_v2,
+ status_code=300)
+
+ self.requests.post("%s/v2.0/tokens" % BASE_URI, text=FAKE_ADMIN_TOKEN)
+
+ self.token_id = self.examples.VALID_DIABLO_TOKEN
+ token_response = self.examples.JSON_TOKEN_RESPONSES[self.token_id]
+
+ url = '%s/v2.0/tokens/%s' % (BASE_URI, self.token_id)
+ self.requests.get(url, text=token_response)
+
+ self.set_middleware()
+
+ 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.assertIn('keystone.token_info', req.environ)
+
+
+class NoMemcacheAuthToken(BaseAuthTokenMiddlewareTest):
+ """These tests will not have the memcache module available."""
+
+ def setUp(self):
+ super(NoMemcacheAuthToken, self).setUp()
+ self.useFixture(utils.DisableModuleFixture('memcache'))
+
+ def test_nomemcache(self):
+ conf = {
+ 'admin_token': 'admin_token1',
+ 'auth_host': 'keystone.example.com',
+ 'auth_port': 1234,
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'auth_uri': 'https://keystone.example.com:1234',
+ }
+
+ auth_token.AuthProtocol(FakeApp(), conf)
+
+
+class CachePoolTest(BaseAuthTokenMiddlewareTest):
+ def test_use_cache_from_env(self):
+ """If `swift.cache` is set in the environment and `cache` is set in the
+ config then the env cache is used.
+ """
+ env = {'swift.cache': 'CACHE_TEST'}
+ conf = {
+ 'cache': 'swift.cache'
+ }
+ self.set_middleware(conf=conf)
+ self.middleware._token_cache.initialize(env)
+ with self.middleware._token_cache._cache_pool.reserve() as cache:
+ self.assertEqual(cache, 'CACHE_TEST')
+
+ def test_not_use_cache_from_env(self):
+ """If `swift.cache` is set in the environment but `cache` isn't set in
+ the config then the env cache isn't used.
+ """
+ self.set_middleware()
+ env = {'swift.cache': 'CACHE_TEST'}
+ self.middleware._token_cache.initialize(env)
+ with self.middleware._token_cache._cache_pool.reserve() as cache:
+ self.assertNotEqual(cache, 'CACHE_TEST')
+
+ def test_multiple_context_managers_share_single_client(self):
+ self.set_middleware()
+ token_cache = self.middleware._token_cache
+ env = {}
+ token_cache.initialize(env)
+
+ caches = []
+
+ with token_cache._cache_pool.reserve() as cache:
+ caches.append(cache)
+
+ with token_cache._cache_pool.reserve() as cache:
+ caches.append(cache)
+
+ self.assertIs(caches[0], caches[1])
+ self.assertEqual(set(caches), set(token_cache._cache_pool))
+
+ def test_nested_context_managers_create_multiple_clients(self):
+ self.set_middleware()
+ env = {}
+ self.middleware._token_cache.initialize(env)
+ token_cache = self.middleware._token_cache
+
+ with token_cache._cache_pool.reserve() as outer_cache:
+ with token_cache._cache_pool.reserve() as inner_cache:
+ self.assertNotEqual(outer_cache, inner_cache)
+
+ self.assertEqual(
+ set([inner_cache, outer_cache]),
+ set(token_cache._cache_pool))
+
+
+class GeneralAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+ testresources.ResourcedTestCase):
+ """These tests are not affected by the token format
+ (see CommonAuthTokenMiddlewareTest).
+ """
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ 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_token_is_v2_accepts_v2(self):
+ token = self.examples.UUID_TOKEN_DEFAULT
+ token_response = self.examples.TOKEN_RESPONSES[token]
+ self.assertTrue(auth_token._token_is_v2(token_response))
+
+ def test_token_is_v2_rejects_v3(self):
+ token = self.examples.v3_UUID_TOKEN_DEFAULT
+ token_response = self.examples.TOKEN_RESPONSES[token]
+ self.assertFalse(auth_token._token_is_v2(token_response))
+
+ def test_token_is_v3_rejects_v2(self):
+ token = self.examples.UUID_TOKEN_DEFAULT
+ token_response = self.examples.TOKEN_RESPONSES[token]
+ self.assertFalse(auth_token._token_is_v3(token_response))
+
+ def test_token_is_v3_accepts_v3(self):
+ token = self.examples.v3_UUID_TOKEN_DEFAULT
+ token_response = self.examples.TOKEN_RESPONSES[token]
+ self.assertTrue(auth_token._token_is_v3(token_response))
+
+ @testtools.skipUnless(memcached_available(), 'memcached not available')
+ def test_encrypt_cache_data(self):
+ conf = {
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'memcache_security_strategy': 'encrypt',
+ 'memcache_secret_key': 'mysecret'
+ }
+ self.set_middleware(conf=conf)
+ token = b'my_token'
+ some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4)
+ expires = timeutils.strtime(some_time_later)
+ data = ('this_data', expires)
+ token_cache = self.middleware._token_cache
+ token_cache.initialize({})
+ token_cache._cache_store(token, data)
+ self.assertEqual(token_cache._cache_get(token), data[0])
+
+ @testtools.skipUnless(memcached_available(), 'memcached not available')
+ def test_sign_cache_data(self):
+ conf = {
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'memcache_security_strategy': 'mac',
+ 'memcache_secret_key': 'mysecret'
+ }
+ self.set_middleware(conf=conf)
+ token = b'my_token'
+ some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4)
+ expires = timeutils.strtime(some_time_later)
+ data = ('this_data', expires)
+ token_cache = self.middleware._token_cache
+ token_cache.initialize({})
+ token_cache._cache_store(token, data)
+ self.assertEqual(token_cache._cache_get(token), data[0])
+
+ @testtools.skipUnless(memcached_available(), 'memcached not available')
+ def test_no_memcache_protection(self):
+ conf = {
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'memcache_secret_key': 'mysecret'
+ }
+ self.set_middleware(conf=conf)
+ token = 'my_token'
+ some_time_later = timeutils.utcnow() + datetime.timedelta(hours=4)
+ expires = timeutils.strtime(some_time_later)
+ data = ('this_data', expires)
+ token_cache = self.middleware._token_cache
+ token_cache.initialize({})
+ token_cache._cache_store(token, data)
+ self.assertEqual(token_cache._cache_get(token), data[0])
+
+ def test_assert_valid_memcache_protection_config(self):
+ # test missing memcache_secret_key
+ conf = {
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'memcache_security_strategy': 'Encrypt'
+ }
+ self.assertRaises(auth_token.ConfigurationError, self.set_middleware,
+ conf=conf)
+ # test invalue memcache_security_strategy
+ conf = {
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'memcache_security_strategy': 'whatever'
+ }
+ self.assertRaises(auth_token.ConfigurationError, self.set_middleware,
+ conf=conf)
+ # test missing memcache_secret_key
+ conf = {
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'memcache_security_strategy': 'mac'
+ }
+ self.assertRaises(auth_token.ConfigurationError, self.set_middleware,
+ conf=conf)
+ conf = {
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'memcache_security_strategy': 'Encrypt',
+ 'memcache_secret_key': ''
+ }
+ self.assertRaises(auth_token.ConfigurationError, self.set_middleware,
+ conf=conf)
+ conf = {
+ 'memcached_servers': MEMCACHED_SERVERS,
+ 'memcache_security_strategy': 'mAc',
+ 'memcache_secret_key': ''
+ }
+ self.assertRaises(auth_token.ConfigurationError, self.set_middleware,
+ conf=conf)
+
+ def test_config_revocation_cache_timeout(self):
+ conf = {
+ 'revocation_cache_time': 24,
+ 'auth_uri': 'https://keystone.example.com:1234',
+ }
+ middleware = auth_token.AuthProtocol(self.fake_app, conf)
+ self.assertEqual(middleware.token_revocation_list_cache_timeout,
+ datetime.timedelta(seconds=24))
+
+ def test_conf_values_type_convert(self):
+ conf = {
+ 'revocation_cache_time': '24',
+ 'identity_uri': 'https://keystone.example.com:1234',
+ 'include_service_catalog': '0',
+ 'nonexsit_option': '0',
+ }
+
+ middleware = auth_token.AuthProtocol(self.fake_app, conf)
+ self.assertEqual(datetime.timedelta(seconds=24),
+ middleware.token_revocation_list_cache_timeout)
+ self.assertEqual(False, middleware.include_service_catalog)
+ self.assertEqual('https://keystone.example.com:1234',
+ middleware.identity_uri)
+ self.assertEqual('0', middleware.conf['nonexsit_option'])
+
+ def test_conf_values_type_convert_with_wrong_value(self):
+ conf = {
+ 'include_service_catalog': '123',
+ }
+ self.assertRaises(auth_token.ConfigurationError,
+ auth_token.AuthProtocol, self.fake_app, conf)
+
+
+class CommonAuthTokenMiddlewareTest(object):
+ """These tests are run once using v2 tokens and again using v3 tokens."""
+
+ 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):
+ del self.conf['identity_uri']
+ conf = {
+ 'auth_host': '2001:2013:1:f101::1',
+ 'auth_port': 1234,
+ 'auth_protocol': 'http',
+ 'auth_uri': None,
+ }
+ 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'))
+ else:
+ self.assertNotIn('X-Service-Catalog', req.headers)
+ self.assertEqual(body, [FakeApp.SUCCESS])
+ self.assertIn('keystone.token_info', req.environ)
+ return req
+
+ def test_valid_uuid_request(self):
+ for _ in range(2): # Do it twice because first result was cached.
+ token = self.token_dict['uuid_token_default']
+ self.assert_valid_request_200(token)
+ self.assert_valid_last_url(token)
+
+ def test_valid_uuid_request_with_auth_fragments(self):
+ del self.conf['identity_uri']
+ self.conf['auth_protocol'] = 'https'
+ self.conf['auth_host'] = 'keystone.example.com'
+ self.conf['auth_port'] = 1234
+ self.conf['auth_admin_prefix'] = '/testadmin'
+ self.set_middleware()
+ self.assert_valid_request_200(self.token_dict['uuid_token_default'])
+ self.assert_valid_last_url(self.token_dict['uuid_token_default'])
+
+ def _test_cache_revoked(self, token, revoked_form=None):
+ # When the token is cached and revoked, 401 is returned.
+ self.middleware.check_revocations_for_cached = True
+
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = token
+
+ # Token should be cached as ok after this.
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(200, self.response_status)
+
+ # Put it in revocation list.
+ self.middleware.token_revocation_list = self.get_revocation_list_json(
+ token_ids=[revoked_form or token])
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(401, self.response_status)
+
+ def test_cached_revoked_uuid(self):
+ # When the UUID token is cached and revoked, 401 is returned.
+ self._test_cache_revoked(self.token_dict['uuid_token_default'])
+
+ def test_valid_signed_request(self):
+ for _ in range(2): # Do it twice because first result was cached.
+ self.assert_valid_request_200(
+ self.token_dict['signed_token_scoped'])
+ # ensure that signed requests do not generate HTTP traffic
+ self.assertLastPath(None)
+
+ def test_valid_signed_compressed_request(self):
+ self.assert_valid_request_200(
+ self.token_dict['signed_token_scoped_pkiz'])
+ # 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 test_revoked_token_receives_401_sha256(self):
+ self.conf['hash_algorithms'] = ['sha256', 'md5']
+ self.set_middleware()
+ self.middleware.token_revocation_list = (
+ self.get_revocation_list_json(mode='sha256'))
+ 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 test_cached_revoked_pki(self):
+ # When the PKI token is cached and revoked, 401 is returned.
+ token = self.token_dict['signed_token_scoped']
+ revoked_form = cms.cms_hash_token(token)
+ self._test_cache_revoked(token, revoked_form)
+
+ def test_cached_revoked_pkiz(self):
+ # When the PKI token is cached and revoked, 401 is returned.
+ token = self.token_dict['signed_token_scoped_pkiz']
+ revoked_form = cms.cms_hash_token(token)
+ self._test_cache_revoked(token, revoked_form)
+
+ def test_revoked_token_receives_401_md5_secondary(self):
+ # When hash_algorithms has 'md5' as the secondary hash and the
+ # revocation list contains the md5 hash for a token, that token is
+ # considered revoked so returns 401.
+ self.conf['hash_algorithms'] = ['sha256', 'md5']
+ self.set_middleware()
+ 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 _test_revoked_hashed_token(self, token_key):
+ # If hash_algorithms is set as ['sha256', 'md5'],
+ # and check_revocations_for_cached is True,
+ # and a token is in the cache because it was successfully validated
+ # using the md5 hash, then
+ # if the token is in the revocation list by md5 hash, it'll be
+ # rejected and auth_token returns 401.
+ self.conf['hash_algorithms'] = ['sha256', 'md5']
+ self.conf['check_revocations_for_cached'] = True
+ self.set_middleware()
+
+ token = self.token_dict[token_key]
+
+ # Put the token in the revocation list.
+ token_hashed = cms.cms_hash_token(token)
+ self.middleware.token_revocation_list = self.get_revocation_list_json(
+ token_ids=[token_hashed])
+
+ # request is using the hashed token, is valid so goes in
+ # cache using the given hash.
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = token_hashed
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(200, self.response_status)
+
+ # This time use the PKI(Z) token
+ req.headers['X-Auth-Token'] = token
+ self.middleware(req.environ, self.start_fake_response)
+
+ # Should find the token in the cache and revocation list.
+ self.assertEqual(401, self.response_status)
+
+ def test_revoked_hashed_pki_token(self):
+ self._test_revoked_hashed_token('signed_token_scoped')
+
+ def test_revoked_hashed_pkiz_token(self):
+ self._test_revoked_hashed_token('signed_token_scoped_pkiz')
+
+ def get_revocation_list_json(self, token_ids=None, mode=None):
+ if token_ids is None:
+ key = 'revoked_token_hash' + (('_' + mode) if mode else '')
+ token_ids = [self.token_dict[key]]
+ 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_hash']])
+ 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_hash']])
+ self.assertTrue(result)
+
+ def test_is_signed_token_revoked_returns_true_sha256(self):
+ self.conf['hash_algorithms'] = ['sha256', 'md5']
+ self.set_middleware()
+ self.middleware.token_revocation_list = (
+ self.get_revocation_list_json(mode='sha256'))
+ result = self.middleware.is_signed_token_revoked(
+ [self.token_dict['revoked_token_hash_sha256']])
+ 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'],
+ [self.token_dict['revoked_token_hash']])
+
+ def test_verify_signed_token_raises_exception_for_revoked_token_s256(self):
+ self.conf['hash_algorithms'] = ['sha256', 'md5']
+ self.set_middleware()
+ self.middleware.token_revocation_list = (
+ self.get_revocation_list_json(mode='sha256'))
+ self.assertRaises(auth_token.InvalidUserToken,
+ self.middleware.verify_signed_token,
+ self.token_dict['revoked_token'],
+ [self.token_dict['revoked_token_hash_sha256'],
+ self.token_dict['revoked_token_hash']])
+
+ def test_verify_signed_token_raises_exception_for_revoked_pkiz_token(self):
+ self.middleware.token_revocation_list = (
+ self.examples.REVOKED_TOKEN_PKIZ_LIST_JSON)
+ self.assertRaises(auth_token.InvalidUserToken,
+ self.middleware.verify_pkiz_token,
+ self.token_dict['revoked_token_pkiz'],
+ [self.token_dict['revoked_token_pkiz_hash']])
+
+ def assertIsValidJSON(self, text):
+ json.loads(text)
+
+ def test_verify_signed_token_succeeds_for_unrevoked_token(self):
+ self.middleware.token_revocation_list = self.get_revocation_list_json()
+ text = self.middleware.verify_signed_token(
+ self.token_dict['signed_token_scoped'],
+ [self.token_dict['signed_token_scoped_hash']])
+ self.assertIsValidJSON(text)
+
+ def test_verify_signed_compressed_token_succeeds_for_unrevoked_token(self):
+ self.middleware.token_revocation_list = self.get_revocation_list_json()
+ text = self.middleware.verify_pkiz_token(
+ self.token_dict['signed_token_scoped_pkiz'],
+ [self.token_dict['signed_token_scoped_hash']])
+ self.assertIsValidJSON(text)
+
+ def test_verify_signed_token_succeeds_for_unrevoked_token_sha256(self):
+ self.conf['hash_algorithms'] = ['sha256', 'md5']
+ self.set_middleware()
+ self.middleware.token_revocation_list = (
+ self.get_revocation_list_json(mode='sha256'))
+ text = self.middleware.verify_signed_token(
+ self.token_dict['signed_token_scoped'],
+ [self.token_dict['signed_token_scoped_hash_sha256'],
+ self.token_dict['signed_token_scoped_hash']])
+ self.assertIsValidJSON(text)
+
+ 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_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.utcfromtimestamp(mtime)
+ self.assertEqual(fetched_time,
+ self.middleware.token_revocation_list_fetched_time)
+
+ @testtools.skipUnless(TimezoneFixture.supported(),
+ 'TimezoneFixture not supported')
+ def test_get_token_revocation_list_fetched_time_returns_utc(self):
+ with TimezoneFixture('UTC-1'):
+ self.middleware.token_revocation_list = jsonutils.dumps(
+ self.examples.REVOCATION_LIST)
+ self.middleware.token_revocation_list_fetched_time = None
+ fetched_time = self.middleware.token_revocation_list_fetched_time
+ self.assertTrue(timeutils.is_soon(fetched_time, 1))
+
+ 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,
+ self.examples.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):
+ self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI, text='{}')
+
+ 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, self.examples.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
+ self.requests.get(invalid_uri, text="", status_code=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'] = self.examples.INVALID_SIGNED_TOKEN
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(401, self.response_status)
+ self.assertEqual("Keystone uri='https://keystone.example.com:1234'",
+ self.response_headers['WWW-Authenticate'])
+
+ def test_request_invalid_signed_pkiz_token(self):
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = self.examples.INVALID_SIGNED_PKIZ_TOKEN
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(401, self.response_status)
+ self.assertEqual("Keystone uri='https://keystone.example.com:1234'",
+ self.response_headers['WWW-Authenticate'])
+
+ 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, mode='md5'):
+ token_id = cms.cms_hash_token(token, mode=mode)
+ return self.middleware._token_cache._cache_get(token_id)
+
+ def test_memcache(self):
+ 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.assertIsNotNone(self._get_cached_token(token))
+
+ def test_expired(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.assertEqual(self.response_status, 401)
+
+ def test_memcache_set_invalid_uuid(self):
+ invalid_uri = "%s/v2.0/tokens/invalid-token" % BASE_URI
+ self.requests.get(invalid_uri, status_code=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, hash_algorithms=None,
+ exp_mode='md5'):
+ req = webob.Request.blank('/')
+ token = self.token_dict['signed_token_scoped_expired']
+ req.headers['X-Auth-Token'] = token
+ if hash_algorithms:
+ self.conf['hash_algorithms'] = hash_algorithms
+ self.set_middleware()
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertRaises(auth_token.InvalidUserToken,
+ self._get_cached_token, token, mode=exp_mode)
+
+ def test_memcache_set_invalid_signed(self):
+ self._test_memcache_set_invalid_signed()
+
+ def test_memcache_set_invalid_signed_sha256_md5(self):
+ hash_algorithms = ['sha256', 'md5']
+ self._test_memcache_set_invalid_signed(hash_algorithms=hash_algorithms,
+ exp_mode='sha256')
+
+ def test_memcache_set_invalid_signed_sha256(self):
+ hash_algorithms = ['sha256']
+ self._test_memcache_set_invalid_signed(hash_algorithms=hash_algorithms,
+ exp_mode='sha256')
+
+ def test_memcache_set_expired(self, extra_conf={}, extra_environ={}):
+ 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)
+
+ now = datetime.datetime.utcnow()
+ self.useFixture(TimeFixture(now))
+
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertIsNotNone(self._get_cached_token(token))
+
+ timeutils.advance_time_seconds(token_cache_time)
+ self.assertIsNone(self._get_cached_token(token))
+
+ 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_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.assertIsNone(self._get_cached_token(ERROR_TOKEN))
+ 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)
+
+ def test_nocatalog(self):
+ conf = {
+ 'include_service_catalog': False
+ }
+ self.set_middleware(conf=conf)
+ self.assert_valid_request_200(self.token_dict['uuid_token_default'],
+ with_catalog=False)
+
+ def assert_kerberos_bind(self, token, bind_level,
+ use_kerberos=True, success=True):
+ conf = {
+ 'enforce_token_bind': bind_level,
+ 'auth_version': self.auth_version,
+ }
+ self.set_middleware(conf=conf)
+
+ req = webob.Request.blank('/')
+ req.headers['X-Auth-Token'] = token
+
+ if use_kerberos:
+ if use_kerberos is True:
+ req.environ['REMOTE_USER'] = self.examples.KERBEROS_BIND
+ else:
+ req.environ['REMOTE_USER'] = use_kerberos
+
+ req.environ['AUTH_TYPE'] = 'Negotiate'
+
+ body = self.middleware(req.environ, self.start_fake_response)
+
+ if success:
+ self.assertEqual(self.response_status, 200)
+ self.assertEqual(body, [FakeApp.SUCCESS])
+ self.assertIn('keystone.token_info', req.environ)
+ self.assert_valid_last_url(token)
+ else:
+ self.assertEqual(self.response_status, 401)
+ self.assertEqual(self.response_headers['WWW-Authenticate'],
+ "Keystone uri='https://keystone.example.com:1234'"
+ )
+
+ def test_uuid_bind_token_disabled_with_kerb_user(self):
+ for use_kerberos in [True, False]:
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='disabled',
+ use_kerberos=use_kerberos,
+ success=True)
+
+ def test_uuid_bind_token_disabled_with_incorrect_ticket(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='kerberos',
+ use_kerberos='ronald@MCDONALDS.COM',
+ success=False)
+
+ def test_uuid_bind_token_permissive_with_kerb_user(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='permissive',
+ use_kerberos=True,
+ success=True)
+
+ def test_uuid_bind_token_permissive_without_kerb_user(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='permissive',
+ use_kerberos=False,
+ success=False)
+
+ def test_uuid_bind_token_permissive_with_unknown_bind(self):
+ token = self.token_dict['uuid_token_unknown_bind']
+
+ for use_kerberos in [True, False]:
+ self.assert_kerberos_bind(token,
+ bind_level='permissive',
+ use_kerberos=use_kerberos,
+ success=True)
+
+ def test_uuid_bind_token_permissive_with_incorrect_ticket(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='kerberos',
+ use_kerberos='ronald@MCDONALDS.COM',
+ success=False)
+
+ def test_uuid_bind_token_strict_with_kerb_user(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='strict',
+ use_kerberos=True,
+ success=True)
+
+ def test_uuid_bind_token_strict_with_kerbout_user(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='strict',
+ use_kerberos=False,
+ success=False)
+
+ def test_uuid_bind_token_strict_with_unknown_bind(self):
+ token = self.token_dict['uuid_token_unknown_bind']
+
+ for use_kerberos in [True, False]:
+ self.assert_kerberos_bind(token,
+ bind_level='strict',
+ use_kerberos=use_kerberos,
+ success=False)
+
+ def test_uuid_bind_token_required_with_kerb_user(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='required',
+ use_kerberos=True,
+ success=True)
+
+ def test_uuid_bind_token_required_without_kerb_user(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='required',
+ use_kerberos=False,
+ success=False)
+
+ def test_uuid_bind_token_required_with_unknown_bind(self):
+ token = self.token_dict['uuid_token_unknown_bind']
+
+ for use_kerberos in [True, False]:
+ self.assert_kerberos_bind(token,
+ bind_level='required',
+ use_kerberos=use_kerberos,
+ success=False)
+
+ def test_uuid_bind_token_required_without_bind(self):
+ for use_kerberos in [True, False]:
+ self.assert_kerberos_bind(self.token_dict['uuid_token_default'],
+ bind_level='required',
+ use_kerberos=use_kerberos,
+ success=False)
+
+ def test_uuid_bind_token_named_kerberos_with_kerb_user(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='kerberos',
+ use_kerberos=True,
+ success=True)
+
+ def test_uuid_bind_token_named_kerberos_without_kerb_user(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='kerberos',
+ use_kerberos=False,
+ success=False)
+
+ def test_uuid_bind_token_named_kerberos_with_unknown_bind(self):
+ token = self.token_dict['uuid_token_unknown_bind']
+
+ for use_kerberos in [True, False]:
+ self.assert_kerberos_bind(token,
+ bind_level='kerberos',
+ use_kerberos=use_kerberos,
+ success=False)
+
+ def test_uuid_bind_token_named_kerberos_without_bind(self):
+ for use_kerberos in [True, False]:
+ self.assert_kerberos_bind(self.token_dict['uuid_token_default'],
+ bind_level='kerberos',
+ use_kerberos=use_kerberos,
+ success=False)
+
+ def test_uuid_bind_token_named_kerberos_with_incorrect_ticket(self):
+ self.assert_kerberos_bind(self.token_dict['uuid_token_bind'],
+ bind_level='kerberos',
+ use_kerberos='ronald@MCDONALDS.COM',
+ success=False)
+
+ def test_uuid_bind_token_with_unknown_named_FOO(self):
+ token = self.token_dict['uuid_token_bind']
+
+ for use_kerberos in [True, False]:
+ self.assert_kerberos_bind(token,
+ bind_level='FOO',
+ use_kerberos=use_kerberos,
+ success=False)
+
+
+class V2CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest,
+ testresources.ResourcedTestCase):
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ def __init__(self, *args, **kwargs):
+ super(V2CertDownloadMiddlewareTest, self).__init__(*args, **kwargs)
+ self.auth_version = 'v2.0'
+ self.fake_app = None
+ self.ca_path = '/v2.0/certificates/ca'
+ self.signing_path = '/v2.0/certificates/signing'
+
+ def setUp(self):
+ super(V2CertDownloadMiddlewareTest, self).setUp(
+ auth_version=self.auth_version,
+ fake_app=self.fake_app)
+ self.base_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.base_dir)
+ self.cert_dir = os.path.join(self.base_dir, 'certs')
+ os.makedirs(self.cert_dir, stat.S_IRWXU)
+ conf = {
+ 'signing_dir': self.cert_dir,
+ 'auth_version': self.auth_version,
+ }
+ self.set_middleware(conf=conf)
+
+ # 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()
+
+ self.requests.get("%s%s" % (BASE_URI, self.ca_path),
+ status_code=404)
+ url = "%s%s" % (BASE_URI, self.signing_path)
+ self.requests.get(url, status_code=404)
+ self.assertRaises(exceptions.CertificateConfigError,
+ self.middleware.verify_signed_token,
+ self.examples.SIGNED_TOKEN_SCOPED,
+ [self.examples.SIGNED_TOKEN_SCOPED_HASH])
+
+ def test_fetch_signing_cert(self):
+ data = 'FAKE CERT'
+ url = '%s%s' % (BASE_URI, self.signing_path)
+ self.requests.get(url, text=data)
+ self.middleware.fetch_signing_cert()
+
+ with open(self.middleware.signing_cert_file_name, 'r') as f:
+ self.assertEqual(f.read(), data)
+
+ self.assertLastPath("/testadmin%s" % self.signing_path)
+
+ def test_fetch_signing_ca(self):
+ data = 'FAKE CA'
+ self.requests.get("%s%s" % (BASE_URI, self.ca_path), text=data)
+ self.middleware.fetch_ca_cert()
+
+ with open(self.middleware.signing_ca_file_name, 'r') as f:
+ self.assertEqual(f.read(), data)
+
+ self.assertLastPath("/testadmin%s" % self.ca_path)
+
+ def test_prefix_trailing_slash(self):
+ del self.conf['identity_uri']
+ self.conf['auth_protocol'] = 'https'
+ self.conf['auth_host'] = 'keystone.example.com'
+ self.conf['auth_port'] = 1234
+ self.conf['auth_admin_prefix'] = '/newadmin/'
+
+ self.requests.get("%s/newadmin%s" % (BASE_HOST, self.ca_path),
+ text='FAKECA')
+ url = "%s/newadmin%s" % (BASE_HOST, self.signing_path)
+ self.requests.get(url, text='FAKECERT')
+
+ self.set_middleware(conf=self.conf)
+
+ self.middleware.fetch_ca_cert()
+
+ self.assertLastPath('/newadmin%s' % self.ca_path)
+
+ self.middleware.fetch_signing_cert()
+
+ self.assertLastPath('/newadmin%s' % self.signing_path)
+
+ def test_without_prefix(self):
+ del self.conf['identity_uri']
+ self.conf['auth_protocol'] = 'https'
+ self.conf['auth_host'] = 'keystone.example.com'
+ self.conf['auth_port'] = 1234
+ self.conf['auth_admin_prefix'] = ''
+
+ self.requests.get("%s%s" % (BASE_HOST, self.ca_path), text='FAKECA')
+ self.requests.get("%s%s" % (BASE_HOST, self.signing_path),
+ text='FAKECERT')
+
+ self.set_middleware(conf=self.conf)
+
+ self.middleware.fetch_ca_cert()
+
+ self.assertLastPath(self.ca_path)
+
+ self.middleware.fetch_signing_cert()
+
+ self.assertLastPath(self.signing_path)
+
+
+class V3CertDownloadMiddlewareTest(V2CertDownloadMiddlewareTest):
+
+ def __init__(self, *args, **kwargs):
+ super(V3CertDownloadMiddlewareTest, self).__init__(*args, **kwargs)
+ self.auth_version = 'v3.0'
+ self.fake_app = v3FakeApp
+ self.ca_path = '/v3/OS-SIMPLE-CERT/ca'
+ self.signing_path = '/v3/OS-SIMPLE-CERT/certificates'
+
+
+def network_error_response(method, uri, headers):
+ raise auth_token.NetworkError("Network connection error.")
+
+
+class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+ CommonAuthTokenMiddlewareTest,
+ testresources.ResourcedTestCase):
+ """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 specific, they are included here.
+
+ """
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ def setUp(self):
+ super(v2AuthTokenMiddlewareTest, self).setUp()
+
+ self.token_dict = {
+ 'uuid_token_default': self.examples.UUID_TOKEN_DEFAULT,
+ 'uuid_token_unscoped': self.examples.UUID_TOKEN_UNSCOPED,
+ 'uuid_token_bind': self.examples.UUID_TOKEN_BIND,
+ 'uuid_token_unknown_bind': self.examples.UUID_TOKEN_UNKNOWN_BIND,
+ 'signed_token_scoped': self.examples.SIGNED_TOKEN_SCOPED,
+ 'signed_token_scoped_pkiz': self.examples.SIGNED_TOKEN_SCOPED_PKIZ,
+ 'signed_token_scoped_hash': self.examples.SIGNED_TOKEN_SCOPED_HASH,
+ 'signed_token_scoped_hash_sha256':
+ self.examples.SIGNED_TOKEN_SCOPED_HASH_SHA256,
+ 'signed_token_scoped_expired':
+ self.examples.SIGNED_TOKEN_SCOPED_EXPIRED,
+ 'revoked_token': self.examples.REVOKED_TOKEN,
+ 'revoked_token_pkiz': self.examples.REVOKED_TOKEN_PKIZ,
+ 'revoked_token_pkiz_hash':
+ self.examples.REVOKED_TOKEN_PKIZ_HASH,
+ 'revoked_token_hash': self.examples.REVOKED_TOKEN_HASH,
+ 'revoked_token_hash_sha256':
+ self.examples.REVOKED_TOKEN_HASH_SHA256,
+ }
+
+ self.requests.get("%s/" % BASE_URI,
+ text=VERSION_LIST_v2,
+ status_code=300)
+
+ self.requests.post("%s/v2.0/tokens" % BASE_URI,
+ text=FAKE_ADMIN_TOKEN)
+
+ self.requests.get("%s/v2.0/tokens/revoked" % BASE_URI,
+ text=self.examples.SIGNED_REVOCATION_LIST)
+
+ for token in (self.examples.UUID_TOKEN_DEFAULT,
+ self.examples.UUID_TOKEN_UNSCOPED,
+ self.examples.UUID_TOKEN_BIND,
+ self.examples.UUID_TOKEN_UNKNOWN_BIND,
+ self.examples.UUID_TOKEN_NO_SERVICE_CATALOG,
+ self.examples.SIGNED_TOKEN_SCOPED_KEY,
+ self.examples.SIGNED_TOKEN_SCOPED_PKIZ_KEY,):
+ text = self.examples.JSON_TOKEN_RESPONSES[token]
+ self.requests.get('%s/v2.0/tokens/%s' % (BASE_URI, token),
+ text=text)
+
+ self.requests.get('%s/v2.0/tokens/%s' % (BASE_URI, ERROR_TOKEN),
+ text=network_error_response)
+
+ self.set_middleware()
+
+ 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, [FakeApp.SUCCESS])
+ self.assertIn('keystone.token_info', 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(
+ self.examples.UUID_TOKEN_DEFAULT)
+
+ def test_default_tenant_signed_token(self):
+ self.assert_unscoped_default_tenant_auto_scopes(
+ self.examples.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(
+ self.examples.UUID_TOKEN_UNSCOPED)
+
+ def test_unscoped_pki_token_receives_401(self):
+ self.assert_unscoped_token_receives_401(
+ self.examples.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'] = (
+ self.examples.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, [FakeApp.SUCCESS])
+
+
+class CrossVersionAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+ testresources.ResourcedTestCase):
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ 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'
+ }
+
+ self.requests.get('%s/' % BASE_URI,
+ text=VERSION_LIST_v3,
+ status_code=300)
+
+ self.requests.post('%s/v2.0/tokens' % BASE_URI, text=FAKE_ADMIN_TOKEN)
+
+ token = self.examples.UUID_TOKEN_DEFAULT
+ url = '%s/v2.0/tokens/%s' % (BASE_URI, token)
+ response_body = self.examples.JSON_TOKEN_RESPONSES[token]
+ self.requests.get(url, text=response_body)
+
+ 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'] = self.examples.UUID_TOKEN_DEFAULT
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 200)
+ self.assertLastPath("/testadmin/v2.0/tokens/%s" %
+ self.examples.UUID_TOKEN_DEFAULT)
+
+
+class v3AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
+ CommonAuthTokenMiddlewareTest,
+ testresources.ResourcedTestCase):
+ """Test auth_token middleware with v3 tokens.
+
+ Re-execute the AuthTokenMiddlewareTest class tests, but with 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
+
+ """
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ def setUp(self):
+ super(v3AuthTokenMiddlewareTest, self).setUp(
+ auth_version='v3.0',
+ fake_app=v3FakeApp)
+
+ self.token_dict = {
+ 'uuid_token_default': self.examples.v3_UUID_TOKEN_DEFAULT,
+ 'uuid_token_unscoped': self.examples.v3_UUID_TOKEN_UNSCOPED,
+ 'uuid_token_bind': self.examples.v3_UUID_TOKEN_BIND,
+ 'uuid_token_unknown_bind':
+ self.examples.v3_UUID_TOKEN_UNKNOWN_BIND,
+ 'signed_token_scoped': self.examples.SIGNED_v3_TOKEN_SCOPED,
+ 'signed_token_scoped_pkiz':
+ self.examples.SIGNED_v3_TOKEN_SCOPED_PKIZ,
+ 'signed_token_scoped_hash':
+ self.examples.SIGNED_v3_TOKEN_SCOPED_HASH,
+ 'signed_token_scoped_hash_sha256':
+ self.examples.SIGNED_v3_TOKEN_SCOPED_HASH_SHA256,
+ 'signed_token_scoped_expired':
+ self.examples.SIGNED_TOKEN_SCOPED_EXPIRED,
+ 'revoked_token': self.examples.REVOKED_v3_TOKEN,
+ 'revoked_token_pkiz': self.examples.REVOKED_v3_TOKEN_PKIZ,
+ 'revoked_token_hash': self.examples.REVOKED_v3_TOKEN_HASH,
+ 'revoked_token_hash_sha256':
+ self.examples.REVOKED_v3_TOKEN_HASH_SHA256,
+ 'revoked_token_pkiz_hash':
+ self.examples.REVOKED_v3_PKIZ_TOKEN_HASH,
+ }
+
+ self.requests.get(BASE_URI, text=VERSION_LIST_v3, status_code=300)
+
+ # TODO(jamielennox): auth_token middleware uses a v2 admin token
+ # regardless of the auth_version that is set.
+ self.requests.post('%s/v2.0/tokens' % BASE_URI, text=FAKE_ADMIN_TOKEN)
+
+ # TODO(jamielennox): there is no v3 revocation url yet, it uses v2
+ self.requests.get('%s/v2.0/tokens/revoked' % BASE_URI,
+ text=self.examples.SIGNED_REVOCATION_LIST)
+
+ self.requests.get('%s/v3/auth/tokens' % BASE_URI,
+ text=self.token_response)
+
+ self.set_middleware()
+
+ def token_response(self, request, context):
+ auth_id = request.headers.get('X-Auth-Token')
+ token_id = request.headers.get('X-Subject-Token')
+ self.assertEqual(auth_id, FAKE_ADMIN_TOKEN_ID)
+
+ response = ""
+
+ if token_id == ERROR_TOKEN:
+ raise auth_token.NetworkError("Network connection error.")
+
+ try:
+ response = self.examples.JSON_TOKEN_RESPONSES[token_id]
+ except KeyError:
+ context.status_code = 404
+
+ return 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(self.examples.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(
+ self.examples.v3_UUID_TOKEN_DOMAIN_SCOPED)
+ self.assertLastPath('/testadmin/v3/auth/tokens')
+
+ def test_gives_v2_catalog(self):
+ self.set_middleware()
+ req = self.assert_valid_request_200(
+ self.examples.SIGNED_v3_TOKEN_SCOPED)
+
+ catalog = jsonutils.loads(req.headers['X-Service-Catalog'])
+
+ for service in catalog:
+ for endpoint in service['endpoints']:
+ # no point checking everything, just that it's in v2 format
+ self.assertIn('adminURL', endpoint)
+ self.assertIn('publicURL', endpoint)
+ self.assertIn('adminURL', endpoint)
+
+
+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'))
+
+
+class TokenExpirationTest(BaseAuthTokenMiddlewareTest):
+ def setUp(self):
+ super(TokenExpirationTest, self).setUp()
+ self.now = timeutils.utcnow()
+ self.delta = datetime.timedelta(hours=1)
+ self.one_hour_ago = timeutils.isotime(self.now - self.delta,
+ subsecond=True)
+ self.one_hour_earlier = timeutils.isotime(self.now + self.delta,
+ subsecond=True)
+
+ def create_v2_token_fixture(self, expires=None):
+ v2_fixture = {
+ 'access': {
+ 'token': {
+ 'id': 'blah',
+ 'expires': expires or self.one_hour_earlier,
+ 'tenant': {
+ 'id': 'tenant_id1',
+ 'name': 'tenant_name1',
+ },
+ },
+ 'user': {
+ 'id': 'user_id1',
+ 'name': 'user_name1',
+ 'roles': [
+ {'name': 'role1'},
+ {'name': 'role2'},
+ ],
+ },
+ 'serviceCatalog': {}
+ },
+ }
+
+ return v2_fixture
+
+ def create_v3_token_fixture(self, expires=None):
+
+ v3_fixture = {
+ 'token': {
+ 'expires_at': expires or self.one_hour_earlier,
+ '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': {}
+ }
+ }
+
+ return v3_fixture
+
+ def test_no_data(self):
+ data = {}
+ self.assertRaises(auth_token.InvalidUserToken,
+ auth_token.confirm_token_not_expired,
+ data)
+
+ def test_bad_data(self):
+ data = {'my_happy_token_dict': 'woo'}
+ self.assertRaises(auth_token.InvalidUserToken,
+ auth_token.confirm_token_not_expired,
+ data)
+
+ def test_v2_token_not_expired(self):
+ data = self.create_v2_token_fixture()
+ expected_expires = data['access']['token']['expires']
+ actual_expires = auth_token.confirm_token_not_expired(data)
+ self.assertEqual(actual_expires, expected_expires)
+
+ def test_v2_token_expired(self):
+ data = self.create_v2_token_fixture(expires=self.one_hour_ago)
+ self.assertRaises(auth_token.InvalidUserToken,
+ auth_token.confirm_token_not_expired,
+ data)
+
+ def test_v2_token_with_timezone_offset_not_expired(self):
+ self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z'))
+ data = self.create_v2_token_fixture(
+ expires='2000-01-01T00:05:10.000123-05:00')
+ expected_expires = '2000-01-01T05:05:10.000123Z'
+ actual_expires = auth_token.confirm_token_not_expired(data)
+ self.assertEqual(actual_expires, expected_expires)
+
+ def test_v2_token_with_timezone_offset_expired(self):
+ self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z'))
+ data = self.create_v2_token_fixture(
+ expires='2000-01-01T00:05:10.000123+05:00')
+ data['access']['token']['expires'] = '2000-01-01T00:05:10.000123+05:00'
+ self.assertRaises(auth_token.InvalidUserToken,
+ auth_token.confirm_token_not_expired,
+ data)
+
+ def test_v3_token_not_expired(self):
+ data = self.create_v3_token_fixture()
+ expected_expires = data['token']['expires_at']
+ actual_expires = auth_token.confirm_token_not_expired(data)
+ self.assertEqual(actual_expires, expected_expires)
+
+ def test_v3_token_expired(self):
+ data = self.create_v3_token_fixture(expires=self.one_hour_ago)
+ self.assertRaises(auth_token.InvalidUserToken,
+ auth_token.confirm_token_not_expired,
+ data)
+
+ def test_v3_token_with_timezone_offset_not_expired(self):
+ self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z'))
+ data = self.create_v3_token_fixture(
+ expires='2000-01-01T00:05:10.000123-05:00')
+ expected_expires = '2000-01-01T05:05:10.000123Z'
+
+ actual_expires = auth_token.confirm_token_not_expired(data)
+ self.assertEqual(actual_expires, expected_expires)
+
+ def test_v3_token_with_timezone_offset_expired(self):
+ self.useFixture(TimeFixture('2000-01-01T00:01:10.000123Z'))
+ data = self.create_v3_token_fixture(
+ expires='2000-01-01T00:05:10.000123+05:00')
+ self.assertRaises(auth_token.InvalidUserToken,
+ auth_token.confirm_token_not_expired,
+ data)
+
+ def test_cached_token_not_expired(self):
+ token = 'mytoken'
+ data = 'this_data'
+ self.set_middleware()
+ self.middleware._token_cache.initialize({})
+ some_time_later = timeutils.strtime(at=(self.now + self.delta))
+ expires = some_time_later
+ self.middleware._token_cache.store(token, data, expires)
+ self.assertEqual(self.middleware._token_cache._cache_get(token), data)
+
+ def test_cached_token_not_expired_with_old_style_nix_timestamp(self):
+ """Ensure we cannot retrieve a token from the cache.
+
+ Getting a token from the cache should return None when the token data
+ in the cache stores the expires time as a \*nix style timestamp.
+
+ """
+ token = 'mytoken'
+ data = 'this_data'
+ self.set_middleware()
+ token_cache = self.middleware._token_cache
+ token_cache.initialize({})
+ some_time_later = self.now + self.delta
+ # Store a unix timestamp in the cache.
+ expires = calendar.timegm(some_time_later.timetuple())
+ token_cache.store(token, data, expires)
+ self.assertIsNone(token_cache._cache_get(token))
+
+ def test_cached_token_expired(self):
+ token = 'mytoken'
+ data = 'this_data'
+ self.set_middleware()
+ self.middleware._token_cache.initialize({})
+ some_time_earlier = timeutils.strtime(at=(self.now - self.delta))
+ expires = some_time_earlier
+ self.middleware._token_cache.store(token, data, expires)
+ self.assertThat(lambda: self.middleware._token_cache._cache_get(token),
+ matchers.raises(auth_token.InvalidUserToken))
+
+ def test_cached_token_with_timezone_offset_not_expired(self):
+ token = 'mytoken'
+ data = 'this_data'
+ self.set_middleware()
+ self.middleware._token_cache.initialize({})
+ timezone_offset = datetime.timedelta(hours=2)
+ some_time_later = self.now - timezone_offset + self.delta
+ expires = timeutils.strtime(some_time_later) + '-02:00'
+ self.middleware._token_cache.store(token, data, expires)
+ self.assertEqual(self.middleware._token_cache._cache_get(token), data)
+
+ def test_cached_token_with_timezone_offset_expired(self):
+ token = 'mytoken'
+ data = 'this_data'
+ self.set_middleware()
+ self.middleware._token_cache.initialize({})
+ timezone_offset = datetime.timedelta(hours=2)
+ some_time_earlier = self.now - timezone_offset - self.delta
+ expires = timeutils.strtime(some_time_earlier) + '-02:00'
+ self.middleware._token_cache.store(token, data, expires)
+ self.assertThat(lambda: self.middleware._token_cache._cache_get(token),
+ matchers.raises(auth_token.InvalidUserToken))
+
+
+class CatalogConversionTests(BaseAuthTokenMiddlewareTest):
+
+ PUBLIC_URL = 'http://server:5000/v2.0'
+ ADMIN_URL = 'http://admin:35357/v2.0'
+ INTERNAL_URL = 'http://internal:5000/v2.0'
+
+ REGION_ONE = 'RegionOne'
+ REGION_TWO = 'RegionTwo'
+ REGION_THREE = 'RegionThree'
+
+ def test_basic_convert(self):
+ token = fixture.V3Token()
+ s = token.add_service(type='identity')
+ s.add_standard_endpoints(public=self.PUBLIC_URL,
+ admin=self.ADMIN_URL,
+ internal=self.INTERNAL_URL,
+ region=self.REGION_ONE)
+
+ auth_ref = access.AccessInfo.factory(body=token)
+ catalog_data = auth_ref.service_catalog.get_data()
+ catalog = auth_token._v3_to_v2_catalog(catalog_data)
+
+ self.assertEqual(1, len(catalog))
+ service = catalog[0]
+ self.assertEqual(1, len(service['endpoints']))
+ endpoints = service['endpoints'][0]
+
+ self.assertEqual('identity', service['type'])
+ self.assertEqual(4, len(endpoints))
+ self.assertEqual(self.PUBLIC_URL, endpoints['publicURL'])
+ self.assertEqual(self.ADMIN_URL, endpoints['adminURL'])
+ self.assertEqual(self.INTERNAL_URL, endpoints['internalURL'])
+ self.assertEqual(self.REGION_ONE, endpoints['region'])
+
+ def test_multi_region(self):
+ token = fixture.V3Token()
+ s = token.add_service(type='identity')
+
+ s.add_endpoint('internal', self.INTERNAL_URL, region=self.REGION_ONE)
+ s.add_endpoint('public', self.PUBLIC_URL, region=self.REGION_TWO)
+ s.add_endpoint('admin', self.ADMIN_URL, region=self.REGION_THREE)
+
+ auth_ref = access.AccessInfo.factory(body=token)
+ catalog_data = auth_ref.service_catalog.get_data()
+ catalog = auth_token._v3_to_v2_catalog(catalog_data)
+
+ self.assertEqual(1, len(catalog))
+ service = catalog[0]
+
+ # the 3 regions will come through as 3 separate endpoints
+ expected = [{'internalURL': self.INTERNAL_URL,
+ 'region': self.REGION_ONE},
+ {'publicURL': self.PUBLIC_URL,
+ 'region': self.REGION_TWO},
+ {'adminURL': self.ADMIN_URL,
+ 'region': self.REGION_THREE}]
+
+ self.assertEqual('identity', service['type'])
+ self.assertEqual(3, len(service['endpoints']))
+ for e in expected:
+ self.assertIn(e, expected)
+
+
+def load_tests(loader, tests, pattern):
+ return testresources.OptimisingTestSuite(tests)
diff --git a/keystoneclient/tests/unit/test_base.py b/keystoneclient/tests/unit/test_base.py
new file mode 100644
index 0000000..2e7fc5e
--- /dev/null
+++ b/keystoneclient/tests/unit/test_base.py
@@ -0,0 +1,161 @@
+# 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.unit 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), "<Resource baz=spam, foo=bar>")
+
+ 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._adapter.get = self.mox.CreateMockAnything()
+ self.client._adapter.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.assertIsNone(r.human_id)
+ 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/keystoneclient/tests/unit/test_cms.py b/keystoneclient/tests/unit/test_cms.py
new file mode 100644
index 0000000..019730d
--- /dev/null
+++ b/keystoneclient/tests/unit/test_cms.py
@@ -0,0 +1,160 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import errno
+import os
+import subprocess
+
+import mock
+import testresources
+from testtools import matchers
+
+from keystoneclient.common import cms
+from keystoneclient import exceptions
+from keystoneclient.tests.unit import client_fixtures
+from keystoneclient.tests.unit import utils
+
+
+class CMSTest(utils.TestCase, testresources.ResourcedTestCase):
+
+ """Unit tests for the keystoneclient.common.cms module."""
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ def __init__(self, *args, **kwargs):
+ super(CMSTest, self).__init__(*args, **kwargs)
+ process = subprocess.Popen(['openssl', 'version'],
+ stdout=subprocess.PIPE)
+ out, err = process.communicate()
+ # Example output: 'OpenSSL 0.9.8za 5 Jun 2014'
+ openssl_version = out.split()[1]
+
+ if err or openssl_version.startswith(b'0'):
+ raise Exception('Your version of OpenSSL is not supported. '
+ 'You will need to update it to 1.0 or later.')
+
+ def test_cms_verify(self):
+ self.assertRaises(exceptions.CertificateConfigError,
+ cms.cms_verify,
+ 'data',
+ 'no_exist_cert_file',
+ 'no_exist_ca_file')
+
+ def test_token_tocms_to_token(self):
+ with open(os.path.join(client_fixtures.CMSDIR,
+ 'auth_token_scoped.pem')) as f:
+ AUTH_TOKEN_SCOPED_CMS = f.read()
+
+ self.assertEqual(cms.token_to_cms(self.examples.SIGNED_TOKEN_SCOPED),
+ AUTH_TOKEN_SCOPED_CMS)
+
+ tok = cms.cms_to_token(cms.token_to_cms(
+ self.examples.SIGNED_TOKEN_SCOPED))
+ self.assertEqual(tok, self.examples.SIGNED_TOKEN_SCOPED)
+
+ def test_asn1_token(self):
+ self.assertTrue(cms.is_asn1_token(self.examples.SIGNED_TOKEN_SCOPED))
+ self.assertFalse(cms.is_asn1_token('FOOBAR'))
+
+ def test_cms_sign_token_no_files(self):
+ self.assertRaises(subprocess.CalledProcessError,
+ cms.cms_sign_token,
+ self.examples.TOKEN_SCOPED_DATA,
+ '/no/such/file', '/no/such/key')
+
+ def test_cms_sign_token_no_files_pkiz(self):
+ self.assertRaises(subprocess.CalledProcessError,
+ cms.pkiz_sign,
+ self.examples.TOKEN_SCOPED_DATA,
+ '/no/such/file', '/no/such/key')
+
+ def test_cms_sign_token_success(self):
+ self.assertTrue(
+ cms.pkiz_sign(self.examples.TOKEN_SCOPED_DATA,
+ self.examples.SIGNING_CERT_FILE,
+ self.examples.SIGNING_KEY_FILE))
+
+ def test_cms_verify_token_no_files(self):
+ self.assertRaises(exceptions.CertificateConfigError,
+ cms.cms_verify,
+ self.examples.SIGNED_TOKEN_SCOPED,
+ '/no/such/file', '/no/such/key')
+
+ def test_cms_verify_token_no_oserror(self):
+ def raise_OSError(*args):
+ e = OSError()
+ e.errno = errno.EPIPE
+ raise e
+
+ with mock.patch('subprocess.Popen.communicate', new=raise_OSError):
+ try:
+ cms.cms_verify("x", '/no/such/file', '/no/such/key')
+ except exceptions.CertificateConfigError as e:
+ self.assertIn('/no/such/file', e.output)
+ self.assertIn('Hit OSError ', e.output)
+ else:
+ self.fail('Expected exceptions.CertificateConfigError')
+
+ def test_cms_verify_token_scoped(self):
+ cms_content = cms.token_to_cms(self.examples.SIGNED_TOKEN_SCOPED)
+ self.assertTrue(cms.cms_verify(cms_content,
+ self.examples.SIGNING_CERT_FILE,
+ self.examples.SIGNING_CA_FILE))
+
+ def test_cms_verify_token_scoped_expired(self):
+ cms_content = cms.token_to_cms(
+ self.examples.SIGNED_TOKEN_SCOPED_EXPIRED)
+ self.assertTrue(cms.cms_verify(cms_content,
+ self.examples.SIGNING_CERT_FILE,
+ self.examples.SIGNING_CA_FILE))
+
+ def test_cms_verify_token_unscoped(self):
+ cms_content = cms.token_to_cms(self.examples.SIGNED_TOKEN_UNSCOPED)
+ self.assertTrue(cms.cms_verify(cms_content,
+ self.examples.SIGNING_CERT_FILE,
+ self.examples.SIGNING_CA_FILE))
+
+ def test_cms_verify_token_v3_scoped(self):
+ cms_content = cms.token_to_cms(self.examples.SIGNED_v3_TOKEN_SCOPED)
+ self.assertTrue(cms.cms_verify(cms_content,
+ self.examples.SIGNING_CERT_FILE,
+ self.examples.SIGNING_CA_FILE))
+
+ def test_cms_hash_token_no_token_id(self):
+ token_id = None
+ self.assertThat(cms.cms_hash_token(token_id), matchers.Is(None))
+
+ def test_cms_hash_token_not_pki(self):
+ """If the token_id is not a PKI token then it returns the token_id."""
+ token = 'something'
+ self.assertFalse(cms.is_asn1_token(token))
+ self.assertThat(cms.cms_hash_token(token), matchers.Is(token))
+
+ def test_cms_hash_token_default_md5(self):
+ """The default hash method is md5."""
+ token = self.examples.SIGNED_TOKEN_SCOPED
+ token_id_default = cms.cms_hash_token(token)
+ token_id_md5 = cms.cms_hash_token(token, mode='md5')
+ self.assertThat(token_id_default, matchers.Equals(token_id_md5))
+ # md5 hash is 32 chars.
+ self.assertThat(token_id_default, matchers.HasLength(32))
+
+ def test_cms_hash_token_sha256(self):
+ """Can also hash with sha256."""
+ token = self.examples.SIGNED_TOKEN_SCOPED
+ token_id = cms.cms_hash_token(token, mode='sha256')
+ # sha256 hash is 64 chars.
+ self.assertThat(token_id, matchers.HasLength(64))
+
+
+def load_tests(loader, tests, pattern):
+ return testresources.OptimisingTestSuite(tests)
diff --git a/keystoneclient/tests/unit/test_discovery.py b/keystoneclient/tests/unit/test_discovery.py
new file mode 100644
index 0000000..6c208a3
--- /dev/null
+++ b/keystoneclient/tests/unit/test_discovery.py
@@ -0,0 +1,802 @@
+# 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 re
+import uuid
+
+from oslo_serialization import jsonutils
+import six
+from testtools import matchers
+
+from keystoneclient import _discover
+from keystoneclient.auth import token_endpoint
+from keystoneclient import client
+from keystoneclient import discover
+from keystoneclient import exceptions
+from keystoneclient import fixture
+from keystoneclient import session
+from keystoneclient.tests.unit import utils
+from keystoneclient.v2_0 import client as v2_client
+from keystoneclient.v3 import client as v3_client
+
+
+BASE_HOST = 'http://keystone.example.com'
+BASE_URL = "%s:5000/" % BASE_HOST
+UPDATED = '2013-03-06T00:00:00Z'
+
+TEST_SERVICE_CATALOG = [{
+ "endpoints": [{
+ "adminURL": "%s:8774/v1.0" % BASE_HOST,
+ "region": "RegionOne",
+ "internalURL": "%s://127.0.0.1:8774/v1.0" % BASE_HOST,
+ "publicURL": "%s:8774/v1.0/" % BASE_HOST
+ }],
+ "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": "%s:35357/v2.0" % BASE_HOST,
+ "region": "RegionOne",
+ "internalURL": "%s:5000/v2.0" % BASE_HOST,
+ "publicURL": "%s:5000/v2.0" % BASE_HOST
+ }],
+ "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"
+}]
+
+V2_URL = "%sv2.0" % BASE_URL
+V2_VERSION = fixture.V2Discovery(V2_URL)
+V2_VERSION.updated_str = UPDATED
+
+V2_AUTH_RESPONSE = jsonutils.dumps({
+ "access": {
+ "token": {
+ "expires": "2020-01-01T00:00:10.000123Z",
+ "id": 'fakeToken',
+ "tenant": {
+ "id": '1'
+ },
+ },
+ "user": {
+ "id": 'test'
+ },
+ "serviceCatalog": TEST_SERVICE_CATALOG,
+ },
+})
+
+V3_URL = "%sv3" % BASE_URL
+V3_VERSION = fixture.V3Discovery(V3_URL)
+V3_MEDIA_TYPES = V3_VERSION.media_types
+V3_VERSION.updated_str = UPDATED
+
+V3_TOKEN = six.u('3e2813b7ba0b4006840c3825860b86ed'),
+V3_AUTH_RESPONSE = jsonutils.dumps({
+ "token": {
+ "methods": [
+ "token",
+ "password"
+ ],
+
+ "expires_at": "2020-01-01T00:00:10.000123Z",
+ "project": {
+ "domain": {
+ "id": '1',
+ "name": 'test-domain'
+ },
+ "id": '1',
+ "name": 'test-project'
+ },
+ "user": {
+ "domain": {
+ "id": '1',
+ "name": 'test-domain'
+ },
+ "id": '1',
+ "name": 'test-user'
+ },
+ "issued_at": "2013-05-29T16:55:21.468960Z",
+ },
+})
+
+CINDER_EXAMPLES = {
+ "versions": [
+ {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "%sv1/" % BASE_URL,
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "CURRENT",
+ "updated": "2012-11-21T11:33:21Z",
+ "id": "v2.0",
+ "links": [
+ {
+ "href": "%sv2/" % BASE_URL,
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+}
+
+GLANCE_EXAMPLES = {
+ "versions": [
+ {
+ "status": "CURRENT",
+ "id": "v2.2",
+ "links": [
+ {
+ "href": "%sv2/" % BASE_URL,
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "SUPPORTED",
+ "id": "v2.1",
+ "links": [
+ {
+ "href": "%sv2/" % BASE_URL,
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "SUPPORTED",
+ "id": "v2.0",
+ "links": [
+ {
+ "href": "%sv2/" % BASE_URL,
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "CURRENT",
+ "id": "v1.1",
+ "links": [
+ {
+ "href": "%sv1/" % BASE_URL,
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "SUPPORTED",
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "%sv1/" % BASE_URL,
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+}
+
+
+def _create_version_list(versions):
+ return jsonutils.dumps({'versions': {'values': versions}})
+
+
+def _create_single_version(version):
+ return jsonutils.dumps({'version': version})
+
+
+V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION])
+V2_VERSION_LIST = _create_version_list([V2_VERSION])
+
+V3_VERSION_ENTRY = _create_single_version(V3_VERSION)
+V2_VERSION_ENTRY = _create_single_version(V2_VERSION)
+
+
+class AvailableVersionsTests(utils.TestCase):
+
+ def test_available_versions_basics(self):
+ examples = {'keystone': V3_VERSION_LIST,
+ 'cinder': jsonutils.dumps(CINDER_EXAMPLES),
+ 'glance': jsonutils.dumps(GLANCE_EXAMPLES)}
+
+ for path, text in six.iteritems(examples):
+ url = "%s%s" % (BASE_URL, path)
+
+ self.requests.get(url, status_code=300, text=text)
+ versions = discover.available_versions(url)
+
+ for v in versions:
+ for n in ('id', 'status', 'links'):
+ msg = '%s missing from %s version data' % (n, path)
+ self.assertThat(v, matchers.Annotate(msg,
+ matchers.Contains(n)))
+
+ def test_available_versions_individual(self):
+ self.requests.get(V3_URL, status_code=200, text=V3_VERSION_ENTRY)
+
+ versions = discover.available_versions(V3_URL)
+
+ for v in versions:
+ self.assertEqual(v['id'], 'v3.0')
+ self.assertEqual(v['status'], 'stable')
+ self.assertIn('media-types', v)
+ self.assertIn('links', v)
+
+ def test_available_keystone_data(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+
+ versions = discover.available_versions(BASE_URL)
+ self.assertEqual(2, len(versions))
+
+ for v in versions:
+ self.assertIn(v['id'], ('v2.0', 'v3.0'))
+ self.assertEqual(v['updated'], UPDATED)
+ self.assertEqual(v['status'], 'stable')
+
+ if v['id'] == 'v3.0':
+ self.assertEqual(v['media-types'], V3_MEDIA_TYPES)
+
+ def test_available_cinder_data(self):
+ text = jsonutils.dumps(CINDER_EXAMPLES)
+ self.requests.get(BASE_URL, status_code=300, text=text)
+
+ versions = discover.available_versions(BASE_URL)
+ self.assertEqual(2, len(versions))
+
+ for v in versions:
+ self.assertEqual(v['status'], 'CURRENT')
+ if v['id'] == 'v1.0':
+ self.assertEqual(v['updated'], '2012-01-04T11:33:21Z')
+ elif v['id'] == 'v2.0':
+ self.assertEqual(v['updated'], '2012-11-21T11:33:21Z')
+ else:
+ self.fail("Invalid version found")
+
+ def test_available_glance_data(self):
+ text = jsonutils.dumps(GLANCE_EXAMPLES)
+ self.requests.get(BASE_URL, status_code=200, text=text)
+
+ versions = discover.available_versions(BASE_URL)
+ self.assertEqual(5, len(versions))
+
+ for v in versions:
+ if v['id'] in ('v2.2', 'v1.1'):
+ self.assertEqual(v['status'], 'CURRENT')
+ elif v['id'] in ('v2.1', 'v2.0', 'v1.0'):
+ self.assertEqual(v['status'], 'SUPPORTED')
+ else:
+ self.fail("Invalid version found")
+
+
+class ClientDiscoveryTests(utils.TestCase):
+
+ def assertCreatesV3(self, **kwargs):
+ self.requests.post('%s/auth/tokens' % V3_URL,
+ text=V3_AUTH_RESPONSE,
+ headers={'X-Subject-Token': V3_TOKEN})
+
+ kwargs.setdefault('username', 'foo')
+ kwargs.setdefault('password', 'bar')
+ keystone = client.Client(**kwargs)
+ self.assertIsInstance(keystone, v3_client.Client)
+ return keystone
+
+ def assertCreatesV2(self, **kwargs):
+ self.requests.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE)
+
+ kwargs.setdefault('username', 'foo')
+ kwargs.setdefault('password', 'bar')
+ keystone = client.Client(**kwargs)
+ self.assertIsInstance(keystone, v2_client.Client)
+ return keystone
+
+ def assertVersionNotAvailable(self, **kwargs):
+ kwargs.setdefault('username', 'foo')
+ kwargs.setdefault('password', 'bar')
+
+ self.assertRaises(exceptions.VersionNotAvailable,
+ client.Client, **kwargs)
+
+ def assertDiscoveryFailure(self, **kwargs):
+ kwargs.setdefault('username', 'foo')
+ kwargs.setdefault('password', 'bar')
+
+ self.assertRaises(exceptions.DiscoveryFailure,
+ client.Client, **kwargs)
+
+ def test_discover_v3(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+
+ self.assertCreatesV3(auth_url=BASE_URL)
+
+ def test_discover_v2(self):
+ self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
+ self.requests.post("%s/tokens" % V2_URL, text=V2_AUTH_RESPONSE)
+
+ self.assertCreatesV2(auth_url=BASE_URL)
+
+ def test_discover_endpoint_v2(self):
+ self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
+ self.assertCreatesV2(endpoint=BASE_URL, token='fake-token')
+
+ def test_discover_endpoint_v3(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+ self.assertCreatesV3(endpoint=BASE_URL, token='fake-token')
+
+ def test_discover_invalid_major_version(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+
+ self.assertVersionNotAvailable(auth_url=BASE_URL, version=5)
+
+ def test_discover_200_response_fails(self):
+ self.requests.get(BASE_URL, text='ok')
+ self.assertDiscoveryFailure(auth_url=BASE_URL)
+
+ def test_discover_minor_greater_than_available_fails(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+
+ self.assertVersionNotAvailable(endpoint=BASE_URL, version=3.4)
+
+ def test_discover_individual_version_v2(self):
+ self.requests.get(V2_URL, text=V2_VERSION_ENTRY)
+
+ self.assertCreatesV2(auth_url=V2_URL)
+
+ def test_discover_individual_version_v3(self):
+ self.requests.get(V3_URL, text=V3_VERSION_ENTRY)
+
+ self.assertCreatesV3(auth_url=V3_URL)
+
+ def test_discover_individual_endpoint_v2(self):
+ self.requests.get(V2_URL, text=V2_VERSION_ENTRY)
+ self.assertCreatesV2(endpoint=V2_URL, token='fake-token')
+
+ def test_discover_individual_endpoint_v3(self):
+ self.requests.get(V3_URL, text=V3_VERSION_ENTRY)
+ self.assertCreatesV3(endpoint=V3_URL, token='fake-token')
+
+ def test_discover_fail_to_create_bad_individual_version(self):
+ self.requests.get(V2_URL, text=V2_VERSION_ENTRY)
+ self.requests.get(V3_URL, text=V3_VERSION_ENTRY)
+
+ self.assertVersionNotAvailable(auth_url=V2_URL, version=3)
+ self.assertVersionNotAvailable(auth_url=V3_URL, version=2)
+
+ def test_discover_unstable_versions(self):
+ version_list = fixture.DiscoveryList(BASE_URL, v3_status='beta')
+ self.requests.get(BASE_URL, status_code=300, json=version_list)
+
+ self.assertCreatesV2(auth_url=BASE_URL)
+ self.assertVersionNotAvailable(auth_url=BASE_URL, version=3)
+ self.assertCreatesV3(auth_url=BASE_URL, unstable=True)
+
+ def test_discover_forwards_original_ip(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+
+ ip = '192.168.1.1'
+ self.assertCreatesV3(auth_url=BASE_URL, original_ip=ip)
+
+ self.assertThat(self.requests.last_request.headers['forwarded'],
+ matchers.Contains(ip))
+
+ def test_discover_bad_args(self):
+ self.assertRaises(exceptions.DiscoveryFailure,
+ client.Client)
+
+ def test_discover_bad_response(self):
+ self.requests.get(BASE_URL, status_code=300, json={'FOO': 'BAR'})
+ self.assertDiscoveryFailure(auth_url=BASE_URL)
+
+ def test_discovery_ignore_invalid(self):
+ resp = [{'id': 'v3.0',
+ 'links': [1, 2, 3, 4], # invalid links
+ 'media-types': V3_MEDIA_TYPES,
+ 'status': 'stable',
+ 'updated': UPDATED}]
+ self.requests.get(BASE_URL, status_code=300,
+ text=_create_version_list(resp))
+ self.assertDiscoveryFailure(auth_url=BASE_URL)
+
+ def test_ignore_entry_without_links(self):
+ v3 = V3_VERSION.copy()
+ v3['links'] = []
+ self.requests.get(BASE_URL, status_code=300,
+ text=_create_version_list([v3, V2_VERSION]))
+ self.assertCreatesV2(auth_url=BASE_URL)
+
+ def test_ignore_entry_without_status(self):
+ v3 = V3_VERSION.copy()
+ del v3['status']
+ self.requests.get(BASE_URL, status_code=300,
+ text=_create_version_list([v3, V2_VERSION]))
+ self.assertCreatesV2(auth_url=BASE_URL)
+
+ def test_greater_version_than_required(self):
+ versions = fixture.DiscoveryList(BASE_URL, v3_id='v3.6')
+ self.requests.get(BASE_URL, json=versions)
+ self.assertCreatesV3(auth_url=BASE_URL, version=(3, 4))
+
+ def test_lesser_version_than_required(self):
+ versions = fixture.DiscoveryList(BASE_URL, v3_id='v3.4')
+ self.requests.get(BASE_URL, json=versions)
+ self.assertVersionNotAvailable(auth_url=BASE_URL, version=(3, 6))
+
+ def test_bad_response(self):
+ self.requests.get(BASE_URL, status_code=300, text="Ugly Duckling")
+ self.assertDiscoveryFailure(auth_url=BASE_URL)
+
+ def test_pass_client_arguments(self):
+ self.requests.get(BASE_URL, status_code=300, text=V2_VERSION_LIST)
+ kwargs = {'original_ip': '100', 'use_keyring': False,
+ 'stale_duration': 15}
+
+ cl = self.assertCreatesV2(auth_url=BASE_URL, **kwargs)
+
+ self.assertEqual(cl.original_ip, '100')
+ self.assertEqual(cl.stale_duration, 15)
+ self.assertFalse(cl.use_keyring)
+
+ def test_overriding_stored_kwargs(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+
+ self.requests.post("%s/auth/tokens" % V3_URL,
+ text=V3_AUTH_RESPONSE,
+ headers={'X-Subject-Token': V3_TOKEN})
+
+ disc = discover.Discover(auth_url=BASE_URL, debug=False,
+ username='foo')
+ client = disc.create_client(debug=True, password='bar')
+
+ self.assertIsInstance(client, v3_client.Client)
+ self.assertTrue(client.debug_log)
+ self.assertFalse(disc._client_kwargs['debug'])
+ self.assertEqual(client.username, 'foo')
+ self.assertEqual(client.password, 'bar')
+
+ def test_available_versions(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_ENTRY)
+ disc = discover.Discover(auth_url=BASE_URL)
+
+ versions = disc.available_versions()
+ self.assertEqual(1, len(versions))
+ self.assertEqual(V3_VERSION, versions[0])
+
+ def test_unknown_client_version(self):
+ V4_VERSION = {'id': 'v4.0',
+ 'links': [{'href': 'http://url', 'rel': 'self'}],
+ 'media-types': V3_MEDIA_TYPES,
+ 'status': 'stable',
+ 'updated': UPDATED}
+ versions = fixture.DiscoveryList()
+ versions.add_version(V4_VERSION)
+ self.requests.get(BASE_URL, status_code=300, json=versions)
+
+ disc = discover.Discover(auth_url=BASE_URL)
+ self.assertRaises(exceptions.DiscoveryFailure,
+ disc.create_client, version=4)
+
+ def test_discovery_fail_for_missing_v3(self):
+ versions = fixture.DiscoveryList(v2=True, v3=False)
+ self.requests.get(BASE_URL, status_code=300, json=versions)
+
+ disc = discover.Discover(auth_url=BASE_URL)
+ self.assertRaises(exceptions.DiscoveryFailure,
+ disc.create_client, version=(3, 0))
+
+ def _do_discovery_call(self, token=None, **kwargs):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+
+ if not token:
+ token = uuid.uuid4().hex
+
+ url = 'http://testurl'
+ a = token_endpoint.Token(url, token)
+ s = session.Session(auth=a)
+
+ # will default to true as there is a plugin on the session
+ discover.Discover(s, auth_url=BASE_URL, **kwargs)
+
+ self.assertEqual(BASE_URL, self.requests.last_request.url)
+
+ def test_setting_authenticated_true(self):
+ token = uuid.uuid4().hex
+ self._do_discovery_call(token)
+ self.assertRequestHeaderEqual('X-Auth-Token', token)
+
+ def test_setting_authenticated_false(self):
+ self._do_discovery_call(authenticated=False)
+ self.assertNotIn('X-Auth-Token', self.requests.last_request.headers)
+
+
+class DiscoverQueryTests(utils.TestCase):
+
+ def test_available_keystone_data(self):
+ self.requests.get(BASE_URL, status_code=300, text=V3_VERSION_LIST)
+
+ disc = discover.Discover(auth_url=BASE_URL)
+ versions = disc.version_data()
+
+ self.assertEqual((2, 0), versions[0]['version'])
+ self.assertEqual('stable', versions[0]['raw_status'])
+ self.assertEqual(V2_URL, versions[0]['url'])
+ self.assertEqual((3, 0), versions[1]['version'])
+ self.assertEqual('stable', versions[1]['raw_status'])
+ self.assertEqual(V3_URL, versions[1]['url'])
+
+ version = disc.data_for('v3.0')
+ self.assertEqual((3, 0), version['version'])
+ self.assertEqual('stable', version['raw_status'])
+ self.assertEqual(V3_URL, version['url'])
+
+ version = disc.data_for(2)
+ self.assertEqual((2, 0), version['version'])
+ self.assertEqual('stable', version['raw_status'])
+ self.assertEqual(V2_URL, version['url'])
+
+ self.assertIsNone(disc.url_for('v4'))
+ self.assertEqual(V3_URL, disc.url_for('v3'))
+ self.assertEqual(V2_URL, disc.url_for('v2'))
+
+ def test_available_cinder_data(self):
+ text = jsonutils.dumps(CINDER_EXAMPLES)
+ self.requests.get(BASE_URL, status_code=300, text=text)
+
+ v1_url = "%sv1/" % BASE_URL
+ v2_url = "%sv2/" % BASE_URL
+
+ disc = discover.Discover(auth_url=BASE_URL)
+ versions = disc.version_data()
+
+ self.assertEqual((1, 0), versions[0]['version'])
+ self.assertEqual('CURRENT', versions[0]['raw_status'])
+ self.assertEqual(v1_url, versions[0]['url'])
+ self.assertEqual((2, 0), versions[1]['version'])
+ self.assertEqual('CURRENT', versions[1]['raw_status'])
+ self.assertEqual(v2_url, versions[1]['url'])
+
+ version = disc.data_for('v2.0')
+ self.assertEqual((2, 0), version['version'])
+ self.assertEqual('CURRENT', version['raw_status'])
+ self.assertEqual(v2_url, version['url'])
+
+ version = disc.data_for(1)
+ self.assertEqual((1, 0), version['version'])
+ self.assertEqual('CURRENT', version['raw_status'])
+ self.assertEqual(v1_url, version['url'])
+
+ self.assertIsNone(disc.url_for('v3'))
+ self.assertEqual(v2_url, disc.url_for('v2'))
+ self.assertEqual(v1_url, disc.url_for('v1'))
+
+ def test_available_glance_data(self):
+ text = jsonutils.dumps(GLANCE_EXAMPLES)
+ self.requests.get(BASE_URL, text=text)
+
+ v1_url = "%sv1/" % BASE_URL
+ v2_url = "%sv2/" % BASE_URL
+
+ disc = discover.Discover(auth_url=BASE_URL)
+ versions = disc.version_data()
+
+ self.assertEqual((1, 0), versions[0]['version'])
+ self.assertEqual('SUPPORTED', versions[0]['raw_status'])
+ self.assertEqual(v1_url, versions[0]['url'])
+ self.assertEqual((1, 1), versions[1]['version'])
+ self.assertEqual('CURRENT', versions[1]['raw_status'])
+ self.assertEqual(v1_url, versions[1]['url'])
+ self.assertEqual((2, 0), versions[2]['version'])
+ self.assertEqual('SUPPORTED', versions[2]['raw_status'])
+ self.assertEqual(v2_url, versions[2]['url'])
+ self.assertEqual((2, 1), versions[3]['version'])
+ self.assertEqual('SUPPORTED', versions[3]['raw_status'])
+ self.assertEqual(v2_url, versions[3]['url'])
+ self.assertEqual((2, 2), versions[4]['version'])
+ self.assertEqual('CURRENT', versions[4]['raw_status'])
+ self.assertEqual(v2_url, versions[4]['url'])
+
+ for ver in (2, 2.1, 2.2):
+ version = disc.data_for(ver)
+ self.assertEqual((2, 2), version['version'])
+ self.assertEqual('CURRENT', version['raw_status'])
+ self.assertEqual(v2_url, version['url'])
+ self.assertEqual(v2_url, disc.url_for(ver))
+
+ for ver in (1, 1.1):
+ version = disc.data_for(ver)
+ self.assertEqual((1, 1), version['version'])
+ self.assertEqual('CURRENT', version['raw_status'])
+ self.assertEqual(v1_url, version['url'])
+ self.assertEqual(v1_url, disc.url_for(ver))
+
+ self.assertIsNone(disc.url_for('v3'))
+ self.assertIsNone(disc.url_for('v2.3'))
+
+ def test_allow_deprecated(self):
+ status = 'deprecated'
+ version_list = [{'id': 'v3.0',
+ 'links': [{'href': V3_URL, 'rel': 'self'}],
+ 'media-types': V3_MEDIA_TYPES,
+ 'status': status,
+ 'updated': UPDATED}]
+ text = jsonutils.dumps({'versions': version_list})
+ self.requests.get(BASE_URL, text=text)
+
+ disc = discover.Discover(auth_url=BASE_URL)
+
+ # deprecated is allowed by default
+ versions = disc.version_data(allow_deprecated=False)
+ self.assertEqual(0, len(versions))
+
+ versions = disc.version_data(allow_deprecated=True)
+ self.assertEqual(1, len(versions))
+ self.assertEqual(status, versions[0]['raw_status'])
+ self.assertEqual(V3_URL, versions[0]['url'])
+ self.assertEqual((3, 0), versions[0]['version'])
+
+ def test_allow_experimental(self):
+ status = 'experimental'
+ version_list = [{'id': 'v3.0',
+ 'links': [{'href': V3_URL, 'rel': 'self'}],
+ 'media-types': V3_MEDIA_TYPES,
+ 'status': status,
+ 'updated': UPDATED}]
+ text = jsonutils.dumps({'versions': version_list})
+ self.requests.get(BASE_URL, text=text)
+
+ disc = discover.Discover(auth_url=BASE_URL)
+
+ versions = disc.version_data()
+ self.assertEqual(0, len(versions))
+
+ versions = disc.version_data(allow_experimental=True)
+ self.assertEqual(1, len(versions))
+ self.assertEqual(status, versions[0]['raw_status'])
+ self.assertEqual(V3_URL, versions[0]['url'])
+ self.assertEqual((3, 0), versions[0]['version'])
+
+ def test_allow_unknown(self):
+ status = 'abcdef'
+ version_list = fixture.DiscoveryList(BASE_URL, v2=False,
+ v3_status=status)
+ self.requests.get(BASE_URL, json=version_list)
+ disc = discover.Discover(auth_url=BASE_URL)
+
+ versions = disc.version_data()
+ self.assertEqual(0, len(versions))
+
+ versions = disc.version_data(allow_unknown=True)
+ self.assertEqual(1, len(versions))
+ self.assertEqual(status, versions[0]['raw_status'])
+ self.assertEqual(V3_URL, versions[0]['url'])
+ self.assertEqual((3, 0), versions[0]['version'])
+
+ def test_ignoring_invalid_lnks(self):
+ version_list = [{'id': 'v3.0',
+ 'links': [{'href': V3_URL, 'rel': 'self'}],
+ 'media-types': V3_MEDIA_TYPES,
+ 'status': 'stable',
+ 'updated': UPDATED},
+ {'id': 'v3.1',
+ 'media-types': V3_MEDIA_TYPES,
+ 'status': 'stable',
+ 'updated': UPDATED},
+ {'media-types': V3_MEDIA_TYPES,
+ 'status': 'stable',
+ 'updated': UPDATED,
+ 'links': [{'href': V3_URL, 'rel': 'self'}],
+ }]
+
+ text = jsonutils.dumps({'versions': version_list})
+ self.requests.get(BASE_URL, text=text)
+
+ disc = discover.Discover(auth_url=BASE_URL)
+
+ # raw_version_data will return all choices, even invalid ones
+ versions = disc.raw_version_data()
+ self.assertEqual(3, len(versions))
+
+ # only the version with both id and links will be actually returned
+ versions = disc.version_data()
+ self.assertEqual(1, len(versions))
+
+
+class CatalogHackTests(utils.TestCase):
+
+ TEST_URL = 'http://keystone.server:5000/v2.0'
+ OTHER_URL = 'http://other.server:5000/path'
+
+ IDENTITY = 'identity'
+
+ BASE_URL = 'http://keystone.server:5000/'
+ V2_URL = BASE_URL + 'v2.0'
+ V3_URL = BASE_URL + 'v3'
+
+ def setUp(self):
+ super(CatalogHackTests, self).setUp()
+ self.hacks = _discover._VersionHacks()
+ self.hacks.add_discover_hack(self.IDENTITY,
+ re.compile('/v2.0/?$'),
+ '/')
+
+ def test_version_hacks(self):
+ self.assertEqual(self.BASE_URL,
+ self.hacks.get_discover_hack(self.IDENTITY,
+ self.V2_URL))
+
+ self.assertEqual(self.BASE_URL,
+ self.hacks.get_discover_hack(self.IDENTITY,
+ self.V2_URL + '/'))
+
+ self.assertEqual(self.OTHER_URL,
+ self.hacks.get_discover_hack(self.IDENTITY,
+ self.OTHER_URL))
+
+ def test_ignored_non_service_type(self):
+ self.assertEqual(self.V2_URL,
+ self.hacks.get_discover_hack('other', self.V2_URL))
+
+
+class DiscoverUtils(utils.TestCase):
+
+ def test_version_number(self):
+ def assertVersion(inp, out):
+ self.assertEqual(out, _discover.normalize_version_number(inp))
+
+ def versionRaises(inp):
+ self.assertRaises(TypeError,
+ _discover.normalize_version_number,
+ inp)
+
+ assertVersion('v1.2', (1, 2))
+ assertVersion('v11', (11, 0))
+ assertVersion('1.2', (1, 2))
+ assertVersion('1.5.1', (1, 5, 1))
+ assertVersion('1', (1, 0))
+ assertVersion(1, (1, 0))
+ assertVersion(5.2, (5, 2))
+ assertVersion((6, 1), (6, 1))
+ assertVersion([1, 4], (1, 4))
+
+ versionRaises('hello')
+ versionRaises('1.a')
+ versionRaises('vacuum')
diff --git a/keystoneclient/tests/unit/test_ec2utils.py b/keystoneclient/tests/unit/test_ec2utils.py
new file mode 100644
index 0000000..71fc176
--- /dev/null
+++ b/keystoneclient/tests/unit/test_ec2utils.py
@@ -0,0 +1,262 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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 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 = {'Action': 'CreateUser',
+ 'UserName': 'NewUser',
+ 'Version': '2010-05-08',
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-Credential': 'AKIAEXAMPLE/20140611/'
+ 'us-east-1/iam/aws4_request',
+ 'X-Amz-Date': '20140611T231318Z',
+ 'X-Amz-Expires': '30',
+ 'X-Amz-SignedHeaders': 'host',
+ 'X-Amz-Signature': 'ced6826de92d2bdeed8f846f0bf508e8'
+ '559e98e4b0199114b84c54174deb456c'}
+ 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/keystoneclient/tests/unit/test_fixtures.py b/keystoneclient/tests/unit/test_fixtures.py
new file mode 100644
index 0000000..8080c82
--- /dev/null
+++ b/keystoneclient/tests/unit/test_fixtures.py
@@ -0,0 +1,237 @@
+# 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 six
+
+from keystoneclient import fixture
+from keystoneclient.tests.unit import utils
+
+
+class V2TokenTests(utils.TestCase):
+
+ def test_unscoped(self):
+ token_id = uuid.uuid4().hex
+ user_id = uuid.uuid4().hex
+ user_name = uuid.uuid4().hex
+
+ token = fixture.V2Token(token_id=token_id,
+ user_id=user_id,
+ user_name=user_name)
+
+ self.assertEqual(token_id, token.token_id)
+ self.assertEqual(token_id, token['access']['token']['id'])
+ self.assertEqual(user_id, token.user_id)
+ self.assertEqual(user_id, token['access']['user']['id'])
+ self.assertEqual(user_name, token.user_name)
+ self.assertEqual(user_name, token['access']['user']['name'])
+
+ def test_tenant_scoped(self):
+ tenant_id = uuid.uuid4().hex
+ tenant_name = uuid.uuid4().hex
+
+ token = fixture.V2Token(tenant_id=tenant_id,
+ tenant_name=tenant_name)
+
+ self.assertEqual(tenant_id, token.tenant_id)
+ self.assertEqual(tenant_id, token['access']['token']['tenant']['id'])
+ self.assertEqual(tenant_name, token.tenant_name)
+ tn = token['access']['token']['tenant']['name']
+ self.assertEqual(tenant_name, tn)
+
+ def test_trust_scoped(self):
+ trust_id = uuid.uuid4().hex
+ trustee_user_id = uuid.uuid4().hex
+
+ token = fixture.V2Token(trust_id=trust_id,
+ trustee_user_id=trustee_user_id)
+ trust = token['access']['trust']
+
+ self.assertEqual(trust_id, token.trust_id)
+ self.assertEqual(trust_id, trust['id'])
+ self.assertEqual(trustee_user_id, token.trustee_user_id)
+ self.assertEqual(trustee_user_id, trust['trustee_user_id'])
+
+ def test_roles(self):
+ role_id1 = uuid.uuid4().hex
+ role_name1 = uuid.uuid4().hex
+ role_id2 = uuid.uuid4().hex
+ role_name2 = uuid.uuid4().hex
+
+ token = fixture.V2Token()
+ token.add_role(id=role_id1, name=role_name1)
+ token.add_role(id=role_id2, name=role_name2)
+
+ role_names = token['access']['user']['roles']
+ role_ids = token['access']['metadata']['roles']
+
+ self.assertEqual(set([role_id1, role_id2]), set(role_ids))
+ for r in (role_name1, role_name2):
+ self.assertIn({'name': r}, role_names)
+
+ def test_services(self):
+ service_type = uuid.uuid4().hex
+ service_name = uuid.uuid4().hex
+ region = uuid.uuid4().hex
+
+ public = uuid.uuid4().hex
+ admin = uuid.uuid4().hex
+ internal = uuid.uuid4().hex
+
+ token = fixture.V2Token()
+ svc = token.add_service(type=service_type, name=service_name)
+
+ svc.add_endpoint(public=public,
+ admin=admin,
+ internal=internal,
+ region=region)
+
+ self.assertEqual(1, len(token['access']['serviceCatalog']))
+ service = token['access']['serviceCatalog'][0]['endpoints'][0]
+
+ self.assertEqual(public, service['publicURL'])
+ self.assertEqual(internal, service['internalURL'])
+ self.assertEqual(admin, service['adminURL'])
+ self.assertEqual(region, service['region'])
+
+
+class V3TokenTests(utils.TestCase):
+
+ def test_unscoped(self):
+ user_id = uuid.uuid4().hex
+ user_name = uuid.uuid4().hex
+ user_domain_id = uuid.uuid4().hex
+ user_domain_name = uuid.uuid4().hex
+
+ token = fixture.V3Token(user_id=user_id,
+ user_name=user_name,
+ user_domain_id=user_domain_id,
+ user_domain_name=user_domain_name)
+
+ self.assertEqual(user_id, token.user_id)
+ self.assertEqual(user_id, token['token']['user']['id'])
+ self.assertEqual(user_name, token.user_name)
+ self.assertEqual(user_name, token['token']['user']['name'])
+
+ user_domain = token['token']['user']['domain']
+
+ self.assertEqual(user_domain_id, token.user_domain_id)
+ self.assertEqual(user_domain_id, user_domain['id'])
+ self.assertEqual(user_domain_name, token.user_domain_name)
+ self.assertEqual(user_domain_name, user_domain['name'])
+
+ def test_project_scoped(self):
+ project_id = uuid.uuid4().hex
+ project_name = uuid.uuid4().hex
+ project_domain_id = uuid.uuid4().hex
+ project_domain_name = uuid.uuid4().hex
+
+ token = fixture.V3Token(project_id=project_id,
+ project_name=project_name,
+ project_domain_id=project_domain_id,
+ project_domain_name=project_domain_name)
+
+ self.assertEqual(project_id, token.project_id)
+ self.assertEqual(project_id, token['token']['project']['id'])
+ self.assertEqual(project_name, token.project_name)
+ self.assertEqual(project_name, token['token']['project']['name'])
+
+ project_domain = token['token']['project']['domain']
+
+ self.assertEqual(project_domain_id, token.project_domain_id)
+ self.assertEqual(project_domain_id, project_domain['id'])
+ self.assertEqual(project_domain_name, token.project_domain_name)
+ self.assertEqual(project_domain_name, project_domain['name'])
+
+ def test_domain_scoped(self):
+ domain_id = uuid.uuid4().hex
+ domain_name = uuid.uuid4().hex
+
+ token = fixture.V3Token(domain_id=domain_id,
+ domain_name=domain_name)
+
+ self.assertEqual(domain_id, token.domain_id)
+ self.assertEqual(domain_id, token['token']['domain']['id'])
+ self.assertEqual(domain_name, token.domain_name)
+ self.assertEqual(domain_name, token['token']['domain']['name'])
+
+ def test_roles(self):
+ role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
+ role2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
+
+ token = fixture.V3Token()
+ token.add_role(**role1)
+ token.add_role(**role2)
+
+ self.assertEqual(2, len(token['token']['roles']))
+
+ self.assertIn(role1, token['token']['roles'])
+ self.assertIn(role2, token['token']['roles'])
+
+ def test_trust_scoped(self):
+ trust_id = uuid.uuid4().hex
+ trustee_user_id = uuid.uuid4().hex
+ trustor_user_id = uuid.uuid4().hex
+ impersonation = True
+
+ token = fixture.V3Token(trust_id=trust_id,
+ trustee_user_id=trustee_user_id,
+ trustor_user_id=trustor_user_id,
+ trust_impersonation=impersonation)
+
+ trust = token['token']['OS-TRUST:trust']
+ self.assertEqual(trust_id, token.trust_id)
+ self.assertEqual(trust_id, trust['id'])
+ self.assertEqual(trustee_user_id, token.trustee_user_id)
+ self.assertEqual(trustee_user_id, trust['trustee_user']['id'])
+ self.assertEqual(trustor_user_id, token.trustor_user_id)
+ self.assertEqual(trustor_user_id, trust['trustor_user']['id'])
+ self.assertEqual(impersonation, token.trust_impersonation)
+ self.assertEqual(impersonation, trust['impersonation'])
+
+ def test_oauth_scoped(self):
+ access_id = uuid.uuid4().hex
+ consumer_id = uuid.uuid4().hex
+
+ token = fixture.V3Token(oauth_access_token_id=access_id,
+ oauth_consumer_id=consumer_id)
+
+ oauth = token['token']['OS-OAUTH1']
+
+ self.assertEqual(access_id, token.oauth_access_token_id)
+ self.assertEqual(access_id, oauth['access_token_id'])
+ self.assertEqual(consumer_id, token.oauth_consumer_id)
+ self.assertEqual(consumer_id, oauth['consumer_id'])
+
+ def test_catalog(self):
+ service_type = uuid.uuid4().hex
+ service_name = uuid.uuid4().hex
+ region = uuid.uuid4().hex
+ endpoints = {'public': uuid.uuid4().hex,
+ 'internal': uuid.uuid4().hex,
+ 'admin': uuid.uuid4().hex}
+
+ token = fixture.V3Token()
+ svc = token.add_service(type=service_type, name=service_name)
+ svc.add_standard_endpoints(region=region, **endpoints)
+
+ self.assertEqual(1, len(token['token']['catalog']))
+ service = token['token']['catalog'][0]
+ self.assertEqual(3, len(service['endpoints']))
+
+ self.assertEqual(service_name, service['name'])
+ self.assertEqual(service_type, service['type'])
+
+ for interface, url in six.iteritems(endpoints):
+ endpoint = {'interface': interface, 'url': url, 'region': region}
+ self.assertIn(endpoint, service['endpoints'])
diff --git a/keystoneclient/tests/unit/test_hacking_checks.py b/keystoneclient/tests/unit/test_hacking_checks.py
new file mode 100644
index 0000000..220d258
--- /dev/null
+++ b/keystoneclient/tests/unit/test_hacking_checks.py
@@ -0,0 +1,47 @@
+# 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 textwrap
+
+import mock
+import pep8
+import testtools
+
+from keystoneclient.hacking import checks
+from keystoneclient.tests.unit import client_fixtures
+
+
+class TestCheckOsloNamespaceImports(testtools.TestCase):
+
+ # We are patching pep8 so that only the check under test is actually
+ # installed.
+ @mock.patch('pep8._checks',
+ {'physical_line': {}, 'logical_line': {}, 'tree': {}})
+ def run_check(self, code):
+ pep8.register_check(checks.check_oslo_namespace_imports)
+
+ lines = textwrap.dedent(code).strip().splitlines(True)
+
+ checker = pep8.Checker(lines=lines)
+ checker.check_all()
+ checker.report._deferred_print.sort()
+ return checker.report._deferred_print
+
+ def assert_has_errors(self, code, expected_errors=None):
+ actual_errors = [e[:3] for e in self.run_check(code)]
+ self.assertEqual(expected_errors or [], actual_errors)
+
+ def test(self):
+ code_ex = self.useFixture(client_fixtures.HackingCode())
+ code = code_ex.oslo_namespace_imports['code']
+ errors = code_ex.oslo_namespace_imports['expected_errors']
+ self.assert_has_errors(code, expected_errors=errors)
diff --git a/keystoneclient/tests/unit/test_http.py b/keystoneclient/tests/unit/test_http.py
new file mode 100644
index 0000000..6dfceec
--- /dev/null
+++ b/keystoneclient/tests/unit/test_http.py
@@ -0,0 +1,208 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 logging
+
+import six
+from testtools import matchers
+
+from keystoneclient import exceptions
+from keystoneclient import httpclient
+from keystoneclient import session
+from keystoneclient.tests.unit 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')
+
+ def test_get(self):
+ cl = get_authed_client()
+
+ self.stub_url('GET', text=RESPONSE_BODY)
+
+ resp, body = cl.get("/hi")
+ self.assertEqual(self.requests.last_request.method, 'GET')
+ self.assertEqual(self.requests.last_request.url, self.TEST_URL)
+
+ self.assertRequestHeaderEqual('X-Auth-Token', 'token')
+ self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT)
+
+ # Automatic JSON parsing
+ self.assertEqual(body, {"hi": "there"})
+
+ def test_get_error_with_plaintext_resp(self):
+ cl = get_authed_client()
+ self.stub_url('GET', status_code=400,
+ text='Some evil plaintext string')
+
+ self.assertRaises(exceptions.BadRequest, cl.get, '/hi')
+
+ 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('GET', status_code=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.')
+
+ def test_post(self):
+ cl = get_authed_client()
+
+ self.stub_url('POST')
+ cl.post("/hi", body=[1, 2, 3])
+
+ self.assertEqual(self.requests.last_request.method, 'POST')
+ self.assertEqual(self.requests.last_request.body, '[1, 2, 3]')
+
+ self.assertRequestHeaderEqual('X-Auth-Token', 'token')
+ self.assertRequestHeaderEqual('Content-Type', 'application/json')
+ self.assertRequestHeaderEqual('User-Agent', httpclient.USER_AGENT)
+
+ 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('GET')
+
+ cl.request(self.TEST_URL, 'GET')
+ forwarded = "for=%s;by=%s" % (ORIGINAL_IP, httpclient.USER_AGENT)
+ self.assertRequestHeaderEqual('Forwarded', forwarded)
+
+ 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(utils.TestCase):
+
+ url = 'http://keystone.test.com/'
+
+ def setUp(self):
+ super(BasicRequestTests, self).setUp()
+ self.logger_message = six.moves.cStringIO()
+ handler = logging.StreamHandler(self.logger_message)
+ handler.setLevel(logging.DEBUG)
+
+ self.logger = logging.getLogger(session.__name__)
+ level = self.logger.getEffectiveLevel()
+ self.logger.setLevel(logging.DEBUG)
+ self.logger.addHandler(handler)
+
+ self.addCleanup(self.logger.removeHandler, handler)
+ self.addCleanup(self.logger.setLevel, level)
+
+ def request(self, method='GET', response='Test Response', status_code=200,
+ url=None, **kwargs):
+ if not url:
+ url = self.url
+
+ self.requests.register_uri(method, url, text=response,
+ status_code=status_code)
+
+ return httpclient.request(url, method, **kwargs)
+
+ def test_basic_params(self):
+ method = 'GET'
+ response = 'Test Response'
+ status = 200
+
+ self.request(method=method, status_code=status, response=response)
+
+ self.assertEqual(self.requests.last_request.method, method)
+
+ logger_message = self.logger_message.getvalue()
+
+ self.assertThat(logger_message, matchers.Contains('curl'))
+ self.assertThat(logger_message, matchers.Contains('-X %s' %
+ method))
+ self.assertThat(logger_message, matchers.Contains(self.url))
+
+ self.assertThat(logger_message, matchers.Contains(str(status)))
+ self.assertThat(logger_message, matchers.Contains(response))
+
+ def test_headers(self):
+ headers = {'key': 'val', 'test': 'other'}
+
+ self.request(headers=headers)
+
+ for k, v in six.iteritems(headers):
+ self.assertRequestHeaderEqual(k, v)
+
+ for header in six.iteritems(headers):
+ self.assertThat(self.logger_message.getvalue(),
+ matchers.Contains('-H "%s: %s"' % header))
+
+ def test_body(self):
+ data = "BODY DATA"
+ self.request(response=data)
+ logger_message = self.logger_message.getvalue()
+ self.assertThat(logger_message, matchers.Contains('BODY:'))
+ self.assertThat(logger_message, matchers.Contains(data))
diff --git a/keystoneclient/tests/unit/test_https.py b/keystoneclient/tests/unit/test_https.py
new file mode 100644
index 0000000..e574d37
--- /dev/null
+++ b/keystoneclient/tests/unit/test_https.py
@@ -0,0 +1,107 @@
+# 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.unit import utils
+
+
+FAKE_RESPONSE = utils.TestResponse({
+ "status_code": 200,
+ "text": '{"hi": "there"}',
+})
+
+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()
+ self.addCleanup(self.request_patcher.stop)
+
+ @mock.patch.object(requests, 'request')
+ def test_get(self, MOCK_REQUEST):
+ MOCK_REQUEST.return_value = FAKE_RESPONSE
+ cl = get_authed_client()
+
+ 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"})
+
+ @mock.patch.object(requests, 'request')
+ def test_post(self, MOCK_REQUEST):
+ MOCK_REQUEST.return_value = FAKE_RESPONSE
+ cl = get_authed_client()
+
+ 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')
+
+ @mock.patch.object(requests, 'request')
+ def test_post_auth(self, MOCK_REQUEST):
+ MOCK_REQUEST.return_value = FAKE_RESPONSE
+ 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/keystoneclient/tests/unit/test_keyring.py b/keystoneclient/tests/unit/test_keyring.py
new file mode 100644
index 0000000..a54009e
--- /dev/null
+++ b/keystoneclient/tests/unit/test_keyring.py
@@ -0,0 +1,187 @@
+# 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 oslo_utils import timeutils
+
+from keystoneclient import access
+from keystoneclient import httpclient
+from keystoneclient.tests.unit import utils
+from keystoneclient.tests.unit.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
+ method = 'get_raw_token_from_identity_service'
+ with mock.patch.object(cl, method) as meth:
+ meth.return_value = (True, PROJECT_SCOPED_TOKEN)
+
+ self.assertTrue(cl.authenticate())
+
+ self.assertEqual(1, meth.call_count)
+
+ # 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
+ method = 'get_raw_token_from_identity_service'
+ with mock.patch.object(cl, method) as meth:
+ meth.return_value = (True, PROJECT_SCOPED_TOKEN)
+
+ self.assertTrue(cl.authenticate())
+
+ self.assertEqual(1, meth.call_count)
+
+ # 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
+ method = 'get_raw_token_from_identity_service'
+ with mock.patch.object(cl, method) as meth:
+ meth.return_value = (True, PROJECT_SCOPED_TOKEN)
+
+ self.assertTrue(cl.authenticate())
+
+ self.assertEqual(1, meth.call_count)
+
+ # 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/keystoneclient/tests/unit/test_memcache_crypt.py b/keystoneclient/tests/unit/test_memcache_crypt.py
new file mode 100644
index 0000000..be07b24
--- /dev/null
+++ b/keystoneclient/tests/unit/test_memcache_crypt.py
@@ -0,0 +1,97 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import six
+import testtools
+
+from keystoneclient.middleware import memcache_crypt
+
+
+class MemcacheCryptPositiveTests(testtools.TestCase):
+ def _setup_keys(self, strategy):
+ return memcache_crypt.derive_keys(b'token', b'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'))
+
+ # For Python 3, we want to test these functions with both str and bytes
+ # as input.
+ if six.PY3:
+ self.assertTrue(ctc(b'abcd', b'abcd'))
+ self.assertTrue(ctc(b'', b''))
+ self.assertFalse(ctc(b'abcd', b'efgh'))
+ self.assertFalse(ctc(b'abc', b'abcd'))
+ self.assertFalse(ctc(b'abc', b'abc\x00'))
+ self.assertFalse(ctc(b'', b'abc'))
+
+ def test_derive_keys(self):
+ keys = self._setup_keys(b'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(b'MAC')
+ k2 = self._setup_keys(b'ENCRYPT')
+ self.assertNotEqual(k1, k2)
+
+ def test_sign_data(self):
+ keys = self._setup_keys(b'MAC')
+ sig = memcache_crypt.sign_data(keys['MAC'], b'data')
+ self.assertEqual(len(sig), memcache_crypt.DIGEST_LENGTH_B64)
+
+ def test_encryption(self):
+ keys = self._setup_keys(b'ENCRYPT')
+ # what you put in is what you get out
+ for data in [b'data', b'1234567890123456', b'\x00\xFF' * 13
+ ] + [six.int2byte(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 = b'My Pretty Little Data'
+ for strategy in [b'MAC', b'ENCRYPT']:
+ keys = self._setup_keys(strategy)
+ protected = memcache_crypt.protect_data(keys, data)
+ self.assertNotEqual(protected, data)
+ if strategy == b'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/keystoneclient/tests/unit/test_s3_token_middleware.py b/keystoneclient/tests/unit/test_s3_token_middleware.py
new file mode 100644
index 0000000..63f9e72
--- /dev/null
+++ b/keystoneclient/tests/unit/test_s3_token_middleware.py
@@ -0,0 +1,233 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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
+from oslo_serialization import jsonutils
+import requests
+import six
+import testtools
+import webob
+
+from keystoneclient.middleware import s3_token
+from keystoneclient.tests.unit import utils
+
+
+GOOD_RESPONSE = {'access': {'token': {'id': 'TOKEN_ID',
+ 'tenant': {'id': 'TENANT_ID'}}}}
+
+
+class FakeApp(object):
+ """This represents a WSGI app protected by the auth_token middleware."""
+ def __call__(self, env, start_response):
+ resp = webob.Response()
+ resp.environ = env
+ return resp(env, start_response)
+
+
+class S3TokenMiddlewareTestBase(utils.TestCase):
+
+ TEST_PROTOCOL = 'https'
+ TEST_HOST = 'fakehost'
+ TEST_PORT = 35357
+ TEST_URL = '%s://%s:%d/v2.0/s3tokens' % (TEST_PROTOCOL,
+ TEST_HOST,
+ TEST_PORT)
+
+ def setUp(self):
+ super(S3TokenMiddlewareTestBase, self).setUp()
+
+ self.conf = {
+ 'auth_host': self.TEST_HOST,
+ 'auth_port': self.TEST_PORT,
+ 'auth_protocol': self.TEST_PROTOCOL,
+ }
+
+ def start_fake_response(self, status, headers):
+ self.response_status = int(status.split(' ', 1)[0])
+ self.response_headers = dict(headers)
+
+
+class S3TokenMiddlewareTestGood(S3TokenMiddlewareTestBase):
+
+ def setUp(self):
+ super(S3TokenMiddlewareTestGood, self).setUp()
+ self.middleware = s3_token.S3Token(FakeApp(), self.conf)
+
+ self.requests.post(self.TEST_URL, status_code=201, json=GOOD_RESPONSE)
+
+ # Ignore the request and pass to the next middleware in the
+ # pipeline if no path has been specified.
+ def test_no_path_request(self):
+ req = webob.Request.blank('/')
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 200)
+
+ # Ignore the request and pass to the next middleware in the
+ # pipeline if no Authorization header has been specified
+ def test_without_authorization(self):
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 200)
+
+ def test_without_auth_storage_token(self):
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ req.headers['Authorization'] = 'badboy'
+ self.middleware(req.environ, self.start_fake_response)
+ self.assertEqual(self.response_status, 200)
+
+ def test_authorized(self):
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ req.headers['Authorization'] = 'access:signature'
+ req.headers['X-Storage-Token'] = 'token'
+ req.get_response(self.middleware)
+ self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID'))
+ self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID')
+
+ def test_authorized_http(self):
+ TEST_URL = 'http://%s:%d/v2.0/s3tokens' % (self.TEST_HOST,
+ self.TEST_PORT)
+
+ self.requests.post(TEST_URL, status_code=201, json=GOOD_RESPONSE)
+
+ self.middleware = (
+ s3_token.filter_factory({'auth_protocol': 'http',
+ 'auth_host': self.TEST_HOST,
+ 'auth_port': self.TEST_PORT})(FakeApp()))
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ req.headers['Authorization'] = 'access:signature'
+ req.headers['X-Storage-Token'] = 'token'
+ req.get_response(self.middleware)
+ self.assertTrue(req.path.startswith('/v1/AUTH_TENANT_ID'))
+ self.assertEqual(req.headers['X-Auth-Token'], 'TOKEN_ID')
+
+ def test_authorization_nova_toconnect(self):
+ req = webob.Request.blank('/v1/AUTH_swiftint/c/o')
+ req.headers['Authorization'] = 'access:FORCED_TENANT_ID:signature'
+ req.headers['X-Storage-Token'] = 'token'
+ req.get_response(self.middleware)
+ path = req.environ['PATH_INFO']
+ self.assertTrue(path.startswith('/v1/AUTH_FORCED_TENANT_ID'))
+
+ @mock.patch.object(requests, 'post')
+ def test_insecure(self, MOCK_REQUEST):
+ self.middleware = (
+ s3_token.filter_factory({'insecure': True})(FakeApp()))
+
+ text_return_value = jsonutils.dumps(GOOD_RESPONSE)
+ if six.PY3:
+ text_return_value = text_return_value.encode()
+ MOCK_REQUEST.return_value = utils.TestResponse({
+ 'status_code': 201,
+ 'text': text_return_value})
+
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ req.headers['Authorization'] = 'access:signature'
+ req.headers['X-Storage-Token'] = 'token'
+ req.get_response(self.middleware)
+
+ self.assertTrue(MOCK_REQUEST.called)
+ mock_args, mock_kwargs = MOCK_REQUEST.call_args
+ self.assertIs(mock_kwargs['verify'], False)
+
+
+class S3TokenMiddlewareTestBad(S3TokenMiddlewareTestBase):
+ def setUp(self):
+ super(S3TokenMiddlewareTestBad, self).setUp()
+ self.middleware = s3_token.S3Token(FakeApp(), self.conf)
+
+ def test_unauthorized_token(self):
+ ret = {"error":
+ {"message": "EC2 access key not found.",
+ "code": 401,
+ "title": "Unauthorized"}}
+ self.requests.post(self.TEST_URL, status_code=403, json=ret)
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ req.headers['Authorization'] = 'access:signature'
+ req.headers['X-Storage-Token'] = 'token'
+ resp = req.get_response(self.middleware)
+ s3_denied_req = self.middleware.deny_request('AccessDenied')
+ self.assertEqual(resp.body, s3_denied_req.body)
+ self.assertEqual(resp.status_int, s3_denied_req.status_int)
+
+ def test_bogus_authorization(self):
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ req.headers['Authorization'] = 'badboy'
+ req.headers['X-Storage-Token'] = 'token'
+ resp = req.get_response(self.middleware)
+ self.assertEqual(resp.status_int, 400)
+ s3_invalid_req = self.middleware.deny_request('InvalidURI')
+ self.assertEqual(resp.body, s3_invalid_req.body)
+ self.assertEqual(resp.status_int, s3_invalid_req.status_int)
+
+ def test_fail_to_connect_to_keystone(self):
+ with mock.patch.object(self.middleware, '_json_request') as o:
+ s3_invalid_req = self.middleware.deny_request('InvalidURI')
+ o.side_effect = s3_token.ServiceError(s3_invalid_req)
+
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ req.headers['Authorization'] = 'access:signature'
+ req.headers['X-Storage-Token'] = 'token'
+ resp = req.get_response(self.middleware)
+ self.assertEqual(resp.body, s3_invalid_req.body)
+ self.assertEqual(resp.status_int, s3_invalid_req.status_int)
+
+ def test_bad_reply(self):
+ self.requests.post(self.TEST_URL, status_code=201, text="<badreply>")
+
+ req = webob.Request.blank('/v1/AUTH_cfa/c/o')
+ req.headers['Authorization'] = 'access:signature'
+ req.headers['X-Storage-Token'] = 'token'
+ resp = req.get_response(self.middleware)
+ s3_invalid_req = self.middleware.deny_request('InvalidURI')
+ self.assertEqual(resp.body, s3_invalid_req.body)
+ self.assertEqual(resp.status_int, s3_invalid_req.status_int)
+
+
+class S3TokenMiddlewareTestUtil(testtools.TestCase):
+ def test_split_path_failed(self):
+ self.assertRaises(ValueError, s3_token.split_path, '')
+ self.assertRaises(ValueError, s3_token.split_path, '/')
+ self.assertRaises(ValueError, s3_token.split_path, '//')
+ self.assertRaises(ValueError, s3_token.split_path, '//a')
+ self.assertRaises(ValueError, s3_token.split_path, '/a/c')
+ self.assertRaises(ValueError, s3_token.split_path, '//c')
+ self.assertRaises(ValueError, s3_token.split_path, '/a/c/')
+ self.assertRaises(ValueError, s3_token.split_path, '/a//')
+ self.assertRaises(ValueError, s3_token.split_path, '/a', 2)
+ self.assertRaises(ValueError, s3_token.split_path, '/a', 2, 3)
+ self.assertRaises(ValueError, s3_token.split_path, '/a', 2, 3, True)
+ self.assertRaises(ValueError, s3_token.split_path, '/a/c/o/r', 3, 3)
+ self.assertRaises(ValueError, s3_token.split_path, '/a', 5, 4)
+
+ def test_split_path_success(self):
+ self.assertEqual(s3_token.split_path('/a'), ['a'])
+ self.assertEqual(s3_token.split_path('/a/'), ['a'])
+ self.assertEqual(s3_token.split_path('/a/c', 2), ['a', 'c'])
+ self.assertEqual(s3_token.split_path('/a/c/o', 3), ['a', 'c', 'o'])
+ self.assertEqual(s3_token.split_path('/a/c/o/r', 3, 3, True),
+ ['a', 'c', 'o/r'])
+ self.assertEqual(s3_token.split_path('/a/c', 2, 3, True),
+ ['a', 'c', None])
+ self.assertEqual(s3_token.split_path('/a/c/', 2), ['a', 'c'])
+ self.assertEqual(s3_token.split_path('/a/c/', 2, 3), ['a', 'c', ''])
+
+ def test_split_path_invalid_path(self):
+ try:
+ s3_token.split_path('o\nn e', 2)
+ except ValueError as err:
+ self.assertEqual(str(err), 'Invalid path: o%0An%20e')
+ try:
+ s3_token.split_path('o\nn e', 2, 3, True)
+ except ValueError as err:
+ self.assertEqual(str(err), 'Invalid path: o%0An%20e')
diff --git a/keystoneclient/tests/unit/test_session.py b/keystoneclient/tests/unit/test_session.py
new file mode 100644
index 0000000..1d01c3a
--- /dev/null
+++ b/keystoneclient/tests/unit/test_session.py
@@ -0,0 +1,866 @@
+# 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 itertools
+import uuid
+
+import mock
+from oslo_config import cfg
+from oslo_config import fixture as config
+from oslo_serialization import jsonutils
+import requests
+import six
+from testtools import matchers
+
+from keystoneclient import adapter
+from keystoneclient.auth import base
+from keystoneclient import exceptions
+from keystoneclient import session as client_session
+from keystoneclient.tests.unit import utils
+
+
+class SessionTests(utils.TestCase):
+
+ TEST_URL = 'http://127.0.0.1:5000/'
+
+ def test_get(self):
+ session = client_session.Session()
+ self.stub_url('GET', text='response')
+ resp = session.get(self.TEST_URL)
+
+ self.assertEqual('GET', self.requests.last_request.method)
+ self.assertEqual(resp.text, 'response')
+ self.assertTrue(resp.ok)
+
+ def test_post(self):
+ session = client_session.Session()
+ self.stub_url('POST', text='response')
+ resp = session.post(self.TEST_URL, json={'hello': 'world'})
+
+ self.assertEqual('POST', self.requests.last_request.method)
+ self.assertEqual(resp.text, 'response')
+ self.assertTrue(resp.ok)
+ self.assertRequestBodyIs(json={'hello': 'world'})
+
+ def test_head(self):
+ session = client_session.Session()
+ self.stub_url('HEAD')
+ resp = session.head(self.TEST_URL)
+
+ self.assertEqual('HEAD', self.requests.last_request.method)
+ self.assertTrue(resp.ok)
+ self.assertRequestBodyIs('')
+
+ def test_put(self):
+ session = client_session.Session()
+ self.stub_url('PUT', text='response')
+ resp = session.put(self.TEST_URL, json={'hello': 'world'})
+
+ self.assertEqual('PUT', self.requests.last_request.method)
+ self.assertEqual(resp.text, 'response')
+ self.assertTrue(resp.ok)
+ self.assertRequestBodyIs(json={'hello': 'world'})
+
+ def test_delete(self):
+ session = client_session.Session()
+ self.stub_url('DELETE', text='response')
+ resp = session.delete(self.TEST_URL)
+
+ self.assertEqual('DELETE', self.requests.last_request.method)
+ self.assertTrue(resp.ok)
+ self.assertEqual(resp.text, 'response')
+
+ def test_patch(self):
+ session = client_session.Session()
+ self.stub_url('PATCH', text='response')
+ resp = session.patch(self.TEST_URL, json={'hello': 'world'})
+
+ self.assertEqual('PATCH', self.requests.last_request.method)
+ self.assertTrue(resp.ok)
+ self.assertEqual(resp.text, 'response')
+ self.assertRequestBodyIs(json={'hello': 'world'})
+
+ def test_user_agent(self):
+ session = client_session.Session(user_agent='test-agent')
+ self.stub_url('GET', text='response')
+ resp = session.get(self.TEST_URL)
+
+ self.assertTrue(resp.ok)
+ self.assertRequestHeaderEqual('User-Agent', 'test-agent')
+
+ resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'})
+ self.assertTrue(resp.ok)
+ self.assertRequestHeaderEqual('User-Agent', 'new-agent')
+
+ resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'},
+ user_agent='overrides-agent')
+ self.assertTrue(resp.ok)
+ self.assertRequestHeaderEqual('User-Agent', 'overrides-agent')
+
+ def test_http_session_opts(self):
+ session = client_session.Session(cert='cert.pem', timeout=5,
+ verify='certs')
+
+ FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'})
+ RESP = mock.Mock(return_value=FAKE_RESP)
+
+ with mock.patch.object(session.session, 'request', RESP) as mocked:
+ session.post(self.TEST_URL, data='value')
+
+ mock_args, mock_kwargs = mocked.call_args
+
+ self.assertEqual(mock_args[0], 'POST')
+ self.assertEqual(mock_args[1], self.TEST_URL)
+ self.assertEqual(mock_kwargs['data'], 'value')
+ self.assertEqual(mock_kwargs['cert'], 'cert.pem')
+ self.assertEqual(mock_kwargs['verify'], 'certs')
+ self.assertEqual(mock_kwargs['timeout'], 5)
+
+ def test_not_found(self):
+ session = client_session.Session()
+ self.stub_url('GET', status_code=404)
+ self.assertRaises(exceptions.NotFound, session.get, self.TEST_URL)
+
+ def test_server_error(self):
+ session = client_session.Session()
+ self.stub_url('GET', status_code=500)
+ self.assertRaises(exceptions.InternalServerError,
+ session.get, self.TEST_URL)
+
+ def test_session_debug_output(self):
+ """Test request and response headers in debug logs
+
+ in order to redact secure headers while debug is true.
+ """
+ session = client_session.Session(verify=False)
+ headers = {'HEADERA': 'HEADERVALB'}
+ security_headers = {'Authorization': uuid.uuid4().hex,
+ 'X-Auth-Token': uuid.uuid4().hex,
+ 'X-Subject-Token': uuid.uuid4().hex, }
+ body = 'BODYRESPONSE'
+ data = 'BODYDATA'
+ all_headers = dict(
+ itertools.chain(headers.items(), security_headers.items()))
+ self.stub_url('POST', text=body, headers=all_headers)
+ resp = session.post(self.TEST_URL, headers=all_headers, data=data)
+ self.assertEqual(resp.status_code, 200)
+
+ self.assertIn('curl', self.logger.output)
+ self.assertIn('POST', self.logger.output)
+ self.assertIn('--insecure', self.logger.output)
+ self.assertIn(body, self.logger.output)
+ self.assertIn("'%s'" % data, self.logger.output)
+
+ for k, v in six.iteritems(headers):
+ self.assertIn(k, self.logger.output)
+ self.assertIn(v, self.logger.output)
+
+ # Assert that response headers contains actual values and
+ # only debug logs has been masked
+ for k, v in six.iteritems(security_headers):
+ self.assertIn('%s: {SHA1}' % k, self.logger.output)
+ self.assertEqual(v, resp.headers[k])
+ self.assertNotIn(v, self.logger.output)
+
+ def test_logging_cacerts(self):
+ path_to_certs = '/path/to/certs'
+ session = client_session.Session(verify=path_to_certs)
+
+ self.stub_url('GET', text='text')
+ session.get(self.TEST_URL)
+
+ self.assertIn('--cacert', self.logger.output)
+ self.assertIn(path_to_certs, self.logger.output)
+
+ def test_connect_retries(self):
+
+ def _timeout_error(request, context):
+ raise requests.exceptions.Timeout()
+
+ self.stub_url('GET', text=_timeout_error)
+
+ session = client_session.Session()
+ retries = 3
+
+ with mock.patch('time.sleep') as m:
+ self.assertRaises(exceptions.RequestTimeout,
+ session.get,
+ self.TEST_URL, connect_retries=retries)
+
+ self.assertEqual(retries, m.call_count)
+ # 3 retries finishing with 2.0 means 0.5, 1.0 and 2.0
+ m.assert_called_with(2.0)
+
+ # we count retries so there will be one initial request + 3 retries
+ self.assertThat(self.requests.request_history,
+ matchers.HasLength(retries + 1))
+
+ def test_uses_tcp_keepalive_by_default(self):
+ session = client_session.Session()
+ requests_session = session.session
+ self.assertIsInstance(requests_session.adapters['http://'],
+ client_session.TCPKeepAliveAdapter)
+ self.assertIsInstance(requests_session.adapters['https://'],
+ client_session.TCPKeepAliveAdapter)
+
+ def test_does_not_set_tcp_keepalive_on_custom_sessions(self):
+ mock_session = mock.Mock()
+ client_session.Session(session=mock_session)
+ self.assertFalse(mock_session.mount.called)
+
+
+class RedirectTests(utils.TestCase):
+
+ REDIRECT_CHAIN = ['http://myhost:3445/',
+ 'http://anotherhost:6555/',
+ 'http://thirdhost/',
+ 'http://finaldestination:55/']
+
+ DEFAULT_REDIRECT_BODY = 'Redirect'
+ DEFAULT_RESP_BODY = 'Found'
+
+ def setup_redirects(self, method='GET', status_code=305,
+ redirect_kwargs={}, final_kwargs={}):
+ redirect_kwargs.setdefault('text', self.DEFAULT_REDIRECT_BODY)
+
+ for s, d in zip(self.REDIRECT_CHAIN, self.REDIRECT_CHAIN[1:]):
+ self.requests.register_uri(method, s, status_code=status_code,
+ headers={'Location': d},
+ **redirect_kwargs)
+
+ final_kwargs.setdefault('status_code', 200)
+ final_kwargs.setdefault('text', self.DEFAULT_RESP_BODY)
+ self.requests.register_uri(method, self.REDIRECT_CHAIN[-1],
+ **final_kwargs)
+
+ def assertResponse(self, resp):
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.text, self.DEFAULT_RESP_BODY)
+
+ def test_basic_get(self):
+ session = client_session.Session()
+ self.setup_redirects()
+ resp = session.get(self.REDIRECT_CHAIN[-2])
+ self.assertResponse(resp)
+
+ def test_basic_post_keeps_correct_method(self):
+ session = client_session.Session()
+ self.setup_redirects(method='POST', status_code=301)
+ resp = session.post(self.REDIRECT_CHAIN[-2])
+ self.assertResponse(resp)
+
+ def test_redirect_forever(self):
+ session = client_session.Session(redirect=True)
+ self.setup_redirects()
+ resp = session.get(self.REDIRECT_CHAIN[0])
+ self.assertResponse(resp)
+ self.assertTrue(len(resp.history), len(self.REDIRECT_CHAIN))
+
+ def test_no_redirect(self):
+ session = client_session.Session(redirect=False)
+ self.setup_redirects()
+ resp = session.get(self.REDIRECT_CHAIN[0])
+ self.assertEqual(resp.status_code, 305)
+ self.assertEqual(resp.url, self.REDIRECT_CHAIN[0])
+
+ def test_redirect_limit(self):
+ self.setup_redirects()
+ for i in (1, 2):
+ session = client_session.Session(redirect=i)
+ resp = session.get(self.REDIRECT_CHAIN[0])
+ self.assertEqual(resp.status_code, 305)
+ self.assertEqual(resp.url, self.REDIRECT_CHAIN[i])
+ self.assertEqual(resp.text, self.DEFAULT_REDIRECT_BODY)
+
+ def test_history_matches_requests(self):
+ self.setup_redirects(status_code=301)
+ session = client_session.Session(redirect=True)
+ req_resp = requests.get(self.REDIRECT_CHAIN[0],
+ allow_redirects=True)
+
+ ses_resp = session.get(self.REDIRECT_CHAIN[0])
+
+ self.assertEqual(len(req_resp.history), len(ses_resp.history))
+
+ for r, s in zip(req_resp.history, ses_resp.history):
+ self.assertEqual(r.url, s.url)
+ self.assertEqual(r.status_code, s.status_code)
+
+
+class ConstructSessionFromArgsTests(utils.TestCase):
+
+ KEY = 'keyfile'
+ CERT = 'certfile'
+ CACERT = 'cacert-path'
+
+ def _s(self, k=None, **kwargs):
+ k = k or kwargs
+ return client_session.Session.construct(k)
+
+ def test_verify(self):
+ self.assertFalse(self._s(insecure=True).verify)
+ self.assertTrue(self._s(verify=True, insecure=True).verify)
+ self.assertFalse(self._s(verify=False, insecure=True).verify)
+ self.assertEqual(self._s(cacert=self.CACERT).verify, self.CACERT)
+
+ def test_cert(self):
+ tup = (self.CERT, self.KEY)
+ self.assertEqual(self._s(cert=tup).cert, tup)
+ self.assertEqual(self._s(cert=self.CERT, key=self.KEY).cert, tup)
+ self.assertIsNone(self._s(key=self.KEY).cert)
+
+ def test_pass_through(self):
+ value = 42 # only a number because timeout needs to be
+ for key in ['timeout', 'session', 'original_ip', 'user_agent']:
+ args = {key: value}
+ self.assertEqual(getattr(self._s(args), key), value)
+ self.assertNotIn(key, args)
+
+
+class AuthPlugin(base.BaseAuthPlugin):
+ """Very simple debug authentication plugin.
+
+ Takes Parameters such that it can throw exceptions at the right times.
+ """
+
+ TEST_TOKEN = 'aToken'
+ TEST_USER_ID = 'aUser'
+ TEST_PROJECT_ID = 'aProject'
+
+ SERVICE_URLS = {
+ 'identity': {'public': 'http://identity-public:1111/v2.0',
+ 'admin': 'http://identity-admin:1111/v2.0'},
+ 'compute': {'public': 'http://compute-public:2222/v1.0',
+ 'admin': 'http://compute-admin:2222/v1.0'},
+ 'image': {'public': 'http://image-public:3333/v2.0',
+ 'admin': 'http://image-admin:3333/v2.0'}
+ }
+
+ def __init__(self, token=TEST_TOKEN, invalidate=True):
+ self.token = token
+ self._invalidate = invalidate
+
+ def get_token(self, session):
+ return self.token
+
+ def get_endpoint(self, session, service_type=None, interface=None,
+ **kwargs):
+ try:
+ return self.SERVICE_URLS[service_type][interface]
+ except (KeyError, AttributeError):
+ return None
+
+ def invalidate(self):
+ return self._invalidate
+
+ def get_user_id(self, session):
+ return self.TEST_USER_ID
+
+ def get_project_id(self, session):
+ return self.TEST_PROJECT_ID
+
+
+class CalledAuthPlugin(base.BaseAuthPlugin):
+
+ ENDPOINT = 'http://fakeendpoint/'
+
+ def __init__(self, invalidate=True):
+ self.get_token_called = False
+ self.get_endpoint_called = False
+ self.endpoint_arguments = {}
+ self.invalidate_called = False
+ self._invalidate = invalidate
+
+ def get_token(self, session):
+ self.get_token_called = True
+ return 'aToken'
+
+ def get_endpoint(self, session, **kwargs):
+ self.get_endpoint_called = True
+ self.endpoint_arguments = kwargs
+ return self.ENDPOINT
+
+ def invalidate(self):
+ self.invalidate_called = True
+ return self._invalidate
+
+
+class SessionAuthTests(utils.TestCase):
+
+ TEST_URL = 'http://127.0.0.1:5000/'
+ TEST_JSON = {'hello': 'world'}
+
+ def stub_service_url(self, service_type, interface, path,
+ method='GET', **kwargs):
+ base_url = AuthPlugin.SERVICE_URLS[service_type][interface]
+ uri = "%s/%s" % (base_url.rstrip('/'), path.lstrip('/'))
+
+ self.requests.register_uri(method, uri, **kwargs)
+
+ def test_auth_plugin_default_with_plugin(self):
+ self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON)
+
+ # if there is an auth_plugin then it should default to authenticated
+ auth = AuthPlugin()
+ sess = client_session.Session(auth=auth)
+ resp = sess.get(self.TEST_URL)
+ self.assertDictEqual(resp.json(), self.TEST_JSON)
+
+ self.assertRequestHeaderEqual('X-Auth-Token', AuthPlugin.TEST_TOKEN)
+
+ def test_auth_plugin_disable(self):
+ self.stub_url('GET', base_url=self.TEST_URL, json=self.TEST_JSON)
+
+ auth = AuthPlugin()
+ sess = client_session.Session(auth=auth)
+ resp = sess.get(self.TEST_URL, authenticated=False)
+ self.assertDictEqual(resp.json(), self.TEST_JSON)
+
+ self.assertRequestHeaderEqual('X-Auth-Token', None)
+
+ def test_service_type_urls(self):
+ service_type = 'compute'
+ interface = 'public'
+ path = '/instances'
+ status = 200
+ body = 'SUCCESS'
+
+ self.stub_service_url(service_type=service_type,
+ interface=interface,
+ path=path,
+ status_code=status,
+ text=body)
+
+ sess = client_session.Session(auth=AuthPlugin())
+ resp = sess.get(path,
+ endpoint_filter={'service_type': service_type,
+ 'interface': interface})
+
+ self.assertEqual(self.requests.last_request.url,
+ AuthPlugin.SERVICE_URLS['compute']['public'] + path)
+ self.assertEqual(resp.text, body)
+ self.assertEqual(resp.status_code, status)
+
+ def test_service_url_raises_if_no_auth_plugin(self):
+ sess = client_session.Session()
+ self.assertRaises(exceptions.MissingAuthPlugin,
+ sess.get, '/path',
+ endpoint_filter={'service_type': 'compute',
+ 'interface': 'public'})
+
+ def test_service_url_raises_if_no_url_returned(self):
+ sess = client_session.Session(auth=AuthPlugin())
+ self.assertRaises(exceptions.EndpointNotFound,
+ sess.get, '/path',
+ endpoint_filter={'service_type': 'unknown',
+ 'interface': 'public'})
+
+ def test_raises_exc_only_when_asked(self):
+ # A request that returns a HTTP error should by default raise an
+ # exception by default, if you specify raise_exc=False then it will not
+ self.requests.get(self.TEST_URL, status_code=401)
+
+ sess = client_session.Session()
+ self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL)
+
+ resp = sess.get(self.TEST_URL, raise_exc=False)
+ self.assertEqual(401, resp.status_code)
+
+ def test_passed_auth_plugin(self):
+ passed = CalledAuthPlugin()
+ sess = client_session.Session()
+
+ self.requests.get(CalledAuthPlugin.ENDPOINT + 'path',
+ status_code=200)
+ endpoint_filter = {'service_type': 'identity'}
+
+ # no plugin with authenticated won't work
+ self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path',
+ authenticated=True)
+
+ # no plugin with an endpoint filter won't work
+ self.assertRaises(exceptions.MissingAuthPlugin, sess.get, 'path',
+ authenticated=False, endpoint_filter=endpoint_filter)
+
+ resp = sess.get('path', auth=passed, endpoint_filter=endpoint_filter)
+
+ self.assertEqual(200, resp.status_code)
+ self.assertTrue(passed.get_endpoint_called)
+ self.assertTrue(passed.get_token_called)
+
+ def test_passed_auth_plugin_overrides(self):
+ fixed = CalledAuthPlugin()
+ passed = CalledAuthPlugin()
+
+ sess = client_session.Session(fixed)
+
+ self.requests.get(CalledAuthPlugin.ENDPOINT + 'path',
+ status_code=200)
+
+ resp = sess.get('path', auth=passed,
+ endpoint_filter={'service_type': 'identity'})
+
+ self.assertEqual(200, resp.status_code)
+ self.assertTrue(passed.get_endpoint_called)
+ self.assertTrue(passed.get_token_called)
+ self.assertFalse(fixed.get_endpoint_called)
+ self.assertFalse(fixed.get_token_called)
+
+ def test_requests_auth_plugin(self):
+ sess = client_session.Session()
+
+ requests_auth = object()
+
+ FAKE_RESP = utils.TestResponse({'status_code': 200, 'text': 'resp'})
+ RESP = mock.Mock(return_value=FAKE_RESP)
+
+ with mock.patch.object(sess.session, 'request', RESP) as mocked:
+ sess.get(self.TEST_URL, requests_auth=requests_auth)
+
+ mocked.assert_called_once_with('GET', self.TEST_URL,
+ headers=mock.ANY,
+ allow_redirects=mock.ANY,
+ auth=requests_auth,
+ verify=mock.ANY)
+
+ def test_reauth_called(self):
+ auth = CalledAuthPlugin(invalidate=True)
+ sess = client_session.Session(auth=auth)
+
+ self.requests.get(self.TEST_URL,
+ [{'text': 'Failed', 'status_code': 401},
+ {'text': 'Hello', 'status_code': 200}])
+
+ # allow_reauth=True is the default
+ resp = sess.get(self.TEST_URL, authenticated=True)
+
+ self.assertEqual(200, resp.status_code)
+ self.assertEqual('Hello', resp.text)
+ self.assertTrue(auth.invalidate_called)
+
+ def test_reauth_not_called(self):
+ auth = CalledAuthPlugin(invalidate=True)
+ sess = client_session.Session(auth=auth)
+
+ self.requests.get(self.TEST_URL,
+ [{'text': 'Failed', 'status_code': 401},
+ {'text': 'Hello', 'status_code': 200}])
+
+ self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL,
+ authenticated=True, allow_reauth=False)
+ self.assertFalse(auth.invalidate_called)
+
+ def test_endpoint_override_overrides_filter(self):
+ auth = CalledAuthPlugin()
+ sess = client_session.Session(auth=auth)
+
+ override_base = 'http://mytest/'
+ path = 'path'
+ override_url = override_base + path
+ resp_text = uuid.uuid4().hex
+
+ self.requests.get(override_url, text=resp_text)
+
+ resp = sess.get(path,
+ endpoint_override=override_base,
+ endpoint_filter={'service_type': 'identity'})
+
+ self.assertEqual(resp_text, resp.text)
+ self.assertEqual(override_url, self.requests.last_request.url)
+
+ self.assertTrue(auth.get_token_called)
+ self.assertFalse(auth.get_endpoint_called)
+
+ def test_endpoint_override_ignore_full_url(self):
+ auth = CalledAuthPlugin()
+ sess = client_session.Session(auth=auth)
+
+ path = 'path'
+ url = self.TEST_URL + path
+
+ resp_text = uuid.uuid4().hex
+ self.requests.get(url, text=resp_text)
+
+ resp = sess.get(url,
+ endpoint_override='http://someother.url',
+ endpoint_filter={'service_type': 'identity'})
+
+ self.assertEqual(resp_text, resp.text)
+ self.assertEqual(url, self.requests.last_request.url)
+
+ self.assertTrue(auth.get_token_called)
+ self.assertFalse(auth.get_endpoint_called)
+
+ def test_user_and_project_id(self):
+ auth = AuthPlugin()
+ sess = client_session.Session(auth=auth)
+
+ self.assertEqual(auth.TEST_USER_ID, sess.get_user_id())
+ self.assertEqual(auth.TEST_PROJECT_ID, sess.get_project_id())
+
+
+class AdapterTest(utils.TestCase):
+
+ SERVICE_TYPE = uuid.uuid4().hex
+ SERVICE_NAME = uuid.uuid4().hex
+ INTERFACE = uuid.uuid4().hex
+ REGION_NAME = uuid.uuid4().hex
+ USER_AGENT = uuid.uuid4().hex
+ VERSION = uuid.uuid4().hex
+
+ TEST_URL = CalledAuthPlugin.ENDPOINT
+
+ def _create_loaded_adapter(self):
+ auth = CalledAuthPlugin()
+ sess = client_session.Session()
+ return adapter.Adapter(sess,
+ auth=auth,
+ service_type=self.SERVICE_TYPE,
+ service_name=self.SERVICE_NAME,
+ interface=self.INTERFACE,
+ region_name=self.REGION_NAME,
+ user_agent=self.USER_AGENT,
+ version=self.VERSION)
+
+ def _verify_endpoint_called(self, adpt):
+ self.assertEqual(self.SERVICE_TYPE,
+ adpt.auth.endpoint_arguments['service_type'])
+ self.assertEqual(self.SERVICE_NAME,
+ adpt.auth.endpoint_arguments['service_name'])
+ self.assertEqual(self.INTERFACE,
+ adpt.auth.endpoint_arguments['interface'])
+ self.assertEqual(self.REGION_NAME,
+ adpt.auth.endpoint_arguments['region_name'])
+ self.assertEqual(self.VERSION,
+ adpt.auth.endpoint_arguments['version'])
+
+ def test_setting_variables_on_request(self):
+ response = uuid.uuid4().hex
+ self.stub_url('GET', text=response)
+ adpt = self._create_loaded_adapter()
+ resp = adpt.get('/')
+ self.assertEqual(resp.text, response)
+
+ self._verify_endpoint_called(adpt)
+ self.assertTrue(adpt.auth.get_token_called)
+ self.assertRequestHeaderEqual('User-Agent', self.USER_AGENT)
+
+ def test_setting_variables_on_get_endpoint(self):
+ adpt = self._create_loaded_adapter()
+ url = adpt.get_endpoint()
+
+ self.assertEqual(self.TEST_URL, url)
+ self._verify_endpoint_called(adpt)
+
+ def test_legacy_binding(self):
+ key = uuid.uuid4().hex
+ val = uuid.uuid4().hex
+ response = jsonutils.dumps({key: val})
+
+ self.stub_url('GET', text=response)
+
+ auth = CalledAuthPlugin()
+ sess = client_session.Session(auth=auth)
+ adpt = adapter.LegacyJsonAdapter(sess,
+ service_type=self.SERVICE_TYPE,
+ user_agent=self.USER_AGENT)
+
+ resp, body = adpt.get('/')
+ self.assertEqual(self.SERVICE_TYPE,
+ auth.endpoint_arguments['service_type'])
+ self.assertEqual(resp.text, response)
+ self.assertEqual(val, body[key])
+
+ def test_legacy_binding_non_json_resp(self):
+ response = uuid.uuid4().hex
+ self.stub_url('GET', text=response,
+ headers={'Content-Type': 'text/html'})
+
+ auth = CalledAuthPlugin()
+ sess = client_session.Session(auth=auth)
+ adpt = adapter.LegacyJsonAdapter(sess,
+ service_type=self.SERVICE_TYPE,
+ user_agent=self.USER_AGENT)
+
+ resp, body = adpt.get('/')
+ self.assertEqual(self.SERVICE_TYPE,
+ auth.endpoint_arguments['service_type'])
+ self.assertEqual(resp.text, response)
+ self.assertIsNone(body)
+
+ def test_methods(self):
+ sess = client_session.Session()
+ adpt = adapter.Adapter(sess)
+ url = 'http://url'
+
+ for method in ['get', 'head', 'post', 'put', 'patch', 'delete']:
+ with mock.patch.object(adpt, 'request') as m:
+ getattr(adpt, method)(url)
+ m.assert_called_once_with(url, method.upper())
+
+ def test_setting_endpoint_override(self):
+ endpoint_override = 'http://overrideurl'
+ path = '/path'
+ endpoint_url = endpoint_override + path
+
+ auth = CalledAuthPlugin()
+ sess = client_session.Session(auth=auth)
+ adpt = adapter.Adapter(sess, endpoint_override=endpoint_override)
+
+ response = uuid.uuid4().hex
+ self.requests.get(endpoint_url, text=response)
+
+ resp = adpt.get(path)
+
+ self.assertEqual(response, resp.text)
+ self.assertEqual(endpoint_url, self.requests.last_request.url)
+
+ self.assertEqual(endpoint_override, adpt.get_endpoint())
+
+ def test_adapter_invalidate(self):
+ auth = CalledAuthPlugin()
+ sess = client_session.Session()
+ adpt = adapter.Adapter(sess, auth=auth)
+
+ adpt.invalidate()
+
+ self.assertTrue(auth.invalidate_called)
+
+ def test_adapter_get_token(self):
+ auth = CalledAuthPlugin()
+ sess = client_session.Session()
+ adpt = adapter.Adapter(sess, auth=auth)
+
+ self.assertEqual(self.TEST_TOKEN, adpt.get_token())
+ self.assertTrue(auth.get_token_called)
+
+ def test_adapter_connect_retries(self):
+ retries = 2
+ sess = client_session.Session()
+ adpt = adapter.Adapter(sess, connect_retries=retries)
+
+ def _refused_error(request, context):
+ raise requests.exceptions.ConnectionError()
+
+ self.stub_url('GET', text=_refused_error)
+
+ with mock.patch('time.sleep') as m:
+ self.assertRaises(exceptions.ConnectionRefused,
+ adpt.get, self.TEST_URL)
+ self.assertEqual(retries, m.call_count)
+
+ # we count retries so there will be one initial request + 2 retries
+ self.assertThat(self.requests.request_history,
+ matchers.HasLength(retries + 1))
+
+ def test_user_and_project_id(self):
+ auth = AuthPlugin()
+ sess = client_session.Session()
+ adpt = adapter.Adapter(sess, auth=auth)
+
+ self.assertEqual(auth.TEST_USER_ID, adpt.get_user_id())
+ self.assertEqual(auth.TEST_PROJECT_ID, adpt.get_project_id())
+
+
+class ConfLoadingTests(utils.TestCase):
+
+ GROUP = 'sessiongroup'
+
+ def setUp(self):
+ super(ConfLoadingTests, self).setUp()
+
+ self.conf_fixture = self.useFixture(config.Config())
+ client_session.Session.register_conf_options(self.conf_fixture.conf,
+ self.GROUP)
+
+ def config(self, **kwargs):
+ kwargs['group'] = self.GROUP
+ self.conf_fixture.config(**kwargs)
+
+ def get_session(self, **kwargs):
+ return client_session.Session.load_from_conf_options(
+ self.conf_fixture.conf,
+ self.GROUP,
+ **kwargs)
+
+ def test_insecure_timeout(self):
+ self.config(insecure=True, timeout=5)
+ s = self.get_session()
+
+ self.assertFalse(s.verify)
+ self.assertEqual(5, s.timeout)
+
+ def test_client_certs(self):
+ cert = '/path/to/certfile'
+ key = '/path/to/keyfile'
+
+ self.config(certfile=cert, keyfile=key)
+ s = self.get_session()
+
+ self.assertTrue(s.verify)
+ self.assertEqual((cert, key), s.cert)
+
+ def test_cacert(self):
+ cafile = '/path/to/cacert'
+
+ self.config(cafile=cafile)
+ s = self.get_session()
+
+ self.assertEqual(cafile, s.verify)
+
+ def test_deprecated(self):
+ def new_deprecated():
+ return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex)
+
+ opt_names = ['cafile', 'certfile', 'keyfile', 'insecure', 'timeout']
+ depr = dict([(n, [new_deprecated()]) for n in opt_names])
+ opts = client_session.Session.get_conf_options(deprecated_opts=depr)
+
+ self.assertThat(opt_names, matchers.HasLength(len(opts)))
+ for opt in opts:
+ self.assertIn(depr[opt.name][0], opt.deprecated_opts)
+
+
+class CliLoadingTests(utils.TestCase):
+
+ def setUp(self):
+ super(CliLoadingTests, self).setUp()
+
+ self.parser = argparse.ArgumentParser()
+ client_session.Session.register_cli_options(self.parser)
+
+ def get_session(self, val, **kwargs):
+ args = self.parser.parse_args(val.split())
+ return client_session.Session.load_from_cli_options(args, **kwargs)
+
+ def test_insecure_timeout(self):
+ s = self.get_session('--insecure --timeout 5.5')
+
+ self.assertFalse(s.verify)
+ self.assertEqual(5.5, s.timeout)
+
+ def test_client_certs(self):
+ cert = '/path/to/certfile'
+ key = '/path/to/keyfile'
+
+ s = self.get_session('--os-cert %s --os-key %s' % (cert, key))
+
+ self.assertTrue(s.verify)
+ self.assertEqual((cert, key), s.cert)
+
+ def test_cacert(self):
+ cacert = '/path/to/cacert'
+
+ s = self.get_session('--os-cacert %s' % cacert)
+
+ self.assertEqual(cacert, s.verify)
diff --git a/keystoneclient/tests/unit/test_shell.py b/keystoneclient/tests/unit/test_shell.py
new file mode 100644
index 0000000..452f9e7
--- /dev/null
+++ b/keystoneclient/tests/unit/test_shell.py
@@ -0,0 +1,532 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import json
+import logging
+import os
+import sys
+import uuid
+
+import fixtures
+import mock
+import six
+import testtools
+from testtools import matchers
+
+from keystoneclient import exceptions
+from keystoneclient import session
+from keystoneclient import shell as openstack_shell
+from keystoneclient.tests.unit 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/'
+
+
+# Make a fake shell object, a helping wrapper to call it
+def shell(cmd):
+ openstack_shell.OpenStackIdentityShell().main(cmd.split())
+
+
+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 os.environ:
+ if var.startswith("OS_"):
+ self.useFixture(fixtures.EnvironmentVariable(var, ""))
+
+ for var in self.FAKE_ENV:
+ self.useFixture(fixtures.EnvironmentVariable(var,
+ self.FAKE_ENV[var]))
+
+ 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 = six.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_help_command_with_no_action_choices(self):
+ required = 'usage: keystone user-update'
+ help_text = self.shell('help user-update')
+ 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_debug(self):
+ logging_mock = mock.MagicMock()
+ with mock.patch('logging.basicConfig', logging_mock):
+ self.assertRaises(exceptions.CommandError,
+ self.shell, '--debug user-list')
+ self.assertTrue(logging_mock.called)
+ self.assertEqual([(), {'level': logging.DEBUG}],
+ list(logging_mock.call_args))
+
+ 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('--os-token=%s user-list' % uuid.uuid4().hex)
+
+ def test_endpoint_no_token(self):
+ with testtools.ExpectedException(
+ exceptions.CommandError, 'Expecting a token provided'):
+ self.shell('--os-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)]))
+
+ # Test os-identity-api-version fall back to 2.0
+ shell('--os-identity-api-version 3.0 user-list')
+ assert do_tenant_mock.called
+ self.assertTrue(b.os_identity_api_version, '2.0')
+
+ 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=secret --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', 'secret', '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=secret --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', 'secret', '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=secret --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', 'secret', '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=secret --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', 'secret', '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=secret --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', 'secret', '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.object(session.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,
+ allow_redirects=False,
+ 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)]))
+
+ def test_shell_keyboard_interrupt(self):
+ shell_mock = mock.MagicMock()
+ with mock.patch('keystoneclient.shell.OpenStackIdentityShell.main',
+ shell_mock):
+ try:
+ shell_mock.side_effect = KeyboardInterrupt()
+ openstack_shell.main()
+ except SystemExit as ex:
+ self.assertEqual(130, ex.code)
diff --git a/keystoneclient/tests/unit/test_utils.py b/keystoneclient/tests/unit/test_utils.py
new file mode 100644
index 0000000..8f0de9b
--- /dev/null
+++ b/keystoneclient/tests/unit/test_utils.py
@@ -0,0 +1,240 @@
+# 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 logging
+import sys
+
+import six
+import testresources
+from testtools import matchers
+
+from keystoneclient import exceptions
+from keystoneclient.tests.unit import client_fixtures
+from keystoneclient.tests.unit 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)
+
+
+class FakeObject(object):
+ def __init__(self, name):
+ self.name = name
+
+
+class PrintTestCase(test_utils.TestCase):
+ def setUp(self):
+ super(PrintTestCase, self).setUp()
+ self.old_stdout = sys.stdout
+ self.stdout = six.moves.cStringIO()
+ self.addCleanup(setattr, self, 'stdout', None)
+ sys.stdout = self.stdout
+ self.addCleanup(setattr, sys, 'stdout', self.old_stdout)
+
+ def test_print_list_unicode(self):
+ name = six.u('\u540d\u5b57')
+ objs = [FakeObject(name)]
+ # NOTE(Jeffrey4l) If the text's encode is proper, this method will not
+ # raise UnicodeEncodeError exceptions
+ utils.print_list(objs, ['name'])
+ output = self.stdout.getvalue()
+ # In Python 2, output will be bytes, while in Python 3, it will not.
+ # Let's decode the value if needed.
+ if isinstance(output, six.binary_type):
+ output = output.decode('utf-8')
+ self.assertIn(name, output)
+
+ def test_print_dict_unicode(self):
+ name = six.u('\u540d\u5b57')
+ utils.print_dict({'name': name})
+ output = self.stdout.getvalue()
+ # In Python 2, output will be bytes, while in Python 3, it will not.
+ # Let's decode the value if needed.
+ if isinstance(output, six.binary_type):
+ output = output.decode('utf-8')
+ self.assertIn(name, output)
+
+
+class TestPositional(test_utils.TestCase):
+
+ @utils.positional(1)
+ def no_vars(self):
+ # positional doesn't enforce anything here
+ return True
+
+ @utils.positional(3, utils.positional.EXCEPT)
+ def mixed_except(self, arg, kwarg1=None, kwarg2=None):
+ # self, arg, and kwarg1 may be passed positionally
+ return (arg, kwarg1, kwarg2)
+
+ @utils.positional(3, utils.positional.WARN)
+ def mixed_warn(self, arg, kwarg1=None, kwarg2=None):
+ # self, arg, and kwarg1 may be passed positionally, only a warning
+ # is emitted
+ return (arg, kwarg1, kwarg2)
+
+ def test_nothing(self):
+ self.assertTrue(self.no_vars())
+
+ def test_mixed_except(self):
+ self.assertEqual((1, 2, 3), self.mixed_except(1, 2, kwarg2=3))
+ self.assertEqual((1, 2, 3), self.mixed_except(1, kwarg1=2, kwarg2=3))
+ self.assertEqual((1, None, None), self.mixed_except(1))
+ self.assertRaises(TypeError, self.mixed_except, 1, 2, 3)
+
+ def test_mixed_warn(self):
+ logger_message = six.moves.cStringIO()
+ handler = logging.StreamHandler(logger_message)
+ handler.setLevel(logging.DEBUG)
+
+ logger = logging.getLogger(utils.__name__)
+ level = logger.getEffectiveLevel()
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(handler)
+
+ self.addCleanup(logger.removeHandler, handler)
+ self.addCleanup(logger.setLevel, level)
+
+ self.mixed_warn(1, 2, 3)
+
+ self.assertIn('takes at most 3 positional', logger_message.getvalue())
+
+ @utils.positional(enforcement=utils.positional.EXCEPT)
+ def inspect_func(self, arg, kwarg=None):
+ return (arg, kwarg)
+
+ def test_inspect_positions(self):
+ self.assertEqual((1, None), self.inspect_func(1))
+ self.assertEqual((1, 2), self.inspect_func(1, kwarg=2))
+ self.assertRaises(TypeError, self.inspect_func)
+ self.assertRaises(TypeError, self.inspect_func, 1, 2)
+
+ @utils.positional.classmethod(1)
+ def class_method(cls, a, b):
+ return (cls, a, b)
+
+ @utils.positional.method(1)
+ def normal_method(self, a, b):
+ self.assertIsInstance(self, TestPositional)
+ return (self, a, b)
+
+ def test_class_method(self):
+ self.assertEqual((TestPositional, 1, 2), self.class_method(1, b=2))
+ self.assertRaises(TypeError, self.class_method, 1, 2)
+
+ def test_normal_method(self):
+ self.assertEqual((self, 1, 2), self.normal_method(1, b=2))
+ self.assertRaises(TypeError, self.normal_method, 1, 2)
+
+
+class HashSignedTokenTestCase(test_utils.TestCase,
+ testresources.ResourcedTestCase):
+ """Unit tests for utils.hash_signed_token()."""
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ def test_default_md5(self):
+ """The default hash method is md5."""
+ token = self.examples.SIGNED_TOKEN_SCOPED
+ if six.PY3:
+ token = token.encode('utf-8')
+ token_id_default = utils.hash_signed_token(token)
+ token_id_md5 = utils.hash_signed_token(token, mode='md5')
+ self.assertThat(token_id_default, matchers.Equals(token_id_md5))
+ # md5 hash is 32 chars.
+ self.assertThat(token_id_default, matchers.HasLength(32))
+
+ def test_sha256(self):
+ """Can also hash with sha256."""
+ token = self.examples.SIGNED_TOKEN_SCOPED
+ if six.PY3:
+ token = token.encode('utf-8')
+ token_id = utils.hash_signed_token(token, mode='sha256')
+ # sha256 hash is 64 chars.
+ self.assertThat(token_id, matchers.HasLength(64))
+
+
+def load_tests(loader, tests, pattern):
+ return testresources.OptimisingTestSuite(tests)
diff --git a/keystoneclient/tests/unit/utils.py b/keystoneclient/tests/unit/utils.py
new file mode 100644
index 0000000..038f34c
--- /dev/null
+++ b/keystoneclient/tests/unit/utils.py
@@ -0,0 +1,209 @@
+# 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 logging
+import sys
+import time
+import uuid
+
+import fixtures
+import mock
+from mox3 import mox
+from oslo_serialization import jsonutils
+import requests
+from requests_mock.contrib import fixture
+import six
+from six.moves.urllib import parse as urlparse
+import testtools
+
+
+class TestCase(testtools.TestCase):
+
+ TEST_DOMAIN_ID = '1'
+ TEST_DOMAIN_NAME = 'aDomain'
+ TEST_GROUP_ID = uuid.uuid4().hex
+ TEST_ROLE_ID = uuid.uuid4().hex
+ TEST_TENANT_ID = '1'
+ TEST_TENANT_NAME = 'aTenant'
+ TEST_TOKEN = 'aToken'
+ TEST_TRUST_ID = 'aTrust'
+ TEST_USER = 'test'
+ TEST_USER_ID = uuid.uuid4().hex
+
+ TEST_ROOT_URL = 'http://127.0.0.1:5000/'
+
+ def setUp(self):
+ super(TestCase, self).setUp()
+ self.mox = mox.Mox()
+ self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG))
+ self.time_patcher = mock.patch.object(time, 'time', lambda: 1234)
+ self.time_patcher.start()
+
+ self.requests = self.useFixture(fixture.Fixture())
+
+ 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['text'] = jsonutils.dumps(json)
+ headers = kwargs.setdefault('headers', {})
+ headers['Content-Type'] = 'application/json'
+
+ if parts:
+ url = '/'.join([p.strip('/') for p in [base_url] + parts])
+ else:
+ url = base_url
+
+ url = url.replace("/?", "?")
+ self.requests.register_uri(method, url, **kwargs)
+
+ def assertRequestBodyIs(self, body=None, json=None):
+ last_request_body = self.requests.last_request.body
+ if json:
+ val = jsonutils.loads(last_request_body)
+ self.assertEqual(json, val)
+ elif body:
+ self.assertEqual(body, last_request_body)
+
+ def assertQueryStringIs(self, qs=''):
+ """Verify the QueryString matches what is expected.
+
+ The qs parameter should be of the format \'foo=bar&abc=xyz\'
+ """
+ expected = urlparse.parse_qs(qs, keep_blank_values=True)
+ parts = urlparse.urlparse(self.requests.last_request.url)
+ querystring = urlparse.parse_qs(parts.query, keep_blank_values=True)
+ self.assertEqual(expected, querystring)
+
+ def assertQueryStringContains(self, **kwargs):
+ """Verify the query string contains the expected parameters.
+
+ This method is used to verify that the query string for the most recent
+ request made contains all the parameters provided as ``kwargs``, and
+ that the value of each parameter contains the value for the kwarg. If
+ the value for the kwarg is an empty string (''), then all that's
+ verified is that the parameter is present.
+
+ """
+ parts = urlparse.urlparse(self.requests.last_request.url)
+ qs = urlparse.parse_qs(parts.query, keep_blank_values=True)
+
+ for k, v in six.iteritems(kwargs):
+ self.assertIn(k, qs)
+ self.assertIn(v, qs[k])
+
+ def assertRequestHeaderEqual(self, name, val):
+ """Verify that the last request made contains a header and its value
+
+ The request must have already been made.
+ """
+ headers = self.requests.last_request.headers
+ self.assertEqual(headers.get(name), 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', 200)
+ 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')
+ else:
+ self.status_code = data
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ @property
+ def text(self):
+ return self.content
+
+
+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 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
diff --git a/keystoneclient/tests/unit/v2_0/__init__.py b/keystoneclient/tests/unit/v2_0/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/__init__.py
diff --git a/keystoneclient/tests/unit/v2_0/client_fixtures.py b/keystoneclient/tests/unit/v2_0/client_fixtures.py
new file mode 100644
index 0000000..39d808e
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/client_fixtures.py
@@ -0,0 +1,124 @@
+# 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
+
+from keystoneclient import fixture
+
+
+def unscoped_token():
+ return fixture.V2Token(token_id='3e2813b7ba0b4006840c3825860b86ed',
+ expires='2012-10-03T16:58:01Z',
+ user_id='c4da488862bd435c9e6c0275a0d0e49a',
+ user_name='exampleuser')
+
+
+def project_scoped_token():
+ _TENANT_ID = '225da22d3ce34b15877ea70b2a575f58'
+
+ f = fixture.V2Token(token_id='04c7d5ffaeef485f9dc69c06db285bdb',
+ expires='2012-10-03T16:53:36Z',
+ tenant_id='225da22d3ce34b15877ea70b2a575f58',
+ tenant_name='exampleproject',
+ user_id='c4da488862bd435c9e6c0275a0d0e49a',
+ user_name='exampleuser')
+
+ f.add_role(id='member_id', name='Member')
+
+ s = f.add_service('volume', 'Volume Service')
+ s.add_endpoint(public='http://public.com:8776/v1/%s' % _TENANT_ID,
+ admin='http://admin:8776/v1/%s' % _TENANT_ID,
+ internal='http://internal:8776/v1/%s' % _TENANT_ID,
+ region='RegionOne')
+
+ s = f.add_service('image', 'Image Service')
+ s.add_endpoint(public='http://public.com:9292/v1',
+ admin='http://admin:9292/v1',
+ internal='http://internal:9292/v1',
+ region='RegionOne')
+
+ s = f.add_service('compute', 'Compute Service')
+ s.add_endpoint(public='http://public.com:8774/v2/%s' % _TENANT_ID,
+ admin='http://admin:8774/v2/%s' % _TENANT_ID,
+ internal='http://internal:8774/v2/%s' % _TENANT_ID,
+ region='RegionOne')
+
+ s = f.add_service('ec2', 'EC2 Service')
+ s.add_endpoint(public='http://public.com:8773/services/Cloud',
+ admin='http://admin:8773/services/Admin',
+ internal='http://internal:8773/services/Cloud',
+ region='RegionOne')
+
+ s = f.add_service('identity', 'Identity Service')
+ s.add_endpoint(public='http://public.com:5000/v2.0',
+ admin='http://admin:35357/v2.0',
+ internal='http://internal:5000/v2.0',
+ region='RegionOne')
+
+ return f
+
+
+def auth_response_body():
+ f = fixture.V2Token(token_id='ab48a9efdfedb23ty3494',
+ expires='2010-11-01T03:32:15-05:00',
+ tenant_id='345',
+ tenant_name='My Project',
+ user_id='123',
+ user_name='jqsmith')
+
+ f.add_role(id='234', name='compute:admin')
+ role = f.add_role(id='235', name='object-store:admin')
+ role['tenantId'] = '1'
+
+ s = f.add_service('compute', 'Cloud Servers')
+ endpoint = s.add_endpoint(public='https://compute.north.host/v1/1234',
+ internal='https://compute.north.host/v1/1234',
+ region='North')
+ endpoint['tenantId'] = '1'
+ endpoint['versionId'] = '1.0'
+ endpoint['versionInfo'] = 'https://compute.north.host/v1.0/'
+ endpoint['versionList'] = 'https://compute.north.host/'
+
+ endpoint = s.add_endpoint(public='https://compute.north.host/v1.1/3456',
+ internal='https://compute.north.host/v1.1/3456',
+ region='North')
+ endpoint['tenantId'] = '2'
+ endpoint['versionId'] = '1.1'
+ endpoint['versionInfo'] = 'https://compute.north.host/v1.1/'
+ endpoint['versionList'] = 'https://compute.north.host/'
+
+ s = f.add_service('object-store', 'Cloud Files')
+ endpoint = s.add_endpoint(public='https://swift.north.host/v1/blah',
+ internal='https://swift.north.host/v1/blah',
+ region='South')
+ endpoint['tenantId'] = '11'
+ endpoint['versionId'] = '1.0'
+ endpoint['versionInfo'] = 'uri'
+ endpoint['versionList'] = 'uri'
+
+ endpoint = s.add_endpoint(public='https://swift.north.host/v1.1/blah',
+ internal='https://compute.north.host/v1.1/blah',
+ region='South')
+ endpoint['tenantId'] = '2'
+ endpoint['versionId'] = '1.1'
+ endpoint['versionInfo'] = 'https://swift.north.host/v1.1/'
+ endpoint['versionList'] = 'https://swift.north.host/'
+
+ s = f.add_service('image', 'Image Servers')
+ s.add_endpoint(public='https://image.north.host/v1/',
+ internal='https://image-internal.north.host/v1/',
+ region='North')
+ s.add_endpoint(public='https://image.south.host/v1/',
+ internal='https://image-internal.south.host/v1/',
+ region='South')
+
+ return f
diff --git a/keystoneclient/tests/unit/v2_0/test_access.py b/keystoneclient/tests/unit/v2_0/test_access.py
new file mode 100644
index 0000000..f05f138
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_access.py
@@ -0,0 +1,201 @@
+# 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 uuid
+
+from oslo_utils import timeutils
+import testresources
+
+from keystoneclient import access
+from keystoneclient import fixture
+from keystoneclient.tests.unit import client_fixtures as token_data
+from keystoneclient.tests.unit.v2_0 import client_fixtures
+from keystoneclient.tests.unit.v2_0 import utils
+
+
+class AccessInfoTest(utils.TestCase, testresources.ResourcedTestCase):
+
+ resources = [('examples', token_data.EXAMPLES_RESOURCE)]
+
+ def test_building_unscoped_accessinfo(self):
+ token = client_fixtures.unscoped_token()
+ auth_ref = access.AccessInfo.factory(body=token)
+
+ self.assertTrue(auth_ref)
+ self.assertIn('token', auth_ref)
+
+ self.assertEqual(auth_ref.auth_token,
+ '3e2813b7ba0b4006840c3825860b86ed')
+ self.assertEqual(auth_ref.username, 'exampleuser')
+ self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a')
+
+ self.assertEqual(auth_ref.role_ids, [])
+ self.assertEqual(auth_ref.role_names, [])
+
+ self.assertIsNone(auth_ref.tenant_name)
+ self.assertIsNone(auth_ref.tenant_id)
+
+ self.assertIsNone(auth_ref.auth_url)
+ self.assertIsNone(auth_ref.management_url)
+
+ 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.assertEqual(auth_ref.expires, token.expires)
+ self.assertEqual(auth_ref.issued, token.issued)
+
+ def test_will_expire_soon(self):
+ token = client_fixtures.unscoped_token()
+ expires = timeutils.utcnow() + datetime.timedelta(minutes=5)
+ token.expires = expires
+ auth_ref = access.AccessInfo.factory(body=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):
+ token = client_fixtures.project_scoped_token()
+ auth_ref = access.AccessInfo.factory(body=token)
+
+ self.assertTrue(auth_ref)
+ self.assertIn('token', auth_ref)
+ self.assertIn('serviceCatalog', auth_ref)
+ self.assertTrue(auth_ref['serviceCatalog'])
+
+ self.assertEqual(auth_ref.auth_token,
+ '04c7d5ffaeef485f9dc69c06db285bdb')
+ self.assertEqual(auth_ref.username, 'exampleuser')
+ self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a')
+
+ self.assertEqual(auth_ref.role_ids, ['member_id'])
+ self.assertEqual(auth_ref.role_names, ['Member'])
+
+ self.assertEqual(auth_ref.tenant_name, 'exampleproject')
+ self.assertEqual(auth_ref.tenant_id,
+ '225da22d3ce34b15877ea70b2a575f58')
+
+ self.assertEqual(auth_ref.tenant_name, auth_ref.project_name)
+ self.assertEqual(auth_ref.tenant_id, auth_ref.project_id)
+
+ self.assertEqual(auth_ref.auth_url, ('http://public.com:5000/v2.0',))
+ self.assertEqual(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):
+ diablo_token = self.examples.TOKEN_RESPONSES[
+ self.examples.VALID_DIABLO_TOKEN]
+ auth_ref = access.AccessInfo.factory(body=diablo_token)
+
+ self.assertTrue(auth_ref)
+ self.assertEqual(auth_ref.username, 'user_name1')
+ self.assertEqual(auth_ref.project_id, 'tenant_id1')
+ self.assertEqual(auth_ref.project_name, 'tenant_id1')
+ 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.assertEqual(auth_ref.role_names, ['role1', 'role2'])
+ self.assertFalse(auth_ref.scoped)
+
+ def test_grizzly_token(self):
+ grizzly_token = self.examples.TOKEN_RESPONSES[
+ self.examples.SIGNED_TOKEN_SCOPED_KEY]
+ auth_ref = access.AccessInfo.factory(body=grizzly_token)
+
+ self.assertEqual(auth_ref.project_id, 'tenant_id1')
+ self.assertEqual(auth_ref.project_name, 'tenant_name1')
+ 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.assertEqual(auth_ref.role_names, ['role1', 'role2'])
+
+ def test_v2_roles(self):
+ role_id = 'a'
+ role_name = 'b'
+
+ token = fixture.V2Token()
+ token.set_scope()
+ token.add_role(id=role_id, name=role_name)
+
+ auth_ref = access.AccessInfo.factory(body=token)
+
+ self.assertEqual([role_id], auth_ref.role_ids)
+ self.assertEqual([role_id], auth_ref['metadata']['roles'])
+ self.assertEqual([role_name], auth_ref.role_names)
+ self.assertEqual([{'name': role_name}], auth_ref['user']['roles'])
+
+ def test_trusts(self):
+ user_id = uuid.uuid4().hex
+ trust_id = uuid.uuid4().hex
+
+ token = fixture.V2Token(user_id=user_id, trust_id=trust_id)
+ token.set_scope()
+ token.add_role()
+
+ auth_ref = access.AccessInfo.factory(body=token)
+
+ self.assertEqual(trust_id, auth_ref.trust_id)
+ self.assertEqual(user_id, auth_ref.trustee_user_id)
+
+ self.assertEqual(trust_id, token['access']['trust']['id'])
+
+ def test_override_auth_token(self):
+ token = fixture.V2Token()
+ token.set_scope()
+ token.add_role()
+
+ new_auth_token = uuid.uuid4().hex
+
+ auth_ref = access.AccessInfo.factory(body=token)
+
+ self.assertEqual(token.token_id, auth_ref.auth_token)
+
+ auth_ref.auth_token = new_auth_token
+ self.assertEqual(new_auth_token, auth_ref.auth_token)
+
+ del auth_ref.auth_token
+ self.assertEqual(token.token_id, auth_ref.auth_token)
+
+ def test_override_auth_token_in_factory(self):
+ token = fixture.V2Token()
+ token.set_scope()
+ token.add_role()
+
+ new_auth_token = uuid.uuid4().hex
+
+ auth_ref = access.AccessInfo.factory(body=token,
+ auth_token=new_auth_token)
+
+ self.assertEqual(new_auth_token, auth_ref.auth_token)
+ del auth_ref.auth_token
+ self.assertEqual(token.token_id, auth_ref.auth_token)
+
+
+def load_tests(loader, tests, pattern):
+ return testresources.OptimisingTestSuite(tests)
diff --git a/keystoneclient/tests/unit/v2_0/test_auth.py b/keystoneclient/tests/unit/v2_0/test_auth.py
new file mode 100644
index 0000000..e61f5c8
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_auth.py
@@ -0,0 +1,258 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import datetime
+
+from oslo_serialization import jsonutils
+from oslo_utils import timeutils
+
+from keystoneclient import exceptions
+from keystoneclient.tests.unit.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,
+ },
+ }
+
+ def test_authenticate_success_expired(self):
+ resp_a = copy.deepcopy(self.TEST_RESPONSE_DICT)
+ resp_b = copy.deepcopy(self.TEST_RESPONSE_DICT)
+ headers = {'Content-Type': 'application/json'}
+
+ # Build an expired token
+ resp_a['access']['token']['expires'] = (
+ (timeutils.utcnow() - datetime.timedelta(1)).isoformat())
+
+ # Build a new response
+ TEST_TOKEN = "abcdef"
+ resp_b['access']['token']['expires'] = '2020-01-01T00:00:10.000123Z'
+ resp_b['access']['token']['id'] = TEST_TOKEN
+
+ # return expired first, and then the new response
+ self.stub_auth(response_list=[{'json': resp_a, 'headers': headers},
+ {'json': resp_b, 'headers': headers}])
+
+ 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)
+
+ 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_code=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)
+
+ def test_auth_redirect(self):
+ self.stub_auth(status_code=305, text='Use Proxy',
+ headers={'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)
+
+ 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)
+
+ 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)
+
+ def test_auth_url_token_authentication(self):
+ fake_token = 'fake_token'
+ fake_url = '/fake-url'
+ fake_resp = {'result': True}
+
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url('GET', [fake_url], json=fake_resp,
+ base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT)
+
+ cl = client.Client(auth_url=self.TEST_URL,
+ token=fake_token)
+ json_body = jsonutils.loads(self.requests.last_request.body)
+ self.assertEqual(json_body['auth']['token']['id'], fake_token)
+
+ resp, body = cl.get(fake_url)
+ self.assertEqual(fake_resp, body)
+
+ token = self.requests.last_request.headers.get('X-Auth-Token')
+ self.assertEqual(self.TEST_TOKEN, token)
+
+ 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)
+
+ def test_authenticate_success_token_scoped_trust(self):
+ del self.TEST_REQUEST_BODY['auth']['passwordCredentials']
+ self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN}
+ self.TEST_REQUEST_BODY['auth']['trust_id'] = self.TEST_TRUST_ID
+ response = self.TEST_RESPONSE_DICT.copy()
+ response['access']['trust'] = {"trustee_user_id": self.TEST_USER,
+ "id": self.TEST_TRUST_ID}
+ self.stub_auth(json=response)
+
+ cs = client.Client(token=self.TEST_TOKEN,
+ tenant_id=self.TEST_TENANT_ID,
+ trust_id=self.TEST_TRUST_ID,
+ auth_url=self.TEST_URL)
+ self.assertTrue(cs.auth_ref.trust_scoped)
+ self.assertEqual(cs.auth_ref.trust_id, self.TEST_TRUST_ID)
+ self.assertEqual(cs.auth_ref.trustee_user_id, self.TEST_USER)
+ self.assertRequestBodyIs(json=self.TEST_REQUEST_BODY)
+
+ 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)
+
+ def test_allow_override_of_auth_token(self):
+ fake_url = '/fake-url'
+ fake_token = 'fake_token'
+ fake_resp = {'result': True}
+
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url('GET', [fake_url], json=fake_resp,
+ base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT)
+
+ cl = client.Client(username='exampleuser',
+ password='password',
+ tenant_name='exampleproject',
+ auth_url=self.TEST_URL)
+
+ self.assertEqual(cl.auth_token, self.TEST_TOKEN)
+
+ # the token returned from the authentication will be used
+ resp, body = cl.get(fake_url)
+ self.assertEqual(fake_resp, body)
+
+ token = self.requests.last_request.headers.get('X-Auth-Token')
+ self.assertEqual(self.TEST_TOKEN, token)
+
+ # then override that token and the new token shall be used
+ cl.auth_token = fake_token
+
+ resp, body = cl.get(fake_url)
+ self.assertEqual(fake_resp, body)
+
+ token = self.requests.last_request.headers.get('X-Auth-Token')
+ self.assertEqual(fake_token, token)
+
+ # if we clear that overridden token then we fall back to the original
+ del cl.auth_token
+
+ resp, body = cl.get(fake_url)
+ self.assertEqual(fake_resp, body)
+
+ token = self.requests.last_request.headers.get('X-Auth-Token')
+ self.assertEqual(self.TEST_TOKEN, token)
diff --git a/keystoneclient/tests/unit/v2_0/test_certificates.py b/keystoneclient/tests/unit/v2_0/test_certificates.py
new file mode 100644
index 0000000..fc19d81
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_certificates.py
@@ -0,0 +1,40 @@
+# Copyright 2014 IBM Corp.
+# 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 testresources
+
+from keystoneclient.tests.unit import client_fixtures
+from keystoneclient.tests.unit.v2_0 import utils
+
+
+class CertificateTests(utils.TestCase, testresources.ResourcedTestCase):
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ def test_get_ca_certificate(self):
+ self.stub_url('GET', ['certificates', 'ca'],
+ headers={'Content-Type': 'text/html; charset=UTF-8'},
+ text=self.examples.SIGNING_CA)
+ res = self.client.certificates.get_ca_certificate()
+ self.assertEqual(self.examples.SIGNING_CA, res)
+
+ def test_get_signing_certificate(self):
+ self.stub_url('GET', ['certificates', 'signing'],
+ headers={'Content-Type': 'text/html; charset=UTF-8'},
+ text=self.examples.SIGNING_CERT)
+ res = self.client.certificates.get_signing_certificate()
+ self.assertEqual(self.examples.SIGNING_CERT, res)
+
+
+def load_tests(loader, tests, pattern):
+ return testresources.OptimisingTestSuite(tests)
diff --git a/keystoneclient/tests/unit/v2_0/test_client.py b/keystoneclient/tests/unit/v2_0/test_client.py
new file mode 100644
index 0000000..2700b31
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_client.py
@@ -0,0 +1,184 @@
+# 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 uuid
+
+import six
+
+from keystoneclient.auth import token_endpoint
+from keystoneclient import exceptions
+from keystoneclient import fixture
+from keystoneclient import session
+from keystoneclient.tests.unit.v2_0 import client_fixtures
+from keystoneclient.tests.unit.v2_0 import utils
+from keystoneclient.v2_0 import client
+
+
+class KeystoneClientTest(utils.TestCase):
+
+ def test_unscoped_init(self):
+ token = client_fixtures.unscoped_token()
+ self.stub_auth(json=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)
+ self.assertIsNone(c.get_project_id(session=None))
+ self.assertEqual(token.user_id, c.get_user_id(session=None))
+
+ def test_scoped_init(self):
+ token = client_fixtures.project_scoped_token()
+ self.stub_auth(json=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)
+
+ self.assertEqual(token.tenant_id, c.get_project_id(session=None))
+ self.assertEqual(token.user_id, c.get_user_id(session=None))
+
+ 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.assertEqual(new_client.username, 'exampleuser')
+ self.assertIsNone(new_client.password)
+ self.assertEqual(new_client.management_url,
+ 'http://admin:35357/v2.0')
+
+ 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.assertEqual(new_client.auth_url, new_auth_url)
+ self.assertEqual(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')
+
+ def test_management_url_is_updated(self):
+ first = fixture.V2Token()
+ first.set_scope()
+ admin_url = 'http://admin:35357/v2.0'
+ second_url = 'http://secondurl:35357/v2.0'
+
+ s = first.add_service('identity')
+ s.add_endpoint(public='http://public.com:5000/v2.0',
+ admin=admin_url)
+
+ second = fixture.V2Token()
+ second.set_scope()
+ s = second.add_service('identity')
+ s.add_endpoint(public='http://secondurl:5000/v2.0',
+ admin=second_url)
+
+ self.stub_auth(response_list=[{'json': first}, {'json': second}])
+
+ cl = client.Client(username='exampleuser',
+ password='password',
+ tenant_name='exampleproject',
+ auth_url=self.TEST_URL)
+ self.assertEqual(cl.management_url, admin_url)
+
+ cl.authenticate()
+ self.assertEqual(cl.management_url, second_url)
+
+ def test_client_with_region_name_passes_to_service_catalog(self):
+ # NOTE(jamielennox): this is deprecated behaviour that should be
+ # removed ASAP, however must remain compatible.
+ self.stub_auth(json=client_fixtures.auth_response_body())
+
+ cl = client.Client(username='exampleuser',
+ password='password',
+ tenant_name='exampleproject',
+ auth_url=self.TEST_URL,
+ region_name='North')
+ self.assertEqual(cl.service_catalog.url_for(service_type='image'),
+ 'https://image.north.host/v1/')
+
+ cl = client.Client(username='exampleuser',
+ password='password',
+ tenant_name='exampleproject',
+ auth_url=self.TEST_URL,
+ region_name='South')
+ self.assertEqual(cl.service_catalog.url_for(service_type='image'),
+ 'https://image.south.host/v1/')
+
+ def test_client_without_auth_params(self):
+ self.assertRaises(exceptions.AuthorizationFailure,
+ client.Client,
+ tenant_name='exampleproject',
+ auth_url=self.TEST_URL)
+
+ def test_client_params(self):
+ opts = {'auth': token_endpoint.Token('a', 'b'),
+ 'connect_retries': 50,
+ 'endpoint_override': uuid.uuid4().hex,
+ 'interface': uuid.uuid4().hex,
+ 'region_name': uuid.uuid4().hex,
+ 'service_name': uuid.uuid4().hex,
+ 'user_agent': uuid.uuid4().hex,
+ }
+
+ sess = session.Session()
+ cl = client.Client(session=sess, **opts)
+
+ for k, v in six.iteritems(opts):
+ self.assertEqual(v, getattr(cl._adapter, k))
+
+ self.assertEqual('identity', cl._adapter.service_type)
+ self.assertEqual('v2.0', cl._adapter.version)
diff --git a/keystoneclient/tests/unit/v2_0/test_discovery.py b/keystoneclient/tests/unit/v2_0/test_discovery.py
new file mode 100644
index 0000000..348038a
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_discovery.py
@@ -0,0 +1,80 @@
+# 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.generic import client
+from keystoneclient.tests.unit.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",
+ }],
+ }],
+ },
+ }
+
+ def test_get_versions(self):
+ self.stub_url('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.assertEqual(
+ versions['v2.0']['url'],
+ self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0]
+ ['href'])
+
+ def test_get_version_local(self):
+ self.stub_url('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.assertEqual(
+ versions['v2.0']['url'],
+ self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0]
+ ['href'])
diff --git a/keystoneclient/tests/unit/v2_0/test_ec2.py b/keystoneclient/tests/unit/v2_0/test_ec2.py
new file mode 100644
index 0000000..e08d228
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_ec2.py
@@ -0,0 +1,107 @@
+# 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.tests.unit.v2_0 import utils
+from keystoneclient.v2_0 import ec2
+
+
+class EC2Tests(utils.TestCase):
+
+ 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('POST', ['users', user_id, 'credentials',
+ 'OS-EC2'], json=resp_body)
+
+ cred = self.client.ec2.create(user_id, tenant_id)
+ self.assertIsInstance(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)
+
+ 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('GET', ['users', user_id, 'credentials',
+ 'OS-EC2', 'access'], json=resp_body)
+
+ cred = self.client.ec2.get(user_id, 'access')
+ self.assertIsInstance(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')
+
+ 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('GET', ['users', user_id, 'credentials',
+ 'OS-EC2'], json=resp_body)
+
+ creds = self.client.ec2.list(user_id)
+ self.assertEqual(len(creds), 2)
+ cred = creds[0]
+ self.assertIsInstance(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')
+
+ def test_delete(self):
+ user_id = 'usr'
+ access = 'access'
+ self.stub_url('DELETE', ['users', user_id, 'credentials',
+ 'OS-EC2', access], status_code=204)
+ self.client.ec2.delete(user_id, access)
diff --git a/keystoneclient/tests/unit/v2_0/test_endpoints.py b/keystoneclient/tests/unit/v2_0/test_endpoints.py
new file mode 100644
index 0000000..ffc1c65
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_endpoints.py
@@ -0,0 +1,147 @@
+# 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.unit.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',
+ }
+ ]
+ }
+
+ def test_create_with_optional_params(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": uuid.uuid4().hex,
+ }
+ }
+
+ resp_body = {
+ "endpoint": {
+ "adminurl": "http://host-3:8774/v1.1/$(tenant_id)s",
+ "region": "RegionOne",
+ "id": uuid.uuid4().hex,
+ "internalurl": "http://host-3:8774/v1.1/$(tenant_id)s",
+ "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s",
+ }
+ }
+
+ self.stub_url('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.assertIsInstance(endpoint, endpoints.Endpoint)
+ self.assertRequestBodyIs(json=req_body)
+
+ def test_create_with_optional_params_as_none(self):
+ req_body_without_defaults = {
+ "endpoint": {
+ "region": "RegionOne",
+ "service_id": uuid.uuid4().hex,
+ "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s",
+ "adminurl": None,
+ "internalurl": None,
+ }
+ }
+
+ resp_body = {
+ "endpoint": {
+ "region": "RegionOne",
+ "id": uuid.uuid4().hex,
+ "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s",
+ "adminurl": None,
+ "internalurl": None,
+ }
+ }
+
+ self.stub_url('POST', ['endpoints'], json=resp_body)
+
+ endpoint_without_defaults = self.client.endpoints.create(
+ region=req_body_without_defaults['endpoint']['region'],
+ publicurl=req_body_without_defaults['endpoint']['publicurl'],
+ service_id=req_body_without_defaults['endpoint']['service_id'],
+ adminurl=None,
+ internalurl=None
+ )
+ self.assertIsInstance(endpoint_without_defaults, endpoints.Endpoint)
+ self.assertRequestBodyIs(json=req_body_without_defaults)
+
+ def test_create_without_optional_params(self):
+ req_body_without_defaults = {
+ "endpoint": {
+ "region": "RegionOne",
+ "service_id": uuid.uuid4().hex,
+ "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s",
+ "adminurl": None,
+ "internalurl": None,
+ }
+ }
+
+ resp_body = {
+ "endpoint": {
+ "region": "RegionOne",
+ "id": uuid.uuid4().hex,
+ "publicurl": "http://host-3:8774/v1.1/$(tenant_id)s",
+ "adminurl": None,
+ "internalurl": None,
+ }
+ }
+
+ self.stub_url('POST', ['endpoints'], json=resp_body)
+
+ endpoint_without_defaults = self.client.endpoints.create(
+ region=req_body_without_defaults['endpoint']['region'],
+ publicurl=req_body_without_defaults['endpoint']['publicurl'],
+ service_id=req_body_without_defaults['endpoint']['service_id']
+ )
+ self.assertIsInstance(endpoint_without_defaults, endpoints.Endpoint)
+ self.assertRequestBodyIs(json=req_body_without_defaults)
+
+ def test_delete(self):
+ self.stub_url('DELETE', ['endpoints', '8f953'], status_code=204)
+ self.client.endpoints.delete('8f953')
+
+ def test_list(self):
+ self.stub_url('GET', ['endpoints'], json=self.TEST_ENDPOINTS)
+
+ endpoint_list = self.client.endpoints.list()
+ [self.assertIsInstance(r, endpoints.Endpoint)
+ for r in endpoint_list]
diff --git a/keystoneclient/tests/unit/v2_0/test_extensions.py b/keystoneclient/tests/unit/v2_0/test_extensions.py
new file mode 100644
index 0000000..d09943f
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_extensions.py
@@ -0,0 +1,63 @@
+# 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.tests.unit.v2_0 import utils
+from keystoneclient.v2_0 import extensions
+
+
+class ExtensionTests(utils.TestCase):
+ def setUp(self):
+ super(ExtensionTests, self).setUp()
+ self.TEST_EXTENSIONS = {
+ 'extensions': {
+ "values": [
+ {
+ 'name': 'OpenStack Keystone User CRUD',
+ 'namespace': 'http://docs.openstack.org/'
+ 'identity/api/ext/OS-KSCRUD/v1.0',
+ 'updated': '2013-07-07T12:00:0-00:00',
+ 'alias': 'OS-KSCRUD',
+ 'description':
+ 'OpenStack extensions to Keystone v2.0 API'
+ ' enabling User Operations.',
+ 'links':
+ '[{"href":'
+ '"https://github.com/openstack/identity-api", "type":'
+ ' "text/html", "rel": "describedby"}]',
+ },
+ {
+ 'name': 'OpenStack EC2 API',
+ 'namespace': 'http://docs.openstack.org/'
+ 'identity/api/ext/OS-EC2/v1.0',
+ 'updated': '2013-09-07T12:00:0-00:00',
+ 'alias': 'OS-EC2',
+ 'description': 'OpenStack EC2 Credentials backend.',
+ 'links': '[{"href":'
+ '"https://github.com/openstack/identity-api", "type":'
+ ' "text/html", "rel": "describedby"}]',
+ }
+ ]
+ }
+ }
+
+ def test_list(self):
+ self.stub_url('GET', ['extensions'], json=self.TEST_EXTENSIONS)
+ extensions_list = self.client.extensions.list()
+ self.assertEqual(2, len(extensions_list))
+ for extension in extensions_list:
+ self.assertIsInstance(extension, extensions.Extension)
+ self.assertIsNotNone(extension.alias)
+ self.assertIsNotNone(extension.description)
+ self.assertIsNotNone(extension.links)
+ self.assertIsNotNone(extension.name)
+ self.assertIsNotNone(extension.namespace)
+ self.assertIsNotNone(extension.updated)
diff --git a/keystoneclient/tests/unit/v2_0/test_roles.py b/keystoneclient/tests/unit/v2_0/test_roles.py
new file mode 100644
index 0000000..74ad1ed
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_roles.py
@@ -0,0 +1,121 @@
+# 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.unit.v2_0 import utils
+from keystoneclient.v2_0 import roles
+
+
+class RoleTests(utils.TestCase):
+ def setUp(self):
+ super(RoleTests, self).setUp()
+
+ self.ADMIN_ROLE_ID = uuid.uuid4().hex
+ self.MEMBER_ROLE_ID = uuid.uuid4().hex
+
+ self.TEST_ROLES = {
+ "roles": {
+ "values": [
+ {
+ "name": "admin",
+ "id": self.ADMIN_ROLE_ID,
+ },
+ {
+ "name": "member",
+ "id": self.MEMBER_ROLE_ID,
+ }
+ ],
+ },
+ }
+
+ def test_create(self):
+ req_body = {
+ "role": {
+ "name": "sysadmin",
+ }
+ }
+ role_id = uuid.uuid4().hex
+ resp_body = {
+ "role": {
+ "name": "sysadmin",
+ "id": role_id,
+ }
+ }
+ self.stub_url('POST', ['OS-KSADM', 'roles'], json=resp_body)
+
+ role = self.client.roles.create(req_body['role']['name'])
+ self.assertRequestBodyIs(json=req_body)
+ self.assertIsInstance(role, roles.Role)
+ self.assertEqual(role.id, role_id)
+ self.assertEqual(role.name, req_body['role']['name'])
+
+ def test_delete(self):
+ self.stub_url('DELETE',
+ ['OS-KSADM', 'roles', self.ADMIN_ROLE_ID],
+ status_code=204)
+ self.client.roles.delete(self.ADMIN_ROLE_ID)
+
+ def test_get(self):
+ self.stub_url('GET', ['OS-KSADM', 'roles', self.ADMIN_ROLE_ID],
+ json={'role': self.TEST_ROLES['roles']['values'][0]})
+
+ role = self.client.roles.get(self.ADMIN_ROLE_ID)
+ self.assertIsInstance(role, roles.Role)
+ self.assertEqual(role.id, self.ADMIN_ROLE_ID)
+ self.assertEqual(role.name, 'admin')
+
+ def test_list(self):
+ self.stub_url('GET', ['OS-KSADM', 'roles'],
+ json=self.TEST_ROLES)
+
+ role_list = self.client.roles.list()
+ [self.assertIsInstance(r, roles.Role) for r in role_list]
+
+ def test_roles_for_user(self):
+ self.stub_url('GET', ['users', 'foo', 'roles'],
+ json=self.TEST_ROLES)
+
+ role_list = self.client.roles.roles_for_user('foo')
+ [self.assertIsInstance(r, roles.Role) for r in role_list]
+
+ def test_roles_for_user_tenant(self):
+ self.stub_url('GET', ['tenants', 'barrr', 'users', 'foo',
+ 'roles'], json=self.TEST_ROLES)
+
+ role_list = self.client.roles.roles_for_user('foo', 'barrr')
+ [self.assertIsInstance(r, roles.Role) for r in role_list]
+
+ def test_add_user_role(self):
+ self.stub_url('PUT', ['users', 'foo', 'roles', 'OS-KSADM',
+ 'barrr'], status_code=204)
+
+ self.client.roles.add_user_role('foo', 'barrr')
+
+ def test_add_user_role_tenant(self):
+ id_ = uuid.uuid4().hex
+ self.stub_url('PUT', ['tenants', id_, 'users', 'foo', 'roles',
+ 'OS-KSADM', 'barrr'], status_code=204)
+
+ self.client.roles.add_user_role('foo', 'barrr', id_)
+
+ def test_remove_user_role(self):
+ self.stub_url('DELETE', ['users', 'foo', 'roles', 'OS-KSADM',
+ 'barrr'], status_code=204)
+ self.client.roles.remove_user_role('foo', 'barrr')
+
+ def test_remove_user_role_tenant(self):
+ id_ = uuid.uuid4().hex
+ self.stub_url('DELETE', ['tenants', id_, 'users', 'foo',
+ 'roles', 'OS-KSADM', 'barrr'],
+ status_code=204)
+ self.client.roles.remove_user_role('foo', 'barrr', id_)
diff --git a/keystoneclient/tests/unit/v2_0/test_service_catalog.py b/keystoneclient/tests/unit/v2_0/test_service_catalog.py
new file mode 100644
index 0000000..e9ebf50
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_service_catalog.py
@@ -0,0 +1,175 @@
+# 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 access
+from keystoneclient import exceptions
+from keystoneclient.tests.unit.v2_0 import client_fixtures
+from keystoneclient.tests.unit.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.assertEqual(sc.url_for(service_type='compute'),
+ "https://compute.north.host/v1/1234")
+ self.assertEqual(sc.url_for('tenantId', '1', service_type='compute'),
+ "https://compute.north.host/v1/1234")
+ self.assertEqual(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.assertEqual(public_ep['compute'][1]['tenantId'], '2')
+ self.assertEqual(public_ep['compute'][1]['versionId'], '1.1')
+ self.assertEqual(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.assertEqual(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.assertEqual(url, "https://image-internal.south.host/v1/")
+
+ def test_service_catalog_empty(self):
+ self.AUTH_RESPONSE_BODY['access']['serviceCatalog'] = []
+ auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY)
+ self.assertRaises(exceptions.EmptyCatalog,
+ auth_ref.service_catalog.url_for,
+ service_type='image',
+ endpoint_type='internalURL')
+
+ def test_service_catalog_get_endpoints_region_names(self):
+ auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ endpoints = sc.get_endpoints(service_type='image', region_name='North')
+ self.assertEqual(len(endpoints), 1)
+ self.assertEqual(endpoints['image'][0]['publicURL'],
+ 'https://image.north.host/v1/')
+
+ endpoints = sc.get_endpoints(service_type='image', region_name='South')
+ self.assertEqual(len(endpoints), 1)
+ self.assertEqual(endpoints['image'][0]['publicURL'],
+ 'https://image.south.host/v1/')
+
+ endpoints = sc.get_endpoints(service_type='compute')
+ self.assertEqual(len(endpoints['compute']), 2)
+
+ endpoints = sc.get_endpoints(service_type='compute',
+ region_name='North')
+ self.assertEqual(len(endpoints['compute']), 2)
+
+ endpoints = sc.get_endpoints(service_type='compute',
+ region_name='West')
+ self.assertEqual(len(endpoints['compute']), 0)
+
+ def test_service_catalog_url_for_region_names(self):
+ auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ url = sc.url_for(service_type='image', region_name='North')
+ self.assertEqual(url, 'https://image.north.host/v1/')
+
+ url = sc.url_for(service_type='image', region_name='South')
+ self.assertEqual(url, 'https://image.south.host/v1/')
+
+ url = sc.url_for(service_type='compute',
+ region_name='North',
+ attr='versionId',
+ filter_value='1.1')
+ self.assertEqual(url, 'https://compute.north.host/v1.1/3456')
+
+ self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
+ service_type='image', region_name='West')
+
+ def test_servcie_catalog_get_url_region_names(self):
+ auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ urls = sc.get_urls(service_type='image')
+ self.assertEqual(len(urls), 2)
+
+ urls = sc.get_urls(service_type='image', region_name='North')
+ self.assertEqual(len(urls), 1)
+ self.assertEqual(urls[0], 'https://image.north.host/v1/')
+
+ urls = sc.get_urls(service_type='image', region_name='South')
+ self.assertEqual(len(urls), 1)
+ self.assertEqual(urls[0], 'https://image.south.host/v1/')
+
+ urls = sc.get_urls(service_type='image', region_name='West')
+ self.assertIsNone(urls)
+
+ def test_service_catalog_param_overrides_body_region(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')
+ self.assertEqual(url, 'https://image.north.host/v1/')
+
+ url = sc.url_for(service_type='image', region_name='South')
+ self.assertEqual(url, 'https://image.south.host/v1/')
+
+ endpoints = sc.get_endpoints(service_type='image')
+ self.assertEqual(len(endpoints['image']), 1)
+ self.assertEqual(endpoints['image'][0]['publicURL'],
+ 'https://image.north.host/v1/')
+
+ endpoints = sc.get_endpoints(service_type='image', region_name='South')
+ self.assertEqual(len(endpoints['image']), 1)
+ self.assertEqual(endpoints['image'][0]['publicURL'],
+ 'https://image.south.host/v1/')
+
+ def test_service_catalog_service_name(self):
+ auth_ref = access.AccessInfo.factory(resp=None,
+ body=self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ url = sc.url_for(service_name='Image Servers', endpoint_type='public',
+ service_type='image', region_name='North')
+ self.assertEqual('https://image.north.host/v1/', url)
+
+ self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
+ service_name='Image Servers', service_type='compute')
+
+ urls = sc.get_urls(service_type='image', service_name='Image Servers',
+ endpoint_type='public')
+
+ self.assertIn('https://image.north.host/v1/', urls)
+ self.assertIn('https://image.south.host/v1/', urls)
+
+ urls = sc.get_urls(service_type='image', service_name='Servers',
+ endpoint_type='public')
+
+ self.assertIsNone(urls)
diff --git a/keystoneclient/tests/unit/v2_0/test_services.py b/keystoneclient/tests/unit/v2_0/test_services.py
new file mode 100644
index 0000000..2172745
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_services.py
@@ -0,0 +1,98 @@
+# 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.unit.v2_0 import utils
+from keystoneclient.v2_0 import services
+
+
+class ServiceTests(utils.TestCase):
+ def setUp(self):
+ super(ServiceTests, self).setUp()
+
+ self.NOVA_SERVICE_ID = uuid.uuid4().hex
+ self.KEYSTONE_SERVICE_ID = uuid.uuid4().hex
+
+ self.TEST_SERVICES = {
+ "OS-KSADM:services": {
+ "values": [
+ {
+ "name": "nova",
+ "type": "compute",
+ "description": "Nova-compatible service.",
+ "id": self.NOVA_SERVICE_ID
+ },
+ {
+ "name": "keystone",
+ "type": "identity",
+ "description": "Keystone-compatible service.",
+ "id": self.KEYSTONE_SERVICE_ID
+ },
+ ],
+ },
+ }
+
+ def test_create(self):
+ req_body = {
+ "OS-KSADM:service": {
+ "name": "swift",
+ "type": "object-store",
+ "description": "Swift-compatible service.",
+ }
+ }
+ service_id = uuid.uuid4().hex
+ resp_body = {
+ "OS-KSADM:service": {
+ "name": "swift",
+ "type": "object-store",
+ "description": "Swift-compatible service.",
+ "id": service_id,
+ }
+ }
+ self.stub_url('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.assertIsInstance(service, services.Service)
+ self.assertEqual(service.id, service_id)
+ self.assertEqual(service.name, req_body['OS-KSADM:service']['name'])
+ self.assertRequestBodyIs(json=req_body)
+
+ def test_delete(self):
+ self.stub_url('DELETE',
+ ['OS-KSADM', 'services', self.NOVA_SERVICE_ID],
+ status_code=204)
+
+ self.client.services.delete(self.NOVA_SERVICE_ID)
+
+ def test_get(self):
+ test_services = self.TEST_SERVICES['OS-KSADM:services']['values'][0]
+
+ self.stub_url('GET', ['OS-KSADM', 'services', self.NOVA_SERVICE_ID],
+ json={'OS-KSADM:service': test_services})
+
+ service = self.client.services.get(self.NOVA_SERVICE_ID)
+ self.assertIsInstance(service, services.Service)
+ self.assertEqual(service.id, self.NOVA_SERVICE_ID)
+ self.assertEqual(service.name, 'nova')
+ self.assertEqual(service.type, 'compute')
+
+ def test_list(self):
+ self.stub_url('GET', ['OS-KSADM', 'services'],
+ json=self.TEST_SERVICES)
+
+ service_list = self.client.services.list()
+ [self.assertIsInstance(r, services.Service)
+ for r in service_list]
diff --git a/keystoneclient/tests/unit/v2_0/test_shell.py b/keystoneclient/tests/unit/v2_0/test_shell.py
new file mode 100644
index 0000000..be91d23
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_shell.py
@@ -0,0 +1,464 @@
+# 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
+import sys
+
+import mock
+from oslo_serialization import jsonutils
+import six
+from testtools import matchers
+
+from keystoneclient import fixture
+from keystoneclient.tests.unit.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/'
+DEFAULT_ADMIN_URL = 'http://127.0.0.1:35357/v2.0/'
+
+
+class ShellTests(utils.TestCase):
+
+ TEST_URL = DEFAULT_ADMIN_URL
+
+ def setUp(self):
+ """Patch os.environ to avoid required auth info."""
+
+ super(ShellTests, self).setUp()
+
+ 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()
+
+ self.token = fixture.V2Token()
+ self.token.set_scope()
+ svc = self.token.add_service('identity')
+ svc.add_endpoint(public=DEFAULT_AUTH_URL,
+ admin=DEFAULT_ADMIN_URL)
+
+ self.stub_auth(json=self.token, base_url=DEFAULT_AUTH_URL)
+
+ def tearDown(self):
+ os.environ = self.old_environment
+ super(ShellTests, self).tearDown()
+
+ def run_command(self, cmd):
+ orig = sys.stdout
+ try:
+ sys.stdout = six.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, path, base_url=TEST_URL):
+ self.assertEqual(method, self.requests.last_request.method)
+ self.assertEqual(base_url + path.lstrip('/'),
+ self.requests.last_request.url)
+
+ def test_user_list(self):
+ self.stub_url('GET', ['users'], json={'users': []})
+ self.run_command('user-list')
+ self.assert_called('GET', '/users')
+
+ def test_user_create(self):
+ self.stub_url('POST', ['users'], json={'user': {}})
+ self.run_command('user-create --name new-user')
+
+ self.assert_called('POST', '/users')
+ self.assertRequestBodyIs(json={'user': {'email': None,
+ 'password': None,
+ 'enabled': True,
+ 'name': 'new-user',
+ 'tenantId': None}})
+
+ @mock.patch('sys.stdin', autospec=True)
+ def test_user_create_password_prompt(self, mock_stdin):
+ self.stub_url('POST', ['users'], json={'user': {}})
+
+ with mock.patch('getpass.getpass') as mock_getpass:
+ del(os.environ['OS_PASSWORD'])
+ mock_stdin.isatty = lambda: True
+ mock_getpass.return_value = 'newpass'
+ self.run_command('user-create --name new-user --pass')
+
+ self.assert_called('POST', '/users')
+ self.assertRequestBodyIs(json={'user': {'email': None,
+ 'password': 'newpass',
+ 'enabled': True,
+ 'name': 'new-user',
+ 'tenantId': None}})
+
+ def test_user_get(self):
+ self.stub_url('GET', ['users', '1'],
+ json={'user': {'id': '1'}})
+ self.run_command('user-get 1')
+ self.assert_called('GET', '/users/1')
+
+ def test_user_delete(self):
+ self.stub_url('GET', ['users', '1'],
+ json={'user': {'id': '1'}})
+ self.stub_url('DELETE', ['users', '1'])
+ self.run_command('user-delete 1')
+ self.assert_called('DELETE', '/users/1')
+
+ def test_user_password_update(self):
+ self.stub_url('GET', ['users', '1'],
+ json={'user': {'id': '1'}})
+ self.stub_url('PUT', ['users', '1', 'OS-KSADM', 'password'])
+ self.run_command('user-password-update --pass newpass 1')
+ self.assert_called('PUT', '/users/1/OS-KSADM/password')
+
+ def test_user_update(self):
+ self.stub_url('PUT', ['users', '1'])
+ self.stub_url('GET', ['users', '1'],
+ json={"user": {"tenantId": "1",
+ "enabled": "true",
+ "id": "1",
+ "name": "username"}})
+
+ self.run_command('user-update --name new-user1'
+ ' --email user@email.com --enabled true 1')
+ self.assert_called('PUT', '/users/1')
+ body = {'user': {'id': '1', 'email': 'user@email.com',
+ 'enabled': True, 'name': 'new-user1'}}
+ self.assertRequestBodyIs(json=body)
+
+ 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.assert_called('PUT', '/users/1')
+ self.assertRequestBodyIs(json={'user': {'id': '1', 'email': ''}})
+
+ def test_role_create(self):
+ self.stub_url('POST', ['OS-KSADM', 'roles'], json={'role': {}})
+ self.run_command('role-create --name new-role')
+ self.assert_called('POST', '/OS-KSADM/roles')
+ self.assertRequestBodyIs(json={"role": {"name": "new-role"}})
+
+ def test_role_get(self):
+ self.stub_url('GET', ['OS-KSADM', 'roles', '1'],
+ json={'role': {'id': '1'}})
+ self.run_command('role-get 1')
+ self.assert_called('GET', '/OS-KSADM/roles/1')
+
+ def test_role_list(self):
+ self.stub_url('GET', ['OS-KSADM', 'roles'], json={'roles': []})
+ self.run_command('role-list')
+ self.assert_called('GET', '/OS-KSADM/roles')
+
+ def test_role_delete(self):
+ self.stub_url('GET', ['OS-KSADM', 'roles', '1'],
+ json={'role': {'id': '1'}})
+ self.stub_url('DELETE', ['OS-KSADM', 'roles', '1'])
+ self.run_command('role-delete 1')
+ self.assert_called('DELETE', '/OS-KSADM/roles/1')
+
+ def test_user_role_add(self):
+ self.stub_url('GET', ['users', '1'],
+ json={'user': {'id': '1'}})
+ self.stub_url('GET', ['OS-KSADM', 'roles', '1'],
+ json={'role': {'id': '1'}})
+
+ self.stub_url('PUT', ['users', '1', 'roles', 'OS-KSADM', '1'])
+ self.run_command('user-role-add --user_id 1 --role_id 1')
+ self.assert_called('PUT', '/users/1/roles/OS-KSADM/1')
+
+ def test_user_role_list(self):
+ self.stub_url('GET', ['tenants', self.token.tenant_id],
+ json={'tenant': {'id': self.token.tenant_id}})
+ self.stub_url('GET', ['tenants', self.token.tenant_id,
+ 'users', self.token.user_id, 'roles'],
+ json={'roles': []})
+
+ url = '/tenants/%s/users/%s/roles' % (self.token.tenant_id,
+ self.token.user_id)
+
+ self.run_command('user-role-list --user_id %s --tenant-id %s' %
+ (self.token.user_id, self.token.tenant_id))
+ self.assert_called('GET', url)
+
+ self.run_command('user-role-list --user_id %s' % self.token.user_id)
+ self.assert_called('GET', url)
+
+ self.run_command('user-role-list')
+ self.assert_called('GET', url)
+
+ def test_user_role_remove(self):
+ self.stub_url('GET', ['users', '1'],
+ json={'user': {'id': 1}})
+ self.stub_url('GET', ['OS-KSADM', 'roles', '1'],
+ json={'role': {'id': 1}})
+ self.stub_url('DELETE',
+ ['users', '1', 'roles', 'OS-KSADM', '1'])
+
+ self.run_command('user-role-remove --user_id 1 --role_id 1')
+ self.assert_called('DELETE', '/users/1/roles/OS-KSADM/1')
+
+ def test_tenant_create(self):
+ self.stub_url('POST', ['tenants'], json={'tenant': {}})
+ self.run_command('tenant-create --name new-tenant')
+ self.assertRequestBodyIs(json={"tenant": {"enabled": True,
+ "name": "new-tenant",
+ "description": None}})
+
+ def test_tenant_get(self):
+ self.stub_url('GET', ['tenants', '2'], json={'tenant': {}})
+ self.run_command('tenant-get 2')
+ self.assert_called('GET', '/tenants/2')
+
+ def test_tenant_list(self):
+ self.stub_url('GET', ['tenants'], json={'tenants': []})
+ self.run_command('tenant-list')
+ self.assert_called('GET', '/tenants')
+
+ def test_tenant_update(self):
+ self.stub_url('GET', ['tenants', '1'],
+ json={'tenant': {'id': '1'}})
+ self.stub_url('GET', ['tenants', '2'],
+ json={'tenant': {'id': '2'}})
+ self.stub_url('POST', ['tenants', '2'],
+ json={'tenant': {'id': '2'}})
+ self.run_command('tenant-update'
+ ' --name new-tenant1 --enabled false'
+ ' --description desc 2')
+ self.assert_called('POST', '/tenants/2')
+ self.assertRequestBodyIs(json={"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.stub_url('GET', ['tenants', '2'],
+ json={'tenant': {'id': '2'}})
+ self.stub_url('DELETE', ['tenants', '2'])
+ self.run_command('tenant-delete 2')
+ self.assert_called('DELETE', '/tenants/2')
+
+ def test_service_create_with_required_arguments_only(self):
+ self.stub_url('POST', ['OS-KSADM', 'services'],
+ json={'OS-KSADM:service': {}})
+ self.run_command('service-create --type compute')
+ self.assert_called('POST', '/OS-KSADM/services')
+ json = {"OS-KSADM:service": {"type": "compute",
+ "name": None,
+ "description": None}}
+ self.assertRequestBodyIs(json=json)
+
+ def test_service_create_with_all_arguments(self):
+ self.stub_url('POST', ['OS-KSADM', 'services'],
+ json={'OS-KSADM:service': {}})
+ self.run_command('service-create --type compute '
+ '--name service1 --description desc1')
+ self.assert_called('POST', '/OS-KSADM/services')
+ json = {"OS-KSADM:service": {"type": "compute",
+ "name": "service1",
+ "description": "desc1"}}
+ self.assertRequestBodyIs(json=json)
+
+ def test_service_get(self):
+ self.stub_url('GET', ['OS-KSADM', 'services', '1'],
+ json={'OS-KSADM:service': {'id': '1'}})
+ self.run_command('service-get 1')
+ self.assert_called('GET', '/OS-KSADM/services/1')
+
+ def test_service_list(self):
+ self.stub_url('GET', ['OS-KSADM', 'services'],
+ json={'OS-KSADM:services': []})
+ self.run_command('service-list')
+ self.assert_called('GET', '/OS-KSADM/services')
+
+ def test_service_delete(self):
+ self.stub_url('GET', ['OS-KSADM', 'services', '1'],
+ json={'OS-KSADM:service': {'id': 1}})
+ self.stub_url('DELETE', ['OS-KSADM', 'services', '1'])
+ self.run_command('service-delete 1')
+ self.assert_called('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.stub_url('POST',
+ ['users', self.token.user_id, 'credentials', 'OS-EC2'],
+ json={'credential': {}})
+
+ url = '/users/%s/credentials/OS-EC2' % self.token.user_id
+ self.run_command('ec2-credentials-create --tenant-id 1 '
+ '--user-id %s' % self.token.user_id)
+ self.assert_called('POST', url)
+ self.assertRequestBodyIs(json={'tenant_id': '1'})
+
+ self.run_command('ec2-credentials-create --tenant-id 1')
+ self.assert_called('POST', url)
+ self.assertRequestBodyIs(json={'tenant_id': '1'})
+
+ self.run_command('ec2-credentials-create')
+ self.assert_called('POST', url)
+ self.assertRequestBodyIs(json={'tenant_id': self.token.tenant_id})
+
+ def test_ec2_credentials_delete(self):
+ self.stub_url('DELETE',
+ ['users', self.token.user_id,
+ 'credentials', 'OS-EC2', '2'])
+ self.run_command('ec2-credentials-delete --access 2 --user-id %s' %
+ self.token.user_id)
+
+ url = '/users/%s/credentials/OS-EC2/2' % self.token.user_id
+ self.assert_called('DELETE', url)
+
+ self.run_command('ec2-credentials-delete --access 2')
+ self.assert_called('DELETE', url)
+
+ def test_ec2_credentials_list(self):
+ self.stub_url('GET',
+ ['users', self.token.user_id, 'credentials', 'OS-EC2'],
+ json={'credentials': []})
+ self.run_command('ec2-credentials-list --user-id %s'
+ % self.token.user_id)
+
+ url = '/users/%s/credentials/OS-EC2' % self.token.user_id
+ self.assert_called('GET', url)
+
+ self.run_command('ec2-credentials-list')
+ self.assert_called('GET', url)
+
+ def test_ec2_credentials_get(self):
+ self.stub_url('GET',
+ ['users', '1', 'credentials', 'OS-EC2', '2'],
+ json={'credential': {}})
+ self.run_command('ec2-credentials-get --access 2 --user-id 1')
+ self.assert_called('GET', '/users/1/credentials/OS-EC2/2')
+
+ def test_bootstrap(self):
+ user = {'user': {'id': '1'}}
+ role = {'role': {'id': '1'}}
+ tenant = {'tenant': {'id': '1'}}
+
+ token = fixture.V2Token(user_id=1, tenant_id=1)
+ token.add_role(id=1)
+ svc = token.add_service('identity')
+ svc.add_endpoint(public=DEFAULT_AUTH_URL,
+ admin=DEFAULT_ADMIN_URL)
+
+ self.stub_auth(json=token)
+
+ self.stub_url('POST', ['OS-KSADM', 'roles'], json=role)
+ self.stub_url('GET', ['OS-KSADM', 'roles', '1'], json=role)
+ self.stub_url('POST', ['tenants'], json=tenant)
+ self.stub_url('GET', ['tenants', '1'], json=tenant)
+ self.stub_url('POST', ['users'], json=user)
+ self.stub_url('GET', ['users', '1'], json=user)
+ self.stub_url('PUT',
+ ['tenants', '1', 'users', '1', 'roles', 'OS-KSADM', '1'],
+ json=role)
+
+ self.run_command('bootstrap --user-name new-user'
+ ' --pass 1 --role-name admin'
+ ' --tenant-name new-tenant')
+
+ def called_anytime(method, path, json=None):
+ test_url = self.TEST_URL.strip('/')
+ for r in self.requests.request_history:
+ if not r.method == method:
+ continue
+ if not r.url == test_url + path:
+ continue
+
+ if json:
+ json_body = jsonutils.loads(r.body)
+ if not json_body == json:
+ continue
+
+ return True
+
+ raise AssertionError('URL never called')
+
+ called_anytime('POST', '/users', {'user': {'email': None,
+ 'password': '1',
+ 'enabled': True,
+ 'name': 'new-user',
+ 'tenantId': None}})
+
+ called_anytime('POST', '/tenants', {"tenant": {"enabled": True,
+ "name": "new-tenant",
+ "description": None}})
+
+ called_anytime('POST', '/OS-KSADM/roles',
+ {"role": {"name": "admin"}})
+
+ 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.stub_url('PATCH',
+ ['OS-KSCRUD', 'users', self.token.user_id],
+ base_url=DEFAULT_AUTH_URL)
+ self.run_command('password-update --current-password oldpass'
+ ' --new-password newpass')
+ self.assert_called('PATCH',
+ '/OS-KSCRUD/users/%s' % self.token.user_id,
+ base_url=DEFAULT_AUTH_URL)
+ self.assertRequestBodyIs(json={'user': {'original_password': 'oldpass',
+ 'password': 'newpass'}})
+
+ def test_endpoint_create(self):
+ self.stub_url('GET', ['OS-KSADM', 'services', '1'],
+ json={'OS-KSADM:service': {'id': '1'}})
+ self.stub_url('POST', ['endpoints'], json={'endpoint': {}})
+ self.run_command('endpoint-create --service-id 1 '
+ '--publicurl=http://example.com:1234/go')
+ self.assert_called('POST', '/endpoints')
+ json = {'endpoint': {'adminurl': None,
+ 'service_id': '1',
+ 'region': 'regionOne',
+ 'internalurl': None,
+ 'publicurl': "http://example.com:1234/go"}}
+ self.assertRequestBodyIs(json=json)
+
+ def test_endpoint_list(self):
+ self.stub_url('GET', ['endpoints'], json={'endpoints': []})
+ self.run_command('endpoint-list')
+ self.assert_called('GET', '/endpoints')
diff --git a/keystoneclient/tests/unit/v2_0/test_tenants.py b/keystoneclient/tests/unit/v2_0/test_tenants.py
new file mode 100644
index 0000000..62ca398
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_tenants.py
@@ -0,0 +1,363 @@
+# 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 import fixture
+from keystoneclient.tests.unit.v2_0 import utils
+from keystoneclient.v2_0 import client
+from keystoneclient.v2_0 import tenants
+from keystoneclient.v2_0 import users
+
+
+class TenantTests(utils.TestCase):
+ def setUp(self):
+ super(TenantTests, self).setUp()
+
+ self.INVIS_ID = uuid.uuid4().hex
+ self.DEMO_ID = uuid.uuid4().hex
+ self.ADMIN_ID = uuid.uuid4().hex
+ self.EXTRAS_ID = uuid.uuid4().hex
+
+ self.TEST_TENANTS = {
+ "tenants": {
+ "values": [
+ {
+ "enabled": True,
+ "description": "A description change!",
+ "name": "invisible_to_admin",
+ "id": self.INVIS_ID,
+ },
+ {
+ "enabled": True,
+ "description": "None",
+ "name": "demo",
+ "id": self.DEMO_ID,
+ },
+ {
+ "enabled": True,
+ "description": "None",
+ "name": "admin",
+ "id": self.ADMIN_ID,
+ },
+ {
+ "extravalue01": "metadata01",
+ "enabled": True,
+ "description": "For testing extras",
+ "name": "test_extras",
+ "id": self.EXTRAS_ID,
+ }
+ ],
+ "links": [],
+ },
+ }
+
+ def test_create(self):
+ req_body = {
+ "tenant": {
+ "name": "tenantX",
+ "description": "Like tenant 9, but better.",
+ "enabled": True,
+ "extravalue01": "metadata01",
+ },
+ }
+ id_ = uuid.uuid4().hex
+ resp_body = {
+ "tenant": {
+ "name": "tenantX",
+ "enabled": True,
+ "id": id_,
+ "description": "Like tenant 9, but better.",
+ "extravalue01": "metadata01",
+ }
+ }
+ self.stub_url('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="don't overwrite priors")
+ self.assertIsInstance(tenant, tenants.Tenant)
+ self.assertEqual(tenant.id, id_)
+ self.assertEqual(tenant.name, "tenantX")
+ self.assertEqual(tenant.description, "Like tenant 9, but better.")
+ self.assertEqual(tenant.extravalue01, "metadata01")
+ self.assertRequestBodyIs(json=req_body)
+
+ 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('POST', ['tenants'], status_code=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)
+
+ def test_delete(self):
+ self.stub_url('DELETE', ['tenants', self.ADMIN_ID], status_code=204)
+ self.client.tenants.delete(self.ADMIN_ID)
+
+ def test_get(self):
+ resp = {'tenant': self.TEST_TENANTS['tenants']['values'][2]}
+ self.stub_url('GET', ['tenants', self.ADMIN_ID], json=resp)
+
+ t = self.client.tenants.get(self.ADMIN_ID)
+ self.assertIsInstance(t, tenants.Tenant)
+ self.assertEqual(t.id, self.ADMIN_ID)
+ self.assertEqual(t.name, 'admin')
+
+ def test_list(self):
+ self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS)
+
+ tenant_list = self.client.tenants.list()
+ [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list]
+
+ def test_list_limit(self):
+ self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS)
+
+ tenant_list = self.client.tenants.list(limit=1)
+ self.assertQueryStringIs('limit=1')
+ [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list]
+
+ def test_list_marker(self):
+ self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS)
+
+ tenant_list = self.client.tenants.list(marker=1)
+ self.assertQueryStringIs('marker=1')
+ [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list]
+
+ def test_list_limit_marker(self):
+ self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS)
+
+ tenant_list = self.client.tenants.list(limit=1, marker=1)
+ self.assertQueryStringIs('marker=1&limit=1')
+ [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list]
+
+ def test_update(self):
+ req_body = {
+ "tenant": {
+ "id": self.EXTRAS_ID,
+ "name": "tenantX",
+ "description": "I changed you!",
+ "enabled": False,
+ "extravalue01": "metadataChanged",
+ # "extraname": "dontoverwrite!",
+ },
+ }
+ resp_body = {
+ "tenant": {
+ "name": "tenantX",
+ "enabled": False,
+ "id": self.EXTRAS_ID,
+ "description": "I changed you!",
+ "extravalue01": "metadataChanged",
+ },
+ }
+
+ self.stub_url('POST', ['tenants', self.EXTRAS_ID], 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="don't overwrite priors")
+ self.assertIsInstance(tenant, tenants.Tenant)
+ self.assertRequestBodyIs(json=req_body)
+ self.assertEqual(tenant.id, self.EXTRAS_ID)
+ self.assertEqual(tenant.name, "tenantX")
+ self.assertEqual(tenant.description, "I changed you!")
+ self.assertFalse(tenant.enabled)
+ self.assertEqual(tenant.extravalue01, "metadataChanged")
+
+ def test_update_empty_description(self):
+ req_body = {
+ "tenant": {
+ "id": self.EXTRAS_ID,
+ "name": "tenantX",
+ "description": "",
+ "enabled": False,
+ },
+ }
+ resp_body = {
+ "tenant": {
+ "name": "tenantX",
+ "enabled": False,
+ "id": self.EXTRAS_ID,
+ "description": "",
+ },
+ }
+ self.stub_url('POST', ['tenants', self.EXTRAS_ID], 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.assertIsInstance(tenant, tenants.Tenant)
+ self.assertRequestBodyIs(json=req_body)
+ self.assertEqual(tenant.id, self.EXTRAS_ID)
+ self.assertEqual(tenant.name, "tenantX")
+ self.assertEqual(tenant.description, "")
+ self.assertFalse(tenant.enabled)
+
+ def test_add_user(self):
+ self.stub_url('PUT',
+ ['tenants', self.EXTRAS_ID, 'users', 'foo', 'roles',
+ 'OS-KSADM', 'barrr'],
+ status_code=204)
+
+ self.client.tenants.add_user(self.EXTRAS_ID, 'foo', 'barrr')
+
+ def test_remove_user(self):
+ self.stub_url('DELETE', ['tenants', self.EXTRAS_ID, 'users',
+ 'foo', 'roles', 'OS-KSADM', 'barrr'],
+ status_code=204)
+
+ self.client.tenants.remove_user(self.EXTRAS_ID, 'foo', 'barrr')
+
+ def test_tenant_add_user(self):
+ self.stub_url('PUT', ['tenants', self.EXTRAS_ID, 'users',
+ 'foo', 'roles', 'OS-KSADM', 'barrr'],
+ status_code=204)
+
+ req_body = {
+ "tenant": {
+ "id": self.EXTRAS_ID,
+ "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.assertIsInstance(tenant, tenants.Tenant)
+
+ def test_tenant_remove_user(self):
+ self.stub_url('DELETE', ['tenants', self.EXTRAS_ID, 'users',
+ 'foo', 'roles', 'OS-KSADM', 'barrr'],
+ status_code=204)
+
+ req_body = {
+ "tenant": {
+ "id": self.EXTRAS_ID,
+ "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.assertIsInstance(tenant, tenants.Tenant)
+
+ def test_tenant_list_users(self):
+ tenant_id = uuid.uuid4().hex
+ user_id1 = uuid.uuid4().hex
+ user_id2 = uuid.uuid4().hex
+
+ tenant_resp = {
+ 'tenant': {
+ 'name': uuid.uuid4().hex,
+ 'enabled': True,
+ 'id': tenant_id,
+ 'description': 'test tenant',
+ }
+ }
+
+ users_resp = {
+ 'users': {
+ 'values': [
+ {
+ 'email': uuid.uuid4().hex,
+ 'enabled': True,
+ 'id': user_id1,
+ 'name': uuid.uuid4().hex,
+ },
+ {
+ 'email': uuid.uuid4().hex,
+ 'enabled': True,
+ 'id': user_id2,
+ 'name': uuid.uuid4().hex,
+ },
+ ]
+ }
+ }
+
+ self.stub_url('GET', ['tenants', tenant_id], json=tenant_resp)
+ self.stub_url('GET',
+ ['tenants', tenant_id, 'users'],
+ json=users_resp)
+
+ tenant = self.client.tenants.get(tenant_id)
+ user_objs = tenant.list_users()
+
+ for u in user_objs:
+ self.assertIsInstance(u, users.User)
+
+ self.assertEqual(set([user_id1, user_id2]),
+ set([u.id for u in user_objs]))
+
+ def test_list_tenants_use_admin_url(self):
+ self.stub_url('GET', ['tenants'], json=self.TEST_TENANTS)
+
+ self.assertEqual(self.TEST_URL, self.client.management_url)
+
+ tenant_list = self.client.tenants.list()
+ [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list]
+
+ self.assertEqual(len(self.TEST_TENANTS['tenants']['values']),
+ len(tenant_list))
+
+ def test_list_tenants_fallback_to_auth_url(self):
+ new_auth_url = 'http://keystone.test:5000/v2.0'
+
+ token = fixture.V2Token(token_id=self.TEST_TOKEN,
+ user_name=self.TEST_USER,
+ user_id=self.TEST_USER_ID)
+
+ self.stub_auth(base_url=new_auth_url, json=token)
+ self.stub_url('GET', ['tenants'], base_url=new_auth_url,
+ json=self.TEST_TENANTS)
+
+ c = client.Client(username=self.TEST_USER,
+ auth_url=new_auth_url,
+ password=uuid.uuid4().hex)
+
+ self.assertIsNone(c.management_url)
+ tenant_list = c.tenants.list()
+ [self.assertIsInstance(t, tenants.Tenant) for t in tenant_list]
+
+ self.assertEqual(len(self.TEST_TENANTS['tenants']['values']),
+ len(tenant_list))
diff --git a/keystoneclient/tests/unit/v2_0/test_tokens.py b/keystoneclient/tests/unit/v2_0/test_tokens.py
new file mode 100644
index 0000000..8a40f82
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_tokens.py
@@ -0,0 +1,208 @@
+# 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 access
+from keystoneclient import exceptions
+from keystoneclient import fixture
+from keystoneclient.tests.unit.v2_0 import utils
+from keystoneclient.v2_0 import client
+from keystoneclient.v2_0 import tokens
+
+
+class TokenTests(utils.TestCase):
+
+ def test_delete(self):
+ id_ = uuid.uuid4().hex
+ self.stub_url('DELETE', ['tokens', id_], status_code=204)
+ self.client.tokens.delete(id_)
+
+ def test_user_password(self):
+ token_fixture = fixture.V2Token(user_name=self.TEST_USER)
+ self.stub_auth(json=token_fixture)
+
+ password = uuid.uuid4().hex
+ token_ref = self.client.tokens.authenticate(username=self.TEST_USER,
+ password=password)
+
+ self.assertIsInstance(token_ref, tokens.Token)
+ self.assertEqual(token_fixture.token_id, token_ref.id)
+ self.assertEqual(token_fixture.expires_str, token_ref.expires)
+
+ req_body = {
+ 'auth': {
+ 'passwordCredentials': {
+ 'username': self.TEST_USER,
+ 'password': password,
+ }
+ }
+ }
+
+ self.assertRequestBodyIs(json=req_body)
+
+ def test_with_token_id(self):
+ token_fixture = fixture.V2Token()
+ self.stub_auth(json=token_fixture)
+
+ token_id = uuid.uuid4().hex
+ token_ref = self.client.tokens.authenticate(token=token_id)
+
+ self.assertIsInstance(token_ref, tokens.Token)
+ self.assertEqual(token_fixture.token_id, token_ref.id)
+ self.assertEqual(token_fixture.expires_str, token_ref.expires)
+
+ req_body = {
+ 'auth': {
+ 'token': {
+ 'id': token_id,
+ }
+ }
+ }
+
+ self.assertRequestBodyIs(json=req_body)
+
+ def test_without_auth_params(self):
+ self.assertRaises(ValueError, self.client.tokens.authenticate)
+ self.assertRaises(ValueError, self.client.tokens.authenticate,
+ tenant_id=uuid.uuid4().hex)
+
+ def test_with_tenant_id(self):
+ token_fixture = fixture.V2Token()
+ token_fixture.set_scope()
+ self.stub_auth(json=token_fixture)
+
+ token_id = uuid.uuid4().hex
+ tenant_id = uuid.uuid4().hex
+ token_ref = self.client.tokens.authenticate(token=token_id,
+ tenant_id=tenant_id)
+
+ self.assertIsInstance(token_ref, tokens.Token)
+ self.assertEqual(token_fixture.token_id, token_ref.id)
+ self.assertEqual(token_fixture.expires_str, token_ref.expires)
+
+ tenant_data = {'id': token_fixture.tenant_id,
+ 'name': token_fixture.tenant_name}
+ self.assertEqual(tenant_data, token_ref.tenant)
+
+ req_body = {
+ 'auth': {
+ 'token': {
+ 'id': token_id,
+ },
+ 'tenantId': tenant_id
+ }
+ }
+
+ self.assertRequestBodyIs(json=req_body)
+
+ def test_with_tenant_name(self):
+ token_fixture = fixture.V2Token()
+ token_fixture.set_scope()
+ self.stub_auth(json=token_fixture)
+
+ token_id = uuid.uuid4().hex
+ tenant_name = uuid.uuid4().hex
+ token_ref = self.client.tokens.authenticate(token=token_id,
+ tenant_name=tenant_name)
+
+ self.assertIsInstance(token_ref, tokens.Token)
+ self.assertEqual(token_fixture.token_id, token_ref.id)
+ self.assertEqual(token_fixture.expires_str, token_ref.expires)
+
+ tenant_data = {'id': token_fixture.tenant_id,
+ 'name': token_fixture.tenant_name}
+ self.assertEqual(tenant_data, token_ref.tenant)
+
+ req_body = {
+ 'auth': {
+ 'token': {
+ 'id': token_id,
+ },
+ 'tenantName': tenant_name
+ }
+ }
+
+ self.assertRequestBodyIs(json=req_body)
+
+ def test_authenticate_use_admin_url(self):
+ token_fixture = fixture.V2Token()
+ token_fixture.set_scope()
+ self.stub_auth(json=token_fixture)
+
+ self.assertEqual(self.TEST_URL, self.client.management_url)
+
+ token_ref = self.client.tokens.authenticate(token=uuid.uuid4().hex)
+ self.assertIsInstance(token_ref, tokens.Token)
+ self.assertEqual(token_fixture.token_id, token_ref.id)
+ self.assertEqual(token_fixture.expires_str, token_ref.expires)
+
+ def test_authenticate_fallback_to_auth_url(self):
+ new_auth_url = 'http://keystone.test:5000/v2.0'
+
+ token_fixture = fixture.V2Token()
+ self.stub_auth(base_url=new_auth_url, json=token_fixture)
+
+ c = client.Client(username=self.TEST_USER,
+ auth_url=new_auth_url,
+ password=uuid.uuid4().hex)
+
+ self.assertIsNone(c.management_url)
+
+ token_ref = c.tokens.authenticate(token=uuid.uuid4().hex)
+ self.assertIsInstance(token_ref, tokens.Token)
+ self.assertEqual(token_fixture.token_id, token_ref.id)
+ self.assertEqual(token_fixture.expires_str, token_ref.expires)
+
+ def test_validate_token(self):
+ id_ = uuid.uuid4().hex
+ token_fixture = fixture.V2Token(token_id=id_)
+ self.stub_url('GET', ['tokens', id_], json=token_fixture)
+
+ token_ref = self.client.tokens.validate(id_)
+ self.assertIsInstance(token_ref, tokens.Token)
+ self.assertEqual(id_, token_ref.id)
+
+ def test_validate_token_invalid_token(self):
+ # If the token is invalid, typically a NotFound is raised.
+
+ id_ = uuid.uuid4().hex
+ # The server is expected to return 404 if the token is invalid.
+ self.stub_url('GET', ['tokens', id_], status_code=404)
+ self.assertRaises(exceptions.NotFound,
+ self.client.tokens.validate, id_)
+
+ def test_validate_token_access_info_with_token_id(self):
+ # Can validate a token passing a string token ID.
+ token_id = uuid.uuid4().hex
+ token_fixture = fixture.V2Token(token_id=token_id)
+ self.stub_url('GET', ['tokens', token_id], json=token_fixture)
+ access_info = self.client.tokens.validate_access_info(token_id)
+ self.assertIsInstance(access_info, access.AccessInfoV2)
+ self.assertEqual(token_id, access_info.auth_token)
+
+ def test_validate_token_access_info_with_access_info(self):
+ # Can validate a token passing an access info.
+ token_id = uuid.uuid4().hex
+ token_fixture = fixture.V2Token(token_id=token_id)
+ self.stub_url('GET', ['tokens', token_id], json=token_fixture)
+ token = access.AccessInfo.factory(body=token_fixture)
+ access_info = self.client.tokens.validate_access_info(token)
+ self.assertIsInstance(access_info, access.AccessInfoV2)
+ self.assertEqual(token_id, access_info.auth_token)
+
+ def test_get_revoked(self):
+ sample_revoked_response = {'signed': '-----BEGIN CMS-----\nMIIB...'}
+ self.stub_url('GET', ['tokens', 'revoked'],
+ json=sample_revoked_response)
+ resp = self.client.tokens.get_revoked()
+ self.assertEqual(sample_revoked_response, resp)
diff --git a/keystoneclient/tests/unit/v2_0/test_users.py b/keystoneclient/tests/unit/v2_0/test_users.py
new file mode 100644
index 0000000..455ca3c
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/test_users.py
@@ -0,0 +1,305 @@
+# 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.unit.v2_0 import utils
+from keystoneclient.v2_0 import roles
+from keystoneclient.v2_0 import users
+
+
+class UserTests(utils.TestCase):
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.ADMIN_USER_ID = uuid.uuid4().hex
+ self.DEMO_USER_ID = uuid.uuid4().hex
+ self.TEST_USERS = {
+ "users": {
+ "values": [
+ {
+ "email": "None",
+ "enabled": True,
+ "id": self.ADMIN_USER_ID,
+ "name": "admin",
+ },
+ {
+ "email": "None",
+ "enabled": True,
+ "id": self.DEMO_USER_ID,
+ "name": "demo",
+ },
+ ]
+ }
+ }
+
+ def test_create(self):
+ tenant_id = uuid.uuid4().hex
+ user_id = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+ req_body = {
+ "user": {
+ "name": "gabriel",
+ "password": password,
+ "tenantId": tenant_id,
+ "email": "test@example.com",
+ "enabled": True,
+ }
+ }
+
+ resp_body = {
+ "user": {
+ "name": "gabriel",
+ "enabled": True,
+ "tenantId": tenant_id,
+ "id": user_id,
+ "password": password,
+ "email": "test@example.com",
+ }
+ }
+
+ self.stub_url('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.assertIsInstance(user, users.User)
+ self.assertEqual(user.id, user_id)
+ self.assertEqual(user.name, "gabriel")
+ self.assertEqual(user.email, "test@example.com")
+ self.assertRequestBodyIs(json=req_body)
+ self.assertNotIn(password, self.logger.output)
+
+ def test_create_user_without_email(self):
+ tenant_id = uuid.uuid4().hex
+ req_body = {
+ "user": {
+ "name": "gabriel",
+ "password": "test",
+ "tenantId": tenant_id,
+ "enabled": True,
+ "email": None,
+ }
+ }
+
+ user_id = uuid.uuid4().hex
+ resp_body = {
+ "user": {
+ "name": "gabriel",
+ "enabled": True,
+ "tenantId": tenant_id,
+ "id": user_id,
+ "password": "test",
+ }
+ }
+
+ self.stub_url('POST', ['users'], json=resp_body)
+
+ user = self.client.users.create(
+ req_body['user']['name'],
+ req_body['user']['password'],
+ tenant_id=req_body['user']['tenantId'],
+ enabled=req_body['user']['enabled'])
+ self.assertIsInstance(user, users.User)
+ self.assertEqual(user.id, user_id)
+ self.assertEqual(user.name, "gabriel")
+ self.assertRequestBodyIs(json=req_body)
+
+ def test_create_user_without_password(self):
+ user_name = 'test'
+ user_id = uuid.uuid4().hex
+ tenant_id = uuid.uuid4().hex
+ user_enabled = True
+ req_body = {
+ 'user': {
+ 'name': user_name,
+ 'password': None,
+ 'tenantId': tenant_id,
+ 'enabled': user_enabled,
+ 'email': None,
+ }
+ }
+ resp_body = {
+ 'user': {
+ 'name': user_name,
+ 'enabled': user_enabled,
+ 'tenantId': tenant_id,
+ 'id': user_id,
+ }
+ }
+
+ self.stub_url('POST', ['users'], json=resp_body)
+
+ user = self.client.users.create(user_name, tenant_id=tenant_id,
+ enabled=user_enabled)
+ self.assertIsInstance(user, users.User)
+ self.assertEqual(user_id, user.id)
+ self.assertEqual(user_name, user.name)
+ self.assertRequestBodyIs(json=req_body)
+
+ def test_delete(self):
+ self.stub_url('DELETE', ['users', self.ADMIN_USER_ID], status_code=204)
+ self.client.users.delete(self.ADMIN_USER_ID)
+
+ def test_get(self):
+ self.stub_url('GET', ['users', self.ADMIN_USER_ID],
+ json={'user': self.TEST_USERS['users']['values'][0]})
+
+ u = self.client.users.get(self.ADMIN_USER_ID)
+ self.assertIsInstance(u, users.User)
+ self.assertEqual(u.id, self.ADMIN_USER_ID)
+ self.assertEqual(u.name, 'admin')
+
+ def test_list(self):
+ self.stub_url('GET', ['users'], json=self.TEST_USERS)
+
+ user_list = self.client.users.list()
+ [self.assertIsInstance(u, users.User) for u in user_list]
+
+ def test_list_limit(self):
+ self.stub_url('GET', ['users'], json=self.TEST_USERS)
+
+ user_list = self.client.users.list(limit=1)
+ self.assertQueryStringIs('limit=1')
+ [self.assertIsInstance(u, users.User) for u in user_list]
+
+ def test_list_marker(self):
+ self.stub_url('GET', ['users'], json=self.TEST_USERS)
+
+ user_list = self.client.users.list(marker='foo')
+ self.assertQueryStringIs('marker=foo')
+ [self.assertIsInstance(u, users.User) for u in user_list]
+
+ def test_list_limit_marker(self):
+ self.stub_url('GET', ['users'], json=self.TEST_USERS)
+
+ user_list = self.client.users.list(limit=1, marker='foo')
+
+ self.assertQueryStringIs('marker=foo&limit=1')
+ [self.assertIsInstance(u, users.User) for u in user_list]
+
+ def test_update(self):
+ req_1 = {
+ "user": {
+ "id": self.DEMO_USER_ID,
+ "email": "gabriel@example.com",
+ "name": "gabriel",
+ }
+ }
+ password = uuid.uuid4().hex
+ req_2 = {
+ "user": {
+ "id": self.DEMO_USER_ID,
+ "password": password,
+ }
+ }
+ tenant_id = uuid.uuid4().hex
+ req_3 = {
+ "user": {
+ "id": self.DEMO_USER_ID,
+ "tenantId": tenant_id,
+ }
+ }
+ req_4 = {
+ "user": {
+ "id": self.DEMO_USER_ID,
+ "enabled": False,
+ }
+ }
+
+ self.stub_url('PUT', ['users', self.DEMO_USER_ID], json=req_1)
+ self.stub_url('PUT',
+ ['users', self.DEMO_USER_ID, 'OS-KSADM', 'password'],
+ json=req_2)
+ self.stub_url('PUT',
+ ['users', self.DEMO_USER_ID, 'OS-KSADM', 'tenant'],
+ json=req_3)
+ self.stub_url('PUT',
+ ['users', self.DEMO_USER_ID, 'OS-KSADM', 'enabled'],
+ json=req_4)
+
+ self.client.users.update(self.DEMO_USER_ID,
+ name='gabriel',
+ email='gabriel@example.com')
+ self.assertRequestBodyIs(json=req_1)
+ self.client.users.update_password(self.DEMO_USER_ID, password)
+ self.assertRequestBodyIs(json=req_2)
+ self.client.users.update_tenant(self.DEMO_USER_ID, tenant_id)
+ self.assertRequestBodyIs(json=req_3)
+ self.client.users.update_enabled(self.DEMO_USER_ID, False)
+ self.assertRequestBodyIs(json=req_4)
+ self.assertNotIn(password, self.logger.output)
+
+ def test_update_own_password(self):
+ old_password = uuid.uuid4().hex
+ new_password = uuid.uuid4().hex
+ req_body = {
+ 'user': {
+ 'password': new_password,
+ 'original_password': old_password
+ }
+ }
+ resp_body = {
+ 'access': {}
+ }
+ user_id = uuid.uuid4().hex
+ self.stub_url('PATCH', ['OS-KSCRUD', 'users', user_id], json=resp_body)
+
+ self.client.user_id = user_id
+ self.client.users.update_own_password(old_password, new_password)
+ self.assertRequestBodyIs(json=req_body)
+ self.assertNotIn(old_password, self.logger.output)
+ self.assertNotIn(new_password, self.logger.output)
+
+ def test_user_role_listing(self):
+ user_id = uuid.uuid4().hex
+ role_id1 = uuid.uuid4().hex
+ role_id2 = uuid.uuid4().hex
+ tenant_id = uuid.uuid4().hex
+
+ user_resp = {
+ 'user': {
+ 'id': user_id,
+ 'email': uuid.uuid4().hex,
+ 'name': uuid.uuid4().hex,
+ }
+ }
+
+ roles_resp = {
+ 'roles': {
+ 'values': [
+ {
+ 'name': uuid.uuid4().hex,
+ 'id': role_id1,
+ },
+ {
+ 'name': uuid.uuid4().hex,
+ 'id': role_id2,
+ }
+ ]
+ }
+ }
+
+ self.stub_url('GET', ['users', user_id], json=user_resp)
+ self.stub_url('GET',
+ ['tenants', tenant_id, 'users', user_id, 'roles'],
+ json=roles_resp)
+
+ user = self.client.users.get(user_id)
+ role_objs = user.list_roles(tenant_id)
+
+ for r in role_objs:
+ self.assertIsInstance(r, roles.Role)
+
+ self.assertEqual(set([role_id1, role_id2]),
+ set([r.id for r in role_objs]))
diff --git a/keystoneclient/tests/unit/v2_0/utils.py b/keystoneclient/tests/unit/v2_0/utils.py
new file mode 100644
index 0000000..475181f
--- /dev/null
+++ b/keystoneclient/tests/unit/v2_0/utils.py
@@ -0,0 +1,88 @@
+# 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.tests.unit 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_ADMIN_IDENTITY_ENDPOINT = "http://127.0.0.1:35357/v2.0"
+
+ 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": TEST_ADMIN_IDENTITY_ENDPOINT,
+ "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('POST', ['tokens'], **kwargs)
diff --git a/keystoneclient/tests/unit/v3/__init__.py b/keystoneclient/tests/unit/v3/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/__init__.py
diff --git a/keystoneclient/tests/unit/v3/client_fixtures.py b/keystoneclient/tests/unit/v3/client_fixtures.py
new file mode 100644
index 0000000..517f9ae
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/client_fixtures.py
@@ -0,0 +1,182 @@
+# 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
+
+from keystoneclient import fixture
+
+
+def unscoped_token():
+ return fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a',
+ user_name='exampleuser',
+ user_domain_id='4e6893b7ba0b4006840c3845660b86ed',
+ user_domain_name='exampledomain',
+ expires='2010-11-01T03:32:15-05:00')
+
+
+def domain_scoped_token():
+ f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a',
+ user_name='exampleuser',
+ user_domain_id='4e6893b7ba0b4006840c3845660b86ed',
+ user_domain_name='exampledomain',
+ expires='2010-11-01T03:32:15-05:00',
+ domain_id='8e9283b7ba0b1038840c3842058b86ab',
+ domain_name='anotherdomain')
+
+ f.add_role(id='76e72a', name='admin')
+ f.add_role(id='f4f392', name='member')
+ region = 'RegionOne'
+
+ s = f.add_service('volume')
+ s.add_standard_endpoints(public='http://public.com:8776/v1/None',
+ internal='http://internal.com:8776/v1/None',
+ admin='http://admin.com:8776/v1/None',
+ region=region)
+
+ s = f.add_service('image')
+ s.add_standard_endpoints(public='http://public.com:9292/v1',
+ internal='http://internal:9292/v1',
+ admin='http://admin:9292/v1',
+ region=region)
+
+ s = f.add_service('compute')
+ s.add_standard_endpoints(public='http://public.com:8774/v1.1/None',
+ internal='http://internal:8774/v1.1/None',
+ admin='http://admin:8774/v1.1/None',
+ region=region)
+
+ s = f.add_service('ec2')
+ s.add_standard_endpoints(public='http://public.com:8773/services/Cloud',
+ internal='http://internal:8773/services/Cloud',
+ admin='http://admin:8773/services/Admin',
+ region=region)
+
+ s = f.add_service('identity')
+ s.add_standard_endpoints(public='http://public.com:5000/v3',
+ internal='http://internal:5000/v3',
+ admin='http://admin:35357/v3',
+ region=region)
+
+ return f
+
+
+def project_scoped_token():
+ f = fixture.V3Token(user_id='c4da488862bd435c9e6c0275a0d0e49a',
+ user_name='exampleuser',
+ user_domain_id='4e6893b7ba0b4006840c3845660b86ed',
+ user_domain_name='exampledomain',
+ expires='2010-11-01T03:32:15-05:00',
+ project_id='225da22d3ce34b15877ea70b2a575f58',
+ project_name='exampleproject',
+ project_domain_id='4e6893b7ba0b4006840c3845660b86ed',
+ project_domain_name='exampledomain')
+
+ f.add_role(id='76e72a', name='admin')
+ f.add_role(id='f4f392', name='member')
+
+ region = 'RegionOne'
+ tenant = '225da22d3ce34b15877ea70b2a575f58'
+
+ s = f.add_service('volume')
+ s.add_standard_endpoints(public='http://public.com:8776/v1/%s' % tenant,
+ internal='http://internal:8776/v1/%s' % tenant,
+ admin='http://admin:8776/v1/%s' % tenant,
+ region=region)
+
+ s = f.add_service('image')
+ s.add_standard_endpoints(public='http://public.com:9292/v1',
+ internal='http://internal:9292/v1',
+ admin='http://admin:9292/v1',
+ region=region)
+
+ s = f.add_service('compute')
+ s.add_standard_endpoints(public='http://public.com:8774/v2/%s' % tenant,
+ internal='http://internal:8774/v2/%s' % tenant,
+ admin='http://admin:8774/v2/%s' % tenant,
+ region=region)
+
+ s = f.add_service('ec2')
+ s.add_standard_endpoints(public='http://public.com:8773/services/Cloud',
+ internal='http://internal:8773/services/Cloud',
+ admin='http://admin:8773/services/Admin',
+ region=region)
+
+ s = f.add_service('identity')
+ s.add_standard_endpoints(public='http://public.com:5000/v3',
+ internal='http://internal:5000/v3',
+ admin='http://admin:35357/v3',
+ region=region)
+
+ return f
+
+
+AUTH_SUBJECT_TOKEN = '3e2813b7ba0b4006840c3825860b86ed'
+
+AUTH_RESPONSE_HEADERS = {
+ 'X-Subject-Token': AUTH_SUBJECT_TOKEN,
+}
+
+
+def auth_response_body():
+ f = fixture.V3Token(user_id='567',
+ user_name='test',
+ user_domain_id='1',
+ user_domain_name='aDomain',
+ expires='2010-11-01T03:32:15-05:00',
+ project_domain_id='123',
+ project_domain_name='aDomain',
+ project_id='345',
+ project_name='aTenant')
+
+ f.add_role(id='76e72a', name='admin')
+ f.add_role(id='f4f392', name='member')
+
+ s = f.add_service('compute', name='nova')
+ s.add_standard_endpoints(
+ public='https://compute.north.host/novapi/public',
+ internal='https://compute.north.host/novapi/internal',
+ admin='https://compute.north.host/novapi/admin',
+ region='North')
+
+ s = f.add_service('object-store', name='swift')
+ s.add_standard_endpoints(
+ public='http://swift.north.host/swiftapi/public',
+ internal='http://swift.north.host/swiftapi/internal',
+ admin='http://swift.north.host/swiftapi/admin',
+ region='South')
+
+ s = f.add_service('image', name='glance')
+ s.add_standard_endpoints(
+ public='http://glance.north.host/glanceapi/public',
+ internal='http://glance.north.host/glanceapi/internal',
+ admin='http://glance.north.host/glanceapi/admin',
+ region='North')
+
+ s.add_standard_endpoints(
+ public='http://glance.south.host/glanceapi/public',
+ internal='http://glance.south.host/glanceapi/internal',
+ admin='http://glance.south.host/glanceapi/admin',
+ region='South')
+
+ return f
+
+
+def trust_token():
+ return fixture.V3Token(user_id='0ca8f6',
+ user_name='exampleuser',
+ user_domain_id='4e6893b7ba0b4006840c3845660b86ed',
+ user_domain_name='exampledomain',
+ expires='2010-11-01T03:32:15-05:00',
+ trust_id='fe0aef',
+ trust_impersonation=False,
+ trustee_user_id='0ca8f6',
+ trustor_user_id='bd263c')
diff --git a/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml b/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml
new file mode 100644
index 0000000..487bcac
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/examples/xml/ADFS_RequestSecurityTokenResponse.xml
@@ -0,0 +1,132 @@
+<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
+ <s:Header>
+ <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal</a:Action>
+ <a:RelatesTo>urn:uuid:487c064b-b7c6-4654-b4d4-715f9961170e</a:RelatesTo>
+ <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
+ <u:Timestamp u:Id="_0">
+ <u:Created>2014-08-05T18:36:14.235Z</u:Created>
+ <u:Expires>2014-08-05T18:41:14.235Z</u:Expires>
+ </u:Timestamp>
+ </o:Security>
+ </s:Header>
+ <s:Body>
+ <trust:RequestSecurityTokenResponseCollection xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
+ <trust:RequestSecurityTokenResponse>
+ <trust:Lifetime>
+ <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-08-05T18:36:14.063Z</wsu:Created>
+ <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-08-05T19:36:14.063Z</wsu:Expires>
+ </trust:Lifetime>
+ <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
+ <wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
+ <wsa:Address>https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS</wsa:Address>
+ </wsa:EndpointReference>
+ </wsp:AppliesTo>
+ <trust:RequestedSecurityToken>
+ <saml:Assertion MajorVersion="1" MinorVersion="1" AssertionID="_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f" Issuer="https://cern.ch/login" IssueInstant="2014-08-05T18:36:14.235Z" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">
+ <saml:Conditions NotBefore="2014-08-05T18:36:14.063Z" NotOnOrAfter="2014-08-05T19:36:14.063Z">
+ <saml:AudienceRestrictionCondition>
+ <saml:Audience>https://ltartari2.cern.ch:5000/Shibboleth.sso/ADFS</saml:Audience>
+ </saml:AudienceRestrictionCondition>
+ </saml:Conditions>
+ <saml:AttributeStatement>
+ <saml:Subject>
+ <saml:NameIdentifier Format="http://schemas.xmlsoap.org/claims/UPN">marek.denis@cern.ch</saml:NameIdentifier>
+ <saml:SubjectConfirmation>
+ <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Attribute AttributeName="UPN" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>marek.denis@cern.ch</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="EmailAddress" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>marek.denis@cern.ch</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="CommonName" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>madenis</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="role" AttributeNamespace="http://schemas.microsoft.com/ws/2008/06/identity/claims">
+ <saml:AttributeValue>CERN Users</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="Group" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>Domain Users</saml:AttributeValue>
+ <saml:AttributeValue>occupants-bldg-31</saml:AttributeValue>
+ <saml:AttributeValue>CERN-Direct-Employees</saml:AttributeValue>
+ <saml:AttributeValue>ca-dev-allowed</saml:AttributeValue>
+ <saml:AttributeValue>cernts-cerntstest-users</saml:AttributeValue>
+ <saml:AttributeValue>staf-fell-pjas-at-cern</saml:AttributeValue>
+ <saml:AttributeValue>ELG-CERN</saml:AttributeValue>
+ <saml:AttributeValue>student-club-new-members</saml:AttributeValue>
+ <saml:AttributeValue>pawel-dynamic-test-82</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="DisplayName" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>Marek Kamil Denis</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="MobileNumber" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>+5555555</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="Building" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>31S-013</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="Firstname" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>Marek Kamil</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="Lastname" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>Denis</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="IdentityClass" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>CERN Registered</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="Federation" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>CERN</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute AttributeName="AuthLevel" AttributeNamespace="http://schemas.xmlsoap.org/claims">
+ <saml:AttributeValue>Normal</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ <saml:AuthenticationStatement AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password" AuthenticationInstant="2014-08-05T18:36:14.032Z">
+ <saml:Subject>
+ <saml:NameIdentifier Format="http://schemas.xmlsoap.org/claims/UPN">marek.denis@cern.ch</saml:NameIdentifier>
+ <saml:SubjectConfirmation>
+ <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ </saml:AuthenticationStatement>
+ <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <SignedInfo>
+ <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
+ <Reference URI="#_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f">
+ <Transforms>
+ <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ </Transforms>
+ <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+ <DigestValue>EaZ/2d0KAY5un9akV3++Npyk6hBc8JuTYs2S3lSxUeQ=</DigestValue>
+ </Reference>
+ </SignedInfo>
+ <SignatureValue>CxYiYvNsbedhHdmDbb9YQCBy6Ppus3bNJdw2g2HLq0VU2yRhv23mUW05I89Hs4yG4OcCo0uOZ3zaeNFbSNXMW+Mr996tAXtujKjgyrCXNJAToE+gwltvGxwY1EluSbe3IzoSM3Ao87mKhxGOSzlDhuN7dQ9Rv6l/J4gUjbOO5SIX4pdZ6mVF7cHEfe9x+H8Lg15YjnElQUEaPi+NSW5jYTdtIpsB4ORxJvALuSt6+4doDYc9wuwBiWkEdnBHAQBINoKpAV2oy0/C85SBX3IdRhxUznmL5yEUmf8JvPccXecMPqJow0L43mnCdu74xPwU0as3MNfYQ10kLvHXHfIExg==</SignatureValue>
+ <KeyInfo>
+ <X509Data>
+ <X509Certificate>MIIIEjCCBfqgAwIBAgIKLYgjvQAAAAAAMDANBgkqhkiG9w0BAQsFADBRMRIwEAYKCZImiZPyLGQBGRYCY2gxFDASBgoJkiaJk/IsZAEZFgRjZXJuMSUwIwYDVQQDExxDRVJOIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMTEwODA4Mzg1NVoXDTIzMDcyOTA5MTkzOFowVjESMBAGCgmSJomT8ixkARkWAmNoMRQwEgYKCZImiZPyLGQBGRYEY2VybjESMBAGA1UECxMJY29tcHV0ZXJzMRYwFAYDVQQDEw1sb2dpbi5jZXJuLmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp6t1C0SGlLddL2M+ltffGioTnDT3eztOxlA9bAGuvB8/Rjym8en6+ET9boM02CyoR5Vpn8iElXVWccAExPIQEq70D6LPe86vb+tYhuKPeLfuICN9Z0SMQ4f+57vk61Co1/uw/8kPvXlyd+Ai8Dsn/G0hpH67bBI9VOQKfpJqclcSJuSlUB5PJffvMUpr29B0eRx8LKFnIHbDILSu6nVbFLcadtWIjbYvoKorXg3J6urtkz+zEDeYMTvA6ZGOFf/Xy5eGtroSq9csSC976tx+umKEPhXBA9AcpiCV9Cj5axN03Aaa+iTE36jpnjcd9d02dy5Q9jE2nUN6KXnB6qF6eQIDAQABo4ID5TCCA+EwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIg73QCYLtjQ2G7Ysrgd71N4WA0GIehd2yb4Wu9TkCAWQCARkwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIFoDBoBgNVHSAEYTBfMF0GCisGAQQBYAoEAQEwTzBNBggrBgEFBQcCARZBaHR0cDovL2NhLWRvY3MuY2Vybi5jaC9jYS1kb2NzL2NwLWNwcy9jZXJuLXRydXN0ZWQtY2EyLWNwLWNwcy5wZGYwJwYJKwYBBAGCNxUKBBowGDAKBggrBgEFBQcDAjAKBggrBgEFBQcDATAdBgNVHQ4EFgQUqtJcwUXasyM6sRaO5nCMFoFDenMwGAYDVR0RBBEwD4INbG9naW4uY2Vybi5jaDAfBgNVHSMEGDAWgBQdkBnqyM7MPI0UsUzZ7BTiYUADYTCCASoGA1UdHwSCASEwggEdMIIBGaCCARWgggERhkdodHRwOi8vY2FmaWxlcy5jZXJuLmNoL2NhZmlsZXMvY3JsL0NFUk4lMjBDZXJ0aWZpY2F0aW9uJTIwQXV0aG9yaXR5LmNybIaBxWxkYXA6Ly8vQ049Q0VSTiUyMENlcnRpZmljYXRpb24lMjBBdXRob3JpdHksQ049Q0VSTlBLSTA3LENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWNlcm4sREM9Y2g/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIIBVAYIKwYBBQUHAQEEggFGMIIBQjBcBggrBgEFBQcwAoZQaHR0cDovL2NhZmlsZXMuY2Vybi5jaC9jYWZpbGVzL2NlcnRpZmljYXRlcy9DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eS5jcnQwgbsGCCsGAQUFBzAChoGubGRhcDovLy9DTj1DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1jZXJuLERDPWNoP2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jZXJuLmNoL29jc3AwDQYJKoZIhvcNAQELBQADggIBAGKZ3bknTCfNuh4TMaL3PuvBFjU8LQ5NKY9GLZvY2ibYMRk5Is6eWRgyUsy1UJRQdaQQPnnysqrGq8VRw/NIFotBBsA978/+jj7v4e5Kr4o8HvwAQNLBxNmF6XkDytpLL701FcNEGRqIsoIhNzihi2VBADLC9HxljEyPT52IR767TMk/+xTOqClceq3sq6WRD4m+xaWRUJyOhn+Pqr+wbhXIw4wzHC6X0hcLj8P9Povtm6VmKkN9JPuymMo/0+zSrUt2+TYfmbbEKYJSP0+sceQ76IKxxmSdKAr1qDNE8v+c3DvPM2PKmfivwaV2l44FdP8ulzqTgphkYcN1daa9Oc+qJeyu/eL7xWzk6Zq5R+jVrMlM0p1y2XczI7Hoc96TMOcbVnwgMcVqRM9p57VItn6XubYPR0C33i1yUZjkWbIfqEjq6Vev6lVgngOyzu+hqC/8SDyORA3dlF9aZOD13kPZdF/JRphHREQtaRydAiYRlE/WHTvOcY52jujDftUR6oY0eWaWkwSHbX+kDFx8IlR8UtQCUgkGHBGwnOYLIGu7SRDGSfOBOiVhxKoHWVk/pL6eKY2SkmyOmmgO4JnQGg95qeAOMG/EQZt/2x8GAavUqGvYy9dPFwFf08678hQqkjNSuex7UD0ku8OP1QKvpP44l6vZhFc6A5XqjdU9lus1</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </Signature>
+ </saml:Assertion>
+ </trust:RequestedSecurityToken>
+ <trust:RequestedAttachedReference>
+ <o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd">
+ <o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f</o:KeyIdentifier>
+ </o:SecurityTokenReference>
+ </trust:RequestedAttachedReference>
+ <trust:RequestedUnattachedReference>
+ <o:SecurityTokenReference k:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:k="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd">
+ <o:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID">_c9e77bc4-a81b-4da7-88c2-72a6ba376d3f</o:KeyIdentifier>
+ </o:SecurityTokenReference>
+ </trust:RequestedUnattachedReference>
+ <trust:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</trust:TokenType>
+ <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
+ <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
+ </trust:RequestSecurityTokenResponse>
+ </trust:RequestSecurityTokenResponseCollection>
+ </s:Body>
+</s:Envelope> \ No newline at end of file
diff --git a/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml b/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml
new file mode 100644
index 0000000..913252e
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/examples/xml/ADFS_fault.xml
@@ -0,0 +1,19 @@
+<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
+ <s:Header>
+ <a:Action s:mustUnderstand="1">http://www.w3.org/2005/08/addressing/soap/fault</a:Action>
+ <a:RelatesTo>urn:uuid:89c47849-2622-4cdc-bb06-1d46c89ed12d</a:RelatesTo>
+ </s:Header>
+ <s:Body>
+ <s:Fault>
+ <s:Code>
+ <s:Value>s:Sender</s:Value>
+ <s:Subcode>
+ <s:Value xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">a:FailedAuthentication</s:Value>
+ </s:Subcode>
+ </s:Code>
+ <s:Reason>
+ <s:Text xml:lang="en-US">At least one security token in the message could not be validated.</s:Text>
+ </s:Reason>
+ </s:Fault>
+ </s:Body>
+</s:Envelope> \ No newline at end of file
diff --git a/keystoneclient/tests/unit/v3/saml2_fixtures.py b/keystoneclient/tests/unit/v3/saml2_fixtures.py
new file mode 100644
index 0000000..2ecae6a
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/saml2_fixtures.py
@@ -0,0 +1,171 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import six
+
+SP_SOAP_RESPONSE = six.b("""<S:Envelope
+xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
+<S:Header>
+<paos:Request xmlns:paos="urn:liberty:paos:2003-08"
+S:actor="http://schemas.xmlsoap.org/soap/actor/next"
+S:mustUnderstand="1"
+responseConsumerURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP"
+service="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"/>
+<ecp:Request xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
+IsPassive="0" S:actor="http://schemas.xmlsoap.org/soap/actor/next"
+S:mustUnderstand="1">
+<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+https://openstack4.local/shibboleth
+</saml:Issuer>
+<samlp:IDPList xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
+<samlp:IDPEntry ProviderID="https://idp.testshib.org/idp/shibboleth"/>
+</samlp:IDPList></ecp:Request>
+<ecp:RelayState xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
+S:actor="http://schemas.xmlsoap.org/soap/actor/next" S:mustUnderstand="1">
+ss:mem:6f1f20fafbb38433467e9d477df67615</ecp:RelayState>
+</S:Header><S:Body><samlp:AuthnRequest
+xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+AssertionConsumerServiceURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP"
+ ID="_a07186e3992e70e92c17b9d249495643" IssueInstant="2014-06-09T09:48:57Z"
+ ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Version="2.0">
+ <saml:Issuer
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+ https://openstack4.local/shibboleth
+ </saml:Issuer><samlp:NameIDPolicy AllowCreate="1"/><samlp:Scoping>
+ <samlp:IDPList>
+ <samlp:IDPEntry ProviderID="https://idp.testshib.org/idp/shibboleth"/>
+ </samlp:IDPList></samlp:Scoping></samlp:AuthnRequest></S:Body></S:Envelope>
+""")
+
+
+SAML2_ASSERTION = six.b("""<?xml version="1.0" encoding="UTF-8"?>
+<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
+<soap11:Header>
+<ecp:Response xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
+AssertionConsumerServiceURL="https://openstack4.local/Shibboleth.sso/SAML2/ECP"
+ soap11:actor="http://schemas.xmlsoap.org/soap/actor/next"
+ soap11:mustUnderstand="1"/>
+ <samlec:GeneratedKey xmlns:samlec="urn:ietf:params:xml:ns:samlec"
+ soap11:actor="http://schemas.xmlsoap.org/soap/actor/next">
+ x=
+ </samlec:GeneratedKey>
+ </soap11:Header>
+ <soap11:Body>
+ <saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
+Destination="https://openstack4.local/Shibboleth.sso/SAML2/ECP"
+ID="_bbbe6298d7ee586c915d952013875440"
+InResponseTo="_a07186e3992e70e92c17b9d249495643"
+IssueInstant="2014-06-09T09:48:58.945Z" Version="2.0">
+<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
+Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
+https://idp.testshib.org/idp/shibboleth
+</saml2:Issuer><saml2p:Status>
+<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+</saml2p:Status>
+<saml2:EncryptedAssertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
+<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
+Id="_e5215ac77a6028a8da8caa8be89bad44"
+Type="http://www.w3.org/2001/04/xmlenc#Element">
+<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"
+xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"/>
+<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+<xenc:EncryptedKey Id="_204349856f6e73c9480afc949d1b4643"
+xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
+<xenc:EncryptionMethod
+Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"
+xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
+<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"
+xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>
+</xenc:EncryptionMethod><ds:KeyInfo><ds:X509Data><ds:X509Certificate>
+</ds:X509Certificate>
+</ds:X509Data></ds:KeyInfo>
+<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
+<xenc:CipherValue>VALUE==</xenc:CipherValue></xenc:CipherData>
+</xenc:EncryptedKey></ds:KeyInfo>
+<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
+<xenc:CipherValue>VALUE=</xenc:CipherValue></xenc:CipherData>
+</xenc:EncryptedData></saml2:EncryptedAssertion></saml2p:Response>
+</soap11:Body></soap11:Envelope>
+""")
+
+UNSCOPED_TOKEN_HEADER = 'UNSCOPED_TOKEN'
+
+UNSCOPED_TOKEN = {
+ "token": {
+ "issued_at": "2014-06-09T09:48:59.643406Z",
+ "extras": {},
+ "methods": ["saml2"],
+ "expires_at": "2014-06-09T10:48:59.643375Z",
+ "user": {
+ "OS-FEDERATION": {
+ "identity_provider": {
+ "id": "testshib"
+ },
+ "protocol": {
+ "id": "saml2"
+ },
+ "groups": [
+ {"id": "1764fa5cf69a49a4918131de5ce4af9a"}
+ ]
+ },
+ "id": "testhib%20user",
+ "name": "testhib user"
+ }
+ }
+}
+
+PROJECTS = {
+ "projects": [
+ {
+ "domain_id": "37ef61",
+ "enabled": 'true',
+ "id": "12d706",
+ "links": {
+ "self": "http://identity:35357/v3/projects/12d706"
+ },
+ "name": "a project name"
+ },
+ {
+ "domain_id": "37ef61",
+ "enabled": 'true',
+ "id": "9ca0eb",
+ "links": {
+ "self": "http://identity:35357/v3/projects/9ca0eb"
+ },
+ "name": "another project"
+ }
+ ],
+ "links": {
+ "self": "http://identity:35357/v3/OS-FEDERATION/projects",
+ "previous": 'null',
+ "next": 'null'
+ }
+}
+
+DOMAINS = {
+ "domains": [
+ {
+ "description": "desc of domain",
+ "enabled": 'true',
+ "id": "37ef61",
+ "links": {
+ "self": "http://identity:35357/v3/domains/37ef61"
+ },
+ "name": "my domain"
+ }
+ ],
+ "links": {
+ "self": "http://identity:35357/v3/OS-FEDERATION/domains",
+ "previous": 'null',
+ "next": 'null'
+ }
+}
diff --git a/keystoneclient/tests/unit/v3/test_access.py b/keystoneclient/tests/unit/v3/test_access.py
new file mode 100644
index 0000000..d3107af
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_access.py
@@ -0,0 +1,191 @@
+# 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 uuid
+
+from oslo_utils import timeutils
+
+from keystoneclient import access
+from keystoneclient import fixture
+from keystoneclient.tests.unit.v3 import client_fixtures
+from keystoneclient.tests.unit.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.assertNotIn('catalog', auth_ref)
+
+ self.assertEqual(auth_ref.auth_token,
+ '3e2813b7ba0b4006840c3825860b86ed')
+ self.assertEqual(auth_ref.username, 'exampleuser')
+ self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a')
+
+ self.assertEqual(auth_ref.role_ids, [])
+ self.assertEqual(auth_ref.role_names, [])
+
+ self.assertIsNone(auth_ref.project_name)
+ self.assertIsNone(auth_ref.project_id)
+
+ self.assertIsNone(auth_ref.auth_url)
+ self.assertIsNone(auth_ref.management_url)
+
+ self.assertFalse(auth_ref.domain_scoped)
+ self.assertFalse(auth_ref.project_scoped)
+
+ self.assertEqual(auth_ref.user_domain_id,
+ '4e6893b7ba0b4006840c3845660b86ed')
+ self.assertEqual(auth_ref.user_domain_name, 'exampledomain')
+
+ self.assertIsNone(auth_ref.project_domain_id)
+ self.assertIsNone(auth_ref.project_domain_name)
+
+ self.assertEqual(auth_ref.expires, timeutils.parse_isotime(
+ UNSCOPED_TOKEN['token']['expires_at']))
+ self.assertEqual(auth_ref.issued, timeutils.parse_isotime(
+ UNSCOPED_TOKEN['token']['issued_at']))
+
+ self.assertEqual(auth_ref.expires, UNSCOPED_TOKEN.expires)
+ self.assertEqual(auth_ref.issued, UNSCOPED_TOKEN.issued)
+
+ 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.assertTrue(auth_ref['catalog'])
+
+ self.assertEqual(auth_ref.auth_token,
+ '3e2813b7ba0b4006840c3825860b86ed')
+ self.assertEqual(auth_ref.username, 'exampleuser')
+ self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a')
+
+ self.assertEqual(auth_ref.role_ids, ['76e72a', 'f4f392'])
+ self.assertEqual(auth_ref.role_names, ['admin', 'member'])
+
+ self.assertEqual(auth_ref.domain_name, 'anotherdomain')
+ self.assertEqual(auth_ref.domain_id,
+ '8e9283b7ba0b1038840c3842058b86ab')
+
+ self.assertIsNone(auth_ref.project_name)
+ self.assertIsNone(auth_ref.project_id)
+
+ self.assertEqual(auth_ref.user_domain_id,
+ '4e6893b7ba0b4006840c3845660b86ed')
+ self.assertEqual(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.assertEqual(auth_ref.auth_token,
+ '3e2813b7ba0b4006840c3825860b86ed')
+ self.assertEqual(auth_ref.username, 'exampleuser')
+ self.assertEqual(auth_ref.user_id, 'c4da488862bd435c9e6c0275a0d0e49a')
+
+ self.assertEqual(auth_ref.role_ids, ['76e72a', 'f4f392'])
+ self.assertEqual(auth_ref.role_names, ['admin', 'member'])
+
+ self.assertIsNone(auth_ref.domain_name)
+ self.assertIsNone(auth_ref.domain_id)
+
+ self.assertEqual(auth_ref.project_name, 'exampleproject')
+ self.assertEqual(auth_ref.project_id,
+ '225da22d3ce34b15877ea70b2a575f58')
+
+ self.assertEqual(auth_ref.tenant_name, auth_ref.project_name)
+ self.assertEqual(auth_ref.tenant_id, auth_ref.project_id)
+
+ self.assertEqual(auth_ref.auth_url,
+ ('http://public.com:5000/v3',))
+ self.assertEqual(auth_ref.management_url,
+ ('http://admin:35357/v3',))
+
+ self.assertEqual(auth_ref.project_domain_id,
+ '4e6893b7ba0b4006840c3845660b86ed')
+ self.assertEqual(auth_ref.project_domain_name, 'exampledomain')
+
+ self.assertEqual(auth_ref.user_domain_id,
+ '4e6893b7ba0b4006840c3845660b86ed')
+ self.assertEqual(auth_ref.user_domain_name, 'exampledomain')
+
+ self.assertFalse(auth_ref.domain_scoped)
+ self.assertTrue(auth_ref.project_scoped)
+
+ def test_oauth_access(self):
+ consumer_id = uuid.uuid4().hex
+ access_token_id = uuid.uuid4().hex
+
+ token = fixture.V3Token()
+ token.set_project_scope()
+ token.set_oauth(access_token_id=access_token_id,
+ consumer_id=consumer_id)
+
+ auth_ref = access.AccessInfo.factory(body=token)
+
+ self.assertEqual(consumer_id, auth_ref.oauth_consumer_id)
+ self.assertEqual(access_token_id, auth_ref.oauth_access_token_id)
+
+ self.assertEqual(consumer_id, auth_ref['OS-OAUTH1']['consumer_id'])
+ self.assertEqual(access_token_id,
+ auth_ref['OS-OAUTH1']['access_token_id'])
+
+ def test_override_auth_token(self):
+ token = fixture.V3Token()
+ token.set_project_scope()
+
+ new_auth_token = uuid.uuid4().hex
+ auth_ref = access.AccessInfo.factory(body=token,
+ auth_token=new_auth_token)
+ self.assertEqual(new_auth_token, auth_ref.auth_token)
+
+ def test_federated_property_standard_token(self):
+ """Check if is_federated property returns expected value."""
+ token = fixture.V3Token()
+ token.set_project_scope()
+ auth_ref = access.AccessInfo.factory(body=token)
+ self.assertFalse(auth_ref.is_federated)
diff --git a/keystoneclient/tests/unit/v3/test_auth.py b/keystoneclient/tests/unit/v3/test_auth.py
new file mode 100644
index 0000000..506b026
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_auth.py
@@ -0,0 +1,352 @@
+# 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 oslo_serialization import jsonutils
+
+from keystoneclient import exceptions
+from keystoneclient.tests.unit.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
+ }
+
+ 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)
+
+ 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_code=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)
+
+ def test_auth_redirect(self):
+ headers = {'Location': self.TEST_ADMIN_URL + '/auth/tokens'}
+ self.stub_auth(status_code=305, text='Use proxy', headers=headers)
+
+ 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"])
+
+ 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"])
+
+ 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)
+
+ 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)
+
+ 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)
+
+ def test_auth_url_token_authentication(self):
+ fake_token = 'fake_token'
+ fake_url = '/fake-url'
+ fake_resp = {'result': True}
+
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url('GET', [fake_url], json=fake_resp,
+ base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT)
+
+ cl = client.Client(auth_url=self.TEST_URL,
+ token=fake_token)
+ body = jsonutils.loads(self.requests.last_request.body)
+ self.assertEqual(body['auth']['identity']['token']['id'], fake_token)
+
+ resp, body = cl.get(fake_url)
+ self.assertEqual(fake_resp, body)
+
+ token = self.requests.last_request.headers.get('X-Auth-Token')
+ self.assertEqual(self.TEST_TOKEN, token)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ def test_allow_override_of_auth_token(self):
+ fake_url = '/fake-url'
+ fake_token = 'fake_token'
+ fake_resp = {'result': True}
+
+ self.stub_auth(json=self.TEST_RESPONSE_DICT)
+ self.stub_url('GET', [fake_url], json=fake_resp,
+ base_url=self.TEST_ADMIN_IDENTITY_ENDPOINT)
+
+ cl = client.Client(username='exampleuser',
+ password='password',
+ tenant_name='exampleproject',
+ auth_url=self.TEST_URL)
+
+ self.assertEqual(cl.auth_token, self.TEST_TOKEN)
+
+ # the token returned from the authentication will be used
+ resp, body = cl.get(fake_url)
+ self.assertEqual(fake_resp, body)
+
+ token = self.requests.last_request.headers.get('X-Auth-Token')
+ self.assertEqual(self.TEST_TOKEN, token)
+
+ # then override that token and the new token shall be used
+ cl.auth_token = fake_token
+
+ resp, body = cl.get(fake_url)
+ self.assertEqual(fake_resp, body)
+
+ token = self.requests.last_request.headers.get('X-Auth-Token')
+ self.assertEqual(fake_token, token)
+
+ # if we clear that overridden token then we fall back to the original
+ del cl.auth_token
+
+ resp, body = cl.get(fake_url)
+ self.assertEqual(fake_resp, body)
+
+ token = self.requests.last_request.headers.get('X-Auth-Token')
+ self.assertEqual(self.TEST_TOKEN, token)
diff --git a/keystoneclient/tests/unit/v3/test_auth_saml2.py b/keystoneclient/tests/unit/v3/test_auth_saml2.py
new file mode 100644
index 0000000..c54cf24
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_auth_saml2.py
@@ -0,0 +1,623 @@
+# 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
+import uuid
+
+from lxml import etree
+from oslo_config import fixture as config
+import requests
+from six.moves import urllib
+
+from keystoneclient.auth import conf
+from keystoneclient.contrib.auth.v3 import saml2
+from keystoneclient import exceptions
+from keystoneclient import session
+from keystoneclient.tests.unit.v3 import client_fixtures
+from keystoneclient.tests.unit.v3 import saml2_fixtures
+from keystoneclient.tests.unit.v3 import utils
+
+ROOTDIR = os.path.dirname(os.path.abspath(__file__))
+XMLDIR = os.path.join(ROOTDIR, 'examples', 'xml/')
+
+
+def make_oneline(s):
+ return etree.tostring(etree.XML(s)).replace(b'\n', b'')
+
+
+def _load_xml(filename):
+ with open(XMLDIR + filename, 'rb') as f:
+ return make_oneline(f.read())
+
+
+class AuthenticateviaSAML2Tests(utils.TestCase):
+
+ GROUP = 'auth'
+
+ class _AuthenticatedResponse(object):
+ headers = {
+ 'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER
+ }
+
+ def json(self):
+ return saml2_fixtures.UNSCOPED_TOKEN
+
+ class _AuthenticatedResponseInvalidJson(_AuthenticatedResponse):
+
+ def json(self):
+ raise ValueError()
+
+ class _AuthentiatedResponseMissingTokenID(_AuthenticatedResponse):
+ headers = {}
+
+ def setUp(self):
+ super(AuthenticateviaSAML2Tests, self).setUp()
+
+ self.conf_fixture = self.useFixture(config.Config())
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+
+ self.session = session.Session()
+
+ self.ECP_SP_EMPTY_REQUEST_HEADERS = {
+ 'Accept': 'text/html; application/vnd.paos+xml',
+ 'PAOS': ('ver="urn:liberty:paos:2003-08";'
+ '"urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"')
+ }
+
+ self.ECP_SP_SAML2_REQUEST_HEADERS = {
+ 'Content-Type': 'application/vnd.paos+xml'
+ }
+
+ self.ECP_SAML2_NAMESPACES = {
+ 'ecp': 'urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp',
+ 'S': 'http://schemas.xmlsoap.org/soap/envelope/',
+ 'paos': 'urn:liberty:paos:2003-08'
+ }
+ self.ECP_RELAY_STATE = '//ecp:RelayState'
+ self.ECP_SERVICE_PROVIDER_CONSUMER_URL = ('/S:Envelope/S:Header/paos:'
+ 'Request/'
+ '@responseConsumerURL')
+ self.ECP_IDP_CONSUMER_URL = ('/S:Envelope/S:Header/ecp:Response/'
+ '@AssertionConsumerServiceURL')
+ self.IDENTITY_PROVIDER = 'testidp'
+ self.IDENTITY_PROVIDER_URL = 'http://local.url'
+ self.PROTOCOL = 'saml2'
+ self.FEDERATION_AUTH_URL = '%s/%s' % (
+ self.TEST_URL,
+ 'OS-FEDERATION/identity_providers/testidp/protocols/saml2/auth')
+ self.SHIB_CONSUMER_URL = ('https://openstack4.local/'
+ 'Shibboleth.sso/SAML2/ECP')
+
+ self.saml2plugin = saml2.Saml2UnscopedToken(
+ self.TEST_URL,
+ self.IDENTITY_PROVIDER, self.IDENTITY_PROVIDER_URL,
+ self.TEST_USER, self.TEST_TOKEN)
+
+ def test_conf_params(self):
+ section = uuid.uuid4().hex
+ identity_provider = uuid.uuid4().hex
+ identity_provider_url = uuid.uuid4().hex
+ username = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+ self.conf_fixture.config(auth_section=section, group=self.GROUP)
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+
+ self.conf_fixture.register_opts(saml2.Saml2UnscopedToken.get_options(),
+ group=section)
+ self.conf_fixture.config(auth_plugin='v3unscopedsaml',
+ identity_provider=identity_provider,
+ identity_provider_url=identity_provider_url,
+ username=username,
+ password=password,
+ group=section)
+
+ a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
+ self.assertEqual(identity_provider, a.identity_provider)
+ self.assertEqual(identity_provider_url, a.identity_provider_url)
+ self.assertEqual(username, a.username)
+ self.assertEqual(password, a.password)
+
+ def test_initial_sp_call(self):
+ """Test initial call, expect SOAP message."""
+ self.requests.get(
+ self.FEDERATION_AUTH_URL,
+ content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE))
+ a = self.saml2plugin._send_service_provider_request(self.session)
+
+ self.assertFalse(a)
+
+ fixture_soap_response = make_oneline(
+ saml2_fixtures.SP_SOAP_RESPONSE)
+
+ sp_soap_response = make_oneline(
+ etree.tostring(self.saml2plugin.saml2_authn_request))
+
+ error_msg = "Expected %s instead of %s" % (fixture_soap_response,
+ sp_soap_response)
+
+ self.assertEqual(fixture_soap_response, sp_soap_response, error_msg)
+
+ self.assertEqual(
+ self.saml2plugin.sp_response_consumer_url, self.SHIB_CONSUMER_URL,
+ "Expected consumer_url set to %s instead of %s" % (
+ self.SHIB_CONSUMER_URL,
+ str(self.saml2plugin.sp_response_consumer_url)))
+
+ def test_initial_sp_call_when_saml_authenticated(self):
+ self.requests.get(
+ self.FEDERATION_AUTH_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
+
+ a = self.saml2plugin._send_service_provider_request(self.session)
+ self.assertTrue(a)
+ self.assertEqual(
+ saml2_fixtures.UNSCOPED_TOKEN['token'],
+ self.saml2plugin.authenticated_response.json()['token'])
+ self.assertEqual(
+ saml2_fixtures.UNSCOPED_TOKEN_HEADER,
+ self.saml2plugin.authenticated_response.headers['X-Subject-Token'])
+
+ def test_get_unscoped_token_when_authenticated(self):
+ self.requests.get(
+ self.FEDERATION_AUTH_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER,
+ 'Content-Type': 'application/json'})
+
+ token, token_body = self.saml2plugin._get_unscoped_token(self.session)
+ self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_body)
+
+ self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER, token)
+
+ def test_initial_sp_call_invalid_response(self):
+ """Send initial SP HTTP request and receive wrong server response."""
+ self.requests.get(self.FEDERATION_AUTH_URL,
+ text='NON XML RESPONSE')
+
+ self.assertRaises(
+ exceptions.AuthorizationFailure,
+ self.saml2plugin._send_service_provider_request,
+ self.session)
+
+ def test_send_authn_req_to_idp(self):
+ self.requests.post(self.IDENTITY_PROVIDER_URL,
+ content=saml2_fixtures.SAML2_ASSERTION)
+
+ self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL
+ self.saml2plugin.saml2_authn_request = etree.XML(
+ saml2_fixtures.SP_SOAP_RESPONSE)
+ self.saml2plugin._send_idp_saml2_authn_request(self.session)
+
+ idp_response = make_oneline(etree.tostring(
+ self.saml2plugin.saml2_idp_authn_response))
+
+ saml2_assertion_oneline = make_oneline(
+ saml2_fixtures.SAML2_ASSERTION)
+ error = "Expected %s instead of %s" % (saml2_fixtures.SAML2_ASSERTION,
+ idp_response)
+ self.assertEqual(idp_response, saml2_assertion_oneline, error)
+
+ def test_fail_basicauth_idp_authentication(self):
+ self.requests.post(self.IDENTITY_PROVIDER_URL, status_code=401)
+
+ self.saml2plugin.sp_response_consumer_url = self.SHIB_CONSUMER_URL
+ self.saml2plugin.saml2_authn_request = etree.XML(
+ saml2_fixtures.SP_SOAP_RESPONSE)
+ self.assertRaises(
+ exceptions.Unauthorized,
+ self.saml2plugin._send_idp_saml2_authn_request,
+ self.session)
+
+ def test_mising_username_password_in_plugin(self):
+ self.assertRaises(TypeError,
+ saml2.Saml2UnscopedToken,
+ self.TEST_URL, self.IDENTITY_PROVIDER,
+ self.IDENTITY_PROVIDER_URL)
+
+ def test_send_authn_response_to_sp(self):
+ self.requests.post(
+ self.SHIB_CONSUMER_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
+
+ self.saml2plugin.relay_state = etree.XML(
+ saml2_fixtures.SP_SOAP_RESPONSE).xpath(
+ self.ECP_RELAY_STATE, namespaces=self.ECP_SAML2_NAMESPACES)[0]
+
+ self.saml2plugin.saml2_idp_authn_response = etree.XML(
+ saml2_fixtures.SAML2_ASSERTION)
+
+ self.saml2plugin.idp_response_consumer_url = self.SHIB_CONSUMER_URL
+ self.saml2plugin._send_service_provider_saml2_authn_response(
+ self.session)
+ token_json = self.saml2plugin.authenticated_response.json()['token']
+ token = self.saml2plugin.authenticated_response.headers[
+ 'X-Subject-Token']
+ self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'],
+ token_json)
+
+ self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER,
+ token)
+
+ def test_consumer_url_mismatch_success(self):
+ self.saml2plugin._check_consumer_urls(
+ self.session, self.SHIB_CONSUMER_URL,
+ self.SHIB_CONSUMER_URL)
+
+ def test_consumer_url_mismatch(self):
+ self.requests.post(self.SHIB_CONSUMER_URL)
+ invalid_consumer_url = uuid.uuid4().hex
+ self.assertRaises(
+ exceptions.ValidationError,
+ self.saml2plugin._check_consumer_urls,
+ self.session, self.SHIB_CONSUMER_URL,
+ invalid_consumer_url)
+
+ def test_custom_302_redirection(self):
+ self.requests.post(
+ self.SHIB_CONSUMER_URL,
+ text='BODY',
+ headers={'location': self.FEDERATION_AUTH_URL},
+ status_code=302)
+
+ self.requests.get(
+ self.FEDERATION_AUTH_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER})
+
+ self.session.redirect = False
+ response = self.session.post(
+ self.SHIB_CONSUMER_URL, data='CLIENT BODY')
+ self.assertEqual(302, response.status_code)
+ self.assertEqual(self.FEDERATION_AUTH_URL,
+ response.headers['location'])
+
+ response = self.saml2plugin._handle_http_302_ecp_redirect(
+ self.session, response, 'GET')
+
+ self.assertEqual(self.FEDERATION_AUTH_URL, response.request.url)
+ self.assertEqual('GET', response.request.method)
+
+ def test_end_to_end_workflow(self):
+ self.requests.get(
+ self.FEDERATION_AUTH_URL,
+ content=make_oneline(saml2_fixtures.SP_SOAP_RESPONSE))
+
+ self.requests.post(self.IDENTITY_PROVIDER_URL,
+ content=saml2_fixtures.SAML2_ASSERTION)
+
+ self.requests.post(
+ self.SHIB_CONSUMER_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers={'X-Subject-Token': saml2_fixtures.UNSCOPED_TOKEN_HEADER,
+ 'Content-Type': 'application/json'})
+
+ self.session.redirect = False
+ response = self.saml2plugin.get_auth_ref(self.session)
+ self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN_HEADER,
+ response.auth_token)
+
+
+class ScopeFederationTokenTests(AuthenticateviaSAML2Tests):
+
+ TEST_TOKEN = client_fixtures.AUTH_SUBJECT_TOKEN
+
+ def setUp(self):
+ super(ScopeFederationTokenTests, self).setUp()
+
+ self.PROJECT_SCOPED_TOKEN_JSON = client_fixtures.project_scoped_token()
+ self.PROJECT_SCOPED_TOKEN_JSON['methods'] = ['saml2']
+
+ # for better readability
+ self.TEST_TENANT_ID = self.PROJECT_SCOPED_TOKEN_JSON.project_id
+ self.TEST_TENANT_NAME = self.PROJECT_SCOPED_TOKEN_JSON.project_name
+
+ self.DOMAIN_SCOPED_TOKEN_JSON = client_fixtures.domain_scoped_token()
+ self.DOMAIN_SCOPED_TOKEN_JSON['methods'] = ['saml2']
+
+ # for better readability
+ self.TEST_DOMAIN_ID = self.DOMAIN_SCOPED_TOKEN_JSON.domain_id
+ self.TEST_DOMAIN_NAME = self.DOMAIN_SCOPED_TOKEN_JSON.domain_name
+
+ self.saml2_scope_plugin = saml2.Saml2ScopedToken(
+ self.TEST_URL, saml2_fixtures.UNSCOPED_TOKEN_HEADER,
+ project_id=self.TEST_TENANT_ID)
+
+ def test_scope_saml2_token_to_project(self):
+ self.stub_auth(json=self.PROJECT_SCOPED_TOKEN_JSON)
+
+ token = self.saml2_scope_plugin.get_auth_ref(self.session)
+ self.assertTrue(token.project_scoped, "Received token is not scoped")
+ self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token)
+ self.assertEqual(self.TEST_TENANT_ID, token.project_id)
+ self.assertEqual(self.TEST_TENANT_NAME, token.project_name)
+
+ def test_scope_saml2_token_to_invalid_project(self):
+ self.stub_auth(status_code=401)
+ self.saml2_scope_plugin.project_id = uuid.uuid4().hex
+ self.saml2_scope_plugin.project_name = None
+ self.assertRaises(exceptions.Unauthorized,
+ self.saml2_scope_plugin.get_auth_ref,
+ self.session)
+
+ def test_scope_saml2_token_to_invalid_domain(self):
+ self.stub_auth(status_code=401)
+ self.saml2_scope_plugin.project_id = None
+ self.saml2_scope_plugin.project_name = None
+ self.saml2_scope_plugin.domain_id = uuid.uuid4().hex
+ self.saml2_scope_plugin.domain_name = None
+ self.assertRaises(exceptions.Unauthorized,
+ self.saml2_scope_plugin.get_auth_ref,
+ self.session)
+
+ def test_scope_saml2_token_to_domain(self):
+ self.stub_auth(json=self.DOMAIN_SCOPED_TOKEN_JSON)
+ token = self.saml2_scope_plugin.get_auth_ref(self.session)
+ self.assertTrue(token.domain_scoped, "Received token is not scoped")
+ self.assertEqual(client_fixtures.AUTH_SUBJECT_TOKEN, token.auth_token)
+ self.assertEqual(self.TEST_DOMAIN_ID, token.domain_id)
+ self.assertEqual(self.TEST_DOMAIN_NAME, token.domain_name)
+
+ def test_dont_set_project_nor_domain(self):
+ self.saml2_scope_plugin.project_id = None
+ self.saml2_scope_plugin.domain_id = None
+ self.assertRaises(exceptions.ValidationError,
+ saml2.Saml2ScopedToken,
+ self.TEST_URL, client_fixtures.AUTH_SUBJECT_TOKEN)
+
+
+class AuthenticateviaADFSTests(utils.TestCase):
+
+ GROUP = 'auth'
+
+ NAMESPACES = {
+ 's': 'http://www.w3.org/2003/05/soap-envelope',
+ 'trust': 'http://docs.oasis-open.org/ws-sx/ws-trust/200512',
+ 'wsa': 'http://www.w3.org/2005/08/addressing',
+ 'wsp': 'http://schemas.xmlsoap.org/ws/2004/09/policy',
+ 'a': 'http://www.w3.org/2005/08/addressing',
+ 'o': ('http://docs.oasis-open.org/wss/2004/01/oasis'
+ '-200401-wss-wssecurity-secext-1.0.xsd')
+ }
+
+ USER_XPATH = ('/s:Envelope/s:Header'
+ '/o:Security'
+ '/o:UsernameToken'
+ '/o:Username')
+ PASSWORD_XPATH = ('/s:Envelope/s:Header'
+ '/o:Security'
+ '/o:UsernameToken'
+ '/o:Password')
+ ADDRESS_XPATH = ('/s:Envelope/s:Body'
+ '/trust:RequestSecurityToken'
+ '/wsp:AppliesTo/wsa:EndpointReference'
+ '/wsa:Address')
+ TO_XPATH = ('/s:Envelope/s:Header'
+ '/a:To')
+
+ @property
+ def _uuid4(self):
+ return '4b911420-4982-4009-8afc-5c596cd487f5'
+
+ def setUp(self):
+ super(AuthenticateviaADFSTests, self).setUp()
+
+ self.conf_fixture = self.useFixture(config.Config())
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+ self.session = session.Session(session=requests.Session())
+
+ self.IDENTITY_PROVIDER = 'adfs'
+ self.IDENTITY_PROVIDER_URL = ('http://adfs.local/adfs/service/trust/13'
+ '/usernamemixed')
+ self.FEDERATION_AUTH_URL = '%s/%s' % (
+ self.TEST_URL,
+ 'OS-FEDERATION/identity_providers/adfs/protocols/saml2/auth')
+ self.SP_ENDPOINT = 'https://openstack4.local/Shibboleth.sso/ADFS'
+
+ self.adfsplugin = saml2.ADFSUnscopedToken(
+ self.TEST_URL, self.IDENTITY_PROVIDER,
+ self.IDENTITY_PROVIDER_URL, self.SP_ENDPOINT,
+ self.TEST_USER, self.TEST_TOKEN)
+
+ self.ADFS_SECURITY_TOKEN_RESPONSE = _load_xml(
+ 'ADFS_RequestSecurityTokenResponse.xml')
+ self.ADFS_FAULT = _load_xml('ADFS_fault.xml')
+
+ def test_conf_params(self):
+ section = uuid.uuid4().hex
+ identity_provider = uuid.uuid4().hex
+ identity_provider_url = uuid.uuid4().hex
+ sp_endpoint = uuid.uuid4().hex
+ username = uuid.uuid4().hex
+ password = uuid.uuid4().hex
+ self.conf_fixture.config(auth_section=section, group=self.GROUP)
+ conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP)
+
+ self.conf_fixture.register_opts(saml2.ADFSUnscopedToken.get_options(),
+ group=section)
+ self.conf_fixture.config(auth_plugin='v3unscopedadfs',
+ identity_provider=identity_provider,
+ identity_provider_url=identity_provider_url,
+ service_provider_endpoint=sp_endpoint,
+ username=username,
+ password=password,
+ group=section)
+
+ a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP)
+ self.assertEqual(identity_provider, a.identity_provider)
+ self.assertEqual(identity_provider_url, a.identity_provider_url)
+ self.assertEqual(sp_endpoint, a.service_provider_endpoint)
+ self.assertEqual(username, a.username)
+ self.assertEqual(password, a.password)
+
+ def test_get_adfs_security_token(self):
+ """Test ADFSUnscopedToken._get_adfs_security_token()."""
+
+ self.requests.post(
+ self.IDENTITY_PROVIDER_URL,
+ content=make_oneline(self.ADFS_SECURITY_TOKEN_RESPONSE),
+ status_code=200)
+
+ self.adfsplugin._prepare_adfs_request()
+ self.adfsplugin._get_adfs_security_token(self.session)
+
+ adfs_response = etree.tostring(self.adfsplugin.adfs_token)
+ fixture_response = self.ADFS_SECURITY_TOKEN_RESPONSE
+
+ self.assertEqual(fixture_response, adfs_response)
+
+ def test_adfs_request_user(self):
+ self.adfsplugin._prepare_adfs_request()
+ user = self.adfsplugin.prepared_request.xpath(
+ self.USER_XPATH, namespaces=self.NAMESPACES)[0]
+ self.assertEqual(self.TEST_USER, user.text)
+
+ def test_adfs_request_password(self):
+ self.adfsplugin._prepare_adfs_request()
+ password = self.adfsplugin.prepared_request.xpath(
+ self.PASSWORD_XPATH, namespaces=self.NAMESPACES)[0]
+ self.assertEqual(self.TEST_TOKEN, password.text)
+
+ def test_adfs_request_to(self):
+ self.adfsplugin._prepare_adfs_request()
+ to = self.adfsplugin.prepared_request.xpath(
+ self.TO_XPATH, namespaces=self.NAMESPACES)[0]
+ self.assertEqual(self.IDENTITY_PROVIDER_URL, to.text)
+
+ def test_prepare_adfs_request_address(self):
+ self.adfsplugin._prepare_adfs_request()
+ address = self.adfsplugin.prepared_request.xpath(
+ self.ADDRESS_XPATH, namespaces=self.NAMESPACES)[0]
+ self.assertEqual(self.SP_ENDPOINT, address.text)
+
+ def test_prepare_sp_request(self):
+ assertion = etree.XML(self.ADFS_SECURITY_TOKEN_RESPONSE)
+ assertion = assertion.xpath(
+ saml2.ADFSUnscopedToken.ADFS_ASSERTION_XPATH,
+ namespaces=saml2.ADFSUnscopedToken.ADFS_TOKEN_NAMESPACES)
+ assertion = assertion[0]
+ assertion = etree.tostring(assertion)
+
+ assertion = assertion.replace(
+ b'http://docs.oasis-open.org/ws-sx/ws-trust/200512',
+ b'http://schemas.xmlsoap.org/ws/2005/02/trust')
+ assertion = urllib.parse.quote(assertion)
+ assertion = 'wa=wsignin1.0&wresult=' + assertion
+
+ self.adfsplugin.adfs_token = etree.XML(
+ self.ADFS_SECURITY_TOKEN_RESPONSE)
+ self.adfsplugin._prepare_sp_request()
+
+ self.assertEqual(assertion, self.adfsplugin.encoded_assertion)
+
+ def test_get_adfs_security_token_authn_fail(self):
+ """Test proper parsing XML fault after bad authentication.
+
+ An exceptions.AuthorizationFailure should be raised including
+ error message from the XML message indicating where was the problem.
+ """
+ self.requests.post(self.IDENTITY_PROVIDER_URL,
+ content=make_oneline(self.ADFS_FAULT),
+ status_code=500)
+
+ self.adfsplugin._prepare_adfs_request()
+ self.assertRaises(exceptions.AuthorizationFailure,
+ self.adfsplugin._get_adfs_security_token,
+ self.session)
+ # TODO(marek-denis): Python3 tests complain about missing 'message'
+ # attributes
+ # self.assertEqual('a:FailedAuthentication', e.message)
+
+ def test_get_adfs_security_token_bad_response(self):
+ """Test proper handling HTTP 500 and mangled (non XML) response.
+
+ This should never happen yet, keystoneclient should be prepared
+ and correctly raise exceptions.InternalServerError once it cannot
+ parse XML fault message
+ """
+ self.requests.post(self.IDENTITY_PROVIDER_URL,
+ content=b'NOT XML',
+ status_code=500)
+ self.adfsplugin._prepare_adfs_request()
+ self.assertRaises(exceptions.InternalServerError,
+ self.adfsplugin._get_adfs_security_token,
+ self.session)
+
+ # TODO(marek-denis): Need to figure out how to properly send cookies
+ # from the request_uri() method.
+ def _send_assertion_to_service_provider(self):
+ """Test whether SP issues a cookie."""
+ cookie = uuid.uuid4().hex
+
+ self.requests.post(self.SP_ENDPOINT,
+ headers={"set-cookie": cookie},
+ status_code=302)
+
+ self.adfsplugin.adfs_token = self._build_adfs_request()
+ self.adfsplugin._prepare_sp_request()
+ self.adfsplugin._send_assertion_to_service_provider(self.session)
+
+ self.assertEqual(1, len(self.session.session.cookies))
+
+ def test_send_assertion_to_service_provider_bad_status(self):
+ self.requests.post(self.SP_ENDPOINT, status_code=500)
+
+ self.adfsplugin.adfs_token = etree.XML(
+ self.ADFS_SECURITY_TOKEN_RESPONSE)
+ self.adfsplugin._prepare_sp_request()
+
+ self.assertRaises(
+ exceptions.InternalServerError,
+ self.adfsplugin._send_assertion_to_service_provider,
+ self.session)
+
+ def test_access_sp_no_cookies_fail(self):
+ # clean cookie jar
+ self.session.session.cookies = []
+
+ self.assertRaises(exceptions.AuthorizationFailure,
+ self.adfsplugin._access_service_provider,
+ self.session)
+
+ def test_check_valid_token_when_authenticated(self):
+ self.requests.get(self.FEDERATION_AUTH_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers=client_fixtures.AUTH_RESPONSE_HEADERS)
+
+ self.session.session.cookies = [object()]
+ self.adfsplugin._access_service_provider(self.session)
+ response = self.adfsplugin.authenticated_response
+
+ self.assertEqual(client_fixtures.AUTH_RESPONSE_HEADERS,
+ response.headers)
+
+ self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'],
+ response.json()['token'])
+
+ def test_end_to_end_workflow(self):
+ self.requests.post(self.IDENTITY_PROVIDER_URL,
+ content=self.ADFS_SECURITY_TOKEN_RESPONSE,
+ status_code=200)
+ self.requests.post(self.SP_ENDPOINT,
+ headers={"set-cookie": 'x'},
+ status_code=302)
+ self.requests.get(self.FEDERATION_AUTH_URL,
+ json=saml2_fixtures.UNSCOPED_TOKEN,
+ headers=client_fixtures.AUTH_RESPONSE_HEADERS)
+
+ # NOTE(marek-denis): We need to mimic this until self.requests can
+ # issue cookies properly.
+ self.session.session.cookies = [object()]
+ token, token_json = self.adfsplugin._get_unscoped_token(self.session)
+ self.assertEqual(token, client_fixtures.AUTH_SUBJECT_TOKEN)
+ self.assertEqual(saml2_fixtures.UNSCOPED_TOKEN['token'], token_json)
diff --git a/keystoneclient/tests/unit/v3/test_client.py b/keystoneclient/tests/unit/v3/test_client.py
new file mode 100644
index 0000000..6be09c1
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_client.py
@@ -0,0 +1,230 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import json
+import uuid
+
+import six
+
+from keystoneclient.auth import token_endpoint
+from keystoneclient import exceptions
+from keystoneclient import session
+from keystoneclient.tests.unit.v3 import client_fixtures
+from keystoneclient.tests.unit.v3 import utils
+from keystoneclient.v3 import client
+
+
+class KeystoneClientTest(utils.TestCase):
+
+ 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.assertEqual(c.auth_user_id,
+ 'c4da488862bd435c9e6c0275a0d0e49a')
+ self.assertFalse(c.has_service_catalog())
+
+ self.assertEqual('c4da488862bd435c9e6c0275a0d0e49a',
+ c.get_user_id(session=None))
+ self.assertIsNone(c.get_project_id(session=None))
+
+ 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.assertEqual(c.auth_user_id,
+ 'c4da488862bd435c9e6c0275a0d0e49a')
+ self.assertEqual(c.auth_domain_id,
+ '8e9283b7ba0b1038840c3842058b86ab')
+
+ 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.assertEqual(c.auth_user_id,
+ 'c4da488862bd435c9e6c0275a0d0e49a')
+ self.assertEqual(c.auth_tenant_id,
+ '225da22d3ce34b15877ea70b2a575f58')
+ self.assertEqual('c4da488862bd435c9e6c0275a0d0e49a',
+ c.get_user_id(session=None))
+ self.assertEqual('225da22d3ce34b15877ea70b2a575f58',
+ c.get_project_id(session=None))
+
+ 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.assertEqual(new_client.username, 'exampleuser')
+ self.assertIsNone(new_client.password)
+ self.assertEqual(new_client.management_url,
+ 'http://admin:35357/v3')
+
+ 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.assertEqual(new_client.auth_url, new_auth_url)
+ self.assertEqual(new_client.username, 'exampleuser')
+ self.assertIsNone(new_client.password)
+ self.assertEqual(new_client.management_url,
+ 'http://admin:35357/v3')
+
+ 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.assertEqual(c.auth_ref.trustee_user_id, '0ca8f6')
+ self.assertEqual(c.auth_ref.trustor_user_id, 'bd263c')
+ self.assertTrue(c.auth_ref.trust_scoped)
+ self.assertEqual(c.auth_user_id, '0ca8f6')
+
+ def test_init_err_no_auth_url(self):
+ self.assertRaises(exceptions.AuthorizationFailure,
+ client.Client,
+ username='exampleuser',
+ password='password')
+
+ def _management_url_is_updated(self, fixture, **kwargs):
+ second = copy.deepcopy(fixture)
+ first_url = 'http://admin:35357/v3'
+ second_url = "http://secondurl:%d/v3'"
+
+ for entry in second['token']['catalog']:
+ if entry['type'] == 'identity':
+ entry['endpoints'] = [{
+ 'url': second_url % 5000,
+ 'region': 'RegionOne',
+ 'interface': 'public'
+ }, {
+ 'url': second_url % 5000,
+ 'region': 'RegionOne',
+ 'interface': 'internal'
+ }, {
+ 'url': second_url % 35357,
+ 'region': 'RegionOne',
+ 'interface': 'admin'
+ }]
+
+ self.stub_auth(response_list=[{'json': fixture}, {'json': second}])
+
+ cl = client.Client(username='exampleuser',
+ password='password',
+ auth_url=self.TEST_URL,
+ **kwargs)
+ self.assertEqual(cl.management_url, first_url)
+
+ cl.authenticate()
+ self.assertEqual(cl.management_url, second_url % 35357)
+
+ def test_management_url_is_updated_with_project(self):
+ self._management_url_is_updated(client_fixtures.project_scoped_token(),
+ project_name='exampleproject')
+
+ def test_management_url_is_updated_with_domain(self):
+ self._management_url_is_updated(client_fixtures.domain_scoped_token(),
+ domain_name='exampledomain')
+
+ def test_client_with_region_name_passes_to_service_catalog(self):
+ # NOTE(jamielennox): this is deprecated behaviour that should be
+ # removed ASAP, however must remain compatible.
+
+ self.stub_auth(json=client_fixtures.auth_response_body())
+
+ cl = client.Client(username='exampleuser',
+ password='password',
+ tenant_name='exampleproject',
+ auth_url=self.TEST_URL,
+ region_name='North')
+ self.assertEqual(cl.service_catalog.url_for(service_type='image'),
+ 'http://glance.north.host/glanceapi/public')
+
+ cl = client.Client(username='exampleuser',
+ password='password',
+ tenant_name='exampleproject',
+ auth_url=self.TEST_URL,
+ region_name='South')
+ self.assertEqual(cl.service_catalog.url_for(service_type='image'),
+ 'http://glance.south.host/glanceapi/public')
+
+ def test_client_without_auth_params(self):
+ self.assertRaises(exceptions.AuthorizationFailure,
+ client.Client,
+ project_name='exampleproject',
+ auth_url=self.TEST_URL)
+
+ def test_client_params(self):
+ opts = {'auth': token_endpoint.Token('a', 'b'),
+ 'connect_retries': 50,
+ 'endpoint_override': uuid.uuid4().hex,
+ 'interface': uuid.uuid4().hex,
+ 'region_name': uuid.uuid4().hex,
+ 'service_name': uuid.uuid4().hex,
+ 'user_agent': uuid.uuid4().hex,
+ }
+
+ sess = session.Session()
+ cl = client.Client(session=sess, **opts)
+
+ for k, v in six.iteritems(opts):
+ self.assertEqual(v, getattr(cl._adapter, k))
+
+ self.assertEqual('identity', cl._adapter.service_type)
+ self.assertEqual('v3', cl._adapter.version)
diff --git a/keystoneclient/tests/unit/v3/test_credentials.py b/keystoneclient/tests/unit/v3/test_credentials.py
new file mode 100644
index 0000000..752f25a
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_credentials.py
@@ -0,0 +1,54 @@
+# 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.unit.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('blob', 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
+
+ @staticmethod
+ def _ref_data_not_blob(ref):
+ ret_ref = ref.copy()
+ ret_ref['data'] = ref['blob']
+ del ret_ref['blob']
+ return ret_ref
+
+ def test_create_data_not_blob(self):
+ # Test create operation with previous, deprecated "data" argument,
+ # which should be translated into "blob" at the API call level
+ req_ref = self.new_ref()
+ api_ref = self._ref_data_not_blob(req_ref)
+ req_ref.pop('id')
+ self.test_create(api_ref, req_ref)
+
+ def test_update_data_not_blob(self):
+ # Likewise test update operation with data instead of blob argument
+ req_ref = self.new_ref()
+ api_ref = self._ref_data_not_blob(req_ref)
+ self.test_update(api_ref, req_ref)
diff --git a/keystoneclient/tests/unit/v3/test_discover.py b/keystoneclient/tests/unit/v3/test_discover.py
new file mode 100644
index 0000000..f73c3d7
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_discover.py
@@ -0,0 +1,80 @@
+# 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.generic import client
+from keystoneclient.tests.unit.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',
+ }
+
+ def test_get_version_local(self):
+ self.requests.get("http://localhost:35357/",
+ status_code=300,
+ json=self.TEST_RESPONSE_DICT)
+
+ cs = client.Client()
+ versions = cs.discover()
+ self.assertIsInstance(versions, dict)
+ self.assertIn('message', versions)
+ self.assertIn('v3.0', versions)
+ self.assertEqual(
+ versions['v3.0']['url'],
+ self.TEST_RESPONSE_DICT['versions']['values'][0]['links'][0]
+ ['href'])
+ self.assertEqual(
+ versions['v2.0']['url'],
+ self.TEST_RESPONSE_DICT['versions']['values'][1]['links'][0]
+ ['href'])
diff --git a/keystoneclient/tests/unit/v3/test_domains.py b/keystoneclient/tests/unit/v3/test_domains.py
new file mode 100644
index 0000000..9cc23e7
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_domains.py
@@ -0,0 +1,47 @@
+# 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.unit.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
+
+ def test_list_filter_name(self):
+ super(DomainTests, self).test_list(name='adomain123')
+
+ def test_list_filter_enabled(self):
+ super(DomainTests, self).test_list(enabled=True)
+
+ def test_list_filter_disabled(self):
+ # False is converted to '0' ref bug #1267530
+ expected_query = {'enabled': '0'}
+ super(DomainTests, self).test_list(expected_query=expected_query,
+ enabled=False)
+
+ def test_update_enabled_defaults_to_none(self):
+ super(DomainTests, self).test_update(
+ req_ref={'name': uuid.uuid4().hex})
diff --git a/keystoneclient/tests/unit/v3/test_endpoint_filter.py b/keystoneclient/tests/unit/v3/test_endpoint_filter.py
new file mode 100644
index 0000000..eaca264
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_endpoint_filter.py
@@ -0,0 +1,149 @@
+# Copyright 2014 OpenStack Foundation
+#
+# 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.unit.v3 import utils
+
+
+class EndpointTestUtils(object):
+ """Mixin class with shared methods between Endpoint Filter & Policy."""
+
+ def new_ref(self, **kwargs):
+ # copied from CrudTests as we need to create endpoint and project
+ # refs for our tests. EndpointFilter is not exactly CRUD API.
+ kwargs.setdefault('id', uuid.uuid4().hex)
+ kwargs.setdefault('enabled', True)
+ return kwargs
+
+ def new_endpoint_ref(self, **kwargs):
+ # copied from EndpointTests as we need endpoint refs for our tests
+ kwargs = 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)
+ return kwargs
+
+
+class EndpointFilterTests(utils.TestCase, EndpointTestUtils):
+ """Test project-endpoint associations (a.k.a. EndpointFilter Extension).
+
+ Endpoint filter provides associations between service endpoints and
+ projects. These assciations are then used to create ad-hoc catalogs for
+ each project-scoped token request.
+
+ """
+
+ def setUp(self):
+ super(EndpointFilterTests, self).setUp()
+ self.manager = self.client.endpoint_filter
+
+ def new_project_ref(self, **kwargs):
+ # copied from ProjectTests as we need project refs for our tests
+ kwargs = self.new_ref(**kwargs)
+ kwargs.setdefault('domain_id', uuid.uuid4().hex)
+ kwargs.setdefault('name', uuid.uuid4().hex)
+ return kwargs
+
+ def test_add_endpoint_to_project_via_id(self):
+ endpoint_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+
+ self.stub_url('PUT',
+ [self.manager.OS_EP_FILTER_EXT, 'projects', project_id,
+ 'endpoints', endpoint_id],
+ status_code=201)
+
+ self.manager.add_endpoint_to_project(project=project_id,
+ endpoint=endpoint_id)
+
+ def test_add_endpoint_to_project_via_obj(self):
+ project_ref = self.new_project_ref()
+ endpoint_ref = self.new_endpoint_ref()
+ project = self.client.projects.resource_class(self.client.projects,
+ project_ref,
+ loaded=True)
+ endpoint = self.client.endpoints.resource_class(self.client.endpoints,
+ endpoint_ref,
+ loaded=True)
+
+ self.stub_url('PUT',
+ [self.manager.OS_EP_FILTER_EXT,
+ 'projects', project_ref['id'],
+ 'endpoints', endpoint_ref['id']],
+ status_code=201)
+
+ self.manager.add_endpoint_to_project(project=project,
+ endpoint=endpoint)
+
+ def test_delete_endpoint_from_project(self):
+ endpoint_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+
+ self.stub_url('DELETE',
+ [self.manager.OS_EP_FILTER_EXT, 'projects', project_id,
+ 'endpoints', endpoint_id],
+ status_code=201)
+
+ self.manager.delete_endpoint_from_project(project=project_id,
+ endpoint=endpoint_id)
+
+ def test_check_endpoint_in_project(self):
+ endpoint_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+
+ self.stub_url('HEAD',
+ [self.manager.OS_EP_FILTER_EXT, 'projects', project_id,
+ 'endpoints', endpoint_id],
+ status_code=201)
+
+ self.manager.check_endpoint_in_project(project=project_id,
+ endpoint=endpoint_id)
+
+ def test_list_endpoints_for_project(self):
+ project_id = uuid.uuid4().hex
+ endpoints = {'endpoints': [self.new_endpoint_ref(),
+ self.new_endpoint_ref()]}
+ self.stub_url('GET',
+ [self.manager.OS_EP_FILTER_EXT, 'projects', project_id,
+ 'endpoints'],
+ json=endpoints,
+ status_code=200)
+
+ endpoints_resp = self.manager.list_endpoints_for_project(
+ project=project_id)
+
+ expected_endpoint_ids = [
+ endpoint['id'] for endpoint in endpoints['endpoints']]
+ actual_endpoint_ids = [endpoint.id for endpoint in endpoints_resp]
+ self.assertEqual(expected_endpoint_ids, actual_endpoint_ids)
+
+ def test_list_projects_for_endpoint(self):
+ endpoint_id = uuid.uuid4().hex
+ projects = {'projects': [self.new_project_ref(),
+ self.new_project_ref()]}
+ self.stub_url('GET',
+ [self.manager.OS_EP_FILTER_EXT, 'endpoints', endpoint_id,
+ 'projects'],
+ json=projects,
+ status_code=200)
+
+ projects_resp = self.manager.list_projects_for_endpoint(
+ endpoint=endpoint_id)
+
+ expected_project_ids = [
+ project['id'] for project in projects['projects']]
+ actual_project_ids = [project.id for project in projects_resp]
+ self.assertEqual(expected_project_ids, actual_project_ids)
diff --git a/keystoneclient/tests/unit/v3/test_endpoint_policy.py b/keystoneclient/tests/unit/v3/test_endpoint_policy.py
new file mode 100644
index 0000000..ce9efb4
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_endpoint_policy.py
@@ -0,0 +1,242 @@
+# Copyright 2014 IBM Corp.
+#
+# 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.unit.v3 import test_endpoint_filter
+from keystoneclient.tests.unit.v3 import utils
+
+
+class EndpointPolicyTests(utils.TestCase,
+ test_endpoint_filter.EndpointTestUtils):
+ """Test policy-endpoint associations (a.k.a. EndpointPolicy Extension)."""
+
+ def setUp(self):
+ super(EndpointPolicyTests, self).setUp()
+ self.manager = self.client.endpoint_policy
+
+ def new_policy_ref(self, **kwargs):
+ kwargs.setdefault('id', uuid.uuid4().hex)
+ kwargs.setdefault('type', uuid.uuid4().hex)
+ kwargs.setdefault('blob', uuid.uuid4().hex)
+ return kwargs
+
+ def new_region_ref(self, **kwargs):
+ kwargs = self.new_ref(**kwargs)
+ return kwargs
+
+ def new_service_ref(self, **kwargs):
+ kwargs = self.new_ref(**kwargs)
+ kwargs.setdefault('name', uuid.uuid4().hex)
+ kwargs.setdefault('type', uuid.uuid4().hex)
+ return kwargs
+
+ def _crud_policy_association_for_endpoint_via_id(
+ self, http_action, manager_action):
+ policy_id = uuid.uuid4().hex
+ endpoint_id = uuid.uuid4().hex
+
+ self.stub_url(http_action,
+ ['policies', policy_id, self.manager.OS_EP_POLICY_EXT,
+ 'endpoints', endpoint_id],
+ status_code=204)
+ manager_action(policy=policy_id, endpoint=endpoint_id)
+
+ def _crud_policy_association_for_endpoint_via_obj(
+ self, http_action, manager_action):
+ policy_ref = self.new_policy_ref()
+ endpoint_ref = self.new_endpoint_ref()
+ policy = self.client.policies.resource_class(
+ self.client.policies, policy_ref, loaded=True)
+ endpoint = self.client.endpoints.resource_class(
+ self.client.endpoints, endpoint_ref, loaded=True)
+
+ self.stub_url(http_action,
+ ['policies', policy_ref['id'],
+ self.manager.OS_EP_POLICY_EXT,
+ 'endpoints', endpoint_ref['id']],
+ status_code=204)
+ manager_action(policy=policy, endpoint=endpoint)
+
+ def test_create_policy_association_for_endpoint_via_id(self):
+ self._crud_policy_association_for_endpoint_via_id(
+ 'PUT', self.manager.create_policy_association_for_endpoint)
+
+ def test_create_policy_association_for_endpoint_via_obj(self):
+ self._crud_policy_association_for_endpoint_via_obj(
+ 'PUT', self.manager.create_policy_association_for_endpoint)
+
+ def test_check_policy_association_for_endpoint_via_id(self):
+ self._crud_policy_association_for_endpoint_via_id(
+ 'HEAD', self.manager.check_policy_association_for_endpoint)
+
+ def test_check_policy_association_for_endpoint_via_obj(self):
+ self._crud_policy_association_for_endpoint_via_obj(
+ 'HEAD', self.manager.check_policy_association_for_endpoint)
+
+ def test_delete_policy_association_for_endpoint_via_id(self):
+ self._crud_policy_association_for_endpoint_via_id(
+ 'DELETE', self.manager.delete_policy_association_for_endpoint)
+
+ def test_delete_policy_association_for_endpoint_via_obj(self):
+ self._crud_policy_association_for_endpoint_via_obj(
+ 'DELETE', self.manager.delete_policy_association_for_endpoint)
+
+ def _crud_policy_association_for_service_via_id(
+ self, http_action, manager_action):
+ policy_id = uuid.uuid4().hex
+ service_id = uuid.uuid4().hex
+
+ self.stub_url(http_action,
+ ['policies', policy_id, self.manager.OS_EP_POLICY_EXT,
+ 'services', service_id],
+ status_code=204)
+ manager_action(policy=policy_id, service=service_id)
+
+ def _crud_policy_association_for_service_via_obj(
+ self, http_action, manager_action):
+ policy_ref = self.new_policy_ref()
+ service_ref = self.new_service_ref()
+ policy = self.client.policies.resource_class(
+ self.client.policies, policy_ref, loaded=True)
+ service = self.client.services.resource_class(
+ self.client.services, service_ref, loaded=True)
+
+ self.stub_url(http_action,
+ ['policies', policy_ref['id'],
+ self.manager.OS_EP_POLICY_EXT,
+ 'services', service_ref['id']],
+ status_code=204)
+ manager_action(policy=policy, service=service)
+
+ def test_create_policy_association_for_service_via_id(self):
+ self._crud_policy_association_for_service_via_id(
+ 'PUT', self.manager.create_policy_association_for_service)
+
+ def test_create_policy_association_for_service_via_obj(self):
+ self._crud_policy_association_for_service_via_obj(
+ 'PUT', self.manager.create_policy_association_for_service)
+
+ def test_check_policy_association_for_service_via_id(self):
+ self._crud_policy_association_for_service_via_id(
+ 'HEAD', self.manager.check_policy_association_for_service)
+
+ def test_check_policy_association_for_service_via_obj(self):
+ self._crud_policy_association_for_service_via_obj(
+ 'HEAD', self.manager.check_policy_association_for_service)
+
+ def test_delete_policy_association_for_service_via_id(self):
+ self._crud_policy_association_for_service_via_id(
+ 'DELETE', self.manager.delete_policy_association_for_service)
+
+ def test_delete_policy_association_for_service_via_obj(self):
+ self._crud_policy_association_for_service_via_obj(
+ 'DELETE', self.manager.delete_policy_association_for_service)
+
+ def _crud_policy_association_for_region_and_service_via_id(
+ self, http_action, manager_action):
+ policy_id = uuid.uuid4().hex
+ region_id = uuid.uuid4().hex
+ service_id = uuid.uuid4().hex
+
+ self.stub_url(http_action,
+ ['policies', policy_id, self.manager.OS_EP_POLICY_EXT,
+ 'services', service_id, 'regions', region_id],
+ status_code=204)
+ manager_action(policy=policy_id, region=region_id, service=service_id)
+
+ def _crud_policy_association_for_region_and_service_via_obj(
+ self, http_action, manager_action):
+ policy_ref = self.new_policy_ref()
+ region_ref = self.new_region_ref()
+ service_ref = self.new_service_ref()
+ policy = self.client.policies.resource_class(
+ self.client.policies, policy_ref, loaded=True)
+ region = self.client.regions.resource_class(
+ self.client.regions, region_ref, loaded=True)
+ service = self.client.services.resource_class(
+ self.client.services, service_ref, loaded=True)
+
+ self.stub_url(http_action,
+ ['policies', policy_ref['id'],
+ self.manager.OS_EP_POLICY_EXT,
+ 'services', service_ref['id'],
+ 'regions', region_ref['id']],
+ status_code=204)
+ manager_action(policy=policy, region=region, service=service)
+
+ def test_create_policy_association_for_region_and_service_via_id(self):
+ self._crud_policy_association_for_region_and_service_via_id(
+ 'PUT',
+ self.manager.create_policy_association_for_region_and_service)
+
+ def test_create_policy_association_for_region_and_service_via_obj(self):
+ self._crud_policy_association_for_region_and_service_via_obj(
+ 'PUT',
+ self.manager.create_policy_association_for_region_and_service)
+
+ def test_check_policy_association_for_region_and_service_via_id(self):
+ self._crud_policy_association_for_region_and_service_via_id(
+ 'HEAD',
+ self.manager.check_policy_association_for_region_and_service)
+
+ def test_check_policy_association_for_region_and_service_via_obj(self):
+ self._crud_policy_association_for_region_and_service_via_obj(
+ 'HEAD',
+ self.manager.check_policy_association_for_region_and_service)
+
+ def test_delete_policy_association_for_region_and_service_via_id(self):
+ self._crud_policy_association_for_region_and_service_via_id(
+ 'DELETE',
+ self.manager.delete_policy_association_for_region_and_service)
+
+ def test_delete_policy_association_for_region_and_service_via_obj(self):
+ self._crud_policy_association_for_region_and_service_via_obj(
+ 'DELETE',
+ self.manager.delete_policy_association_for_region_and_service)
+
+ def test_get_policy_for_endpoint(self):
+ endpoint_id = uuid.uuid4().hex
+ expected_policy = self.new_policy_ref()
+
+ self.stub_url('GET',
+ ['endpoints', endpoint_id, self.manager.OS_EP_POLICY_EXT,
+ 'policy'],
+ json={'policy': expected_policy},
+ status_code=200)
+
+ policy_resp = self.manager.get_policy_for_endpoint(
+ endpoint=endpoint_id)
+
+ self.assertEqual(expected_policy['id'], policy_resp.id)
+ self.assertEqual(expected_policy['blob'], policy_resp.blob)
+ self.assertEqual(expected_policy['type'], policy_resp.type)
+
+ def test_list_endpoints_for_policy(self):
+ policy_id = uuid.uuid4().hex
+ endpoints = {'endpoints': [self.new_endpoint_ref(),
+ self.new_endpoint_ref()]}
+ self.stub_url('GET',
+ ['policies', policy_id, self.manager.OS_EP_POLICY_EXT,
+ 'endpoints'],
+ json=endpoints,
+ status_code=200)
+
+ endpoints_resp = self.manager.list_endpoints_for_policy(
+ policy=policy_id)
+
+ expected_endpoint_ids = [
+ endpoint['id'] for endpoint in endpoints['endpoints']]
+ actual_endpoint_ids = [endpoint.id for endpoint in endpoints_resp]
+ self.assertEqual(expected_endpoint_ids, actual_endpoint_ids)
diff --git a/keystoneclient/tests/unit/v3/test_endpoints.py b/keystoneclient/tests/unit/v3/test_endpoints.py
new file mode 100644
index 0000000..000718a
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_endpoints.py
@@ -0,0 +1,91 @@
+# 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.tests.unit.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(exceptions.ValidationError, 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)
+ ref['endpoint'] = "fake_endpoint"
+ self.assertRaises(exceptions.ValidationError, 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(exceptions.ValidationError, self.manager.list,
+ expected_path=expected_path, interface=interface)
diff --git a/keystoneclient/tests/unit/v3/test_federation.py b/keystoneclient/tests/unit/v3/test_federation.py
new file mode 100644
index 0000000..19ec44f
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_federation.py
@@ -0,0 +1,409 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import uuid
+
+from keystoneclient import access
+from keystoneclient import exceptions
+from keystoneclient import fixture
+from keystoneclient.tests.unit.v3 import utils
+from keystoneclient.v3.contrib.federation import base
+from keystoneclient.v3.contrib.federation import identity_providers
+from keystoneclient.v3.contrib.federation import mappings
+from keystoneclient.v3.contrib.federation import protocols
+from keystoneclient.v3 import domains
+from keystoneclient.v3 import projects
+
+
+class IdentityProviderTests(utils.TestCase, utils.CrudTests):
+ def setUp(self):
+ super(IdentityProviderTests, self).setUp()
+ self.key = 'identity_provider'
+ self.collection_key = 'identity_providers'
+ self.model = identity_providers.IdentityProvider
+ self.manager = self.client.federation.identity_providers
+ self.path_prefix = 'OS-FEDERATION'
+
+ def new_ref(self, **kwargs):
+ kwargs.setdefault('id', uuid.uuid4().hex)
+ kwargs.setdefault('description', uuid.uuid4().hex)
+ kwargs.setdefault('enabled', True)
+ return kwargs
+
+ def test_positional_parameters_expect_fail(self):
+ """Ensure CrudManager raises TypeError exceptions.
+
+ After passing wrong number of positional arguments
+ an exception should be raised.
+
+ Operations to be tested:
+ * create()
+ * get()
+ * list()
+ * delete()
+ * update()
+
+ """
+ POS_PARAM_1 = uuid.uuid4().hex
+ POS_PARAM_2 = uuid.uuid4().hex
+ POS_PARAM_3 = uuid.uuid4().hex
+
+ PARAMETERS = {
+ 'create': (POS_PARAM_1, POS_PARAM_2),
+ 'get': (POS_PARAM_1, POS_PARAM_2),
+ 'list': (POS_PARAM_1, POS_PARAM_2),
+ 'update': (POS_PARAM_1, POS_PARAM_2, POS_PARAM_3),
+ 'delete': (POS_PARAM_1, POS_PARAM_2)
+ }
+
+ for f_name, args in PARAMETERS.items():
+ self.assertRaises(TypeError, getattr(self.manager, f_name),
+ *args)
+
+ def test_create(self, ref=None, req_ref=None):
+ ref = ref or self.new_ref()
+
+ # 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('PUT', entity=ref, id=ref['id'], status_code=201)
+
+ returned = self.manager.create(**ref)
+ self.assertIsInstance(returned, self.model)
+ for attr in req_ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ req_ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(req_ref)
+
+
+class MappingTests(utils.TestCase, utils.CrudTests):
+ def setUp(self):
+ super(MappingTests, self).setUp()
+ self.key = 'mapping'
+ self.collection_key = 'mappings'
+ self.model = mappings.Mapping
+ self.manager = self.client.federation.mappings
+ self.path_prefix = 'OS-FEDERATION'
+
+ def new_ref(self, **kwargs):
+ kwargs.setdefault('id', uuid.uuid4().hex)
+ kwargs.setdefault('rules', [uuid.uuid4().hex,
+ uuid.uuid4().hex])
+ return kwargs
+
+ def test_create(self, ref=None, req_ref=None):
+ ref = ref or self.new_ref()
+ manager_ref = ref.copy()
+ mapping_id = 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()
+
+ self.stub_entity('PUT', entity=req_ref, id=mapping_id,
+ status_code=201)
+
+ returned = self.manager.create(mapping_id=mapping_id, **manager_ref)
+ self.assertIsInstance(returned, self.model)
+ for attr in req_ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ req_ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(manager_ref)
+
+
+class ProtocolTests(utils.TestCase, utils.CrudTests):
+ def setUp(self):
+ super(ProtocolTests, self).setUp()
+ self.key = 'protocol'
+ self.collection_key = 'protocols'
+ self.model = protocols.Protocol
+ self.manager = self.client.federation.protocols
+ self.path_prefix = 'OS-FEDERATION/identity_providers'
+
+ def _transform_to_response(self, ref):
+ """Rebuild dictionary so it can be used as a
+ reference response body.
+
+ """
+ response = copy.deepcopy(ref)
+ response['id'] = response.pop('protocol_id')
+ del response['identity_provider']
+ return response
+
+ def new_ref(self, **kwargs):
+ kwargs.setdefault('mapping', uuid.uuid4().hex)
+ kwargs.setdefault('identity_provider', uuid.uuid4().hex)
+ kwargs.setdefault('protocol_id', uuid.uuid4().hex)
+ return kwargs
+
+ def build_parts(self, identity_provider, protocol_id=None):
+ """Build array used to construct mocking URL.
+
+ Construct and return array with URL parts later used
+ by methods like utils.TestCase.stub_entity().
+ Example of URL:
+ ``OS-FEDERATION/identity_providers/{idp_id}/
+ protocols/{protocol_id}``
+
+ """
+ parts = ['OS-FEDERATION', 'identity_providers',
+ identity_provider, 'protocols']
+ if protocol_id:
+ parts.append(protocol_id)
+ return parts
+
+ def test_build_url_provide_base_url(self):
+ base_url = uuid.uuid4().hex
+ parameters = {'base_url': base_url}
+ url = self.manager.build_url(dict_args_in_out=parameters)
+ self.assertEqual('/'.join([base_url, self.collection_key]), url)
+
+ def test_build_url_w_idp_id(self):
+ """Test whether kwargs ``base_url`` discards object's base_url
+
+ This test shows, that when ``base_url`` is specified in the
+ dict_args_in_out dictionary, values like ``identity_provider_id``
+ are not taken into consideration while building the url.
+
+ """
+ base_url, identity_provider_id = uuid.uuid4().hex, uuid.uuid4().hex
+ parameters = {
+ 'base_url': base_url,
+ 'identity_provider_id': identity_provider_id
+ }
+ url = self.manager.build_url(dict_args_in_out=parameters)
+ self.assertEqual('/'.join([base_url, self.collection_key]), url)
+
+ def test_build_url_default_base_url(self):
+ identity_provider_id = uuid.uuid4().hex
+ parameters = {
+ 'identity_provider_id': identity_provider_id
+ }
+
+ url = self.manager.build_url(dict_args_in_out=parameters)
+ self.assertEqual(
+ '/'.join([self.manager.base_url, identity_provider_id,
+ self.manager.collection_key]), url)
+
+ def test_create(self):
+ """Test creating federation protocol tied to an Identity Provider.
+
+ URL to be tested: PUT /OS-FEDERATION/identity_providers/
+ $identity_provider/protocols/$protocol
+
+ """
+ request_args = self.new_ref()
+ expected = self._transform_to_response(request_args)
+ parts = self.build_parts(request_args['identity_provider'],
+ request_args['protocol_id'])
+ self.stub_entity('PUT', entity=expected,
+ parts=parts, status_code=201)
+ returned = self.manager.create(**request_args)
+ self.assertEqual(expected, returned.to_dict())
+ request_body = {'mapping_id': request_args['mapping']}
+ self.assertEntityRequestBodyIs(request_body)
+
+ def test_get(self):
+ """Fetch federation protocol object.
+
+ URL to be tested: GET /OS-FEDERATION/identity_providers/
+ $identity_provider/protocols/$protocol
+
+ """
+ request_args = self.new_ref()
+ expected = self._transform_to_response(request_args)
+
+ parts = self.build_parts(request_args['identity_provider'],
+ request_args['protocol_id'])
+ self.stub_entity('GET', entity=expected,
+ parts=parts, status_code=201)
+
+ returned = self.manager.get(request_args['identity_provider'],
+ request_args['protocol_id'])
+ self.assertIsInstance(returned, self.model)
+ self.assertEqual(expected, returned.to_dict())
+
+ def test_delete(self):
+ """Delete federation protocol object.
+
+ URL to be tested: DELETE /OS-FEDERATION/identity_providers/
+ $identity_provider/protocols/$protocol
+
+ """
+ request_args = self.new_ref()
+ parts = self.build_parts(request_args['identity_provider'],
+ request_args['protocol_id'])
+
+ self.stub_entity('DELETE', parts=parts, status_code=204)
+
+ self.manager.delete(request_args['identity_provider'],
+ request_args['protocol_id'])
+
+ def test_list(self):
+ """Test listing all federation protocols tied to the Identity Provider.
+
+ URL to be tested: GET /OS-FEDERATION/identity_providers/
+ $identity_provider/protocols
+
+ """
+ def _ref_protocols():
+ return {
+ 'id': uuid.uuid4().hex,
+ 'mapping_id': uuid.uuid4().hex
+ }
+
+ request_args = self.new_ref()
+ expected = [_ref_protocols() for _ in range(3)]
+ parts = self.build_parts(request_args['identity_provider'])
+ self.stub_entity('GET', parts=parts,
+ entity=expected, status_code=200)
+
+ returned = self.manager.list(request_args['identity_provider'])
+ for obj, ref_obj in zip(returned, expected):
+ self.assertEqual(obj.to_dict(), ref_obj)
+
+ def test_list_params(self):
+ request_args = self.new_ref()
+ filter_kwargs = {uuid.uuid4().hex: uuid.uuid4().hex}
+ parts = self.build_parts(request_args['identity_provider'])
+
+ # Return HTTP 401 as we don't accept such requests.
+ self.stub_entity('GET', parts=parts, status_code=401)
+ self.assertRaises(exceptions.Unauthorized,
+ self.manager.list,
+ request_args['identity_provider'],
+ **filter_kwargs)
+ self.assertQueryStringContains(**filter_kwargs)
+
+ def test_update(self):
+ """Test updating federation protocol
+
+ URL to be tested: PATCH /OS-FEDERATION/identity_providers/
+ $identity_provider/protocols/$protocol
+
+ """
+ request_args = self.new_ref()
+ expected = self._transform_to_response(request_args)
+
+ parts = self.build_parts(request_args['identity_provider'],
+ request_args['protocol_id'])
+
+ self.stub_entity('PATCH', parts=parts,
+ entity=expected, status_code=200)
+
+ returned = self.manager.update(request_args['identity_provider'],
+ request_args['protocol_id'],
+ mapping=request_args['mapping'])
+ self.assertIsInstance(returned, self.model)
+ self.assertEqual(expected, returned.to_dict())
+ request_body = {'mapping_id': request_args['mapping']}
+ self.assertEntityRequestBodyIs(request_body)
+
+
+class EntityManagerTests(utils.TestCase):
+ def test_create_object_expect_fail(self):
+ self.assertRaises(TypeError,
+ base.EntityManager,
+ self.client)
+
+
+class FederationProjectTests(utils.TestCase):
+
+ def setUp(self):
+ super(FederationProjectTests, self).setUp()
+ self.key = 'project'
+ self.collection_key = 'projects'
+ self.model = projects.Project
+ self.manager = self.client.federation.projects
+ self.URL = "%s%s" % (self.TEST_URL, '/OS-FEDERATION/projects')
+
+ def new_ref(self, **kwargs):
+ kwargs.setdefault('id', uuid.uuid4().hex)
+ kwargs.setdefault('domain_id', uuid.uuid4().hex)
+ kwargs.setdefault('enabled', True)
+ kwargs.setdefault('name', uuid.uuid4().hex)
+ return kwargs
+
+ def test_list_accessible_projects(self):
+ projects_ref = [self.new_ref(), self.new_ref()]
+ projects_json = {
+ self.collection_key: [self.new_ref(), self.new_ref()]
+ }
+ self.requests.get(self.URL, json=projects_json)
+ returned_list = self.manager.list()
+
+ self.assertEqual(len(projects_ref), len(returned_list))
+ for project in returned_list:
+ self.assertIsInstance(project, self.model)
+
+
+class FederationDomainTests(utils.TestCase):
+
+ def setUp(self):
+ super(FederationDomainTests, self).setUp()
+ self.key = 'domain'
+ self.collection_key = 'domains'
+ self.model = domains.Domain
+ self.manager = self.client.federation.domains
+
+ self.URL = "%s%s" % (self.TEST_URL, '/OS-FEDERATION/domains')
+
+ def new_ref(self, **kwargs):
+ kwargs.setdefault('id', uuid.uuid4().hex)
+ kwargs.setdefault('enabled', True)
+ kwargs.setdefault('name', uuid.uuid4().hex)
+ kwargs.setdefault('description', uuid.uuid4().hex)
+ return kwargs
+
+ def test_list_accessible_domains(self):
+ domains_ref = [self.new_ref(), self.new_ref()]
+ domains_json = {
+ self.collection_key: domains_ref
+ }
+ self.requests.get(self.URL, json=domains_json)
+ returned_list = self.manager.list()
+ self.assertEqual(len(domains_ref), len(returned_list))
+ for domain in returned_list:
+ self.assertIsInstance(domain, self.model)
+
+
+class FederatedTokenTests(utils.TestCase):
+
+ def setUp(self):
+ super(FederatedTokenTests, self).setUp()
+ token = fixture.V3FederationToken()
+ token.set_project_scope()
+ token.add_role()
+ self.federated_token = access.AccessInfo.factory(body=token)
+
+ def test_federated_property_federated_token(self):
+ """Check if is_federated property returns expected value."""
+ self.assertTrue(self.federated_token.is_federated)
+
+ def test_get_user_domain_name(self):
+ """Ensure a federated user's domain name does not exist."""
+ self.assertIsNone(self.federated_token.user_domain_name)
+
+ def test_get_user_domain_id(self):
+ """Ensure a federated user's domain ID does not exist."""
+ self.assertIsNone(self.federated_token.user_domain_id)
diff --git a/keystoneclient/tests/unit/v3/test_groups.py b/keystoneclient/tests/unit/v3/test_groups.py
new file mode 100644
index 0000000..6ed140c
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_groups.py
@@ -0,0 +1,58 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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.unit.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
+
+ def test_list_groups_for_user(self):
+ user_id = uuid.uuid4().hex
+ ref_list = [self.new_ref(), self.new_ref()]
+
+ self.stub_entity('GET',
+ ['users', user_id, self.collection_key],
+ status_code=200, entity=ref_list)
+
+ returned_list = self.manager.list(user=user_id)
+ self.assertEqual(len(ref_list), len(returned_list))
+ [self.assertIsInstance(r, self.model) for r in returned_list]
+
+ def test_list_groups_for_domain(self):
+ ref_list = [self.new_ref(), self.new_ref()]
+ domain_id = uuid.uuid4().hex
+
+ self.stub_entity('GET',
+ [self.collection_key],
+ status_code=200, entity=ref_list)
+
+ returned_list = self.manager.list(domain=domain_id)
+ self.assertTrue(len(ref_list), len(returned_list))
+ [self.assertIsInstance(r, self.model) for r in returned_list]
+
+ self.assertQueryStringIs('domain_id=%s' % domain_id)
diff --git a/keystoneclient/tests/unit/v3/test_oauth1.py b/keystoneclient/tests/unit/v3/test_oauth1.py
new file mode 100644
index 0000000..d259053
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_oauth1.py
@@ -0,0 +1,298 @@
+# 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 mock
+from oslo_utils import timeutils
+import six
+from six.moves.urllib import parse as urlparse
+from testtools import matchers
+
+from keystoneclient import session
+from keystoneclient.tests.unit.v3 import client_fixtures
+from keystoneclient.tests.unit.v3 import utils
+from keystoneclient.v3.contrib.oauth1 import access_tokens
+from keystoneclient.v3.contrib.oauth1 import auth
+from keystoneclient.v3.contrib.oauth1 import consumers
+from keystoneclient.v3.contrib.oauth1 import request_tokens
+
+try:
+ import oauthlib
+ from oauthlib import oauth1
+except ImportError:
+ oauth1 = None
+
+
+class BaseTest(utils.TestCase):
+ def setUp(self):
+ super(BaseTest, self).setUp()
+ if oauth1 is None:
+ self.skipTest('oauthlib package not available')
+
+
+class ConsumerTests(BaseTest, utils.CrudTests):
+ def setUp(self):
+ super(ConsumerTests, self).setUp()
+ self.key = 'consumer'
+ self.collection_key = 'consumers'
+ self.model = consumers.Consumer
+ self.manager = self.client.oauth1.consumers
+ self.path_prefix = 'OS-OAUTH1'
+
+ def new_ref(self, **kwargs):
+ kwargs = super(ConsumerTests, self).new_ref(**kwargs)
+ kwargs.setdefault('description', uuid.uuid4().hex)
+ return kwargs
+
+ def test_description_is_optional(self):
+ consumer_id = uuid.uuid4().hex
+ resp_ref = {'consumer': {'description': None,
+ 'id': consumer_id}}
+
+ self.stub_url('POST',
+ [self.path_prefix, self.collection_key],
+ status_code=201, json=resp_ref)
+
+ consumer = self.manager.create()
+ self.assertEqual(consumer_id, consumer.id)
+ self.assertIsNone(consumer.description)
+
+ def test_description_not_included(self):
+ consumer_id = uuid.uuid4().hex
+ resp_ref = {'consumer': {'id': consumer_id}}
+
+ self.stub_url('POST',
+ [self.path_prefix, self.collection_key],
+ status_code=201, json=resp_ref)
+
+ consumer = self.manager.create()
+ self.assertEqual(consumer_id, consumer.id)
+
+
+class TokenTests(BaseTest):
+ def _new_oauth_token(self):
+ key = uuid.uuid4().hex
+ secret = uuid.uuid4().hex
+ params = {'oauth_token': key, 'oauth_token_secret': secret}
+ token = urlparse.urlencode(params)
+ return (key, secret, token)
+
+ def _new_oauth_token_with_expires_at(self):
+ key, secret, token = self._new_oauth_token()
+ expires_at = timeutils.strtime()
+ params = {'oauth_token': key,
+ 'oauth_token_secret': secret,
+ 'oauth_expires_at': expires_at}
+ token = urlparse.urlencode(params)
+ return (key, secret, expires_at, token)
+
+ def _validate_oauth_headers(self, auth_header, oauth_client):
+ """Assert that the data in the headers matches the data
+ that is produced from oauthlib.
+ """
+
+ self.assertThat(auth_header, matchers.StartsWith('OAuth '))
+ auth_header = auth_header[len('OAuth '):]
+ # NOTE(stevemar): In newer versions of oauthlib there is
+ # an additional argument for getting oauth parameters.
+ # Adding a conditional here to revert back to no arguments
+ # if an earlier version is detected.
+ if tuple(oauthlib.__version__.split('.')) > ('0', '6', '1'):
+ header_params = oauth_client.get_oauth_params(None)
+ else:
+ header_params = oauth_client.get_oauth_params()
+ parameters = dict(header_params)
+
+ self.assertEqual('HMAC-SHA1', parameters['oauth_signature_method'])
+ self.assertEqual('1.0', parameters['oauth_version'])
+ self.assertIsInstance(parameters['oauth_nonce'], six.string_types)
+ self.assertEqual(oauth_client.client_key,
+ parameters['oauth_consumer_key'])
+ if oauth_client.resource_owner_key:
+ self.assertEqual(oauth_client.resource_owner_key,
+ parameters['oauth_token'],)
+ if oauth_client.verifier:
+ self.assertEqual(oauth_client.verifier,
+ parameters['oauth_verifier'])
+ if oauth_client.callback_uri:
+ self.assertEqual(oauth_client.callback_uri,
+ parameters['oauth_callback'])
+ if oauth_client.timestamp:
+ self.assertEqual(oauth_client.timestamp,
+ parameters['oauth_timestamp'])
+ return parameters
+
+
+class RequestTokenTests(TokenTests):
+ def setUp(self):
+ super(RequestTokenTests, self).setUp()
+ self.model = request_tokens.RequestToken
+ self.manager = self.client.oauth1.request_tokens
+ self.path_prefix = 'OS-OAUTH1'
+
+ def test_authorize_request_token(self):
+ request_key = uuid.uuid4().hex
+ info = {'id': request_key,
+ 'key': request_key,
+ 'secret': uuid.uuid4().hex}
+ request_token = request_tokens.RequestToken(self.manager, info)
+
+ verifier = uuid.uuid4().hex
+ resp_ref = {'token': {'oauth_verifier': verifier}}
+ self.stub_url('PUT',
+ [self.path_prefix, 'authorize', request_key],
+ status_code=200, json=resp_ref)
+
+ # Assert the manager is returning the expected data
+ role_id = uuid.uuid4().hex
+ token = request_token.authorize([role_id])
+ self.assertEqual(verifier, token.oauth_verifier)
+
+ # Assert that the request was sent in the expected structure
+ exp_body = {'roles': [{'id': role_id}]}
+ self.assertRequestBodyIs(json=exp_body)
+
+ def test_create_request_token(self):
+ project_id = uuid.uuid4().hex
+ consumer_key = uuid.uuid4().hex
+ consumer_secret = uuid.uuid4().hex
+
+ request_key, request_secret, resp_ref = self._new_oauth_token()
+
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ self.stub_url('POST', [self.path_prefix, 'request_token'],
+ status_code=201, text=resp_ref, headers=headers)
+
+ # Assert the manager is returning request token object
+ request_token = self.manager.create(consumer_key, consumer_secret,
+ project_id)
+ self.assertIsInstance(request_token, self.model)
+ self.assertEqual(request_key, request_token.key)
+ self.assertEqual(request_secret, request_token.secret)
+
+ # Assert that the project id is in the header
+ self.assertRequestHeaderEqual('requested-project-id', project_id)
+ req_headers = self.requests.last_request.headers
+
+ oauth_client = oauth1.Client(consumer_key,
+ client_secret=consumer_secret,
+ signature_method=oauth1.SIGNATURE_HMAC,
+ callback_uri="oob")
+ self._validate_oauth_headers(req_headers['Authorization'],
+ oauth_client)
+
+
+class AccessTokenTests(TokenTests):
+ def setUp(self):
+ super(AccessTokenTests, self).setUp()
+ self.manager = self.client.oauth1.access_tokens
+ self.model = access_tokens.AccessToken
+ self.path_prefix = 'OS-OAUTH1'
+
+ def test_create_access_token_expires_at(self):
+ verifier = uuid.uuid4().hex
+ consumer_key = uuid.uuid4().hex
+ consumer_secret = uuid.uuid4().hex
+ request_key = uuid.uuid4().hex
+ request_secret = uuid.uuid4().hex
+
+ t = self._new_oauth_token_with_expires_at()
+ access_key, access_secret, expires_at, resp_ref = t
+
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ self.stub_url('POST', [self.path_prefix, 'access_token'],
+ status_code=201, text=resp_ref, headers=headers)
+
+ # Assert that the manager creates an access token object
+ access_token = self.manager.create(consumer_key, consumer_secret,
+ request_key, request_secret,
+ verifier)
+ self.assertIsInstance(access_token, self.model)
+ self.assertEqual(access_key, access_token.key)
+ self.assertEqual(access_secret, access_token.secret)
+ self.assertEqual(expires_at, access_token.expires)
+
+ req_headers = self.requests.last_request.headers
+ oauth_client = oauth1.Client(consumer_key,
+ client_secret=consumer_secret,
+ resource_owner_key=request_key,
+ resource_owner_secret=request_secret,
+ signature_method=oauth1.SIGNATURE_HMAC,
+ verifier=verifier,
+ timestamp=expires_at)
+ self._validate_oauth_headers(req_headers['Authorization'],
+ oauth_client)
+
+
+class AuthenticateWithOAuthTests(TokenTests):
+ def setUp(self):
+ super(AuthenticateWithOAuthTests, self).setUp()
+ if oauth1 is None:
+ self.skipTest('optional package oauthlib is not installed')
+
+ def test_oauth_authenticate_success(self):
+ consumer_key = uuid.uuid4().hex
+ consumer_secret = uuid.uuid4().hex
+ access_key = uuid.uuid4().hex
+ access_secret = uuid.uuid4().hex
+
+ # Just use an existing project scoped token and change
+ # the methods to oauth1, and add an OS-OAUTH1 section.
+ oauth_token = client_fixtures.project_scoped_token()
+ oauth_token['methods'] = ["oauth1"]
+ oauth_token['OS-OAUTH1'] = {"consumer_id": consumer_key,
+ "access_token_id": access_key}
+ self.stub_auth(json=oauth_token)
+
+ a = auth.OAuth(self.TEST_URL, consumer_key=consumer_key,
+ consumer_secret=consumer_secret,
+ access_key=access_key,
+ access_secret=access_secret)
+ s = session.Session(auth=a)
+ t = s.get_token()
+ self.assertEqual(self.TEST_TOKEN, t)
+
+ OAUTH_REQUEST_BODY = {
+ "auth": {
+ "identity": {
+ "methods": ["oauth1"],
+ "oauth1": {}
+ }
+ }
+ }
+
+ self.assertRequestBodyIs(json=OAUTH_REQUEST_BODY)
+
+ # Assert that the headers have the same oauthlib data
+ req_headers = self.requests.last_request.headers
+ oauth_client = oauth1.Client(consumer_key,
+ client_secret=consumer_secret,
+ resource_owner_key=access_key,
+ resource_owner_secret=access_secret,
+ signature_method=oauth1.SIGNATURE_HMAC)
+ self._validate_oauth_headers(req_headers['Authorization'],
+ oauth_client)
+
+
+class TestOAuthLibModule(utils.TestCase):
+
+ def test_no_oauthlib_installed(self):
+ with mock.patch.object(auth, 'oauth1', None):
+ self.assertRaises(NotImplementedError,
+ auth.OAuth,
+ self.TEST_URL,
+ consumer_key=uuid.uuid4().hex,
+ consumer_secret=uuid.uuid4().hex,
+ access_key=uuid.uuid4().hex,
+ access_secret=uuid.uuid4().hex)
diff --git a/keystoneclient/tests/unit/v3/test_policies.py b/keystoneclient/tests/unit/v3/test_policies.py
new file mode 100644
index 0000000..a7f8d8a
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_policies.py
@@ -0,0 +1,31 @@
+# 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.unit.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/keystoneclient/tests/unit/v3/test_projects.py b/keystoneclient/tests/unit/v3/test_projects.py
new file mode 100644
index 0000000..5d08bb2
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_projects.py
@@ -0,0 +1,227 @@
+# 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.tests.unit.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)
+ return self._new_project_ref(ref=kwargs)
+
+ def _new_project_ref(self, ref=None):
+ ref = ref or {}
+ ref.setdefault('domain_id', uuid.uuid4().hex)
+ ref.setdefault('enabled', True)
+ ref.setdefault('name', uuid.uuid4().hex)
+ return ref
+
+ def test_list_projects_for_user(self):
+ ref_list = [self.new_ref(), self.new_ref()]
+ user_id = uuid.uuid4().hex
+
+ self.stub_entity('GET',
+ ['users', user_id, self.collection_key],
+ entity=ref_list)
+
+ returned_list = self.manager.list(user=user_id)
+ self.assertEqual(len(ref_list), len(returned_list))
+ [self.assertIsInstance(r, self.model) for r in returned_list]
+
+ def test_list_projects_for_domain(self):
+ ref_list = [self.new_ref(), self.new_ref()]
+ domain_id = uuid.uuid4().hex
+
+ self.stub_entity('GET', [self.collection_key],
+ entity=ref_list)
+
+ returned_list = self.manager.list(domain=domain_id)
+ self.assertEqual(len(ref_list), len(returned_list))
+ [self.assertIsInstance(r, self.model) for r in returned_list]
+
+ self.assertQueryStringIs('domain_id=%s' % domain_id)
+
+ def test_create_with_parent(self):
+ parent_ref = self.new_ref()
+ parent_ref['parent_id'] = uuid.uuid4().hex
+ parent = self.test_create(ref=parent_ref)
+ parent.id = parent_ref['id']
+
+ # Create another project under 'parent' in the hierarchy
+ ref = self.new_ref()
+ ref['parent_id'] = parent.id
+
+ child_ref = ref.copy()
+ del child_ref['parent_id']
+ child_ref['parent'] = parent
+
+ # test_create() pops the 'id' of the mocked response
+ del ref['id']
+
+ # Resource objects may peform lazy-loading. The create() method of
+ # ProjectManager will try to access the 'uuid' attribute of the parent
+ # object, which will trigger a call to fetch the Resource attributes.
+ self.stub_entity('GET', id=parent_ref['id'], entity=parent_ref)
+ self.test_create(ref=child_ref, req_ref=ref)
+
+ def test_create_with_parent_id(self):
+ ref = self._new_project_ref()
+ ref['parent_id'] = uuid.uuid4().hex
+
+ self.stub_entity('POST', entity=ref, status_code=201)
+
+ returned = self.manager.create(name=ref['name'],
+ domain=ref['domain_id'],
+ parent_id=ref['parent_id'])
+
+ self.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(ref)
+
+ def test_create_with_parent_and_parent_id(self):
+ ref = self._new_project_ref()
+ ref['parent_id'] = uuid.uuid4().hex
+
+ self.stub_entity('POST', entity=ref, status_code=201)
+
+ # Should ignore the 'parent_id' argument since we are also passing
+ # 'parent'
+ returned = self.manager.create(name=ref['name'],
+ domain=ref['domain_id'],
+ parent=ref['parent_id'],
+ parent_id=uuid.uuid4().hex)
+
+ self.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(ref)
+
+ def _create_projects_hierarchy(self, hierarchy_size=3):
+ """Creates a project hierarchy with specified size.
+
+ :param hierarchy_size: the desired hierarchy size, default is 3.
+
+ :returns: a list of the projects in the created hierarchy.
+
+ """
+
+ ref = self.new_ref()
+ project_id = ref['id']
+ projects = [ref]
+
+ for i in range(1, hierarchy_size):
+ new_ref = self.new_ref()
+ new_ref['parent_id'] = project_id
+ projects.append(new_ref)
+ project_id = new_ref['id']
+
+ return projects
+
+ def test_get_with_subtree_as_list(self):
+ projects = self._create_projects_hierarchy()
+ ref = projects[0]
+
+ ref['subtree_as_list'] = []
+ for i in range(1, len(projects)):
+ ref['subtree_as_list'].append(projects[i])
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'], subtree_as_list=True)
+ self.assertQueryStringIs('subtree_as_list')
+ for i in range(1, len(projects)):
+ for attr in projects[i]:
+ child = getattr(returned, 'subtree_as_list')[i - 1]
+ self.assertEqual(
+ child[attr],
+ projects[i][attr],
+ 'Expected different %s' % attr)
+
+ def test_get_with_parents_as_list(self):
+ projects = self._create_projects_hierarchy()
+ ref = projects[2]
+
+ ref['parents_as_list'] = []
+ for i in range(0, len(projects) - 1):
+ ref['parents_as_list'].append(projects[i])
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'], parents_as_list=True)
+ self.assertQueryStringIs('parents_as_list')
+ for i in range(0, len(projects) - 1):
+ for attr in projects[i]:
+ parent = getattr(returned, 'parents_as_list')[i]
+ self.assertEqual(
+ parent[attr],
+ projects[i][attr],
+ 'Expected different %s' % attr)
+
+ def test_get_with_parents_as_list_and_subtree_as_list(self):
+ ref = self.new_ref()
+ projects = self._create_projects_hierarchy()
+ ref = projects[1]
+
+ ref['parents_as_list'] = [projects[0]]
+ ref['subtree_as_list'] = [projects[2]]
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'],
+ parents_as_list=True,
+ subtree_as_list=True)
+ self.assertQueryStringIs('subtree_as_list&parents_as_list')
+
+ for attr in projects[0]:
+ parent = getattr(returned, 'parents_as_list')[0]
+ self.assertEqual(
+ parent[attr],
+ projects[0][attr],
+ 'Expected different %s' % attr)
+
+ for attr in projects[2]:
+ child = getattr(returned, 'subtree_as_list')[0]
+ self.assertEqual(
+ child[attr],
+ projects[2][attr],
+ 'Expected different %s' % attr)
+
+ def test_update_with_parent_project(self):
+ ref = self.new_ref()
+ ref['parent_id'] = uuid.uuid4().hex
+
+ self.stub_entity('PATCH', id=ref['id'], entity=ref, status_code=403)
+ req_ref = ref.copy()
+ req_ref.pop('id')
+
+ # NOTE(rodrigods): this is the expected behaviour of the Identity
+ # server, a different implementation might not fail this request.
+ self.assertRaises(exceptions.Forbidden, self.manager.update,
+ ref['id'], **utils.parameterize(req_ref))
diff --git a/keystoneclient/tests/unit/v3/test_regions.py b/keystoneclient/tests/unit/v3/test_regions.py
new file mode 100644
index 0000000..392f79f
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_regions.py
@@ -0,0 +1,37 @@
+# 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.unit.v3 import utils
+from keystoneclient.v3 import regions
+
+
+class RegionTests(utils.TestCase, utils.CrudTests):
+ def setUp(self):
+ super(RegionTests, self).setUp()
+ self.key = 'region'
+ self.collection_key = 'regions'
+ self.model = regions.Region
+ self.manager = self.client.regions
+
+ def new_ref(self, **kwargs):
+ kwargs = super(RegionTests, self).new_ref(**kwargs)
+ kwargs.setdefault('enabled', True)
+ kwargs.setdefault('id', uuid.uuid4().hex)
+ return kwargs
+
+ def test_update_enabled_defaults_to_none(self):
+ super(RegionTests, self).test_update(
+ req_ref={'description': uuid.uuid4().hex})
diff --git a/keystoneclient/tests/unit/v3/test_role_assignments.py b/keystoneclient/tests/unit/v3/test_role_assignments.py
new file mode 100644
index 0000000..1a664c9
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_role_assignments.py
@@ -0,0 +1,210 @@
+# 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.unit.v3 import utils
+from keystoneclient.v3 import role_assignments
+
+
+class RoleAssignmentsTests(utils.TestCase, utils.CrudTests):
+
+ def setUp(self):
+ super(RoleAssignmentsTests, self).setUp()
+ self.key = 'role_assignment'
+ self.collection_key = 'role_assignments'
+ self.model = role_assignments.RoleAssignment
+ self.manager = self.client.role_assignments
+ self.TEST_USER_DOMAIN_LIST = [{
+ 'role': {
+ 'id': self.TEST_ROLE_ID
+ },
+ 'scope': {
+ 'domain': {
+ 'id': self.TEST_DOMAIN_ID
+ }
+ },
+ 'user': {
+ 'id': self.TEST_USER_ID
+ }
+ }]
+ self.TEST_GROUP_PROJECT_LIST = [{
+ 'group': {
+ 'id': self.TEST_GROUP_ID
+ },
+ 'role': {
+ 'id': self.TEST_ROLE_ID
+ },
+ 'scope': {
+ 'project': {
+ 'id': self.TEST_TENANT_ID
+ }
+ }
+ }]
+ self.TEST_USER_PROJECT_LIST = [{
+ 'user': {
+ 'id': self.TEST_USER_ID
+ },
+ 'role': {
+ 'id': self.TEST_ROLE_ID
+ },
+ 'scope': {
+ 'project': {
+ 'id': self.TEST_TENANT_ID
+ }
+ }
+ }]
+
+ self.TEST_ALL_RESPONSE_LIST = (self.TEST_USER_PROJECT_LIST +
+ self.TEST_GROUP_PROJECT_LIST +
+ self.TEST_USER_DOMAIN_LIST)
+
+ def _assert_returned_list(self, ref_list, returned_list):
+ self.assertEqual(len(ref_list), len(returned_list))
+ [self.assertIsInstance(r, self.model) for r in returned_list]
+
+ def test_list_params(self):
+ ref_list = self.TEST_USER_PROJECT_LIST
+ self.stub_entity('GET',
+ [self.collection_key,
+ '?scope.project.id=%s&user.id=%s' %
+ (self.TEST_TENANT_ID, self.TEST_USER_ID)],
+ entity=ref_list)
+
+ returned_list = self.manager.list(user=self.TEST_USER_ID,
+ project=self.TEST_TENANT_ID)
+ self._assert_returned_list(ref_list, returned_list)
+
+ kwargs = {'scope.project.id': self.TEST_TENANT_ID,
+ 'user.id': self.TEST_USER_ID}
+ self.assertQueryStringContains(**kwargs)
+
+ def test_all_assignments_list(self):
+ ref_list = self.TEST_ALL_RESPONSE_LIST
+ self.stub_entity('GET',
+ [self.collection_key],
+ entity=ref_list)
+
+ returned_list = self.manager.list()
+ self._assert_returned_list(ref_list, returned_list)
+
+ kwargs = {}
+ self.assertQueryStringContains(**kwargs)
+
+ def test_project_assignments_list(self):
+ ref_list = self.TEST_GROUP_PROJECT_LIST + self.TEST_USER_PROJECT_LIST
+ self.stub_entity('GET',
+ [self.collection_key,
+ '?scope.project.id=%s' % self.TEST_TENANT_ID],
+ entity=ref_list)
+
+ returned_list = self.manager.list(project=self.TEST_TENANT_ID)
+ self._assert_returned_list(ref_list, returned_list)
+
+ kwargs = {'scope.project.id': self.TEST_TENANT_ID}
+ self.assertQueryStringContains(**kwargs)
+
+ def test_domain_assignments_list(self):
+ ref_list = self.TEST_USER_DOMAIN_LIST
+ self.stub_entity('GET',
+ [self.collection_key,
+ '?scope.domain.id=%s' % self.TEST_DOMAIN_ID],
+ entity=ref_list)
+
+ returned_list = self.manager.list(domain=self.TEST_DOMAIN_ID)
+ self._assert_returned_list(ref_list, returned_list)
+
+ kwargs = {'scope.domain.id': self.TEST_DOMAIN_ID}
+ self.assertQueryStringContains(**kwargs)
+
+ def test_group_assignments_list(self):
+ ref_list = self.TEST_GROUP_PROJECT_LIST
+ self.stub_entity('GET',
+ [self.collection_key,
+ '?group.id=%s' % self.TEST_GROUP_ID],
+ entity=ref_list)
+
+ returned_list = self.manager.list(group=self.TEST_GROUP_ID)
+ self._assert_returned_list(ref_list, returned_list)
+
+ kwargs = {'group.id': self.TEST_GROUP_ID}
+ self.assertQueryStringContains(**kwargs)
+
+ def test_user_assignments_list(self):
+ ref_list = self.TEST_USER_DOMAIN_LIST + self.TEST_USER_PROJECT_LIST
+ self.stub_entity('GET',
+ [self.collection_key,
+ '?user.id=%s' % self.TEST_USER_ID],
+ entity=ref_list)
+
+ returned_list = self.manager.list(user=self.TEST_USER_ID)
+ self._assert_returned_list(ref_list, returned_list)
+
+ kwargs = {'user.id': self.TEST_USER_ID}
+ self.assertQueryStringContains(**kwargs)
+
+ def test_effective_assignments_list(self):
+ ref_list = self.TEST_USER_PROJECT_LIST + self.TEST_USER_DOMAIN_LIST
+ self.stub_entity('GET',
+ [self.collection_key,
+ '?effective=True'],
+ entity=ref_list)
+
+ returned_list = self.manager.list(effective=True)
+ self._assert_returned_list(ref_list, returned_list)
+
+ kwargs = {'effective': 'True'}
+ self.assertQueryStringContains(**kwargs)
+
+ def test_role_assignments_list(self):
+ ref_list = self.TEST_ALL_RESPONSE_LIST
+ self.stub_entity('GET',
+ [self.collection_key,
+ '?role.id=' + self.TEST_ROLE_ID],
+ entity=ref_list)
+
+ returned_list = self.manager.list(role=self.TEST_ROLE_ID)
+ self._assert_returned_list(ref_list, returned_list)
+
+ kwargs = {'role.id': self.TEST_ROLE_ID}
+ self.assertQueryStringContains(**kwargs)
+
+ def test_domain_and_project_list(self):
+ # Should only accept either domain or project, never both
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.list,
+ domain=self.TEST_DOMAIN_ID,
+ project=self.TEST_TENANT_ID)
+
+ def test_user_and_group_list(self):
+ # Should only accept either user or group, never both
+ self.assertRaises(exceptions.ValidationError, self.manager.list,
+ user=self.TEST_USER_ID, group=self.TEST_GROUP_ID)
+
+ def test_create(self):
+ # Create not supported for role assignments
+ self.assertRaises(exceptions.MethodNotImplemented, self.manager.create)
+
+ def test_update(self):
+ # Update not supported for role assignments
+ self.assertRaises(exceptions.MethodNotImplemented, self.manager.update)
+
+ def test_delete(self):
+ # Delete not supported for role assignments
+ self.assertRaises(exceptions.MethodNotImplemented, self.manager.delete)
+
+ def test_get(self):
+ # Get not supported for role assignments
+ self.assertRaises(exceptions.MethodNotImplemented, self.manager.get)
+
+ def test_find(self):
+ # Find not supported for role assignments
+ self.assertRaises(exceptions.MethodNotImplemented, self.manager.find)
diff --git a/keystoneclient/tests/unit/v3/test_roles.py b/keystoneclient/tests/unit/v3/test_roles.py
new file mode 100644
index 0000000..2a71bf3
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_roles.py
@@ -0,0 +1,331 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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.tests.unit.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
+
+ def test_domain_role_grant(self):
+ user_id = uuid.uuid4().hex
+ domain_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('PUT',
+ ['domains', domain_id, 'users', user_id,
+ self.collection_key, ref['id']],
+ status_code=201)
+
+ self.manager.grant(role=ref['id'], domain=domain_id, user=user_id)
+
+ def test_domain_group_role_grant(self):
+ group_id = uuid.uuid4().hex
+ domain_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('PUT',
+ ['domains', domain_id, 'groups', group_id,
+ self.collection_key, ref['id']],
+ status_code=201)
+
+ self.manager.grant(role=ref['id'], domain=domain_id, group=group_id)
+
+ 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('GET',
+ ['domains', domain_id, 'users', user_id,
+ self.collection_key], entity=ref_list)
+
+ self.manager.list(domain=domain_id, user=user_id)
+
+ 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('GET',
+ ['domains', domain_id, 'groups', group_id,
+ self.collection_key], entity=ref_list)
+
+ self.manager.list(domain=domain_id, group=group_id)
+
+ def test_domain_role_check(self):
+ user_id = uuid.uuid4().hex
+ domain_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('HEAD',
+ ['domains', domain_id, 'users', user_id,
+ self.collection_key, ref['id']],
+ status_code=204)
+
+ self.manager.check(role=ref['id'], domain=domain_id,
+ user=user_id)
+
+ 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('HEAD',
+ ['domains', domain_id, 'groups', group_id,
+ self.collection_key, ref['id']],
+ status_code=204)
+
+ self.manager.check(role=ref['id'], domain=domain_id, group=group_id)
+
+ def test_domain_role_revoke(self):
+ user_id = uuid.uuid4().hex
+ domain_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('DELETE',
+ ['domains', domain_id, 'users', user_id,
+ self.collection_key, ref['id']],
+ status_code=204)
+
+ self.manager.revoke(role=ref['id'], domain=domain_id, user=user_id)
+
+ def test_domain_group_role_revoke(self):
+ group_id = uuid.uuid4().hex
+ domain_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('DELETE',
+ ['domains', domain_id, 'groups', group_id,
+ self.collection_key, ref['id']],
+ status_code=204)
+
+ self.manager.revoke(role=ref['id'], domain=domain_id, group=group_id)
+
+ def test_project_role_grant(self):
+ user_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('PUT',
+ ['projects', project_id, 'users', user_id,
+ self.collection_key, ref['id']],
+ status_code=201)
+
+ self.manager.grant(role=ref['id'], project=project_id, user=user_id)
+
+ def test_project_group_role_grant(self):
+ group_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('PUT',
+ ['projects', project_id, 'groups', group_id,
+ self.collection_key, ref['id']],
+ status_code=201)
+
+ self.manager.grant(role=ref['id'], project=project_id, group=group_id)
+
+ 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('GET',
+ ['projects', project_id, 'users', user_id,
+ self.collection_key], entity=ref_list)
+
+ self.manager.list(project=project_id, user=user_id)
+
+ 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('GET',
+ ['projects', project_id, 'groups', group_id,
+ self.collection_key], entity=ref_list)
+
+ self.manager.list(project=project_id, group=group_id)
+
+ def test_project_role_check(self):
+ user_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('HEAD',
+ ['projects', project_id, 'users', user_id,
+ self.collection_key, ref['id']],
+ status_code=200)
+
+ self.manager.check(role=ref['id'], project=project_id, user=user_id)
+
+ def test_project_group_role_check(self):
+ group_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('HEAD',
+ ['projects', project_id, 'groups', group_id,
+ self.collection_key, ref['id']],
+ status_code=200)
+
+ self.manager.check(role=ref['id'], project=project_id, group=group_id)
+
+ def test_project_role_revoke(self):
+ user_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('DELETE',
+ ['projects', project_id, 'users', user_id,
+ self.collection_key, ref['id']],
+ status_code=204)
+
+ self.manager.revoke(role=ref['id'], project=project_id, user=user_id)
+
+ def test_project_group_role_revoke(self):
+ group_id = uuid.uuid4().hex
+ project_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('DELETE',
+ ['projects', project_id, 'groups', group_id,
+ self.collection_key, ref['id']],
+ status_code=204)
+
+ self.manager.revoke(role=ref['id'], project=project_id, group=group_id)
+
+ 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/keystoneclient/tests/unit/v3/test_service_catalog.py b/keystoneclient/tests/unit/v3/test_service_catalog.py
new file mode 100644
index 0000000..a187302
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_service_catalog.py
@@ -0,0 +1,218 @@
+# 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 access
+from keystoneclient import exceptions
+from keystoneclient.tests.unit.v3 import client_fixtures
+from keystoneclient.tests.unit.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
+ })
+
+ self.north_endpoints = {'public':
+ 'http://glance.north.host/glanceapi/public',
+ 'internal':
+ 'http://glance.north.host/glanceapi/internal',
+ 'admin':
+ 'http://glance.north.host/glanceapi/admin'}
+
+ self.south_endpoints = {'public':
+ 'http://glance.south.host/glanceapi/public',
+ 'internal':
+ 'http://glance.south.host/glanceapi/internal',
+ 'admin':
+ 'http://glance.south.host/glanceapi/admin'}
+
+ def test_building_a_service_catalog(self):
+ auth_ref = access.AccessInfo.factory(self.RESPONSE,
+ self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ self.assertEqual(sc.url_for(service_type='compute'),
+ "https://compute.north.host/novapi/public")
+ self.assertEqual(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.assertEqual(public_ep['compute'][0]['region'], 'North')
+ self.assertEqual(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.assertEqual(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.assertEqual(url, "http://glance.south.host/glanceapi/internal")
+
+ def test_service_catalog_empty(self):
+ self.AUTH_RESPONSE_BODY['token']['catalog'] = []
+ auth_ref = access.AccessInfo.factory(self.RESPONSE,
+ self.AUTH_RESPONSE_BODY)
+ self.assertRaises(exceptions.EmptyCatalog,
+ auth_ref.service_catalog.url_for,
+ service_type='image',
+ endpoint_type='internalURL')
+
+ def test_service_catalog_get_endpoints_region_names(self):
+ auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ endpoints = sc.get_endpoints(service_type='image', region_name='North')
+ self.assertEqual(len(endpoints), 1)
+ for endpoint in endpoints['image']:
+ self.assertEqual(endpoint['url'],
+ self.north_endpoints[endpoint['interface']])
+
+ endpoints = sc.get_endpoints(service_type='image', region_name='South')
+ self.assertEqual(len(endpoints), 1)
+ for endpoint in endpoints['image']:
+ self.assertEqual(endpoint['url'],
+ self.south_endpoints[endpoint['interface']])
+
+ endpoints = sc.get_endpoints(service_type='compute')
+ self.assertEqual(len(endpoints['compute']), 3)
+
+ endpoints = sc.get_endpoints(service_type='compute',
+ region_name='North')
+ self.assertEqual(len(endpoints['compute']), 3)
+
+ endpoints = sc.get_endpoints(service_type='compute',
+ region_name='West')
+ self.assertEqual(len(endpoints['compute']), 0)
+
+ def test_service_catalog_url_for_region_names(self):
+ auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ url = sc.url_for(service_type='image', region_name='North')
+ self.assertEqual(url, self.north_endpoints['public'])
+
+ url = sc.url_for(service_type='image', region_name='South')
+ self.assertEqual(url, self.south_endpoints['public'])
+
+ self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
+ service_type='image', region_name='West')
+
+ def test_servcie_catalog_get_url_region_names(self):
+ auth_ref = access.AccessInfo.factory(None, self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ urls = sc.get_urls(service_type='image')
+ self.assertEqual(len(urls), 2)
+
+ urls = sc.get_urls(service_type='image', region_name='North')
+ self.assertEqual(len(urls), 1)
+ self.assertEqual(urls[0], self.north_endpoints['public'])
+
+ urls = sc.get_urls(service_type='image', region_name='South')
+ self.assertEqual(len(urls), 1)
+ self.assertEqual(urls[0], self.south_endpoints['public'])
+
+ urls = sc.get_urls(service_type='image', region_name='West')
+ self.assertIsNone(urls)
+
+ def test_service_catalog_param_overrides_body_region(self):
+ self.AUTH_RESPONSE_BODY['token']['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')
+ self.assertEqual(url, self.north_endpoints['public'])
+
+ url = sc.url_for(service_type='image', region_name='South')
+ self.assertEqual(url, self.south_endpoints['public'])
+
+ endpoints = sc.get_endpoints(service_type='image')
+ self.assertEqual(len(endpoints['image']), 3)
+ for endpoint in endpoints['image']:
+ self.assertEqual(endpoint['url'],
+ self.north_endpoints[endpoint['interface']])
+
+ endpoints = sc.get_endpoints(service_type='image', region_name='South')
+ self.assertEqual(len(endpoints['image']), 3)
+ for endpoint in endpoints['image']:
+ self.assertEqual(endpoint['url'],
+ self.south_endpoints[endpoint['interface']])
+
+ def test_service_catalog_service_name(self):
+ auth_ref = access.AccessInfo.factory(resp=None,
+ body=self.AUTH_RESPONSE_BODY)
+ sc = auth_ref.service_catalog
+
+ url = sc.url_for(service_name='glance', endpoint_type='public',
+ service_type='image', region_name='North')
+ self.assertEqual('http://glance.north.host/glanceapi/public', url)
+
+ url = sc.url_for(service_name='glance', endpoint_type='public',
+ service_type='image', region_name='South')
+ self.assertEqual('http://glance.south.host/glanceapi/public', url)
+
+ self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
+ service_name='glance', service_type='compute')
+
+ urls = sc.get_urls(service_type='image', service_name='glance',
+ endpoint_type='public')
+
+ self.assertIn('http://glance.north.host/glanceapi/public', urls)
+ self.assertIn('http://glance.south.host/glanceapi/public', urls)
+
+ urls = sc.get_urls(service_type='image', service_name='Servers',
+ endpoint_type='public')
+
+ self.assertIsNone(urls)
+
+ def test_service_catalog_without_name(self):
+ pr_auth_ref = access.AccessInfo.factory(
+ resp=None,
+ body=client_fixtures.project_scoped_token())
+ pr_sc = pr_auth_ref.service_catalog
+
+ # this will work because there are no service names on that token
+ url_ref = 'http://public.com:8774/v2/225da22d3ce34b15877ea70b2a575f58'
+ url = pr_sc.url_for(service_type='compute', service_name='NotExist',
+ endpoint_type='public')
+ self.assertEqual(url_ref, url)
+
+ ab_auth_ref = access.AccessInfo.factory(resp=None,
+ body=self.AUTH_RESPONSE_BODY)
+ ab_sc = ab_auth_ref.service_catalog
+
+ # this won't work because there is a name and it's not this one
+ self.assertRaises(exceptions.EndpointNotFound, ab_sc.url_for,
+ service_type='compute', service_name='NotExist',
+ endpoint_type='public')
diff --git a/keystoneclient/tests/unit/v3/test_services.py b/keystoneclient/tests/unit/v3/test_services.py
new file mode 100644
index 0000000..40dde47
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_services.py
@@ -0,0 +1,44 @@
+# 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.unit.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
+
+ def test_list_filter_name(self):
+ filter_name = uuid.uuid4().hex
+ expected_query = {'name': filter_name}
+ super(ServiceTests, self).test_list(expected_query=expected_query,
+ name=filter_name)
+
+ def test_list_filter_type(self):
+ filter_type = uuid.uuid4().hex
+ expected_query = {'type': filter_type}
+ super(ServiceTests, self).test_list(expected_query=expected_query,
+ type=filter_type)
diff --git a/keystoneclient/tests/unit/v3/test_tokens.py b/keystoneclient/tests/unit/v3/test_tokens.py
new file mode 100644
index 0000000..2c27fd0
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_tokens.py
@@ -0,0 +1,110 @@
+# 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 testresources
+
+from keystoneclient import access
+from keystoneclient import exceptions
+from keystoneclient.tests.unit import client_fixtures
+from keystoneclient.tests.unit.v3 import utils
+
+
+class TokenTests(utils.TestCase, testresources.ResourcedTestCase):
+
+ resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
+
+ def test_revoke_token_with_token_id(self):
+ token_id = uuid.uuid4().hex
+ self.stub_url('DELETE', ['/auth/tokens'], status_code=204)
+ self.client.tokens.revoke_token(token_id)
+ self.assertRequestHeaderEqual('X-Subject-Token', token_id)
+
+ def test_revoke_token_with_access_info_instance(self):
+ token_id = uuid.uuid4().hex
+ token_ref = self.examples.TOKEN_RESPONSES[
+ self.examples.v3_UUID_TOKEN_DEFAULT]
+ token = access.AccessInfoV3(token_id, token_ref['token'])
+ self.stub_url('DELETE', ['/auth/tokens'], status_code=204)
+ self.client.tokens.revoke_token(token)
+ self.assertRequestHeaderEqual('X-Subject-Token', token_id)
+
+ def test_get_revoked(self):
+ sample_revoked_response = {'signed': '-----BEGIN CMS-----\nMIIB...'}
+ self.stub_url('GET', ['auth', 'tokens', 'OS-PKI', 'revoked'],
+ json=sample_revoked_response)
+ resp = self.client.tokens.get_revoked()
+ self.assertEqual(sample_revoked_response, resp)
+
+ def test_validate_token_with_token_id(self):
+ # Can validate a token passing a string token ID.
+ token_id = uuid.uuid4().hex
+ token_ref = self.examples.TOKEN_RESPONSES[
+ self.examples.v3_UUID_TOKEN_DEFAULT]
+ self.stub_url('GET', ['auth', 'tokens'],
+ headers={'X-Subject-Token': token_id, }, json=token_ref)
+ access_info = self.client.tokens.validate(token_id)
+
+ self.assertRequestHeaderEqual('X-Subject-Token', token_id)
+ self.assertIsInstance(access_info, access.AccessInfoV3)
+ self.assertEqual(token_id, access_info.auth_token)
+
+ def test_validate_token_with_access_info(self):
+ # Can validate a token passing an access info.
+ token_id = uuid.uuid4().hex
+ token_ref = self.examples.TOKEN_RESPONSES[
+ self.examples.v3_UUID_TOKEN_DEFAULT]
+ token = access.AccessInfoV3(token_id, token_ref['token'])
+ self.stub_url('GET', ['auth', 'tokens'],
+ headers={'X-Subject-Token': token_id, }, json=token_ref)
+ access_info = self.client.tokens.validate(token)
+
+ self.assertRequestHeaderEqual('X-Subject-Token', token_id)
+ self.assertIsInstance(access_info, access.AccessInfoV3)
+ self.assertEqual(token_id, access_info.auth_token)
+
+ def test_validate_token_invalid(self):
+ # When the token is invalid the server typically returns a 404.
+ token_id = uuid.uuid4().hex
+ self.stub_url('GET', ['auth', 'tokens'], status_code=404)
+ self.assertRaises(exceptions.NotFound,
+ self.client.tokens.validate, token_id)
+
+ def test_validate_token_catalog(self):
+ # Can validate a token and a catalog is requested by default.
+ token_id = uuid.uuid4().hex
+ token_ref = self.examples.TOKEN_RESPONSES[
+ self.examples.v3_UUID_TOKEN_DEFAULT]
+ self.stub_url('GET', ['auth', 'tokens'],
+ headers={'X-Subject-Token': token_id, }, json=token_ref)
+ access_info = self.client.tokens.validate(token_id)
+
+ self.assertQueryStringIs()
+ self.assertTrue(access_info.has_service_catalog())
+
+ def test_validate_token_nocatalog(self):
+ # Can validate a token and request no catalog.
+ token_id = uuid.uuid4().hex
+ token_ref = self.examples.TOKEN_RESPONSES[
+ self.examples.v3_UUID_TOKEN_UNSCOPED]
+ self.stub_url('GET', ['auth', 'tokens'],
+ headers={'X-Subject-Token': token_id, }, json=token_ref)
+ access_info = self.client.tokens.validate(token_id,
+ include_catalog=False)
+
+ self.assertQueryStringIs('nocatalog')
+ self.assertFalse(access_info.has_service_catalog())
+
+
+def load_tests(loader, tests, pattern):
+ return testresources.OptimisingTestSuite(tests)
diff --git a/keystoneclient/tests/unit/v3/test_trusts.py b/keystoneclient/tests/unit/v3/test_trusts.py
new file mode 100644
index 0000000..fbd8fde
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_trusts.py
@@ -0,0 +1,112 @@
+# 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 oslo_utils import timeutils
+
+from keystoneclient import exceptions
+from keystoneclient.tests.unit.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_limited_uses(self):
+ ref = self.new_ref()
+ ref['trustor_user_id'] = uuid.uuid4().hex
+ ref['trustee_user_id'] = uuid.uuid4().hex
+ ref['impersonation'] = False
+ ref['remaining_uses'] = 5
+ 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()
+ req_ref.pop('id')
+
+ # 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()
+ req_ref.pop('id')
+
+ # 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()
+ req_ref.pop('id')
+ 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):
+ expected_query = {'trustor_user_id': '12345'}
+ super(TrustTests, self).test_list(expected_query=expected_query,
+ trustor_user='12345')
+
+ def test_list_filter_trustee(self):
+ expected_query = {'trustee_user_id': '12345'}
+ super(TrustTests, self).test_list(expected_query=expected_query,
+ trustee_user='12345')
+
+ def test_update(self):
+ # Update not supported for the OS-TRUST API
+ self.assertRaises(exceptions.MethodNotImplemented, self.manager.update)
diff --git a/keystoneclient/tests/unit/v3/test_users.py b/keystoneclient/tests/unit/v3/test_users.py
new file mode 100644
index 0000000..4645d6e
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/test_users.py
@@ -0,0 +1,279 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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.tests.unit.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
+
+ def test_add_user_to_group(self):
+ group_id = uuid.uuid4().hex
+ ref = self.new_ref()
+ self.stub_url('PUT',
+ ['groups', group_id, self.collection_key, ref['id']],
+ status_code=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)
+
+ def test_list_users_in_group(self):
+ group_id = uuid.uuid4().hex
+ ref_list = [self.new_ref(), self.new_ref()]
+
+ self.stub_entity('GET',
+ ['groups', group_id, self.collection_key],
+ entity=ref_list)
+
+ returned_list = self.manager.list(group=group_id)
+ self.assertEqual(len(ref_list), len(returned_list))
+ [self.assertIsInstance(r, self.model) for r in returned_list]
+
+ def test_check_user_in_group(self):
+ group_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('HEAD',
+ ['groups', group_id, self.collection_key, ref['id']],
+ status_code=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)
+
+ def test_remove_user_from_group(self):
+ group_id = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_url('DELETE',
+ ['groups', group_id, self.collection_key, ref['id']],
+ status_code=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)
+
+ def test_create_doesnt_log_password(self):
+ password = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ self.stub_entity('POST', [self.collection_key],
+ status_code=201, entity=ref)
+
+ req_ref = ref.copy()
+ req_ref.pop('id')
+ param_ref = req_ref.copy()
+
+ param_ref['password'] = password
+ params = utils.parameterize(param_ref)
+
+ self.manager.create(**params)
+
+ self.assertNotIn(password, self.logger.output)
+
+ 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('POST', [self.collection_key],
+ status_code=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.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(req_ref)
+
+ 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('POST',
+ [self.collection_key],
+ status_code=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.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(req_ref)
+
+ def test_update_doesnt_log_password(self):
+ password = uuid.uuid4().hex
+ ref = self.new_ref()
+
+ req_ref = ref.copy()
+ req_ref.pop('id')
+ param_ref = req_ref.copy()
+
+ self.stub_entity('PATCH',
+ [self.collection_key, ref['id']],
+ status_code=200, entity=ref)
+
+ param_ref['password'] = password
+ params = utils.parameterize(param_ref)
+
+ self.manager.update(ref['id'], **params)
+
+ self.assertNotIn(password, self.logger.output)
+
+ 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('PATCH',
+ [self.collection_key, ref['id']],
+ status_code=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.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(req_ref)
+
+ 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('PATCH',
+ [self.collection_key, ref['id']],
+ status_code=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.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(req_ref)
+
+ def test_update_password(self):
+ old_password = uuid.uuid4().hex
+ new_password = uuid.uuid4().hex
+
+ self.stub_url('POST',
+ [self.collection_key, self.TEST_USER, 'password'])
+ self.client.user_id = self.TEST_USER
+ self.manager.update_password(old_password, new_password)
+
+ exp_req_body = {
+ 'user': {
+ 'password': new_password, 'original_password': old_password
+ }
+ }
+
+ self.assertEqual(self.TEST_URL + '/users/test/password',
+ self.requests.last_request.url)
+ self.assertRequestBodyIs(json=exp_req_body)
+ self.assertNotIn(old_password, self.logger.output)
+ self.assertNotIn(new_password, self.logger.output)
+
+ def test_update_password_with_bad_inputs(self):
+ old_password = uuid.uuid4().hex
+ new_password = uuid.uuid4().hex
+
+ # users can't unset their password
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.update_password,
+ old_password, None)
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.update_password,
+ old_password, '')
+
+ # users can't start with empty passwords
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.update_password,
+ None, new_password)
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.update_password,
+ '', new_password)
+
+ # this wouldn't result in any change anyway
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.update_password,
+ None, None)
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.update_password,
+ '', '')
+ password = uuid.uuid4().hex
+ self.assertRaises(exceptions.ValidationError,
+ self.manager.update_password,
+ password, password)
diff --git a/keystoneclient/tests/unit/v3/utils.py b/keystoneclient/tests/unit/v3/utils.py
new file mode 100644
index 0000000..7320687
--- /dev/null
+++ b/keystoneclient/tests/unit/v3/utils.py
@@ -0,0 +1,332 @@
+# 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 six
+from six.moves.urllib import parse as urlparse
+
+from keystoneclient.tests.unit 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_ADMIN_IDENTITY_ENDPOINT = "http://127.0.0.1:35357/v3"
+
+ 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": TEST_ADMIN_IDENTITY_ENDPOINT,
+ "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
+
+ try:
+ response_list = kwargs['response_list']
+ except KeyError:
+ headers = kwargs.setdefault('headers', {})
+ headers['X-Subject-Token'] = subject_token
+ else:
+ for resp in response_list:
+ headers = resp.setdefault('headers', {})
+ headers['X-Subject-Token'] = subject_token
+
+ self.stub_url('POST', ['auth', 'tokens'], **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)
+ kwargs.setdefault(uuid.uuid4().hex, 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))
+
+ 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)
+ if req_ref:
+ req_ref = req_ref.copy()
+ else:
+ req_ref = ref.copy()
+ req_ref.pop('id')
+
+ self.stub_entity('POST', entity=req_ref, status_code=201)
+
+ returned = self.manager.create(**parameterize(manager_ref))
+ self.assertIsInstance(returned, self.model)
+ for attr in req_ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ req_ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(req_ref)
+
+ # The entity created here may be used in other test cases
+ return returned
+
+ def test_get(self, ref=None):
+ ref = ref or self.new_ref()
+
+ self.stub_entity('GET', id=ref['id'], entity=ref)
+
+ returned = self.manager.get(ref['id'])
+ self.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+
+ def _get_expected_path(self, expected_path=None):
+ 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
+
+ return expected_path
+
+ def test_list(self, ref_list=None, expected_path=None,
+ expected_query=None, **filter_kwargs):
+ ref_list = ref_list or [self.new_ref(), self.new_ref()]
+ expected_path = self._get_expected_path(expected_path)
+
+ self.requests.get(urlparse.urljoin(self.TEST_URL, expected_path),
+ json=self.encode(ref_list))
+
+ returned_list = self.manager.list(**filter_kwargs)
+ self.assertEqual(len(ref_list), len(returned_list))
+ [self.assertIsInstance(r, self.model) for r in returned_list]
+
+ qs_args = self.requests.last_request.qs
+ qs_args_expected = expected_query or filter_kwargs
+ for key, value in six.iteritems(qs_args_expected):
+ self.assertIn(key, qs_args)
+ # The querystring value is a list. Note we convert the value to a
+ # string and lower, as the query string is always a string and the
+ # filter_kwargs may contain non-string values, for example a
+ # boolean, causing the comaprison to fail.
+ self.assertIn(str(value).lower(), qs_args[key])
+
+ # Also check that no query string args exist which are not expected
+ for key in qs_args:
+ self.assertIn(key, qs_args_expected)
+
+ def test_list_params(self):
+ ref_list = [self.new_ref()]
+ filter_kwargs = {uuid.uuid4().hex: uuid.uuid4().hex}
+ expected_path = self._get_expected_path()
+
+ self.requests.get(urlparse.urljoin(self.TEST_URL, expected_path),
+ json=self.encode(ref_list))
+
+ self.manager.list(**filter_kwargs)
+ self.assertQueryStringContains(**filter_kwargs)
+
+ def test_find(self, ref=None):
+ ref = ref or self.new_ref()
+ ref_list = [ref]
+
+ self.stub_entity('GET', entity=ref_list)
+
+ returned = self.manager.find(name=getattr(ref, 'name', None))
+ self.assertIsInstance(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=%s' % ref['name'])
+ else:
+ self.assertQueryStringIs('')
+
+ def test_update(self, ref=None, req_ref=None):
+ ref = ref or self.new_ref()
+
+ self.stub_entity('PATCH', id=ref['id'], entity=ref)
+
+ # 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)
+ if req_ref:
+ req_ref = req_ref.copy()
+ else:
+ req_ref = ref.copy()
+ req_ref.pop('id')
+
+ returned = self.manager.update(ref['id'], **parameterize(req_ref))
+ self.assertIsInstance(returned, self.model)
+ for attr in ref:
+ self.assertEqual(
+ getattr(returned, attr),
+ ref[attr],
+ 'Expected different %s' % attr)
+ self.assertEntityRequestBodyIs(req_ref)
+
+ def test_delete(self, ref=None):
+ ref = ref or self.new_ref()
+
+ self.stub_entity('DELETE', id=ref['id'], status_code=204)
+ self.manager.delete(ref['id'])