summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorZiad Sawalha <gihub@highbridgellc.com>2011-06-01 07:50:24 -0700
committerZiad Sawalha <gihub@highbridgellc.com>2011-06-01 07:50:24 -0700
commitcd8e6816ff7cd99db7c50fa2c9a2f96250d05860 (patch)
treea0726f9256ead0e8e4e4e97ac5409aa291e69a45
parent527ed7b06745afca5631c3f725e35f6c705765a2 (diff)
parent5fa9dfbed03a54a40a3ae18f55b849ef4a522b05 (diff)
downloadkeystone-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__.py0
-rw-r--r--test/unit/__init__.py0
-rw-r--r--test/unit/base.py275
-rw-r--r--test/unit/decorators.py49
-rw-r--r--test/unit/test_authn_v2.py89
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)