proper handling of unsupported request methods
This change adds support for responding to unsupported methods for all routes. Invoking a route with an unsupported request method returns a 405 status code, and the response includes the 'Allow' header listing the request methods supported by the requested URL. The OPTIONS request method is also automatically supported for all URLs, with the response returning the 'Allow' header. The way routes are registered has been changed so that unsupported methods can be detected automatically. Change-Id: I2de28e0fc6cd35ed060395405aa3770b1dc73376 Closes-Bug: 1367057 APIImpact DocImpact
This commit is contained in:
parent
c16f539c53
commit
8a2e0aff98
@ -12,6 +12,7 @@
|
||||
# under the License.
|
||||
|
||||
import routes
|
||||
import six
|
||||
|
||||
from heat.api.openstack.v1 import actions
|
||||
from heat.api.openstack.v1 import build_info
|
||||
@ -32,248 +33,368 @@ class API(wsgi.Router):
|
||||
def __init__(self, conf, **local_conf):
|
||||
self.conf = conf
|
||||
mapper = routes.Mapper()
|
||||
default_resource = wsgi.Resource(wsgi.DefaultMethodController(),
|
||||
wsgi.JSONRequestDeserializer())
|
||||
|
||||
def connect(controller, path_prefix, routes):
|
||||
"""
|
||||
This function connects the list of routes to the given
|
||||
controller, prepending the given path_prefix. Then for each URL it
|
||||
finds which request methods aren't handled and configures those
|
||||
to return a 405 error. Finally, it adds a handler for the
|
||||
OPTIONS method to all URLs that returns the list of allowed
|
||||
methods with 204 status code.
|
||||
"""
|
||||
# register the routes with the mapper, while keeping track of which
|
||||
# methods are defined for each URL
|
||||
urls = {}
|
||||
for r in routes:
|
||||
url = path_prefix + r['url']
|
||||
methods = r['method']
|
||||
if isinstance(methods, six.string_types):
|
||||
methods = [methods]
|
||||
methods_str = ','.join(methods)
|
||||
mapper.connect(r['name'], url, controller=controller,
|
||||
action=r['action'],
|
||||
conditions={'method': methods_str})
|
||||
if url not in urls:
|
||||
urls[url] = methods
|
||||
else:
|
||||
urls[url] += methods
|
||||
|
||||
# now register the missing methods to return 405s, and register
|
||||
# a handler for OPTIONS that returns the list of allowed methods
|
||||
for url, methods in urls.items():
|
||||
all_methods = ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
missing_methods = [m for m in all_methods if m not in methods]
|
||||
allowed_methods_str = ','.join(methods)
|
||||
mapper.connect(url,
|
||||
controller=default_resource,
|
||||
action='reject',
|
||||
allowed_methods=allowed_methods_str,
|
||||
conditions={'method': missing_methods})
|
||||
if 'OPTIONS' not in methods:
|
||||
mapper.connect(url,
|
||||
controller=default_resource,
|
||||
action='options',
|
||||
allowed_methods=allowed_methods_str,
|
||||
conditions={'method': 'OPTIONS'})
|
||||
|
||||
# Stacks
|
||||
stacks_resource = stacks.create_resource(conf)
|
||||
with mapper.submapper(controller=stacks_resource,
|
||||
path_prefix="/{tenant_id}") as stack_mapper:
|
||||
connect(controller=stacks_resource,
|
||||
path_prefix='/{tenant_id}',
|
||||
routes=[
|
||||
# Template handling
|
||||
stack_mapper.connect("template_validate",
|
||||
"/validate",
|
||||
action="validate_template",
|
||||
conditions={'method': 'POST'})
|
||||
stack_mapper.connect("resource_types",
|
||||
"/resource_types",
|
||||
action="list_resource_types",
|
||||
conditions={'method': 'GET'})
|
||||
stack_mapper.connect("resource_schema",
|
||||
"/resource_types/{type_name}",
|
||||
action="resource_schema",
|
||||
conditions={'method': 'GET'})
|
||||
stack_mapper.connect("generate_template",
|
||||
"/resource_types/{type_name}/template",
|
||||
action="generate_template",
|
||||
conditions={'method': 'GET'})
|
||||
{
|
||||
'name': 'template_validate',
|
||||
'url': '/validate',
|
||||
'action': 'validate_template',
|
||||
'method': 'POST'
|
||||
},
|
||||
{
|
||||
'name': 'resource_types',
|
||||
'url': '/resource_types',
|
||||
'action': 'list_resource_types',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'resource_schema',
|
||||
'url': '/resource_types/{type_name}',
|
||||
'action': 'resource_schema',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'generate_template',
|
||||
'url': '/resource_types/{type_name}/template',
|
||||
'action': 'generate_template',
|
||||
'method': 'GET'
|
||||
},
|
||||
|
||||
# Stack collection
|
||||
stack_mapper.connect("stack_index",
|
||||
"/stacks",
|
||||
action="index",
|
||||
conditions={'method': 'GET'})
|
||||
stack_mapper.connect("stack_create",
|
||||
"/stacks",
|
||||
action="create",
|
||||
conditions={'method': 'POST'})
|
||||
stack_mapper.connect("stack_preview",
|
||||
"/stacks/preview",
|
||||
action="preview",
|
||||
conditions={'method': 'POST'})
|
||||
stack_mapper.connect("stack_detail",
|
||||
"/stacks/detail",
|
||||
action="detail",
|
||||
conditions={'method': 'GET'})
|
||||
{
|
||||
'name': 'stack_index',
|
||||
'url': '/stacks',
|
||||
'action': 'index',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'stack_create',
|
||||
'url': '/stacks',
|
||||
'action': 'create',
|
||||
'method': 'POST'
|
||||
},
|
||||
{
|
||||
'name': 'stack_preview',
|
||||
'url': '/stacks/preview',
|
||||
'action': 'preview',
|
||||
'method': 'POST'
|
||||
},
|
||||
{
|
||||
'name': 'stack_detail',
|
||||
'url': '/stacks/detail',
|
||||
'action': 'detail',
|
||||
'method': 'GET'
|
||||
},
|
||||
|
||||
# Stack data
|
||||
stack_mapper.connect("stack_lookup",
|
||||
"/stacks/{stack_name}",
|
||||
action="lookup")
|
||||
{
|
||||
'name': 'stack_lookup',
|
||||
'url': '/stacks/{stack_name}',
|
||||
'action': 'lookup',
|
||||
'method': ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
},
|
||||
# \x3A matches on a colon.
|
||||
# Routes treats : specially in its regexp
|
||||
stack_mapper.connect("stack_lookup",
|
||||
r"/stacks/{stack_name:arn\x3A.*}",
|
||||
action="lookup")
|
||||
subpaths = ['resources', 'events', 'template', 'actions']
|
||||
path = "{path:%s}" % '|'.join(subpaths)
|
||||
stack_mapper.connect("stack_lookup_subpath",
|
||||
"/stacks/{stack_name}/" + path,
|
||||
action="lookup",
|
||||
conditions={'method': 'GET'})
|
||||
stack_mapper.connect("stack_lookup_subpath_post",
|
||||
"/stacks/{stack_name}/" + path,
|
||||
action="lookup",
|
||||
conditions={'method': 'POST'})
|
||||
stack_mapper.connect("stack_show",
|
||||
"/stacks/{stack_name}/{stack_id}",
|
||||
action="show",
|
||||
conditions={'method': 'GET'})
|
||||
stack_mapper.connect("stack_template",
|
||||
"/stacks/{stack_name}/{stack_id}/template",
|
||||
action="template",
|
||||
conditions={'method': 'GET'})
|
||||
{
|
||||
'name': 'stack_lookup',
|
||||
'url': r'/stacks/{stack_name:arn\x3A.*}',
|
||||
'action': 'lookup',
|
||||
'method': ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
},
|
||||
{
|
||||
'name': 'stack_lookup_subpath',
|
||||
'url': '/stacks/{stack_name}/'
|
||||
'{path:resources|events|template|actions}',
|
||||
'action': 'lookup',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'stack_lookup_subpath_post',
|
||||
'url': '/stacks/{stack_name}/'
|
||||
'{path:resources|events|template|actions}',
|
||||
'action': 'lookup',
|
||||
'method': 'POST'
|
||||
},
|
||||
{
|
||||
'name': 'stack_show',
|
||||
'url': '/stacks/{stack_name}/{stack_id}',
|
||||
'action': 'show',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'stack_lookup',
|
||||
'url': '/stacks/{stack_name}/{stack_id}/template',
|
||||
'action': 'template',
|
||||
'method': 'GET'
|
||||
},
|
||||
|
||||
# Stack update/delete
|
||||
stack_mapper.connect("stack_update",
|
||||
"/stacks/{stack_name}/{stack_id}",
|
||||
action="update",
|
||||
conditions={'method': 'PUT'})
|
||||
stack_mapper.connect("stack_update_patch",
|
||||
"/stacks/{stack_name}/{stack_id}",
|
||||
action="update_patch",
|
||||
conditions={'method': 'PATCH'})
|
||||
stack_mapper.connect("stack_delete",
|
||||
"/stacks/{stack_name}/{stack_id}",
|
||||
action="delete",
|
||||
conditions={'method': 'DELETE'})
|
||||
{
|
||||
'name': 'stack_update',
|
||||
'url': '/stacks/{stack_name}/{stack_id}',
|
||||
'action': 'update',
|
||||
'method': 'PUT'
|
||||
},
|
||||
{
|
||||
'name': 'stack_update_patch',
|
||||
'url': '/stacks/{stack_name}/{stack_id}',
|
||||
'action': 'update_patch',
|
||||
'method': 'PATCH'
|
||||
},
|
||||
{
|
||||
'name': 'stack_delete',
|
||||
'url': '/stacks/{stack_name}/{stack_id}',
|
||||
'action': 'delete',
|
||||
'method': 'DELETE'
|
||||
},
|
||||
|
||||
# Stack abandon
|
||||
stack_mapper.connect("stack_abandon",
|
||||
"/stacks/{stack_name}/{stack_id}/abandon",
|
||||
action="abandon",
|
||||
conditions={'method': 'DELETE'})
|
||||
|
||||
stack_mapper.connect("stack_snapshot",
|
||||
"/stacks/{stack_name}/{stack_id}/snapshots",
|
||||
action="snapshot",
|
||||
conditions={'method': 'POST'})
|
||||
|
||||
stack_mapper.connect("stack_snapshot_show",
|
||||
"/stacks/{stack_name}/{stack_id}/snapshots/"
|
||||
"{snapshot_id}",
|
||||
action="show_snapshot",
|
||||
conditions={'method': 'GET'})
|
||||
|
||||
stack_mapper.connect("stack_snapshot_delete",
|
||||
"/stacks/{stack_name}/{stack_id}/snapshots/"
|
||||
"{snapshot_id}",
|
||||
action="delete_snapshot",
|
||||
conditions={'method': 'DELETE'})
|
||||
|
||||
stack_mapper.connect("stack_list_snapshots",
|
||||
"/stacks/{stack_name}/{stack_id}/snapshots",
|
||||
action="list_snapshots",
|
||||
conditions={'method': 'GET'})
|
||||
|
||||
stack_mapper.connect("stack_snapshot_restore",
|
||||
"/stacks/{stack_name}/{stack_id}/snapshots/"
|
||||
"{snapshot_id}/restore",
|
||||
action="restore_snapshot",
|
||||
conditions={'method': 'POST'})
|
||||
{
|
||||
'name': 'stack_abandon',
|
||||
'url': '/stacks/{stack_name}/{stack_id}/abandon',
|
||||
'action': 'abandon',
|
||||
'method': 'DELETE'
|
||||
},
|
||||
{
|
||||
'name': 'stack_snapshot',
|
||||
'url': '/stacks/{stack_name}/{stack_id}/snapshots',
|
||||
'action': 'snapshot',
|
||||
'method': 'POST'
|
||||
},
|
||||
{
|
||||
'name': 'stack_snapshot_show',
|
||||
'url': '/stacks/{stack_name}/{stack_id}/snapshots/'
|
||||
'{snapshot_id}',
|
||||
'action': 'show_snapshot',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'stack_snapshot_delete',
|
||||
'url': '/stacks/{stack_name}/{stack_id}/snapshots/'
|
||||
'{snapshot_id}',
|
||||
'action': 'delete_snapshot',
|
||||
'method': 'DELETE'
|
||||
},
|
||||
{
|
||||
'name': 'stack_list_snapshots',
|
||||
'url': '/stacks/{stack_name}/{stack_id}/snapshots',
|
||||
'action': 'list_snapshots',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'stack_snapshot_restore',
|
||||
'url': '/stacks/{stack_name}/{stack_id}/snapshots/'
|
||||
'{snapshot_id}/restore',
|
||||
'action': 'restore_snapshot',
|
||||
'method': 'POST'
|
||||
}
|
||||
])
|
||||
|
||||
# Resources
|
||||
resources_resource = resources.create_resource(conf)
|
||||
stack_path = "/{tenant_id}/stacks/{stack_name}/{stack_id}"
|
||||
with mapper.submapper(controller=resources_resource,
|
||||
path_prefix=stack_path) as res_mapper:
|
||||
|
||||
stack_path = '/{tenant_id}/stacks/{stack_name}/{stack_id}'
|
||||
connect(controller=resources_resource, path_prefix=stack_path,
|
||||
routes=[
|
||||
# Resource collection
|
||||
res_mapper.connect("resource_index",
|
||||
"/resources",
|
||||
action="index",
|
||||
conditions={'method': 'GET'})
|
||||
{
|
||||
'name': 'resource_index',
|
||||
'url': '/resources',
|
||||
'action': 'index',
|
||||
'method': 'GET'
|
||||
},
|
||||
|
||||
# Resource data
|
||||
res_mapper.connect("resource_show",
|
||||
"/resources/{resource_name}",
|
||||
action="show",
|
||||
conditions={'method': 'GET'})
|
||||
res_mapper.connect("resource_metadata_show",
|
||||
"/resources/{resource_name}/metadata",
|
||||
action="metadata",
|
||||
conditions={'method': 'GET'})
|
||||
res_mapper.connect("resource_signal",
|
||||
"/resources/{resource_name}/signal",
|
||||
action="signal",
|
||||
conditions={'method': 'POST'})
|
||||
{
|
||||
'name': 'resource_show',
|
||||
'url': '/resources/{resource_name}',
|
||||
'action': 'show',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'resource_metadata_show',
|
||||
'url': '/resources/{resource_name}/metadata',
|
||||
'action': 'metadata',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'resource_signal',
|
||||
'url': '/resources/{resource_name}/signal',
|
||||
'action': 'signal',
|
||||
'method': 'POST'
|
||||
}
|
||||
])
|
||||
|
||||
# Events
|
||||
events_resource = events.create_resource(conf)
|
||||
with mapper.submapper(controller=events_resource,
|
||||
path_prefix=stack_path) as ev_mapper:
|
||||
|
||||
connect(controller=events_resource, path_prefix=stack_path,
|
||||
routes=[
|
||||
# Stack event collection
|
||||
ev_mapper.connect("event_index_stack",
|
||||
"/events",
|
||||
action="index",
|
||||
conditions={'method': 'GET'})
|
||||
{
|
||||
'name': 'event_index_stack',
|
||||
'url': '/events',
|
||||
'action': 'index',
|
||||
'method': 'GET'
|
||||
},
|
||||
|
||||
# Resource event collection
|
||||
ev_mapper.connect("event_index_resource",
|
||||
"/resources/{resource_name}/events",
|
||||
action="index",
|
||||
conditions={'method': 'GET'})
|
||||
{
|
||||
'name': 'event_index_resource',
|
||||
'url': '/resources/{resource_name}/events',
|
||||
'action': 'index',
|
||||
'method': 'GET'
|
||||
},
|
||||
|
||||
# Event data
|
||||
ev_mapper.connect("event_show",
|
||||
"/resources/{resource_name}/events/{event_id}",
|
||||
action="show",
|
||||
conditions={'method': 'GET'})
|
||||
{
|
||||
'name': 'event_show',
|
||||
'url': '/resources/{resource_name}/events/{event_id}',
|
||||
'action': 'show',
|
||||
'method': 'GET'
|
||||
}
|
||||
])
|
||||
|
||||
# Actions
|
||||
actions_resource = actions.create_resource(conf)
|
||||
with mapper.submapper(controller=actions_resource,
|
||||
path_prefix=stack_path) as ac_mapper:
|
||||
|
||||
ac_mapper.connect("action_stack",
|
||||
"/actions",
|
||||
action="action",
|
||||
conditions={'method': 'POST'})
|
||||
connect(controller=actions_resource, path_prefix=stack_path,
|
||||
routes=[
|
||||
{
|
||||
'name': 'action_stack',
|
||||
'url': '/actions',
|
||||
'action': 'action',
|
||||
'method': 'POST'
|
||||
}
|
||||
])
|
||||
|
||||
# Info
|
||||
info_resource = build_info.create_resource(conf)
|
||||
with mapper.submapper(controller=info_resource,
|
||||
path_prefix="/{tenant_id}") as info_mapper:
|
||||
|
||||
info_mapper.connect('build_info',
|
||||
'/build_info',
|
||||
action='build_info',
|
||||
conditions={'method': 'GET'})
|
||||
connect(controller=info_resource, path_prefix='/{tenant_id}',
|
||||
routes=[
|
||||
{
|
||||
'name': 'build_info',
|
||||
'url': '/build_info',
|
||||
'action': 'build_info',
|
||||
'method': 'GET'
|
||||
}
|
||||
])
|
||||
|
||||
# Software configs
|
||||
software_config_resource = software_configs.create_resource(conf)
|
||||
with mapper.submapper(
|
||||
controller=software_config_resource,
|
||||
path_prefix="/{tenant_id}/software_configs"
|
||||
) as sc_mapper:
|
||||
|
||||
sc_mapper.connect("software_config_create",
|
||||
"",
|
||||
action="create",
|
||||
conditions={'method': 'POST'})
|
||||
|
||||
sc_mapper.connect("software_config_show",
|
||||
"/{config_id}",
|
||||
action="show",
|
||||
conditions={'method': 'GET'})
|
||||
|
||||
sc_mapper.connect("software_config_delete",
|
||||
"/{config_id}",
|
||||
action="delete",
|
||||
conditions={'method': 'DELETE'})
|
||||
connect(controller=software_config_resource,
|
||||
path_prefix='/{tenant_id}/software_configs',
|
||||
routes=[
|
||||
{
|
||||
'name': 'software_config_create',
|
||||
'url': '',
|
||||
'action': 'create',
|
||||
'method': 'POST'
|
||||
},
|
||||
{
|
||||
'name': 'software_config_show',
|
||||
'url': '/{config_id}',
|
||||
'action': 'show',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'software_config_delete',
|
||||
'url': '/{config_id}',
|
||||
'action': 'delete',
|
||||
'method': 'DELETE'
|
||||
}
|
||||
])
|
||||
|
||||
# Software deployments
|
||||
sd_resource = software_deployments.create_resource(conf)
|
||||
with mapper.submapper(
|
||||
controller=sd_resource,
|
||||
path_prefix='/{tenant_id}/software_deployments'
|
||||
) as sa_mapper:
|
||||
|
||||
sa_mapper.connect("software_deployment_index",
|
||||
"",
|
||||
action="index",
|
||||
conditions={'method': 'GET'})
|
||||
|
||||
sa_mapper.connect("software_deployment_metadata",
|
||||
"/metadata/{server_id}",
|
||||
action="metadata",
|
||||
conditions={'method': 'GET'})
|
||||
|
||||
sa_mapper.connect("software_deployment_create",
|
||||
"",
|
||||
action="create",
|
||||
conditions={'method': 'POST'})
|
||||
|
||||
sa_mapper.connect("software_deployment_show",
|
||||
"/{deployment_id}",
|
||||
action="show",
|
||||
conditions={'method': 'GET'})
|
||||
|
||||
sa_mapper.connect("software_deployment_update",
|
||||
"/{deployment_id}",
|
||||
action="update",
|
||||
conditions={'method': 'PUT'})
|
||||
|
||||
sa_mapper.connect("software_deployment_delete",
|
||||
"/{deployment_id}",
|
||||
action="delete",
|
||||
conditions={'method': 'DELETE'})
|
||||
connect(controller=sd_resource,
|
||||
path_prefix='/{tenant_id}/software_deployments',
|
||||
routes=[
|
||||
{
|
||||
'name': 'software_deployment_index',
|
||||
'url': '',
|
||||
'action': 'index',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'software_deployment_metadata',
|
||||
'url': '/metadata/{server_id}',
|
||||
'action': 'metadata',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'software_deployment_create',
|
||||
'url': '',
|
||||
'action': 'create',
|
||||
'method': 'POST'
|
||||
},
|
||||
{
|
||||
'name': 'software_deployment_show',
|
||||
'url': '/{deployment_id}',
|
||||
'action': 'show',
|
||||
'method': 'GET'
|
||||
},
|
||||
{
|
||||
'name': 'software_deployment_update',
|
||||
'url': '/{deployment_id}',
|
||||
'action': 'update',
|
||||
'method': 'PUT'
|
||||
},
|
||||
{
|
||||
'name': 'software_deployment_delete',
|
||||
'url': '/{deployment_id}',
|
||||
'action': 'delete',
|
||||
'method': 'DELETE'
|
||||
}
|
||||
])
|
||||
|
||||
# now that all the routes are defined, add a handler for
|
||||
super(API, self).__init__(mapper)
|
||||
|
@ -440,6 +440,28 @@ def debug_filter(app, conf, **local_conf):
|
||||
return Debug(app)
|
||||
|
||||
|
||||
class DefaultMethodController(object):
|
||||
"""
|
||||
This controller handles the OPTIONS request method and any of the
|
||||
HTTP methods that are not explicitly implemented by the application.
|
||||
"""
|
||||
def options(self, req, allowed_methods, *args, **kwargs):
|
||||
"""
|
||||
Return a response that includes the 'Allow' header listing the methods
|
||||
that are implemented. A 204 status code is used for this response.
|
||||
"""
|
||||
raise webob.exc.HTTPNoContent(headers=[('Allow', allowed_methods)])
|
||||
|
||||
def reject(self, req, allowed_methods, *args, **kwargs):
|
||||
"""
|
||||
Return a 405 method not allowed error. As a convenience, the 'Allow'
|
||||
header with the list of implemented methods is included in the
|
||||
response as well.
|
||||
"""
|
||||
raise webob.exc.HTTPMethodNotAllowed(
|
||||
headers=[('Allow', allowed_methods)])
|
||||
|
||||
|
||||
class Router(object):
|
||||
"""
|
||||
WSGI middleware that maps incoming requests to WSGI apps.
|
||||
|
@ -3616,6 +3616,52 @@ class RoutesTest(common.HeatTestCase):
|
||||
{'tenant_id': 'fake_tenant'}
|
||||
)
|
||||
|
||||
def test_405(self):
|
||||
self.assertRoute(
|
||||
self.m,
|
||||
'/fake_tenant/validate',
|
||||
'GET',
|
||||
'reject',
|
||||
'DefaultMethodController',
|
||||
{'tenant_id': 'fake_tenant', 'allowed_methods': 'POST'}
|
||||
)
|
||||
self.assertRoute(
|
||||
self.m,
|
||||
'/fake_tenant/stacks',
|
||||
'PUT',
|
||||
'reject',
|
||||
'DefaultMethodController',
|
||||
{'tenant_id': 'fake_tenant', 'allowed_methods': 'GET,POST'}
|
||||
)
|
||||
self.assertRoute(
|
||||
self.m,
|
||||
'/fake_tenant/stacks/fake_stack/stack_id',
|
||||
'POST',
|
||||
'reject',
|
||||
'DefaultMethodController',
|
||||
{'tenant_id': 'fake_tenant', 'stack_name': 'fake_stack',
|
||||
'stack_id': 'stack_id', 'allowed_methods': 'GET,PUT,PATCH,DELETE'}
|
||||
)
|
||||
|
||||
def test_options(self):
|
||||
self.assertRoute(
|
||||
self.m,
|
||||
'/fake_tenant/validate',
|
||||
'OPTIONS',
|
||||
'options',
|
||||
'DefaultMethodController',
|
||||
{'tenant_id': 'fake_tenant', 'allowed_methods': 'POST'}
|
||||
)
|
||||
self.assertRoute(
|
||||
self.m,
|
||||
'/fake_tenant/stacks/fake_stack/stack_id',
|
||||
'OPTIONS',
|
||||
'options',
|
||||
'DefaultMethodController',
|
||||
{'tenant_id': 'fake_tenant', 'stack_name': 'fake_stack',
|
||||
'stack_id': 'stack_id', 'allowed_methods': 'GET,PUT,PATCH,DELETE'}
|
||||
)
|
||||
|
||||
|
||||
@mock.patch.object(policy.Enforcer, 'enforce')
|
||||
class ActionControllerTest(ControllerTest, common.HeatTestCase):
|
||||
|
Loading…
Reference in New Issue
Block a user