diff options
| author | Ziad Sawalha <gihub@highbridgellc.com> | 2011-06-01 07:50:24 -0700 |
|---|---|---|
| committer | Ziad Sawalha <gihub@highbridgellc.com> | 2011-06-01 07:50:24 -0700 |
| commit | cd8e6816ff7cd99db7c50fa2c9a2f96250d05860 (patch) | |
| tree | a0726f9256ead0e8e4e4e97ac5409aa291e69a45 | |
| parent | 527ed7b06745afca5631c3f725e35f6c705765a2 (diff) | |
| parent | 5fa9dfbed03a54a40a3ae18f55b849ef4a522b05 (diff) | |
| download | keystone-cd8e6816ff7cd99db7c50fa2c9a2f96250d05860.tar.gz keystone-cd8e6816ff7cd99db7c50fa2c9a2f96250d05860.tar.xz keystone-cd8e6816ff7cd99db7c50fa2c9a2f96250d05860.zip | |
Merge pull request #9 from jaypipes/unittest
Adds unit testing base class that takes care of
| -rw-r--r-- | test/__init__.py | 0 | ||||
| -rw-r--r-- | test/unit/__init__.py | 0 | ||||
| -rw-r--r-- | test/unit/base.py | 275 | ||||
| -rw-r--r-- | test/unit/decorators.py | 49 | ||||
| -rw-r--r-- | test/unit/test_authn_v2.py | 89 |
5 files changed, 413 insertions, 0 deletions
diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/__init__.py diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/unit/__init__.py 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 = '<?xml version="1.0" encoding="UTF-8"?> \ + <passwordCredentials \ + xmlns="http://docs.openstack.org/identity/api/v2.0" \ + password="%s" username="%s" \ + tenantId="%s"/> ' % (self.auth_user['password'], + self.auth_user['id'], + self.auth_user['tenant_id']) + self.get_response() + self.status_ok() + + expected = """ + <auth xmlns="http://docs.openstack.org/identity/api/v2.0"> + <token expires="%s" id="%s" tenantId="%s"/> + <user username="%s" tenantId="%s"/> + </auth> + """ % (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) |
