From 917c4f0721126b514570bbbe34dbcd73a3fb9fd6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 20 Feb 2023 20:45:14 +0000 Subject: [PATCH] tests: Ensure all APIs have a request body schema The idea here is to test that we are doing schema validation against all routes that accept a request body (i.e. POST, PUT, PATCH). We can use these schemas down the line to generate OpenAPI docs like all the cool kids do. Change-Id: Icdf20e7e5b38c9f5324eac39ec1d6327609bf6d4 Signed-off-by: Stephen Finucane --- nova/api/validation/__init__.py | 2 + .../api/openstack/compute/test_schemas.py | 106 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 nova/tests/unit/api/openstack/compute/test_schemas.py diff --git a/nova/api/validation/__init__.py b/nova/api/validation/__init__.py index d08d078a00..d12d2cfac1 100644 --- a/nova/api/validation/__init__.py +++ b/nova/api/validation/__init__.py @@ -109,6 +109,8 @@ def schema(request_body_schema, min_version=None, max_version=None): args, kwargs) return func(*args, **kwargs) + wrapper._request_schema = request_body_schema + return wrapper return add_validator diff --git a/nova/tests/unit/api/openstack/compute/test_schemas.py b/nova/tests/unit/api/openstack/compute/test_schemas.py new file mode 100644 index 0000000000..7cf34c4451 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_schemas.py @@ -0,0 +1,106 @@ +# 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 nova.api.openstack import compute +from nova import test + + +class SchemaTest(test.NoDBTestCase): + + def setUp(self): + super().setUp() + self.router = compute.APIRouterV21() + + def test_schemas(self): + missing_schemas = set() + + def _validate_func(func, method): + if method in ("POST", "PUT", "PATCH"): + # request body validation + if not hasattr(func, '_request_schema'): + missing_schemas.add(func.__qualname__) + + for route in self.router.map.matchlist: + if 'controller' not in route.defaults: + continue + + controller = route.defaults['controller'] + + path = "" + for part in route.routelist: + if isinstance(part, dict): + path += "{" + part["name"] + "}" + else: + path += part + + method = ( + route.conditions.get("method", "GET")[0] + if route.conditions + else "GET" + ) + action = route.defaults["action"] + + if path.endswith('/action'): + # all actions should use POST + assert method == 'POST' + + wsgi_actions = [ + (k, v, controller.controller) for k, v in + controller.controller.wsgi_actions.items() + ] + for sub_controller in controller.sub_controllers: + wsgi_actions += [ + (k, v, sub_controller) for k, v in + sub_controller.wsgi_actions.items() + ] + + for ( + wsgi_action, wsgi_method, action_controller + ) in wsgi_actions: + func = controller.wsgi_actions[wsgi_action] + + if hasattr(action_controller, 'versioned_methods'): + if wsgi_method in action_controller.versioned_methods: + # currently all our actions are unversioned and if + # this changes then we need to fix this + funcs = action_controller.versioned_methods[ + wsgi_method + ] + assert len(funcs) == 1 + func = funcs[0].func + + # method will always be POST for actions + _validate_func(func, method) + else: + # body validation + versioned_methods = getattr( + controller.controller, 'versioned_methods', {} + ) + if action in versioned_methods: + # versioned method + for versioned_method in sorted( + versioned_methods[action], + key=lambda v: v.start_version + ): + func = versioned_method.func + + _validate_func(func, method) + else: + # unversioned method + func = getattr(controller.controller, action) + _validate_func(func, method) + + if missing_schemas: + raise test.TestingException( + f"Found API resources without schemas: " + f"{sorted(missing_schemas)}" + )