diff options
author | Luis A. Garcia <luis@linux.vnet.ibm.com> | 2013-07-18 00:49:49 +0000 |
---|---|---|
committer | Luis A. Garcia <luis@linux.vnet.ibm.com> | 2013-08-03 00:19:17 +0000 |
commit | 9004239b9ffc7d1ef6ae3393a8d500e1d822af4a (patch) | |
tree | 6c7dd692b3577d0f8e60b71f5bb82dea13ce5a90 | |
parent | 0af1565a5eefc2f040e955c1654518c3367758c1 (diff) | |
download | heat-9004239b9ffc7d1ef6ae3393a8d500e1d822af4a.tar.gz |
Enable localizable REST API responses via the Accept-Language header
Add support for doing language resolution for a request, based on the
Accept-Language HTTP header. Using the lazy gettext functionality, from
oslo gettextutils, it is possible to use the resolved language to
translate exception messages to the user requested language and
return that translation from the API.
The patch removes individually imported _() so they don't replace the
one installed service-wide.
Also, it adds the ability to fully re-create a remote error
with the same kwargs with which it was originally created, so that we
can translate it and show it to the user.
Partially implements bp user-locale-api.
Change-Id: I63edc8463836bfff257daa8a2c66ed5d3a444254
-rwxr-xr-x | bin/heat-api | 2 | ||||
-rwxr-xr-x | bin/heat-api-cfn | 2 | ||||
-rwxr-xr-x | bin/heat-api-cloudwatch | 2 | ||||
-rwxr-xr-x | bin/heat-cfn | 2 | ||||
-rwxr-xr-x | bin/heat-engine | 2 | ||||
-rwxr-xr-x | bin/heat-manage | 4 | ||||
-rw-r--r-- | heat/api/middleware/fault.py | 16 | ||||
-rw-r--r-- | heat/api/openstack/v1/__init__.py | 4 | ||||
-rw-r--r-- | heat/api/openstack/v1/events.py | 1 | ||||
-rw-r--r-- | heat/api/openstack/v1/stacks.py | 1 | ||||
-rw-r--r-- | heat/api/openstack/v1/util.py | 2 | ||||
-rw-r--r-- | heat/common/exception.py | 1 | ||||
-rw-r--r-- | heat/common/wsgi.py | 41 | ||||
-rw-r--r-- | heat/openstack/common/exception.py | 3 | ||||
-rw-r--r-- | heat/openstack/common/rpc/common.py | 1 | ||||
-rw-r--r-- | heat/tests/test_api_openstack_v1.py | 87 | ||||
-rw-r--r-- | heat/tests/test_wsgi.py | 52 |
17 files changed, 169 insertions, 54 deletions
diff --git a/bin/heat-api b/bin/heat-api index 4ac438596..8f58c0944 100755 --- a/bin/heat-api +++ b/bin/heat-api @@ -33,7 +33,7 @@ if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')): from heat.openstack.common import gettextutils -gettextutils.install('heat') +gettextutils.install('heat', lazy=True) from oslo.config import cfg diff --git a/bin/heat-api-cfn b/bin/heat-api-cfn index 8eceb6aef..95a946ca8 100755 --- a/bin/heat-api-cfn +++ b/bin/heat-api-cfn @@ -35,7 +35,7 @@ if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')): from heat.openstack.common import gettextutils -gettextutils.install('heat') +gettextutils.install('heat', lazy=True) from oslo.config import cfg diff --git a/bin/heat-api-cloudwatch b/bin/heat-api-cloudwatch index 7453b7744..155a4d48f 100755 --- a/bin/heat-api-cloudwatch +++ b/bin/heat-api-cloudwatch @@ -35,7 +35,7 @@ if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')): from heat.openstack.common import gettextutils -gettextutils.install('heat') +gettextutils.install('heat', lazy=True) from oslo.config import cfg diff --git a/bin/heat-cfn b/bin/heat-cfn index fc31938df..34a5bd0d8 100755 --- a/bin/heat-cfn +++ b/bin/heat-cfn @@ -41,7 +41,7 @@ scriptname = os.path.basename(sys.argv[0]) from heat.openstack.common import gettextutils -gettextutils.install('heat') +gettextutils.install('heat', lazy=True) if scriptname == 'heat-boto': from heat.cfn_client import boto_client as heat_client diff --git a/bin/heat-engine b/bin/heat-engine index 14e6f1851..36412cbae 100755 --- a/bin/heat-engine +++ b/bin/heat-engine @@ -36,7 +36,7 @@ if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'heat', '__init__.py')): from heat.openstack.common import gettextutils -gettextutils.install('heat') +gettextutils.install('heat', lazy=True) from oslo.config import cfg diff --git a/bin/heat-manage b/bin/heat-manage index 95e4a5d14..32bdc8f78 100755 --- a/bin/heat-manage +++ b/bin/heat-manage @@ -25,6 +25,10 @@ POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'heat', '__init__.py')): sys.path.insert(0, POSSIBLE_TOPDIR) +from heat.openstack.common import gettextutils + +gettextutils.install('heat') + from heat.cmd import manage manage.main() diff --git a/heat/api/middleware/fault.py b/heat/api/middleware/fault.py index 9df7ae60e..c8e77093f 100644 --- a/heat/api/middleware/fault.py +++ b/heat/api/middleware/fault.py @@ -23,6 +23,7 @@ Cinder's faultwrapper import traceback import webob +from heat.common import exception from heat.openstack.common import log as logging import heat.openstack.common.rpc.common as rpc_common @@ -79,11 +80,18 @@ class FaultWrapper(wsgi.Middleware): if ex_type.endswith(rpc_common._REMOTE_POSTFIX): ex_type = ex_type[:-len(rpc_common._REMOTE_POSTFIX)] - message = str(ex) - if message.find('\n') > -1: - message, trace = message.split('\n', 1) + if isinstance(ex, exception.OpenstackException): + # If the exception is an OpenstackException it is going to have a + # translated Message object as the message, let's recreate it here + message = (ex.message % ex.kwargs + if hasattr(ex, 'kwargs') else ex.message) + else: + message = ex.message + + trace = str(ex) + if trace.find('\n') > -1: + unused, trace = trace.split('\n', 1) else: - message = str(ex) trace = traceback.format_exc() webob_exc = self.error_map.get(ex_type, diff --git a/heat/api/openstack/v1/__init__.py b/heat/api/openstack/v1/__init__.py index a1ab038f1..f21f89b83 100644 --- a/heat/api/openstack/v1/__init__.py +++ b/heat/api/openstack/v1/__init__.py @@ -15,10 +15,6 @@ import routes -from heat.openstack.common import gettextutils - -gettextutils.install('heat') - from heat.api.openstack.v1 import stacks from heat.api.openstack.v1 import resources from heat.api.openstack.v1 import events diff --git a/heat/api/openstack/v1/events.py b/heat/api/openstack/v1/events.py index 81264a50e..b0c0bbb69 100644 --- a/heat/api/openstack/v1/events.py +++ b/heat/api/openstack/v1/events.py @@ -21,7 +21,6 @@ from heat.common import wsgi from heat.rpc import api as engine_api from heat.common import identifier from heat.rpc import client as rpc_client -from heat.openstack.common.gettextutils import _ summary_keys = [ diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 617f25a3f..a79db25a9 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -29,7 +29,6 @@ from heat.rpc import client as rpc_client from heat.common import urlfetch from heat.openstack.common import log as logging -from heat.openstack.common.gettextutils import _ logger = logging.getLogger(__name__) diff --git a/heat/api/openstack/v1/util.py b/heat/api/openstack/v1/util.py index 43381bfce..b6dcdc5ed 100644 --- a/heat/api/openstack/v1/util.py +++ b/heat/api/openstack/v1/util.py @@ -18,8 +18,6 @@ from functools import wraps from heat.common import identifier -from heat.openstack.common.gettextutils import _ - def tenant_local(handler): ''' diff --git a/heat/common/exception.py b/heat/common/exception.py index eb236a5a3..ed4fbc5c2 100644 --- a/heat/common/exception.py +++ b/heat/common/exception.py @@ -20,7 +20,6 @@ import functools import urlparse import sys -from heat.openstack.common.gettextutils import _ from heat.openstack.common import exception diff --git a/heat/common/wsgi.py b/heat/common/wsgi.py index f108fe68e..a4da6b0eb 100644 --- a/heat/common/wsgi.py +++ b/heat/common/wsgi.py @@ -2,6 +2,7 @@ # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. +# Copyright 2013 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -43,8 +44,8 @@ import webob.dec import webob.exc from heat.common import exception +from heat.openstack.common import gettextutils from heat.openstack.common import importutils -from heat.openstack.common.gettextutils import _ URL_LENGTH_LIMIT = 50000 @@ -431,6 +432,12 @@ class Request(webob.Request): else: return content_type + def best_match_language(self): + """Determine language for returned response.""" + all_languages = gettextutils.get_available_languages('heat') + return self.accept_language.best_match(all_languages, + default_match='en_US') + def is_json_content_type(request): if request.method == 'GET': @@ -589,7 +596,17 @@ class Resource(object): request, **action_args) except TypeError as err: logging.error(_('Exception handling resource: %s') % str(err)) - raise webob.exc.HTTPBadRequest() + msg = _('The server could not comply with the request since\r\n' + 'it is either malformed or otherwise incorrect.\r\n') + err = webob.exc.HTTPBadRequest(msg) + raise translate_exception(err, request.best_match_language()) + except webob.exc.HTTPException as err: + logging.error(_("Returning %(code)s to user: %(explanation)s"), + {'code': err.code, 'explanation': err.explanation}) + raise translate_exception(err, request.best_match_language()) + except Exception as err: + logging.error(_("Unexpected error occurred serving API: %s") % err) + raise translate_exception(err, request.best_match_language()) # Here we support either passing in a serializer or detecting it # based on the content type. @@ -653,6 +670,26 @@ class Resource(object): return args +def translate_exception(exc, locale): + """Translates all translatable elements of the given exception.""" + exc.message = gettextutils.get_localized_message(exc.message, locale) + if isinstance(exc, webob.exc.HTTPError): + # If the explanation is not a Message, that means that the + # explanation is the default, generic and not translatable explanation + # from webop.exc. Since the explanation is the error shown when the + # exception is converted to a response, let's actually swap it with + # message, since message is what gets passed in at construction time + # in the API + if not isinstance(exc.explanation, gettextutils.Message): + exc.explanation = exc.message + exc.detail = '' + else: + exc.explanation = \ + gettextutils.get_localized_message(exc.explanation, locale) + exc.detail = gettextutils.get_localized_message(exc.detail, locale) + return exc + + class BasePasteFactory(object): """A base class for paste app and filter factories. diff --git a/heat/openstack/common/exception.py b/heat/openstack/common/exception.py index ca1195a3e..9ba9fc144 100644 --- a/heat/openstack/common/exception.py +++ b/heat/openstack/common/exception.py @@ -21,8 +21,6 @@ Exceptions common to OpenStack projects import logging -from heat.openstack.common.gettextutils import _ - _FATAL_EXCEPTION_FORMAT_ERRORS = False @@ -120,6 +118,7 @@ class OpenstackException(Exception): def __init__(self, **kwargs): try: + self.kwargs = kwargs self._error_string = self.message % kwargs except Exception as e: diff --git a/heat/openstack/common/rpc/common.py b/heat/openstack/common/rpc/common.py index 31ecbdf3d..365161124 100644 --- a/heat/openstack/common/rpc/common.py +++ b/heat/openstack/common/rpc/common.py @@ -24,7 +24,6 @@ import traceback from oslo.config import cfg import six -from heat.openstack.common.gettextutils import _ from heat.openstack.common import importutils from heat.openstack.common import jsonutils from heat.openstack.common import local diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py index db0275a28..12be4f89c 100644 --- a/heat/tests/test_api_openstack_v1.py +++ b/heat/tests/test_api_openstack_v1.py @@ -47,11 +47,15 @@ def request_with_middleware(middleware, func, req, *args, **kwargs): return resp -def remote_error(ex_type, message=''): - """convert rpc original exception to the one with _Remote suffix.""" - - # NOTE(jianingy): this function helps simulate the real world exceptions +def to_remote_error(error): + """Converts the given exception to the one with the _Remote suffix. + This is how RPC exceptions are recreated on the caller's side, so + this helps better simulate how the exception mechanism actually works. + """ + ex_type = type(error) + kwargs = error.kwargs if hasattr(error, 'kwargs') else {} + message = error.message module = ex_type().__class__.__module__ str_override = lambda self: "%s\n<Traceback>" % message new_ex_type = type(ex_type.__name__ + rpc_common._REMOTE_POSTFIX, @@ -59,7 +63,7 @@ def remote_error(ex_type, message=''): {'__str__': str_override, '__unicode__': str_override}) new_ex_type.__module__ = '%s%s' % (module, rpc_common._REMOTE_POSTFIX) - return new_ex_type() + return new_ex_type(**kwargs) class InstantiationDataTest(HeatTestCase): @@ -398,7 +402,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'method': 'list_stacks', 'args': {}, 'version': self.api_version}, - None).AndRaise(remote_error(AttributeError)) + None).AndRaise(to_remote_error(AttributeError())) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -418,7 +422,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'method': 'list_stacks', 'args': {}, 'version': self.api_version}, - None).AndRaise(remote_error(Exception)) + None).AndRaise(to_remote_error(Exception())) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -513,6 +517,8 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._post('/stacks', json.dumps(body)) + unknown_parameter = heat_exc.UnknownUserParameter(key='a') + missing_parameter = heat_exc.UserParameterMissing(key='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -523,7 +529,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'files': {}, 'args': {'timeout_mins': 30}}, 'version': self.api_version}, - None).AndRaise(remote_error(AttributeError)) + None).AndRaise(to_remote_error(AttributeError())) rpc.call(req.context, self.topic, {'namespace': None, 'method': 'create_stack', @@ -533,7 +539,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'files': {}, 'args': {'timeout_mins': 30}}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.UnknownUserParameter)) + None).AndRaise(to_remote_error(unknown_parameter)) rpc.call(req.context, self.topic, {'namespace': None, 'method': 'create_stack', @@ -543,7 +549,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'files': {}, 'args': {'timeout_mins': 30}}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.UserParameterMissing)) + None).AndRaise(to_remote_error(missing_parameter)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, self.controller.create, @@ -579,6 +585,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._post('/stacks', json.dumps(body)) + error = heat_exc.StackExists(stack_name='s') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -589,7 +596,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'files': {}, 'args': {'timeout_mins': 30}}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackExists)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -612,6 +619,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._post('/stacks', json.dumps(body)) + error = heat_exc.StackValidationFailed(message='') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -622,7 +630,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'files': {}, 'args': {'timeout_mins': 30}}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackValidationFailed)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -678,13 +686,14 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._get('/stacks/%(stack_name)s' % locals()) + error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'identify_stack', 'args': {'stack_name': stack_name}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -727,13 +736,14 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._get('/stacks/%(stack_name)s/resources' % locals()) + error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'identify_stack', 'args': {'stack_name': stack_name}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -820,13 +830,14 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._get('/stacks/%(stack_name)s/%(stack_id)s' % identity) + error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'show_stack', 'args': {'stack_identity': dict(identity)}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -844,13 +855,14 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._get('/stacks/%(stack_name)s/%(stack_id)s' % identity) + error = heat_exc.InvalidTenant(target='a', actual='b') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'show_stack', 'args': {'stack_identity': dict(identity)}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.InvalidTenant)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -887,15 +899,15 @@ class StackControllerTest(ControllerTest, HeatTestCase): def test_get_template_err_notfound(self): identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6') req = self._get('/stacks/%(stack_name)s/%(stack_id)s' % identity) - template = {u'Foo': u'bar'} + error = error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'get_template', 'args': {'stack_identity': dict(identity)}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() @@ -958,6 +970,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._put('/stacks/%(stack_name)s/%(stack_id)s' % identity, json.dumps(body)) + error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -968,7 +981,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'files': {}, 'args': {'timeout_mins': 30}}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1021,6 +1034,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): req = self._delete('/stacks/%(stack_name)s/%(stack_id)s' % identity) + error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') # Engine returns None when delete successful rpc.call(req.context, self.topic, @@ -1028,7 +1042,7 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'method': 'delete_stack', 'args': {'stack_identity': dict(identity)}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1122,13 +1136,14 @@ class StackControllerTest(ControllerTest, HeatTestCase): 'AWS::EC2::EIP', 'AWS::EC2::EIPAssociation'] + error = heat_exc.ServerError(body='') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'list_resource_types', 'args': {}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.ServerError)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1158,13 +1173,15 @@ class StackControllerTest(ControllerTest, HeatTestCase): def test_generate_template_not_found(self): req = self._get('/resource_types/NOT_FOUND/template') + + error = heat_exc.ResourceTypeNotFound(type_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'generate_template', 'args': {'type_name': 'NOT_FOUND'}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.ResourceTypeNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, self.controller.generate_template, @@ -1267,13 +1284,14 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): req = self._get(stack_identity._tenant_path() + '/resources') + error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'list_stack_resources', 'args': {'stack_identity': stack_identity}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1354,6 +1372,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): req = self._get(res_identity._tenant_path()) + error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -1361,7 +1380,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): 'args': {'stack_identity': stack_identity, 'resource_name': res_name}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1384,6 +1403,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): req = self._get(res_identity._tenant_path()) + error = heat_exc.ResourceNotFound(stack_name='a', resource_name='b') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -1391,7 +1411,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): 'args': {'stack_identity': stack_identity, 'resource_name': res_name}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.ResourceNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1414,6 +1434,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): req = self._get(res_identity._tenant_path()) + error = heat_exc.ResourceNotAvailable(resource_name='') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -1421,7 +1442,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): 'args': {'stack_identity': stack_identity, 'resource_name': res_name}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.ResourceNotAvailable)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1488,6 +1509,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): req = self._get(res_identity._tenant_path() + '/metadata') + error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -1495,7 +1517,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): 'args': {'stack_identity': stack_identity, 'resource_name': res_name}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1518,6 +1540,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): req = self._get(res_identity._tenant_path() + '/metadata') + error = heat_exc.ResourceNotFound(stack_name='a', resource_name='b') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, @@ -1525,7 +1548,7 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): 'args': {'stack_identity': stack_identity, 'resource_name': res_name}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.ResourceNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1699,13 +1722,14 @@ class EventControllerTest(ControllerTest, HeatTestCase): req = self._get(stack_identity._tenant_path() + '/events') + error = error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'list_events', 'args': {'stack_identity': stack_identity}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1943,13 +1967,14 @@ class EventControllerTest(ControllerTest, HeatTestCase): req = self._get(stack_identity._tenant_path() + '/resources/' + res_name + '/events/' + event_id) + error = error = heat_exc.StackNotFound(stack_name='a') self.m.StubOutWithMock(rpc, 'call') rpc.call(req.context, self.topic, {'namespace': None, 'method': 'list_events', 'args': {'stack_identity': stack_identity}, 'version': self.api_version}, - None).AndRaise(remote_error(heat_exc.StackNotFound)) + None).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -2380,7 +2405,7 @@ class ActionControllerTest(ControllerTest, HeatTestCase): 'method': 'stack_suspend', 'args': {'stack_identity': stack_identity}, 'version': self.api_version}, - None).AndRaise(remote_error(AttributeError)) + None).AndRaise(to_remote_error(AttributeError())) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, diff --git a/heat/tests/test_wsgi.py b/heat/tests/test_wsgi.py index e6e113027..ab26f2547 100644 --- a/heat/tests/test_wsgi.py +++ b/heat/tests/test_wsgi.py @@ -17,6 +17,7 @@ import datetime +import stubout import webob from heat.common import exception @@ -26,6 +27,10 @@ from heat.tests.common import HeatTestCase class RequestTest(HeatTestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + super(RequestTest, self).setUp() + def test_content_type_missing(self): request = wsgi.Request.blank('/tests/123') self.assertRaises(exception.InvalidContentType, @@ -74,9 +79,28 @@ class RequestTest(HeatTestCase): result = request.best_match_content_type() self.assertEqual(result, "application/json") + def test_best_match_language(self): + # Here we test that we are actually invoking language negotiation + # by webop and also that the default locale always available is en-US + request = wsgi.Request.blank('/') + accepted = 'unknown-lang' + request.headers = {'Accept-Language': accepted} + + def fake_best_match(self, offers, default_match=None): + return default_match + + self.stubs.SmartSet(request.accept_language, + 'best_match', fake_best_match) + + self.assertEqual(request.best_match_language(), 'en_US') + class ResourceTest(HeatTestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + super(ResourceTest, self).setUp() + def test_get_action_args(self): env = { 'wsgiorg.routing_args': [ @@ -160,6 +184,34 @@ class ResourceTest(HeatTestCase): None) self.assertRaises(webob.exc.HTTPBadRequest, resource, request) + def test_resource_call_error_handle_localized(self): + class Controller(object): + def delete(self, req, identity): + return (req, identity) + + actions = {'action': 'delete', 'id': 12, 'body': 'data'} + env = {'wsgiorg.routing_args': [None, actions]} + request = wsgi.Request.blank('/tests/123', environ=env) + request.body = '{"foo" : "value"}' + message_es = "No Encontrado" + translated_ex = webob.exc.HTTPBadRequest(message_es) + + resource = wsgi.Resource(Controller(), + wsgi.JSONRequestDeserializer(), + None) + + def fake_translate_exception(ex, locale): + return translated_ex + + self.stubs.SmartSet(wsgi, + 'translate_exception', fake_translate_exception) + + try: + resource(request) + except webob.exc.HTTPBadRequest as e: + self.assertEquals(message_es, e.message) + self.m.VerifyAll() + class JSONResponseSerializerTest(HeatTestCase): |