tests: Ensure all APIs have a response body schema
Well, in a manner of speaking :) We add a new decorator to highlight the resources that we have fully annotated. This will prevent regressions and give us a quick sentinel for identifying resources that have been updated. Change-Id: Ic2cf231a01b0f053faf40409ae047c8bf9990fd2 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
@@ -32,6 +32,7 @@ It was removed in the 23.0.0 (Wallaby) release.
|
||||
"""
|
||||
|
||||
|
||||
@validation.validated
|
||||
class AdminActionsController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(AdminActionsController, self).__init__()
|
||||
|
@@ -24,6 +24,7 @@ from nova.i18n import _
|
||||
from nova.policies import admin_password as ap_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class AdminPasswordController(wsgi.Controller):
|
||||
|
||||
def __init__(self):
|
||||
|
@@ -40,6 +40,7 @@ def _get_context(req):
|
||||
return req.environ['nova.context']
|
||||
|
||||
|
||||
@validation.validated
|
||||
class AggregateController(wsgi.Controller):
|
||||
"""The Host Aggregates API controller for the OpenStack API."""
|
||||
|
||||
|
@@ -27,6 +27,7 @@ from nova import exception
|
||||
from nova.policies import assisted_volume_snapshots as avs_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class AssistedVolumeSnapshotsController(wsgi.Controller):
|
||||
"""The Assisted volume snapshots API controller for the OpenStack API."""
|
||||
|
||||
|
@@ -55,6 +55,7 @@ def _translate_interface_attachment_view(context, port_info, show_tag=False):
|
||||
return info
|
||||
|
||||
|
||||
@validation.validated
|
||||
class InterfaceAttachmentController(wsgi.Controller):
|
||||
"""The interface attachment API controller for the OpenStack API."""
|
||||
|
||||
|
@@ -25,6 +25,7 @@ CONF = nova.conf.CONF
|
||||
ATTRIBUTE_NAME = "availability_zone"
|
||||
|
||||
|
||||
@validation.validated
|
||||
class AvailabilityZoneController(wsgi.Controller):
|
||||
"""The Availability Zone API controller for the OpenStack API."""
|
||||
|
||||
|
@@ -40,6 +40,7 @@ def _no_ironic_proxy(cmd):
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg % {'cmd': cmd})
|
||||
|
||||
|
||||
@validation.validated
|
||||
class BareMetalNodeController(wsgi.Controller):
|
||||
"""The Bare-Metal Node API controller for the OpenStack API."""
|
||||
|
||||
|
@@ -28,6 +28,7 @@ from nova.policies import console_auth_tokens as cat_policies
|
||||
CONF = nova.conf.CONF
|
||||
|
||||
|
||||
@validation.validated
|
||||
class ConsoleAuthTokensController(wsgi.Controller):
|
||||
|
||||
@wsgi.expected_errors((400, 401, 404), '2.1', '2.30')
|
||||
|
@@ -27,6 +27,7 @@ from nova import exception
|
||||
from nova.policies import console_output as co_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class ConsoleOutputController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(ConsoleOutputController, self).__init__()
|
||||
|
@@ -25,6 +25,7 @@ from nova import exception
|
||||
from nova.policies import create_backup as cb_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class CreateBackupController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(CreateBackupController, self).__init__()
|
||||
|
@@ -26,6 +26,7 @@ from nova import exception
|
||||
from nova.policies import deferred_delete as dd_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class DeferredDeleteController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(DeferredDeleteController, self).__init__()
|
||||
|
@@ -38,6 +38,7 @@ LOG = logging.getLogger(__name__)
|
||||
MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED = 62
|
||||
|
||||
|
||||
@validation.validated
|
||||
class EvacuateController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(EvacuateController, self).__init__()
|
||||
|
@@ -852,6 +852,7 @@ EXTENSION_LIST_LEGACY_V2_COMPATIBLE = sorted(
|
||||
EXTENSION_LIST_LEGACY_V2_COMPATIBLE, key=lambda x: x['alias'])
|
||||
|
||||
|
||||
@validation.validated
|
||||
class ExtensionInfoController(wsgi.Controller):
|
||||
|
||||
@wsgi.expected_errors(())
|
||||
|
@@ -37,6 +37,7 @@ def _marshall_flavor_access(flavor):
|
||||
return {'flavor_access': rval}
|
||||
|
||||
|
||||
@validation.validated
|
||||
class FlavorAccessController(wsgi.Controller):
|
||||
"""The flavor access API controller for the OpenStack API."""
|
||||
|
||||
@@ -58,6 +59,7 @@ class FlavorAccessController(wsgi.Controller):
|
||||
return _marshall_flavor_access(flavor)
|
||||
|
||||
|
||||
@validation.validated
|
||||
class FlavorActionController(wsgi.Controller):
|
||||
"""The flavor access API controller for the OpenStack API."""
|
||||
|
||||
|
@@ -31,6 +31,7 @@ from nova.policies import flavor_manage as fm_policies
|
||||
from nova import utils
|
||||
|
||||
|
||||
@validation.validated
|
||||
class FlavorsController(wsgi.Controller):
|
||||
"""Flavor controller for the OpenStack API."""
|
||||
|
||||
|
@@ -27,6 +27,7 @@ from nova.policies import flavor_extra_specs as fes_policies
|
||||
from nova import utils
|
||||
|
||||
|
||||
@validation.validated
|
||||
class FlavorExtraSpecsController(wsgi.Controller):
|
||||
"""The flavor extra specs API controller for the OpenStack API."""
|
||||
|
||||
|
@@ -34,6 +34,7 @@ def _translate_floating_ip_pools_view(pools):
|
||||
}
|
||||
|
||||
|
||||
@validation.validated
|
||||
class FloatingIPPoolsController(wsgi.Controller):
|
||||
"""The Floating IP Pool API controller for the OpenStack API."""
|
||||
|
||||
|
@@ -178,6 +178,7 @@ class FloatingIPController(wsgi.Controller):
|
||||
raise webob.exc.HTTPNotFound(explanation=exc.format_message())
|
||||
|
||||
|
||||
@validation.validated
|
||||
class FloatingIPActionController(wsgi.Controller):
|
||||
"""This API is deprecated from the Microversion '2.44'."""
|
||||
|
||||
|
@@ -22,6 +22,7 @@ from nova.compute import api as compute
|
||||
from nova.policies import lock_server as ls_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class LockServerController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(LockServerController, self).__init__()
|
||||
|
@@ -31,6 +31,7 @@ from nova.policies import migrate_server as ms_policies
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@validation.validated
|
||||
class MigrateServerController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(MigrateServerController, self).__init__()
|
||||
|
@@ -26,6 +26,7 @@ from nova import exception
|
||||
from nova.policies import multinic as multinic_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class MultinicController(wsgi.Controller):
|
||||
"""This API is deprecated from Microversion '2.44'."""
|
||||
|
||||
|
@@ -24,6 +24,7 @@ from nova import exception
|
||||
from nova.policies import pause_server as ps_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class PauseServerController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(PauseServerController, self).__init__()
|
||||
|
@@ -30,6 +30,7 @@ from nova import utils
|
||||
CONF = nova.conf.CONF
|
||||
|
||||
|
||||
@validation.validated
|
||||
class RescueController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(RescueController, self).__init__()
|
||||
|
@@ -385,6 +385,7 @@ class ServerSecurityGroupController(
|
||||
key=lambda k: (k['tenant_id'], k['name'])))}
|
||||
|
||||
|
||||
@validation.validated
|
||||
class SecurityGroupActionController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(SecurityGroupActionController, self).__init__()
|
||||
|
@@ -29,6 +29,7 @@ from nova.policies import shelve as shelve_policies
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@validation.validated
|
||||
class ShelveController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(ShelveController, self).__init__()
|
||||
|
@@ -23,6 +23,7 @@ from nova import exception
|
||||
from nova.policies import suspend_server as ss_policies
|
||||
|
||||
|
||||
@validation.validated
|
||||
class SuspendServerController(wsgi.Controller):
|
||||
def __init__(self):
|
||||
super(SuspendServerController, self).__init__()
|
||||
|
@@ -74,6 +74,7 @@ VERSIONS = {
|
||||
}
|
||||
|
||||
|
||||
@validation.validated
|
||||
class Versions(wsgi.Resource):
|
||||
|
||||
# The root version API isn't under the microversion control.
|
||||
@@ -108,6 +109,7 @@ class Versions(wsgi.Resource):
|
||||
return args
|
||||
|
||||
|
||||
@validation.validated
|
||||
class VersionsV2(wsgi.Resource):
|
||||
|
||||
def __init__(self):
|
||||
|
@@ -22,6 +22,7 @@ from nova.api.openstack import wsgi
|
||||
from nova.api import validation
|
||||
|
||||
|
||||
@validation.validated
|
||||
class VersionsController(wsgi.Controller):
|
||||
|
||||
@wsgi.expected_errors(404)
|
||||
|
@@ -35,6 +35,11 @@ CONF = nova.conf.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validated(cls):
|
||||
cls._validated = True
|
||||
return cls
|
||||
|
||||
|
||||
class Schemas:
|
||||
"""A microversion-aware schema container.
|
||||
|
||||
|
@@ -30,53 +30,42 @@ class SchemaTest(test.NoDBTestCase):
|
||||
def test_schemas(self):
|
||||
missing_request_schemas = set()
|
||||
missing_query_schemas = set()
|
||||
missing_response_schemas = set()
|
||||
invalid_schemas = set()
|
||||
|
||||
def _validate_func(func, method):
|
||||
def _validate_schema(func, schema):
|
||||
try:
|
||||
self.meta_schema.check_schema(schema)
|
||||
except jsonschema.exceptions.SchemaError:
|
||||
LOG.exception(
|
||||
'schema validation failed for %s',
|
||||
func.__qualname__,
|
||||
)
|
||||
invalid_schemas.add(func.__qualname__)
|
||||
|
||||
def _validate_func(func, method, validated):
|
||||
if method in ("POST", "PUT", "PATCH"):
|
||||
# request body validation
|
||||
if not hasattr(func, 'request_body_schemas'):
|
||||
missing_request_schemas.add(func.__qualname__)
|
||||
else:
|
||||
for schema, _, _ in func.request_body_schemas._schemas:
|
||||
try:
|
||||
self.meta_schema.check_schema(schema)
|
||||
except jsonschema.exceptions.SchemaError:
|
||||
LOG.exception(
|
||||
"Invalid request body schema for %s",
|
||||
func.__qualname__,
|
||||
)
|
||||
invalid_schemas.add(func.__qualname__)
|
||||
break
|
||||
_validate_schema(func, schema)
|
||||
elif method in ("GET",):
|
||||
# request query string validation
|
||||
if not hasattr(func, 'request_query_schemas'):
|
||||
missing_request_schemas.add(func.__qualname__)
|
||||
else:
|
||||
for schema, _, _ in func.request_query_schemas._schemas:
|
||||
try:
|
||||
self.meta_schema.check_schema(schema)
|
||||
except jsonschema.exceptions.SchemaError:
|
||||
LOG.exception(
|
||||
"Invalid request query schema for %s",
|
||||
func.__qualname__,
|
||||
)
|
||||
invalid_schemas.add(func.__qualname__)
|
||||
break
|
||||
_validate_schema(func, schema)
|
||||
|
||||
# TODO(stephenfin): Check for missing schemas once we have added
|
||||
# them all
|
||||
if hasattr(func, 'response_body_schemas'):
|
||||
# response body validation
|
||||
if not hasattr(func, 'response_body_schemas'):
|
||||
if validated:
|
||||
missing_response_schemas.add(func.__qualname__)
|
||||
else:
|
||||
for schema, _, _ in func.response_body_schemas._schemas:
|
||||
try:
|
||||
self.meta_schema.check_schema(schema)
|
||||
except jsonschema.exceptions.SchemaError:
|
||||
LOG.exception(
|
||||
"Invalid response body schema for %s",
|
||||
func.__qualname__,
|
||||
)
|
||||
invalid_schemas.add(func.__qualname__)
|
||||
break
|
||||
_validate_schema(func, schema)
|
||||
|
||||
for route in self.router.map.matchlist:
|
||||
if 'controller' not in route.defaults:
|
||||
@@ -84,6 +73,11 @@ class SchemaTest(test.NoDBTestCase):
|
||||
|
||||
controller = route.defaults['controller']
|
||||
|
||||
validated = getattr(controller.controller, '_validated', False)
|
||||
|
||||
# NOTE: This is effectively a reimplementation of
|
||||
# 'routes.route.Route.make_full_route' that uses OpenAPI-compatible
|
||||
# template strings instead of regexes for parameters
|
||||
path = ""
|
||||
for part in route.routelist:
|
||||
if isinstance(part, dict):
|
||||
@@ -117,24 +111,30 @@ class SchemaTest(test.NoDBTestCase):
|
||||
) in wsgi_actions:
|
||||
func = controller.wsgi_actions[wsgi_action]
|
||||
# method will always be POST for actions
|
||||
_validate_func(func, method)
|
||||
_validate_func(func, method, validated)
|
||||
else:
|
||||
# body validation
|
||||
func = getattr(controller.controller, action)
|
||||
_validate_func(func, method)
|
||||
_validate_func(func, method, validated)
|
||||
|
||||
if missing_request_schemas:
|
||||
raise test.TestingException(
|
||||
f"Found API resources without schemas: "
|
||||
f"Found API resources without request body schemas: "
|
||||
f"{sorted(missing_request_schemas)}"
|
||||
)
|
||||
|
||||
if missing_query_schemas:
|
||||
raise test.TestingException(
|
||||
f"Found API resources without query schemas: "
|
||||
f"Found API resources without request query schemas: "
|
||||
f"{sorted(missing_query_schemas)}"
|
||||
)
|
||||
|
||||
if missing_response_schemas:
|
||||
raise test.TestingException(
|
||||
f"Found API resources without response body schemas: "
|
||||
f"{sorted(missing_response_schemas)}"
|
||||
)
|
||||
|
||||
if invalid_schemas:
|
||||
raise test.TestingException(
|
||||
f"Found API resources with invalid schemas: "
|
||||
|
Reference in New Issue
Block a user