Splits OpenStack Dashboard bits from framework app code.
Moves everything OpenStack-specific (dashboards, apis, etc.) into the openstack_dashboard project, achieving a much cleaner separation between the project-specific code and the generic Horizon framework code. Change-Id: I7235b41d449b26c980668fc3eb4360b24508717b
This commit is contained in:
parent
530a056c45
commit
f99d890fd4
|
@ -0,0 +1,39 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Methods and interface objects used to interact with external APIs.
|
||||
|
||||
API method calls return objects that are in many cases objects with
|
||||
attributes that are direct maps to the data returned from the API http call.
|
||||
Unfortunately, these objects are also often constructed dynamically, making
|
||||
it difficult to know what data is available from the API object. Because of
|
||||
this, all API calls should wrap their returned object in one defined here,
|
||||
using only explicitly defined atributes and/or methods.
|
||||
|
||||
In other words, Horizon developers not working on openstack_dashboard.api
|
||||
shouldn't need to understand the finer details of APIs for
|
||||
Keystone/Nova/Glance/Swift et. al.
|
||||
"""
|
||||
from openstack_dashboard.api.glance import *
|
||||
from openstack_dashboard.api.keystone import *
|
||||
from openstack_dashboard.api.nova import *
|
||||
from openstack_dashboard.api.swift import *
|
||||
from openstack_dashboard.api.quantum import *
|
|
@ -0,0 +1,298 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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 functools import wraps
|
||||
import os
|
||||
|
||||
from django import http
|
||||
from django.conf import settings
|
||||
from django.contrib.messages.storage import default_storage
|
||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
from django.core.handlers import wsgi
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import unittest
|
||||
|
||||
import glanceclient
|
||||
from keystoneclient.v2_0 import client as keystone_client
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
from quantumclient.v2_0 import client as quantum_client
|
||||
from swiftclient import client as swift_client
|
||||
|
||||
import httplib2
|
||||
import mox
|
||||
|
||||
from openstack_auth import utils, user
|
||||
|
||||
from horizon.test import helpers as horizon_helpers
|
||||
from horizon import middleware
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard import context_processors
|
||||
from openstack_dashboard.test.test_data.utils import load_test_data
|
||||
|
||||
|
||||
# Makes output of failing mox tests much easier to read.
|
||||
wsgi.WSGIRequest.__repr__ = lambda self: "<class 'django.http.HttpRequest'>"
|
||||
|
||||
|
||||
def create_stubs(stubs_to_create={}):
|
||||
if not isinstance(stubs_to_create, dict):
|
||||
raise TypeError("create_stub must be passed a dict, but a %s was "
|
||||
"given." % type(stubs_to_create).__name__)
|
||||
|
||||
def inner_stub_out(fn):
|
||||
@wraps(fn)
|
||||
def instance_stub_out(self):
|
||||
for key in stubs_to_create:
|
||||
if not (isinstance(stubs_to_create[key], tuple) or
|
||||
isinstance(stubs_to_create[key], list)):
|
||||
raise TypeError("The values of the create_stub "
|
||||
"dict must be lists or tuples, but "
|
||||
"is a %s."
|
||||
% type(stubs_to_create[key]).__name__)
|
||||
|
||||
for value in stubs_to_create[key]:
|
||||
self.mox.StubOutWithMock(key, value)
|
||||
return fn(self)
|
||||
return instance_stub_out
|
||||
return inner_stub_out
|
||||
|
||||
|
||||
class RequestFactoryWithMessages(RequestFactory):
|
||||
def get(self, *args, **kwargs):
|
||||
req = super(RequestFactoryWithMessages, self).get(*args, **kwargs)
|
||||
req.user = utils.get_user(req)
|
||||
req.session = []
|
||||
req._messages = default_storage(req)
|
||||
return req
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
req = super(RequestFactoryWithMessages, self).post(*args, **kwargs)
|
||||
req.user = utils.get_user(req)
|
||||
req.session = []
|
||||
req._messages = default_storage(req)
|
||||
return req
|
||||
|
||||
|
||||
@unittest.skipIf(os.environ.get('SKIP_UNITTESTS', False),
|
||||
"The SKIP_UNITTESTS env variable is set.")
|
||||
class TestCase(horizon_helpers.TestCase):
|
||||
"""
|
||||
Specialized base test case class for Horizon which gives access to
|
||||
numerous additional features:
|
||||
|
||||
* A full suite of test data through various attached objects and
|
||||
managers (e.g. ``self.servers``, ``self.user``, etc.). See the
|
||||
docs for :class:`~horizon.tests.test_data.utils.TestData` for more
|
||||
information.
|
||||
* The ``mox`` mocking framework via ``self.mox``.
|
||||
* A set of request context data via ``self.context``.
|
||||
* A ``RequestFactory`` class which supports Django's ``contrib.messages``
|
||||
framework via ``self.factory``.
|
||||
* A ready-to-go request object via ``self.request``.
|
||||
* The ability to override specific time data controls for easier testing.
|
||||
* Several handy additional assertion methods.
|
||||
"""
|
||||
def setUp(self):
|
||||
load_test_data(self)
|
||||
self.mox = mox.Mox()
|
||||
self.factory = RequestFactoryWithMessages()
|
||||
self.context = {'authorized_tenants': self.tenants.list()}
|
||||
|
||||
def fake_conn_request(*args, **kwargs):
|
||||
raise Exception("An external URI request tried to escape through "
|
||||
"an httplib2 client. Args: %s, kwargs: %s"
|
||||
% (args, kwargs))
|
||||
|
||||
self._real_conn_request = httplib2.Http._conn_request
|
||||
httplib2.Http._conn_request = fake_conn_request
|
||||
|
||||
self._real_context_processor = context_processors.openstack
|
||||
context_processors.openstack = lambda request: self.context
|
||||
|
||||
self._real_get_user = utils.get_user
|
||||
tenants = self.context['authorized_tenants']
|
||||
self.setActiveUser(id=self.user.id,
|
||||
token=self.token,
|
||||
username=self.user.name,
|
||||
tenant_id=self.tenant.id,
|
||||
service_catalog=self.service_catalog,
|
||||
authorized_tenants=tenants)
|
||||
self.request = http.HttpRequest()
|
||||
self.request.session = self.client._session()
|
||||
self.request.session['token'] = self.token.id
|
||||
middleware.HorizonMiddleware().process_request(self.request)
|
||||
AuthenticationMiddleware().process_request(self.request)
|
||||
os.environ["HORIZON_TEST_RUN"] = "True"
|
||||
|
||||
def tearDown(self):
|
||||
self.mox.UnsetStubs()
|
||||
httplib2.Http._conn_request = self._real_conn_request
|
||||
context_processors.openstack = self._real_context_processor
|
||||
utils.get_user = self._real_get_user
|
||||
self.mox.VerifyAll()
|
||||
del os.environ["HORIZON_TEST_RUN"]
|
||||
|
||||
def setActiveUser(self, id=None, token=None, username=None, tenant_id=None,
|
||||
service_catalog=None, tenant_name=None, roles=None,
|
||||
authorized_tenants=None, enabled=True):
|
||||
def get_user(request):
|
||||
return user.User(id=id,
|
||||
token=token,
|
||||
user=username,
|
||||
tenant_id=tenant_id,
|
||||
service_catalog=service_catalog,
|
||||
roles=roles,
|
||||
enabled=enabled,
|
||||
authorized_tenants=authorized_tenants,
|
||||
endpoint=settings.OPENSTACK_KEYSTONE_URL)
|
||||
utils.get_user = get_user
|
||||
|
||||
def assertRedirectsNoFollow(self, response, expected_url):
|
||||
"""
|
||||
Asserts that the given response issued a 302 redirect without
|
||||
processing the view which is redirected to.
|
||||
"""
|
||||
assert (response.status_code / 100 == 3), \
|
||||
"The response did not return a redirect."
|
||||
self.assertEqual(response._headers.get('location', None),
|
||||
('Location', settings.TESTSERVER + expected_url))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def assertNoFormErrors(self, response, context_name="form"):
|
||||
"""
|
||||
Asserts that the response either does not contain a form in it's
|
||||
context, or that if it does, that form has no errors.
|
||||
"""
|
||||
context = getattr(response, "context", {})
|
||||
if not context or context_name not in context:
|
||||
return True
|
||||
errors = response.context[context_name]._errors
|
||||
assert len(errors) == 0, \
|
||||
"Unexpected errors were found on the form: %s" % errors
|
||||
|
||||
def assertFormErrors(self, response, count=0, message=None,
|
||||
context_name="form"):
|
||||
"""
|
||||
Asserts that the response does contain a form in it's
|
||||
context, and that form has errors, if count were given,
|
||||
it must match the exact numbers of errors
|
||||
"""
|
||||
context = getattr(response, "context", {})
|
||||
assert (context and context_name in context), \
|
||||
"The response did not contain a form."
|
||||
errors = response.context[context_name]._errors
|
||||
if count:
|
||||
assert len(errors) == count, \
|
||||
"%d errors were found on the form, %d expected" % \
|
||||
(len(errors), count)
|
||||
if message and message not in unicode(errors):
|
||||
self.fail("Expected message not found, instead found: %s"
|
||||
% ["%s: %s" % (key, [e for e in field_errors]) for
|
||||
(key, field_errors) in errors.items()])
|
||||
else:
|
||||
assert len(errors) > 0, "No errors were found on the form"
|
||||
|
||||
|
||||
class BaseAdminViewTests(TestCase):
|
||||
"""
|
||||
A ``TestCase`` subclass which sets an active user with the "admin" role
|
||||
for testing admin-only views and functionality.
|
||||
"""
|
||||
def setActiveUser(self, *args, **kwargs):
|
||||
if "roles" not in kwargs:
|
||||
kwargs['roles'] = [self.roles.admin._info]
|
||||
super(BaseAdminViewTests, self).setActiveUser(*args, **kwargs)
|
||||
|
||||
|
||||
class APITestCase(TestCase):
|
||||
"""
|
||||
The ``APITestCase`` class is for use with tests which deal with the
|
||||
underlying clients rather than stubbing out the
|
||||
openstack_dashboard.api.* methods.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(APITestCase, self).setUp()
|
||||
|
||||
def fake_keystoneclient(request, admin=False):
|
||||
"""
|
||||
Wrapper function which returns the stub keystoneclient. Only
|
||||
necessary because the function takes too many arguments to
|
||||
conveniently be a lambda.
|
||||
"""
|
||||
return self.stub_keystoneclient()
|
||||
|
||||
# Store the original clients
|
||||
self._original_glanceclient = api.glance.glanceclient
|
||||
self._original_keystoneclient = api.keystone.keystoneclient
|
||||
self._original_novaclient = api.nova.novaclient
|
||||
self._original_quantumclient = api.quantum.quantumclient
|
||||
|
||||
# Replace the clients with our stubs.
|
||||
api.glance.glanceclient = lambda request: self.stub_glanceclient()
|
||||
api.keystone.keystoneclient = fake_keystoneclient
|
||||
api.nova.novaclient = lambda request: self.stub_novaclient()
|
||||
api.quantum.quantumclient = lambda request: self.stub_quantumclient()
|
||||
|
||||
def tearDown(self):
|
||||
super(APITestCase, self).tearDown()
|
||||
api.glance.glanceclient = self._original_glanceclient
|
||||
api.nova.novaclient = self._original_novaclient
|
||||
api.keystone.keystoneclient = self._original_keystoneclient
|
||||
api.quantum.quantumclient = self._original_quantumclient
|
||||
|
||||
def stub_novaclient(self):
|
||||
if not hasattr(self, "novaclient"):
|
||||
self.mox.StubOutWithMock(nova_client, 'Client')
|
||||
self.novaclient = self.mox.CreateMock(nova_client.Client)
|
||||
return self.novaclient
|
||||
|
||||
def stub_keystoneclient(self):
|
||||
if not hasattr(self, "keystoneclient"):
|
||||
self.mox.StubOutWithMock(keystone_client, 'Client')
|
||||
self.keystoneclient = self.mox.CreateMock(keystone_client.Client)
|
||||
return self.keystoneclient
|
||||
|
||||
def stub_glanceclient(self):
|
||||
if not hasattr(self, "glanceclient"):
|
||||
self.mox.StubOutWithMock(glanceclient, 'Client')
|
||||
self.glanceclient = self.mox.CreateMock(glanceclient.Client)
|
||||
return self.glanceclient
|
||||
|
||||
def stub_quantumclient(self):
|
||||
if not hasattr(self, "quantumclient"):
|
||||
self.mox.StubOutWithMock(quantum_client, 'Client')
|
||||
self.quantumclient = self.mox.CreateMock(quantum_client.Client)
|
||||
return self.quantumclient
|
||||
|
||||
def stub_swiftclient(self, expected_calls=1):
|
||||
if not hasattr(self, "swiftclient"):
|
||||
self.mox.StubOutWithMock(swift_client, 'Connection')
|
||||
self.swiftclient = self.mox.CreateMock(swift_client.Connection)
|
||||
while expected_calls:
|
||||
swift_client.Connection(None,
|
||||
mox.IgnoreArg(),
|
||||
None,
|
||||
preauthtoken=mox.IgnoreArg(),
|
||||
preauthurl=mox.IgnoreArg(),
|
||||
auth_version="2.0") \
|
||||
.AndReturn(self.swiftclient)
|
||||
expected_calls -= 1
|
||||
return self.swiftclient
|
|
@ -1,14 +1,85 @@
|
|||
import os
|
||||
|
||||
from horizon.tests.testsettings import *
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon.test.settings import *
|
||||
from horizon.utils.secret_key import generate_or_read_from_file
|
||||
|
||||
from openstack_dashboard.exceptions import UNAUTHORIZED, RECOVERABLE, NOT_FOUND
|
||||
|
||||
|
||||
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, ".."))
|
||||
|
||||
SECRET_KEY = generate_or_read_from_file(os.path.join(TEST_DIR,
|
||||
'.secret_key_store'))
|
||||
ROOT_URLCONF = 'openstack_dashboard.urls'
|
||||
TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),)
|
||||
STATICFILES_DIRS = (os.path.join(ROOT_PATH, 'static'),)
|
||||
INSTALLED_APPS += ('openstack_dashboard',)
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(TEST_DIR, 'templates'),
|
||||
#os.path.join(ROOT_PATH, 'templates'),
|
||||
)
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS += (
|
||||
'openstack_dashboard.context_processors.openstack',
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.humanize',
|
||||
'django_nose',
|
||||
'openstack_auth',
|
||||
'compressor',
|
||||
'horizon',
|
||||
'openstack_dashboard',
|
||||
'openstack_dashboard.dashboards.project',
|
||||
'openstack_dashboard.dashboards.admin',
|
||||
'openstack_dashboard.dashboards.settings',
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
|
||||
|
||||
SITE_BRANDING = 'OpenStack'
|
||||
|
||||
HORIZON_CONFIG = {
|
||||
'dashboards': ('project', 'admin', 'settings'),
|
||||
'default_dashboard': 'project',
|
||||
"password_validator": {
|
||||
"regex": '^.{8,18}$',
|
||||
"help_text": _("Password must be between 8 and 18 characters.")
|
||||
},
|
||||
'user_home': None,
|
||||
'help_url': "http://docs.openstack.org",
|
||||
'exceptions': {'recoverable': RECOVERABLE,
|
||||
'not_found': NOT_FOUND,
|
||||
'unauthorized': UNAUTHORIZED},
|
||||
}
|
||||
|
||||
AVAILABLE_REGIONS = [
|
||||
('http://localhost:5000/v2.0', 'local'),
|
||||
('http://remote:5000/v2.0', 'remote'),
|
||||
]
|
||||
|
||||
OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0"
|
||||
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member"
|
||||
|
||||
OPENSTACK_KEYSTONE_BACKEND = {
|
||||
'name': 'native',
|
||||
'can_edit_user': True
|
||||
}
|
||||
|
||||
OPENSTACK_HYPERVISOR_FEATURES = {
|
||||
'can_set_mount_point': True
|
||||
}
|
||||
|
||||
LOGGING['loggers']['openstack_dashboard'] = {
|
||||
'handlers': ['test'],
|
||||
'propagate': False,
|
||||
}
|
||||
|
||||
NOSE_ARGS = ['--nocapture',
|
||||
'--nologcapture',
|
||||
'--cover-package=openstack_dashboard',
|
||||
'--cover-inclusive',
|
||||
'--all-modules']
|
||||
|
|
Loading…
Reference in New Issue