diff --git a/manila/tests/api/test_schemas.py b/manila/tests/api/test_schemas.py new file mode 100644 index 0000000000..f9a113b5f3 --- /dev/null +++ b/manila/tests/api/test_schemas.py @@ -0,0 +1,137 @@ +# 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 manila.api.v2 import router +from manila import test + + +class SchemaTest(test.TestCase): + + def setUp(self): + super().setUp() + self.router = router.APIRouter() + + def test_schemas(self): + missing_request_schemas = set() + missing_query_schemas = set() + missing_response_schemas = set() + + def _validate_func(func, method): + if getattr(func, 'removed', False): + return + + if method in ("POST", "PUT", "PATCH"): + # request body validation + if not hasattr(func, '_request_body_schema'): + missing_request_schemas.add(func.__qualname__) + elif method in ("GET",): + # request query string validation + if not hasattr(func, '_request_query_schema'): + missing_query_schemas.add(func.__qualname__) + + # response body validation + if not hasattr(func, '_response_body_schema'): + missing_response_schemas.add(func.__qualname__) + + for route in self.router.map.matchlist: + if 'controller' not in route.defaults: + continue + + controller = route.defaults['controller'] + + if not getattr(controller.controller, '_validated', False): + continue + + # NOTE: This is effectively a reimplementation of + # 'routes.route.Route.make_full_route' that uses OpenAPI-compatible + # template strings instead of regexes for paramters + 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 ( + wsgi_action, wsgi_method, action_controller + ) in wsgi_actions: + versioned_methods = getattr( + action_controller, 'versioned_methods', {} + ) + if wsgi_method 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 = controller.wsgi_actions[wsgi_action] + _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: + if not hasattr(controller.controller, action): + # these are almost certainly because of use of + # routes.mapper.Mapper.resource, which we should remove + continue + + # unversioned method + func = getattr(controller.controller, action) + _validate_func(func, method) + + if missing_request_schemas: + raise self.failureException( + f"Found API resources without request body schemas: " + f"{sorted(missing_request_schemas)}" + ) + + if missing_query_schemas: + raise self.failureException( + f"Found API resources without request query schemas: " + f"{sorted(missing_query_schemas)}" + ) + + if missing_response_schemas: + raise self.failureException( + f"Found API resources without response body schemas: " + f"{sorted(missing_response_schemas)}" + )