From 908ededed96b54760188a010e6bd44b7325cf68c Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 13:11:47 -0500 Subject: Changes to also return role references as a part of user when get token call is made for a specific tenant. --- test/unit/test_common.py | 8 ++++++++ test/unit/test_token.py | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 3 deletions(-) (limited to 'test') diff --git a/test/unit/test_common.py b/test/unit/test_common.py index 11d6a33d..e77f7fc8 100644 --- a/test/unit/test_common.py +++ b/test/unit/test_common.py @@ -739,6 +739,14 @@ def create_role_ref_xml(user_id, role_id, tenant_id, auth_token): "X-Auth-Token": auth_token, "ACCEPT": "application/xml"}) return (resp, content) + +def delete_role_ref(user, role_ref_id, auth_token): + header = httplib2.Http(".cache") + url = '%susers/%s/roleRefs/%s' % (URL, user, role_ref_id) + resp, content = header.request(url, "DELETE", body='', + headers={"Content-Type": "application/json", + "X-Auth-Token": str(auth_token)}) + return (resp, content) def create_role_xml(role_id, auth_token): header = httplib2.Http(".cache") diff --git a/test/unit/test_token.py b/test/unit/test_token.py index bfef2be3..0754f777 100644 --- a/test/unit/test_token.py +++ b/test/unit/test_token.py @@ -21,14 +21,16 @@ import sys sys.path.append(os.path.abspath(os.path.join(os.path.abspath(__file__), '..', '..', '..', '..', 'keystone'))) import unittest - import test_common as utils - +import json +import keystone.logic.types.fault as fault +from lxml import etree class ValidateToken(unittest.TestCase): def setUp(self): self.tenant = utils.get_tenant() + self.user = 'joeuser' self.token = utils.get_token('joeuser', 'secrete', self.tenant, 'token') #self.user = utils.get_user() @@ -36,8 +38,19 @@ class ValidateToken(unittest.TestCase): self.auth_token = utils.get_auth_token() self.exp_auth_token = utils.get_exp_auth_token() #self.disabled_token = utils.get_disabled_token() + resp, content = utils.create_role_ref(self.user, 'Admin', self.tenant, str(self.auth_token)) + obj = json.loads(content) + if not "roleRef" in obj: + raise fault.BadRequestFault("Expecting RoleRef") + roleRef = obj["roleRef"] + if not "id" in roleRef: + self.role_ref_id = None + else: + self.role_ref_id = roleRef["id"] + def tearDown(self): + resp, content = utils.delete_role_ref(self.user, self.role_ref_id, self.auth_token) utils.delete_token(self.token, self.auth_token) def test_validate_token_true(self): @@ -53,6 +66,14 @@ class ValidateToken(unittest.TestCase): self.fail('Service Not Available') self.assertEqual(200, int(resp['status'])) self.assertEqual('application/json', utils.content_type(resp)) + #verify content + obj = json.loads(content) + if not "auth" in obj: + raise self.fail("Expecting Auth") + role_refs = obj["auth"]["user"]["roleRefs"] + role_ref = role_refs[0] + role_ref_id = role_ref["id"] + self.assertEqual(self.role_ref_id, role_ref_id) def test_validate_token_true_xml(self): header = httplib2.Http(".cache") @@ -67,7 +88,28 @@ class ValidateToken(unittest.TestCase): self.fail('Service Not Available') self.assertEqual(200, int(resp['status'])) self.assertEqual('application/xml', utils.content_type(resp)) - + #verify content + dom = etree.Element("root") + dom.append(etree.fromstring(content)) + auth = dom.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "auth") + if auth == None: + self.fail("Expecting Auth") + + user = auth.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "user") + if user == None: + self.fail("Expecting User") + roleRefs = user.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "roleRefs") + if roleRefs == None: + self.fail("Expecting Role Refs") + roleRef = roleRefs.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "roleRef") + if roleRef == None: + self.fail("Expecting Role Refs") + self.assertEqual(str(self.role_ref_id), roleRef.get("id")) + def test_validate_token_expired(self): header = httplib2.Http(".cache") url = '%stokens/%s?belongsTo=%s' % (utils.URL, self.exp_auth_token, -- cgit From 06093efd33b13520d23a24358337548ce5aa813b Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 13:28:43 -0500 Subject: Merging changes --- test/unit/__init__.py | 0 test/unit/base.py | 275 +++++++++++++++++++++++++++++++++++++++++++++ test/unit/decorators.py | 49 ++++++++ test/unit/test_authn_v2.py | 89 +++++++++++++++ 4 files changed, 413 insertions(+) create mode 100644 test/unit/__init__.py create mode 100644 test/unit/base.py create mode 100644 test/unit/decorators.py create mode 100644 test/unit/test_authn_v2.py (limited to 'test') diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/base.py b/test/unit/base.py new file mode 100644 index 00000000..d9ace1da --- /dev/null +++ b/test/unit/base.py @@ -0,0 +1,275 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base test case classes for the unit tests""" + +import datetime +import functools +import json +import httplib +import logging +import pprint +import unittest + +from lxml import etree, objectify +import webob + +from keystone import server +from keystone.db.sqlalchemy import api as db_api + +logger = logging.getLogger('test.unit.base') + + +class ServiceAPITest(unittest.TestCase): + + """ + Base test case class for any unit test that tests the main service API. + """ + + """ + The `api` attribute for this base class is the `server.KeystoneAPI` + controller. + """ + api_class = server.KeystoneAPI + + """ + Dict of configuration options to pass to the API controller + """ + options = {'sql_connection': 'sqlite:///', # in-memory db + 'verbose': False, + 'debug': False} + + """ + Set of dicts of tenant attributes we start each test case with + """ + tenant_fixtures = [ + {'id': 'tenant1', + 'enabled': True, + 'desc': 'tenant1'} + ] + + """ + Attributes of the user the test creates for each test case that + will authenticate against the API. The `auth_user` attribute + will contain the created user with the following attributes. + """ + auth_user_attrs = {'id': 'auth_user', + 'password': 'auth_pass', + 'email': 'auth_user@example.com', + 'enabled': True, + 'tenant_id': 'tenant1'} + """ + Special attribute that is the identifier of the token we use in + authenticating. Makes it easy to test the authentication process. + """ + auth_token_id = 'SPECIALAUTHTOKEN' + + """ + Content-type of requests. Generally, you don't need to manually + change this. Instead, :see test.unit.decorators + """ + content_type = 'json' + + """ + Version of the API to test + """ + api_version = '2.0' + + def setUp(self): + self.api = self.api_class(self.options) + + self.expires = datetime.datetime.utcnow() + self.clear_all_data() + + # Create all our base tenants + for tenant in self.tenant_fixtures: + self.fixture_create_tenant(**tenant) + + # Create the user we will authenticate with + self.auth_user = self.fixture_create_user(**self.auth_user_attrs) + self.auth_token = self.fixture_create_token( + user_id=self.auth_user['id'], + tenant_id=self.auth_user['tenant_id'], + expires=self.expires, + token_id=self.auth_token_id) + + self.add_verify_status_helpers() + + def tearDown(self): + self.clear_all_data() + setattr(self, 'req', None) + setattr(self, 'res', None) + + def clear_all_data(self): + """ + Purges the database of all data + """ + db_api.unregister_models() + logger.debug("Cleared all data from database") + db_api.register_models() + + def fixture_create_tenant(self, **kwargs): + """ + Creates a tenant fixture. + + :params **kwargs: Attributes of the tenant to create + """ + values = kwargs.copy() + tenant = db_api.tenant_create(values) + logger.debug("Created tenant fixture %s", values['id']) + return tenant + + def fixture_create_user(self, **kwargs): + """ + Creates a user fixture. If the user's tenant ID is set, and the tenant + does not exist in the database, the tenant is created. + + :params **kwargs: Attributes of the user to create + """ + values = kwargs.copy() + tenant_id = values.get('tenant_id') + if tenant_id: + if not db_api.tenant_get(tenant_id): + db_api.tenant_create({'id': tenant_id, + 'enabled': True, + 'desc': tenant_id}) + user = db_api.user_create(values) + logger.debug("Created user fixture %s", values['id']) + return user + + def fixture_create_token(self, **kwargs): + """ + Creates a token fixture. + + :params **kwargs: Attributes of the token to create + """ + values = kwargs.copy() + token = db_api.token_create(values) + logger.debug("Created token fixture %s", values['token_id']) + return token + + def get_request(self, method, url, headers=None): + """ + Sets the `req` attribute to a `webob.Request` object that + is constructed with the supplied method and url. Supplied + headers are added to appropriate Content-type headers. + """ + headers = headers or {} + self.req = webob.Request.blank('/v%s/%s' % (self.api_version, + url.lstrip('/'))) + self.req.method = method + self.req.headers = headers + if 'content-type' not in headers: + ct = 'application/%s' % self.content_type + self.req.headers['content-type'] = ct + self.req.headers['accept'] = ct + return self.req + + def get_response(self): + """ + Sets the appropriate headers for the `req` attribute for + the current content type, then calls `req.get_response()` and + sets the `res` attribute to the returned `webob.Response` object + """ + self.res = self.req.get_response(self.api) + logger.debug("%s %s returned %s", self.req.method, self.req.path_qs, + self.res.status) + if self.res.status_int != httplib.OK: + logger.debug("Response Body:") + for line in self.res.body.split("\n"): + logger.debug(line) + return self.res + + def verify_status(self, status_code): + """ + Simple convenience wrapper for validating a response's status + code. + """ + if not getattr(self, 'res'): + raise RuntimeError("Called verify_status() before calling " + "get_response()!") + + self.assertEqual(status_code, self.res.status_int, + "Incorrect status code %d. Expected %d" % + (self.res.status_int, status_code)) + + def add_verify_status_helpers(self): + """ + Adds some convenience helpers using partials... + """ + self.status_ok = functools.partial(self.verify_status, httplib.OK) + + def assert_dict_equal(self, expected, got): + """ + Compares two dicts for equality and prints the dictionaries + nicely formatted for easy comparison if there is a failure. + """ + self.assertEqual(expected, got, "Mappings are not equal.\n" + "Got:\n%s\nExpected:\n%s" % + (pprint.pformat(got), + pprint.pformat(expected))) + + def assert_xml_strings_equal(self, expected, got): + """ + Compares two XML strings for equality by parsing them both + into DOMs. Prints the DOMs nicely formatted for easy comparison + if there is a failure. + """ + # This is a nice little trick... objectify.fromstring() returns + # a DOM different from etree.fromstring(). The objectify version + # removes any different whitespacing... + got = objectify.fromstring(got) + expected = objectify.fromstring(expected) + self.assertEqual(etree.tostring(expected), + etree.tostring(got), "DOMs are not equal.\n" + "Got:\n%s\nExpected:\n%s" % + (etree.tostring(got, pretty_print=True), + etree.tostring(expected, pretty_print=True))) + + +class AdminAPITest(ServiceAPITest): + + """ + Base test case class for any unit test that tests the admin API. The + """ + + """ + The `api` attribute for this base class is the `server.KeystoneAdminAPI` + controller. + """ + api_class = server.KeystoneAdminAPI + + """ + Set of dicts of tenant attributes we start each test case with + """ + tenant_fixtures = [ + {'id': 'tenant1', + 'enabled': True, + 'desc': 'tenant1'}, + {'id': 'tenant2', + 'enabled': True, + 'desc': 'tenant2'} + ] + + """ + Attributes of the user the test creates for each test case that + will authenticate against the API. + """ + auth_user_attrs = {'id': 'admin_user', + 'password': 'admin_pass', + 'email': 'admin_user@example.com', + 'enabled': True, + 'tenant_id': 'tenant2'} diff --git a/test/unit/decorators.py b/test/unit/decorators.py new file mode 100644 index 00000000..17a7d432 --- /dev/null +++ b/test/unit/decorators.py @@ -0,0 +1,49 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Decorators useful in unit tests""" + +import functools + + +def content_type(func, content_type='json'): + """ + Decorator for a test case method that sets the test case's + content_type to 'json' or 'xml' and resets it afterwards to + the original setting. This also asserts that if there is a + value for the test object's `res` attribute, that the content-type + header of the response is correct. + """ + @functools.wraps(func) + def wrapped(*a, **kwargs): + test_obj = a[0] + orig_content_type = test_obj.content_type + try: + test_obj.content_type = content_type + func(*a, **kwargs) + if getattr(test_obj, 'res'): + expected = 'application/%s' % content_type + got = test_obj.res.headers['content-type'].split(';')[0] + test_obj.assertEqual(expected, got, + "Bad content type: %s. Expected: %s" % + (got, expected)) + finally: + test_obj.content_type = orig_content_type + return wrapped + + +jsonify = functools.partial(content_type, content_type='json') +xmlify = functools.partial(content_type, content_type='xml') diff --git a/test/unit/test_authn_v2.py b/test/unit/test_authn_v2.py new file mode 100644 index 00000000..b3550033 --- /dev/null +++ b/test/unit/test_authn_v2.py @@ -0,0 +1,89 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2011 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging + +from test.unit import base +from test.unit.decorators import jsonify, xmlify +from test.unit import test_common as utils + +logger = logging.getLogger('test.unit.test_authn_v2') + + +class TestAuthnV2(base.ServiceAPITest): + + """ + Tests for the /v2.0/tokens auth endpoint + """ + + api_version = '2.0' + + @jsonify + def test_authn_json(self): + url = "/tokens" + req = self.get_request('GET', url) + body = { + "passwordCredentials": { + "username": self.auth_user['id'], + "password": self.auth_user['password'], + "tenantId": self.auth_user['tenant_id'] + } + } + req.body = json.dumps(body) + self.get_response() + self.status_ok() + + expected = { + u'auth': { + u'token': { + u'expires': self.expires.strftime("%Y-%m-%dT%H:%M:%S.%f"), + u'id': self.auth_token_id, + u'tenantId': self.auth_user['tenant_id'] + }, + u'user': { + u'username': self.auth_user['id'], + u'tenantId': self.auth_user['tenant_id'] + } + } + } + self.assert_dict_equal(expected, json.loads(self.res.body)) + + @xmlify + def test_authn_xml(self): + url = "/tokens" + req = self.get_request('GET', url) + req.body = ' \ + ' % (self.auth_user['password'], + self.auth_user['id'], + self.auth_user['tenant_id']) + self.get_response() + self.status_ok() + + expected = """ + + + + + """ % (self.expires.strftime("%Y-%m-%dT%H:%M:%S.%f"), + self.auth_token_id, + self.auth_user['tenant_id'], + self.auth_user['id'], + self.auth_user['tenant_id']) + self.assert_xml_strings_equal(expected, self.res.body) -- cgit From 878794491433f3b6f558c32d92c8d0f2a4e87bb1 Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Wed, 1 Jun 2011 13:29:15 -0500 Subject: Merging changes --- test/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/__init__.py (limited to 'test') diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b -- cgit From 38b977efd20092c40dd93337dfc647082d37722d Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Thu, 2 Jun 2011 12:57:06 -0500 Subject: Changes to support getTenants call for user with admin privelage and regular user. --- test/unit/test_tenants.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'test') diff --git a/test/unit/test_tenants.py b/test/unit/test_tenants.py index ec2a2569..bc639904 100644 --- a/test/unit/test_tenants.py +++ b/test/unit/test_tenants.py @@ -328,7 +328,7 @@ class CreateTenantTest(TenantTest): class GetTenantsTest(TenantTest): - def test_get_tenants(self): + def test_get_tenants_using_admin_token(self): header = httplib2.Http(".cache") resp, content = utils.create_tenant(self.tenant, str(self.auth_token)) url = '%stenants' % (utils.URL) @@ -342,7 +342,7 @@ class GetTenantsTest(TenantTest): self.fail('Service Not Available') self.assertEqual(200, int(resp['status'])) - def test_get_tenants_xml(self): + def test_get_tenants_using_admin_token_xml(self): header = httplib2.Http(".cache") resp, content = utils.create_tenant(self.tenant, str(self.auth_token)) url = '%stenants' % (utils.URL) @@ -357,7 +357,7 @@ class GetTenantsTest(TenantTest): self.fail('Service Not Available') self.assertEqual(200, int(resp['status'])) - def test_get_tenants_unauthorized_token(self): + def test_get_tenants_using_user_token(self): header = httplib2.Http(".cache") resp, content = utils.create_tenant(self.tenant, str(self.auth_token)) url = '%stenants' % (utils.URL) @@ -369,9 +369,9 @@ class GetTenantsTest(TenantTest): self.fail('Identity Fault') elif int(resp['status']) == 503: self.fail('Service Not Available') - self.assertEqual(401, int(resp['status'])) + self.assertEqual(200, int(resp['status'])) - def test_get_tenants_unauthorized_token_xml(self): + def test_get_tenants_using_user_token_xml(self): header = httplib2.Http(".cache") resp, content = utils.create_tenant(self.tenant, str(self.auth_token)) url = '%stenants' % (utils.URL) @@ -384,7 +384,7 @@ class GetTenantsTest(TenantTest): self.fail('Identity Fault') elif int(resp['status']) == 503: self.fail('Service Not Available') - self.assertEqual(401, int(resp['status'])) + self.assertEqual(200, int(resp['status'])) def test_get_tenants_exp_token(self): header = httplib2.Http(".cache") -- cgit