From 24276bf37a14ff0a08debc9176990ce5734294e2 Mon Sep 17 00:00:00 2001 From: Artur Maciag Date: Fri, 21 Feb 2020 15:22:23 +0000 Subject: [PATCH 1/4] Unify find path method of request validator --- openapi_core/validation/request/validators.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index cab719d3..eddb0270 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -36,8 +36,7 @@ def __init__( def validate(self, request): try: - path = self._get_path(request) - operation = self._get_operation(request) + path, operation, _, _, _ = self._find_path(request) # don't process if operation errors except (InvalidServer, InvalidPath, InvalidOperation) as exc: return RequestValidationResult([exc, ], None, None, None) @@ -61,8 +60,7 @@ def validate(self, request): def _validate_parameters(self, request): try: - path = self._get_path(request) - operation = self._get_operation(request) + path, operation, _, _, _ = self._find_path(request) except (InvalidServer, InvalidPath, InvalidOperation) as exc: return RequestValidationResult([exc, ], None, None) @@ -76,7 +74,7 @@ def _validate_parameters(self, request): def _validate_body(self, request): try: - operation = self._get_operation(request) + _, operation, _, _, _ = self._find_path(request) except (InvalidServer, InvalidOperation) as exc: return RequestValidationResult([exc, ], None, None) @@ -90,15 +88,17 @@ def _get_operation_pattern(self, request): server.default_url, request.full_url_pattern ) - def _get_path(self, request): + def _find_path(self, request): operation_pattern = self._get_operation_pattern(request) - return self.spec[operation_pattern] + path = self.spec[operation_pattern] + path_variables = {} + operation = self.spec.get_operation(operation_pattern, request.method) + servers = path.servers or operation.servers or self.spec.servers + server = servers[0] + server_variables = {} - def _get_operation(self, request): - operation_pattern = self._get_operation_pattern(request) - - return self.spec.get_operation(operation_pattern, request.method) + return path, operation, server, path_variables, server_variables def _get_security(self, request, operation): security = operation.security or self.spec.security From 8539d9b0f407f83e38ae4db9ca053cb4a108b728 Mon Sep 17 00:00:00 2001 From: Artur Maciag Date: Fri, 21 Feb 2020 16:08:13 +0000 Subject: [PATCH 2/4] Base validator class --- openapi_core/validation/request/validators.py | 63 +------------- .../validation/response/validators.py | 82 ++++++------------- openapi_core/validation/validators.py | 66 +++++++++++++++ .../integration/validation/test_validators.py | 2 +- 4 files changed, 94 insertions(+), 119 deletions(-) create mode 100644 openapi_core/validation/validators.py diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index eddb0270..cc7ff647 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -22,17 +22,10 @@ RequestParameters, RequestValidationResult, ) from openapi_core.validation.util import get_operation_pattern +from openapi_core.validation.validators import BaseValidator -class RequestValidator(object): - - def __init__( - self, spec, - custom_formatters=None, custom_media_type_deserializers=None, - ): - self.spec = spec - self.custom_formatters = custom_formatters - self.custom_media_type_deserializers = custom_media_type_deserializers +class RequestValidator(BaseValidator): def validate(self, request): try: @@ -81,25 +74,6 @@ def _validate_body(self, request): body, body_errors = self._get_body(request, operation) return RequestValidationResult(body_errors, body, None, None) - def _get_operation_pattern(self, request): - server = self.spec.get_server(request.full_url_pattern) - - return get_operation_pattern( - server.default_url, request.full_url_pattern - ) - - def _find_path(self, request): - operation_pattern = self._get_operation_pattern(request) - - path = self.spec[operation_pattern] - path_variables = {} - operation = self.spec.get_operation(operation_pattern, request.method) - servers = path.servers or operation.servers or self.spec.servers - server = servers[0] - server_variables = {} - - return path, operation, server, path_variables, server_variables - def _get_security(self, request, operation): security = operation.security or self.spec.security if not security: @@ -222,15 +196,6 @@ def _get_body_value(self, request_body, request): raise MissingRequestBody(request) return request.body - def _deserialise_media_type(self, media_type, value): - from openapi_core.deserializing.media_types.factories import ( - MediaTypeDeserializersFactory, - ) - deserializers_factory = MediaTypeDeserializersFactory( - self.custom_media_type_deserializers) - deserializer = deserializers_factory.create(media_type) - return deserializer(value) - def _deserialise_parameter(self, param, value): from openapi_core.deserializing.parameters.factories import ( ParameterDeserializersFactory, @@ -239,27 +204,7 @@ def _deserialise_parameter(self, param, value): deserializer = deserializers_factory.create(param) return deserializer(value) - def _cast(self, param_or_media_type, value): - # return param_or_media_type.cast(value) - if not param_or_media_type.schema: - return value - - from openapi_core.casting.schemas.factories import SchemaCastersFactory - casters_factory = SchemaCastersFactory() - caster = casters_factory.create(param_or_media_type.schema) - return caster(value) - def _unmarshal(self, param_or_media_type, value): - if not param_or_media_type.schema: - return value - - from openapi_core.unmarshalling.schemas.factories import ( - SchemaUnmarshallersFactory, - ) - unmarshallers_factory = SchemaUnmarshallersFactory( - self.spec._resolver, self.custom_formatters, - context=UnmarshalContext.REQUEST, + return super(RequestValidator, self)._unmarshal( + param_or_media_type, value, context=UnmarshalContext.REQUEST, ) - unmarshaller = unmarshallers_factory.create( - param_or_media_type.schema) - return unmarshaller(value) diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index 0bf99134..c41234dc 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -3,6 +3,7 @@ from openapi_core.deserializing.exceptions import DeserializeError from openapi_core.schema.operations.exceptions import InvalidOperation from openapi_core.schema.media_types.exceptions import InvalidContentType +from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.responses.exceptions import ( InvalidResponse, MissingResponseContent, ) @@ -13,24 +14,23 @@ ) from openapi_core.validation.response.datatypes import ResponseValidationResult from openapi_core.validation.util import get_operation_pattern +from openapi_core.validation.validators import BaseValidator -class ResponseValidator(object): - - def __init__( - self, spec, - custom_formatters=None, custom_media_type_deserializers=None, - ): - self.spec = spec - self.custom_formatters = custom_formatters - self.custom_media_type_deserializers = custom_media_type_deserializers +class ResponseValidator(BaseValidator): def validate(self, request, response): + try: + _, operation, _, _, _ = self._find_path(request) + # don't process if operation errors + except (InvalidServer, InvalidPath, InvalidOperation) as exc: + return ResponseValidationResult([exc, ], None, None) + try: operation_response = self._get_operation_response( - request, response) + operation, response) # don't process if operation errors - except (InvalidServer, InvalidOperation, InvalidResponse) as exc: + except InvalidResponse as exc: return ResponseValidationResult([exc, ], None, None) data, data_errors = self._get_data(response, operation_response) @@ -41,28 +41,21 @@ def validate(self, request, response): errors = data_errors + headers_errors return ResponseValidationResult(errors, data, headers) - def _get_operation_pattern(self, request): - server = self.spec.get_server(request.full_url_pattern) - - return get_operation_pattern( - server.default_url, request.full_url_pattern - ) - - def _get_operation(self, request): - operation_pattern = self._get_operation_pattern(request) - - return self.spec.get_operation(operation_pattern, request.method) - - def _get_operation_response(self, request, response): - operation = self._get_operation(request) - + def _get_operation_response(self, operation, response): return operation.get_response(str(response.status_code)) def _validate_data(self, request, response): + try: + _, operation, _, _, _ = self._find_path(request) + # don't process if operation errors + except (InvalidServer, InvalidPath, InvalidOperation) as exc: + return ResponseValidationResult([exc, ], None, None) + try: operation_response = self._get_operation_response( - request, response) - except (InvalidServer, InvalidOperation, InvalidResponse) as exc: + operation, response) + # don't process if operation errors + except InvalidResponse as exc: return ResponseValidationResult([exc, ], None, None) data, data_errors = self._get_data(response, operation_response) @@ -113,36 +106,7 @@ def _get_data_value(self, response): return response.data - def _deserialise_media_type(self, media_type, value): - from openapi_core.deserializing.media_types.factories import ( - MediaTypeDeserializersFactory, - ) - deserializers_factory = MediaTypeDeserializersFactory( - self.custom_media_type_deserializers) - deserializer = deserializers_factory.create(media_type) - return deserializer(value) - - def _cast(self, param_or_media_type, value): - # return param_or_media_type.cast(value) - if not param_or_media_type.schema: - return value - - from openapi_core.casting.schemas.factories import SchemaCastersFactory - casters_factory = SchemaCastersFactory() - caster = casters_factory.create(param_or_media_type.schema) - return caster(value) - def _unmarshal(self, param_or_media_type, value): - if not param_or_media_type.schema: - return value - - from openapi_core.unmarshalling.schemas.factories import ( - SchemaUnmarshallersFactory, - ) - unmarshallers_factory = SchemaUnmarshallersFactory( - self.spec._resolver, self.custom_formatters, - context=UnmarshalContext.RESPONSE, + return super(ResponseValidator, self)._unmarshal( + param_or_media_type, value, context=UnmarshalContext.RESPONSE, ) - unmarshaller = unmarshallers_factory.create( - param_or_media_type.schema) - return unmarshaller(value) diff --git a/openapi_core/validation/validators.py b/openapi_core/validation/validators.py new file mode 100644 index 00000000..6bb230b8 --- /dev/null +++ b/openapi_core/validation/validators.py @@ -0,0 +1,66 @@ +"""OpenAPI core validation validators module""" +from openapi_core.validation.util import get_operation_pattern + + +class BaseValidator(object): + + def __init__( + self, spec, + custom_formatters=None, custom_media_type_deserializers=None, + ): + self.spec = spec + self.custom_formatters = custom_formatters + self.custom_media_type_deserializers = custom_media_type_deserializers + + def _find_path(self, request): + operation_pattern = self._get_operation_pattern(request) + + path = self.spec[operation_pattern] + path_variables = {} + operation = self.spec.get_operation(operation_pattern, request.method) + servers = path.servers or operation.servers or self.spec.servers + server = servers[0] + server_variables = {} + + return path, operation, server, path_variables, server_variables + + def _get_operation_pattern(self, request): + server = self.spec.get_server(request.full_url_pattern) + + return get_operation_pattern( + server.default_url, request.full_url_pattern + ) + + def _deserialise_media_type(self, media_type, value): + from openapi_core.deserializing.media_types.factories import ( + MediaTypeDeserializersFactory, + ) + deserializers_factory = MediaTypeDeserializersFactory( + self.custom_media_type_deserializers) + deserializer = deserializers_factory.create(media_type) + return deserializer(value) + + def _cast(self, param_or_media_type, value): + # return param_or_media_type.cast(value) + if not param_or_media_type.schema: + return value + + from openapi_core.casting.schemas.factories import SchemaCastersFactory + casters_factory = SchemaCastersFactory() + caster = casters_factory.create(param_or_media_type.schema) + return caster(value) + + def _unmarshal(self, param_or_media_type, value, context): + if not param_or_media_type.schema: + return value + + from openapi_core.unmarshalling.schemas.factories import ( + SchemaUnmarshallersFactory, + ) + unmarshallers_factory = SchemaUnmarshallersFactory( + self.spec._resolver, self.custom_formatters, + context=context, + ) + unmarshaller = unmarshallers_factory.create( + param_or_media_type.schema) + return unmarshaller(value) diff --git a/tests/integration/validation/test_validators.py b/tests/integration/validation/test_validators.py index ca3f303e..30c985ab 100644 --- a/tests/integration/validation/test_validators.py +++ b/tests/integration/validation/test_validators.py @@ -439,7 +439,7 @@ def test_invalid_operation(self, validator): result = validator.validate(request, response) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidOperation + assert type(result.errors[0]) == InvalidPath assert result.data is None assert result.headers is None From 817ff5c746b016311611348cac6384eca287fb0f Mon Sep 17 00:00:00 2001 From: Artur Maciag Date: Fri, 21 Feb 2020 16:25:10 +0000 Subject: [PATCH 3/4] Move path finder to separate templating module --- openapi_core/templating/__init__.py | 0 openapi_core/templating/paths/__init__.py | 0 openapi_core/templating/paths/finders.py | 27 +++++++++++++++++++ .../{validation => templating/paths}/util.py | 2 +- openapi_core/validation/request/validators.py | 1 - .../validation/response/validators.py | 1 - openapi_core/validation/validators.py | 21 +++------------ .../test_paths_util.py} | 2 +- 8 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 openapi_core/templating/__init__.py create mode 100644 openapi_core/templating/paths/__init__.py create mode 100644 openapi_core/templating/paths/finders.py rename openapi_core/{validation => templating/paths}/util.py (93%) rename tests/unit/{validation/test_util.py => templating/test_paths_util.py} (85%) diff --git a/openapi_core/templating/__init__.py b/openapi_core/templating/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/templating/paths/__init__.py b/openapi_core/templating/paths/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/templating/paths/finders.py b/openapi_core/templating/paths/finders.py new file mode 100644 index 00000000..a131dcad --- /dev/null +++ b/openapi_core/templating/paths/finders.py @@ -0,0 +1,27 @@ +"""OpenAPI core templating paths finders module""" +from openapi_core.templating.paths.util import get_operation_pattern + + +class PathFinder(object): + + def __init__(self, spec): + self.spec = spec + + def find(self, request): + operation_pattern = self._get_operation_pattern(request) + + path = self.spec[operation_pattern] + path_variables = {} + operation = self.spec.get_operation(operation_pattern, request.method) + servers = path.servers or operation.servers or self.spec.servers + server = servers[0] + server_variables = {} + + return path, operation, server, path_variables, server_variables + + def _get_operation_pattern(self, request): + server = self.spec.get_server(request.full_url_pattern) + + return get_operation_pattern( + server.default_url, request.full_url_pattern + ) diff --git a/openapi_core/validation/util.py b/openapi_core/templating/paths/util.py similarity index 93% rename from openapi_core/validation/util.py rename to openapi_core/templating/paths/util.py index ace45e2a..dc460a29 100644 --- a/openapi_core/validation/util.py +++ b/openapi_core/templating/paths/util.py @@ -1,4 +1,4 @@ -"""OpenAPI core validation util module""" +"""OpenAPI core templating paths util module""" from six.moves.urllib.parse import urlparse diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index cc7ff647..de0ca284 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -21,7 +21,6 @@ from openapi_core.validation.request.datatypes import ( RequestParameters, RequestValidationResult, ) -from openapi_core.validation.util import get_operation_pattern from openapi_core.validation.validators import BaseValidator diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index c41234dc..bed96d87 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -13,7 +13,6 @@ UnmarshalError, ValidateError, ) from openapi_core.validation.response.datatypes import ResponseValidationResult -from openapi_core.validation.util import get_operation_pattern from openapi_core.validation.validators import BaseValidator diff --git a/openapi_core/validation/validators.py b/openapi_core/validation/validators.py index 6bb230b8..d3954549 100644 --- a/openapi_core/validation/validators.py +++ b/openapi_core/validation/validators.py @@ -1,5 +1,4 @@ """OpenAPI core validation validators module""" -from openapi_core.validation.util import get_operation_pattern class BaseValidator(object): @@ -13,23 +12,9 @@ def __init__( self.custom_media_type_deserializers = custom_media_type_deserializers def _find_path(self, request): - operation_pattern = self._get_operation_pattern(request) - - path = self.spec[operation_pattern] - path_variables = {} - operation = self.spec.get_operation(operation_pattern, request.method) - servers = path.servers or operation.servers or self.spec.servers - server = servers[0] - server_variables = {} - - return path, operation, server, path_variables, server_variables - - def _get_operation_pattern(self, request): - server = self.spec.get_server(request.full_url_pattern) - - return get_operation_pattern( - server.default_url, request.full_url_pattern - ) + from openapi_core.templating.paths.finders import PathFinder + finder = PathFinder(self.spec) + return finder.find(request) def _deserialise_media_type(self, media_type, value): from openapi_core.deserializing.media_types.factories import ( diff --git a/tests/unit/validation/test_util.py b/tests/unit/templating/test_paths_util.py similarity index 85% rename from tests/unit/validation/test_util.py rename to tests/unit/templating/test_paths_util.py index 8cf353c8..556fdea3 100644 --- a/tests/unit/validation/test_util.py +++ b/tests/unit/templating/test_paths_util.py @@ -1,4 +1,4 @@ -from openapi_core.validation.util import path_qs +from openapi_core.templating.paths.util import path_qs class TestPathQs(object): From dcb7161af7b273b824e57ed1f61e00bfe72d1899 Mon Sep 17 00:00:00 2001 From: Artur Maciag Date: Fri, 21 Feb 2020 16:33:45 +0000 Subject: [PATCH 4/4] Path pattern finder --- openapi_core/contrib/flask/handlers.py | 8 +- openapi_core/schema/servers/models.py | 10 + openapi_core/templating/datatypes.py | 13 + openapi_core/templating/paths/exceptions.py | 36 ++ openapi_core/templating/paths/finders.py | 88 +++- openapi_core/templating/paths/util.py | 24 -- openapi_core/templating/util.py | 13 + openapi_core/validation/request/validators.py | 10 +- .../validation/response/validators.py | 8 +- openapi_core/validation/validators.py | 4 +- requirements.txt | 2 + requirements_2.7.txt | 1 + setup.cfg | 2 + .../contrib/flask/test_flask_decorator.py | 63 ++- .../contrib/flask/test_flask_views.py | 75 +++- tests/integration/validation/test_minimal.py | 9 +- tests/integration/validation/test_petstore.py | 68 ++- .../integration/validation/test_validators.py | 36 +- tests/unit/templating/test_paths_finders.py | 392 ++++++++++++++++++ tests/unit/templating/test_paths_util.py | 18 - 20 files changed, 753 insertions(+), 127 deletions(-) create mode 100644 openapi_core/templating/datatypes.py create mode 100644 openapi_core/templating/paths/exceptions.py delete mode 100644 openapi_core/templating/paths/util.py create mode 100644 openapi_core/templating/util.py create mode 100644 tests/unit/templating/test_paths_finders.py delete mode 100644 tests/unit/templating/test_paths_util.py diff --git a/openapi_core/contrib/flask/handlers.py b/openapi_core/contrib/flask/handlers.py index bc0cf5b6..29fecf6b 100644 --- a/openapi_core/contrib/flask/handlers.py +++ b/openapi_core/contrib/flask/handlers.py @@ -3,13 +3,17 @@ from flask.json import dumps from openapi_core.schema.media_types.exceptions import InvalidContentType -from openapi_core.schema.servers.exceptions import InvalidServer +from openapi_core.templating.paths.exceptions import ( + ServerNotFound, OperationNotFound, PathNotFound, +) class FlaskOpenAPIErrorsHandler(object): OPENAPI_ERROR_STATUS = { - InvalidServer: 500, + ServerNotFound: 400, + OperationNotFound: 405, + PathNotFound: 404, InvalidContentType: 415, } diff --git a/openapi_core/schema/servers/models.py b/openapi_core/schema/servers/models.py index 08e72ab6..3620b28a 100644 --- a/openapi_core/schema/servers/models.py +++ b/openapi_core/schema/servers/models.py @@ -1,5 +1,6 @@ """OpenAPI core servers models module""" from six import iteritems +from six.moves.urllib.parse import urljoin class Server(object): @@ -25,6 +26,15 @@ def get_url(self, **variables): variables = self.default_variables return self.url.format(**variables) + @staticmethod + def is_absolute(url): + return url.startswith('//') or '://' in url + + def get_absolute_url(self, base_url=None): + if base_url is not None and not self.is_absolute(self.url): + return urljoin(base_url, self.url) + return self.url + class ServerVariable(object): diff --git a/openapi_core/templating/datatypes.py b/openapi_core/templating/datatypes.py new file mode 100644 index 00000000..7087d9e3 --- /dev/null +++ b/openapi_core/templating/datatypes.py @@ -0,0 +1,13 @@ +import attr + + +@attr.s +class TemplateResult(object): + pattern = attr.ib(default=None) + variables = attr.ib(default=None) + + @property + def resolved(self): + if not self.variables: + return self.pattern + return self.pattern.format(**self.variables) diff --git a/openapi_core/templating/paths/exceptions.py b/openapi_core/templating/paths/exceptions.py new file mode 100644 index 00000000..0ed2e7e4 --- /dev/null +++ b/openapi_core/templating/paths/exceptions.py @@ -0,0 +1,36 @@ +import attr + +from openapi_core.exceptions import OpenAPIError + + +class PathError(OpenAPIError): + """Path error""" + + +@attr.s(hash=True) +class PathNotFound(PathError): + """Find path error""" + url = attr.ib() + + def __str__(self): + return "Path not found for {0}".format(self.url) + + +@attr.s(hash=True) +class OperationNotFound(PathError): + """Find path operation error""" + url = attr.ib() + method = attr.ib() + + def __str__(self): + return "Operation {0} not found for {1}".format( + self.method, self.url) + + +@attr.s(hash=True) +class ServerNotFound(PathError): + """Find server error""" + url = attr.ib() + + def __str__(self): + return "Server not found for {0}".format(self.url) diff --git a/openapi_core/templating/paths/finders.py b/openapi_core/templating/paths/finders.py index a131dcad..30d0a4f9 100644 --- a/openapi_core/templating/paths/finders.py +++ b/openapi_core/templating/paths/finders.py @@ -1,27 +1,85 @@ """OpenAPI core templating paths finders module""" -from openapi_core.templating.paths.util import get_operation_pattern +from more_itertools import peekable +from six import iteritems + +from openapi_core.templating.datatypes import TemplateResult +from openapi_core.templating.util import parse, search +from openapi_core.templating.paths.exceptions import ( + PathNotFound, OperationNotFound, ServerNotFound, +) class PathFinder(object): - def __init__(self, spec): + def __init__(self, spec, base_url=None): self.spec = spec + self.base_url = base_url def find(self, request): - operation_pattern = self._get_operation_pattern(request) + paths_iter = self._get_paths_iter(request.full_url_pattern) + paths_iter_peek = peekable(paths_iter) + + if not paths_iter_peek: + raise PathNotFound(request.full_url_pattern) + + operations_iter = self._get_operations_iter( + request.method, paths_iter_peek) + operations_iter_peek = peekable(operations_iter) + + if not operations_iter_peek: + raise OperationNotFound(request.full_url_pattern, request.method) + + servers_iter = self._get_servers_iter( + request.full_url_pattern, operations_iter_peek) - path = self.spec[operation_pattern] - path_variables = {} - operation = self.spec.get_operation(operation_pattern, request.method) - servers = path.servers or operation.servers or self.spec.servers - server = servers[0] - server_variables = {} + try: + return next(servers_iter) + except StopIteration: + raise ServerNotFound(request.full_url_pattern) - return path, operation, server, path_variables, server_variables + def _get_paths_iter(self, full_url_pattern): + for path_pattern, path in iteritems(self.spec.paths): + # simple path + if full_url_pattern.endswith(path_pattern): + path_result = TemplateResult(path_pattern, {}) + yield (path, path_result) + # template path + else: + result = search(path_pattern, full_url_pattern) + if result: + path_result = TemplateResult(path_pattern, result.named) + yield (path, path_result) - def _get_operation_pattern(self, request): - server = self.spec.get_server(request.full_url_pattern) + def _get_operations_iter(self, request_method, paths_iter): + for path, path_result in paths_iter: + if request_method not in path.operations: + continue + operation = path.operations[request_method] + yield (path, operation, path_result) - return get_operation_pattern( - server.default_url, request.full_url_pattern - ) + def _get_servers_iter(self, full_url_pattern, ooperations_iter): + for path, operation, path_result in ooperations_iter: + servers = path.servers or operation.servers or self.spec.servers + for server in servers: + server_url_pattern = full_url_pattern.rsplit( + path_result.resolved, 1)[0] + server_url = server.get_absolute_url(self.base_url) + if server_url.endswith('/'): + server_url = server_url[:-1] + # simple path + if server_url_pattern.startswith(server_url): + server_result = TemplateResult(server.url, {}) + yield ( + path, operation, server, + path_result, server_result, + ) + # template path + else: + result = parse(server.url, server_url_pattern) + if result: + server_result = TemplateResult( + server.url, result.named) + yield ( + path, operation, server, + path_result, server_result, + ) diff --git a/openapi_core/templating/paths/util.py b/openapi_core/templating/paths/util.py deleted file mode 100644 index dc460a29..00000000 --- a/openapi_core/templating/paths/util.py +++ /dev/null @@ -1,24 +0,0 @@ -"""OpenAPI core templating paths util module""" -from six.moves.urllib.parse import urlparse - - -def is_absolute(url): - return url.startswith('//') or '://' in url - - -def path_qs(url): - pr = urlparse(url) - result = pr.path - if pr.query: - result += '?' + pr.query - return result - - -def get_operation_pattern(server_url, request_url_pattern): - """Return an updated request URL pattern with the server URL removed.""" - if server_url[-1] == "/": - # operations have to start with a slash, so do not remove it - server_url = server_url[:-1] - if is_absolute(server_url): - return request_url_pattern.replace(server_url, "", 1) - return path_qs(request_url_pattern).replace(server_url, "", 1) diff --git a/openapi_core/templating/util.py b/openapi_core/templating/util.py new file mode 100644 index 00000000..2643011c --- /dev/null +++ b/openapi_core/templating/util.py @@ -0,0 +1,13 @@ +from parse import Parser + + +def search(path_pattern, full_url_pattern): + p = Parser(path_pattern) + p._expression = p._expression + '$' + return p.search(full_url_pattern) + + +def parse(server_url, server_url_pattern): + p = Parser(server_url) + p._expression = '^' + p._expression + return p.parse(server_url_pattern) diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index de0ca284..cffeef8d 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -5,14 +5,12 @@ from openapi_core.casting.schemas.exceptions import CastError from openapi_core.deserializing.exceptions import DeserializeError from openapi_core.schema.media_types.exceptions import InvalidContentType -from openapi_core.schema.operations.exceptions import InvalidOperation from openapi_core.schema.parameters.exceptions import ( MissingRequiredParameter, MissingParameter, ) -from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.request_bodies.exceptions import MissingRequestBody -from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.security.exceptions import SecurityError +from openapi_core.templating.paths.exceptions import PathError from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( UnmarshalError, ValidateError, @@ -30,7 +28,7 @@ def validate(self, request): try: path, operation, _, _, _ = self._find_path(request) # don't process if operation errors - except (InvalidServer, InvalidPath, InvalidOperation) as exc: + except PathError as exc: return RequestValidationResult([exc, ], None, None, None) try: @@ -53,7 +51,7 @@ def validate(self, request): def _validate_parameters(self, request): try: path, operation, _, _, _ = self._find_path(request) - except (InvalidServer, InvalidPath, InvalidOperation) as exc: + except PathError as exc: return RequestValidationResult([exc, ], None, None) params, params_errors = self._get_parameters( @@ -67,7 +65,7 @@ def _validate_parameters(self, request): def _validate_body(self, request): try: _, operation, _, _, _ = self._find_path(request) - except (InvalidServer, InvalidOperation) as exc: + except PathError as exc: return RequestValidationResult([exc, ], None, None) body, body_errors = self._get_body(request, operation) diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py index bed96d87..07dc1d3f 100644 --- a/openapi_core/validation/response/validators.py +++ b/openapi_core/validation/response/validators.py @@ -1,13 +1,11 @@ """OpenAPI core validation response validators module""" from openapi_core.casting.schemas.exceptions import CastError from openapi_core.deserializing.exceptions import DeserializeError -from openapi_core.schema.operations.exceptions import InvalidOperation from openapi_core.schema.media_types.exceptions import InvalidContentType -from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.responses.exceptions import ( InvalidResponse, MissingResponseContent, ) -from openapi_core.schema.servers.exceptions import InvalidServer +from openapi_core.templating.paths.exceptions import PathError from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( UnmarshalError, ValidateError, @@ -22,7 +20,7 @@ def validate(self, request, response): try: _, operation, _, _, _ = self._find_path(request) # don't process if operation errors - except (InvalidServer, InvalidPath, InvalidOperation) as exc: + except PathError as exc: return ResponseValidationResult([exc, ], None, None) try: @@ -47,7 +45,7 @@ def _validate_data(self, request, response): try: _, operation, _, _, _ = self._find_path(request) # don't process if operation errors - except (InvalidServer, InvalidPath, InvalidOperation) as exc: + except PathError as exc: return ResponseValidationResult([exc, ], None, None) try: diff --git a/openapi_core/validation/validators.py b/openapi_core/validation/validators.py index d3954549..271209cd 100644 --- a/openapi_core/validation/validators.py +++ b/openapi_core/validation/validators.py @@ -5,15 +5,17 @@ class BaseValidator(object): def __init__( self, spec, + base_url=None, custom_formatters=None, custom_media_type_deserializers=None, ): self.spec = spec + self.base_url = base_url self.custom_formatters = custom_formatters self.custom_media_type_deserializers = custom_media_type_deserializers def _find_path(self, request): from openapi_core.templating.paths.finders import PathFinder - finder = PathFinder(self.spec) + finder = PathFinder(self.spec, base_url=self.base_url) return finder.find(request) def _deserialise_media_type(self, media_type, value): diff --git a/requirements.txt b/requirements.txt index de7efe30..b70e99e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ lazy-object-proxy strict_rfc3339 isodate attrs +parse==1.14.0 +more-itertools>=5.0.0 diff --git a/requirements_2.7.txt b/requirements_2.7.txt index cc477126..c9112210 100644 --- a/requirements_2.7.txt +++ b/requirements_2.7.txt @@ -6,3 +6,4 @@ backports.functools-partialmethod enum34 strict_rfc3339 attrs +more-itertools==5.0.0 diff --git a/setup.cfg b/setup.cfg index 3f0d76b0..1d78dd6c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,8 @@ install_requires = isodate attrs werkzeug + parse + more-itertools backports.functools-lru-cache; python_version<"3.0" backports.functools-partialmethod; python_version<"3.0" tests_require = diff --git a/tests/integration/contrib/flask/test_flask_decorator.py b/tests/integration/contrib/flask/test_flask_decorator.py index 77434155..afa5ad20 100644 --- a/tests/integration/contrib/flask/test_flask_decorator.py +++ b/tests/integration/contrib/flask/test_flask_decorator.py @@ -39,13 +39,21 @@ def view_response(*args, **kwargs): return view_response @pytest.fixture(autouse=True) - def view(self, app, decorator, view_response): - @app.route("/browse//") + def details_view(self, app, decorator, view_response): + @app.route("/browse//", methods=['GET', 'POST']) @decorator def browse_details(*args, **kwargs): return view_response(*args, **kwargs) return browse_details + @pytest.fixture(autouse=True) + def list_view(self, app, decorator, view_response): + @app.route("/browse/") + @decorator + def browse_list(*args, **kwargs): + return view_response(*args, **kwargs) + return browse_list + def test_invalid_content_type(self, client): def view_response_callable(*args, **kwargs): from flask.globals import request @@ -80,17 +88,60 @@ def test_server_error(self, client): 'errors': [ { 'class': ( - "" + "" ), - 'status': 500, + 'status': 400, 'title': ( - 'Invalid request server ' + 'Server not found for ' 'https://localhost/browse/{id}/' ), } ] } + assert result.status_code == 400 + assert result.json == expected_data + + def test_operation_error(self, client): + result = client.post('/browse/12/') + + expected_data = { + 'errors': [ + { + 'class': ( + "" + ), + 'status': 405, + 'title': ( + 'Operation post not found for ' + 'http://localhost/browse/{id}/' + ), + } + ] + } + assert result.status_code == 405 + assert result.json == expected_data + + def test_path_error(self, client): + result = client.get('/browse/') + + expected_data = { + 'errors': [ + { + 'class': ( + "" + ), + 'status': 404, + 'title': ( + 'Path not found for ' + 'http://localhost/browse/' + ), + } + ] + } + assert result.status_code == 404 assert result.json == expected_data def test_endpoint_error(self, client): diff --git a/tests/integration/contrib/flask/test_flask_views.py b/tests/integration/contrib/flask/test_flask_views.py index 83706444..92355e2e 100644 --- a/tests/integration/contrib/flask/test_flask_views.py +++ b/tests/integration/contrib/flask/test_flask_views.py @@ -28,17 +28,30 @@ def client(self, app): yield client @pytest.fixture - def view_func(self, spec): + def details_view_func(self, spec): outer = self - class MyView(FlaskOpenAPIView): + class MyDetailsView(FlaskOpenAPIView): def get(self, id): return outer.view_response - return MyView.as_view('browse_details', spec) + + def post(self, id): + return outer.view_response + return MyDetailsView.as_view('browse_details', spec) + + @pytest.fixture + def list_view_func(self, spec): + outer = self + + class MyListView(FlaskOpenAPIView): + def get(self): + return outer.view_response + return MyListView.as_view('browse_list', spec) @pytest.fixture(autouse=True) - def view(self, app, view_func): - app.add_url_rule("/browse//", view_func=view_func) + def view(self, app, details_view_func, list_view_func): + app.add_url_rule("/browse//", view_func=details_view_func) + app.add_url_rule("/browse/", view_func=list_view_func) def test_invalid_content_type(self, client): self.view_response = make_response('success', 200) @@ -68,18 +81,60 @@ def test_server_error(self, client): 'errors': [ { 'class': ( - "" + "" ), - 'status': 500, + 'status': 400, 'title': ( - 'Invalid request server ' + 'Server not found for ' 'https://localhost/browse/{id}/' ), } ] } - assert result.status_code == 500 + assert result.status_code == 400 + assert result.json == expected_data + + def test_operation_error(self, client): + result = client.post('/browse/12/') + + expected_data = { + 'errors': [ + { + 'class': ( + "" + ), + 'status': 405, + 'title': ( + 'Operation post not found for ' + 'http://localhost/browse/{id}/' + ), + } + ] + } + assert result.status_code == 405 + assert result.json == expected_data + + def test_path_error(self, client): + result = client.get('/browse/') + + expected_data = { + 'errors': [ + { + 'class': ( + "" + ), + 'status': 404, + 'title': ( + 'Path not found for ' + 'http://localhost/browse/' + ), + } + ] + } + assert result.status_code == 404 assert result.json == expected_data def test_endpoint_error(self, client): diff --git a/tests/integration/validation/test_minimal.py b/tests/integration/validation/test_minimal.py index 00390aae..7e7e1f54 100644 --- a/tests/integration/validation/test_minimal.py +++ b/tests/integration/validation/test_minimal.py @@ -1,8 +1,9 @@ import pytest -from openapi_core.schema.operations.exceptions import InvalidOperation -from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.shortcuts import create_spec +from openapi_core.templating.paths.exceptions import ( + PathNotFound, OperationNotFound, +) from openapi_core.testing import MockRequest from openapi_core.validation.request.validators import RequestValidator @@ -45,7 +46,7 @@ def test_invalid_operation(self, factory, server, spec_path): result = validator.validate(request) assert len(result.errors) == 1 - assert isinstance(result.errors[0], InvalidOperation) + assert isinstance(result.errors[0], OperationNotFound) assert result.body is None assert result.parameters is None @@ -60,6 +61,6 @@ def test_invalid_path(self, factory, server, spec_path): result = validator.validate(request) assert len(result.errors) == 1 - assert isinstance(result.errors[0], InvalidPath) + assert isinstance(result.errors[0], PathNotFound) assert result.body is None assert result.parameters is None diff --git a/tests/integration/validation/test_petstore.py b/tests/integration/validation/test_petstore.py index a95848d2..15f112bc 100644 --- a/tests/integration/validation/test_petstore.py +++ b/tests/integration/validation/test_petstore.py @@ -16,9 +16,11 @@ MissingRequiredParameter, ) from openapi_core.schema.schemas.enums import SchemaType -from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.shortcuts import ( - create_spec, validate_parameters, validate_body, + create_spec, validate_parameters, validate_body, validate_data, +) +from openapi_core.templating.paths.exceptions import ( + ServerNotFound, ) from openapi_core.testing import MockRequest, MockResponse from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue @@ -163,7 +165,7 @@ def test_get_pets_invalid_response(self, spec, response_validator): ) assert body is None - data_json = { + response_data_json = { 'data': [ { 'id': 1, @@ -173,8 +175,11 @@ def test_get_pets_invalid_response(self, spec, response_validator): } ], } - data = json.dumps(data_json) - response = MockResponse(data) + response_data = json.dumps(response_data_json) + response = MockResponse(response_data) + + with pytest.raises(InvalidSchemaValue): + validate_data(spec, request, response) response_result = response_validator.validate(request, response) @@ -182,7 +187,7 @@ def test_get_pets_invalid_response(self, spec, response_validator): assert response_result.errors == [ InvalidSchemaValue( type=SchemaType.OBJECT, - value=data_json, + value=response_data_json, schema_errors=schema_errors, ), ] @@ -363,7 +368,7 @@ def test_get_pets_none_value(self, spec): assert body is None def test_post_birds(self, spec, spec_dict): - host_url = 'http://petstore.swagger.io/v1' + host_url = 'https://staging.gigantic-server.com/v1' path_pattern = '/v1/pets' pet_name = 'Cat' pet_tag = 'cats' @@ -423,7 +428,7 @@ def test_post_birds(self, spec, spec_dict): assert body.healthy == pet_healthy def test_post_cats(self, spec, spec_dict): - host_url = 'http://petstore.swagger.io/v1' + host_url = 'https://staging.gigantic-server.com/v1' path_pattern = '/v1/pets' pet_name = 'Cat' pet_tag = 'cats' @@ -483,7 +488,7 @@ def test_post_cats(self, spec, spec_dict): assert body.healthy == pet_healthy def test_post_cats_boolean_string(self, spec, spec_dict): - host_url = 'http://petstore.swagger.io/v1' + host_url = 'https://staging.gigantic-server.com/v1' path_pattern = '/v1/pets' pet_name = 'Cat' pet_tag = 'cats' @@ -543,7 +548,7 @@ def test_post_cats_boolean_string(self, spec, spec_dict): assert body.healthy is False def test_post_no_one_of_schema(self, spec, spec_dict): - host_url = 'http://petstore.swagger.io/v1' + host_url = 'https://staging.gigantic-server.com/v1' path_pattern = '/v1/pets' pet_name = 'Cat' alias = 'kitty' @@ -580,7 +585,7 @@ def test_post_no_one_of_schema(self, spec, spec_dict): validate_body(spec, request) def test_post_cats_only_required_body(self, spec, spec_dict): - host_url = 'http://petstore.swagger.io/v1' + host_url = 'https://staging.gigantic-server.com/v1' path_pattern = '/v1/pets' pet_name = 'Cat' pet_healthy = True @@ -625,7 +630,7 @@ def test_post_cats_only_required_body(self, spec, spec_dict): assert not hasattr(body, 'address') def test_post_pets_raises_invalid_mimetype(self, spec): - host_url = 'http://petstore.swagger.io/v1' + host_url = 'https://staging.gigantic-server.com/v1' path_pattern = '/v1/pets' data_json = { 'name': 'Cat', @@ -660,7 +665,7 @@ def test_post_pets_raises_invalid_mimetype(self, spec): validate_body(spec, request) def test_post_pets_missing_cookie(self, spec, spec_dict): - host_url = 'http://petstore.swagger.io/v1' + host_url = 'https://staging.gigantic-server.com/v1' path_pattern = '/v1/pets' pet_name = 'Cat' pet_healthy = True @@ -694,7 +699,7 @@ def test_post_pets_missing_cookie(self, spec, spec_dict): assert not hasattr(body, 'address') def test_post_pets_missing_header(self, spec, spec_dict): - host_url = 'http://petstore.swagger.io/v1' + host_url = 'https://staging.gigantic-server.com/v1' path_pattern = '/v1/pets' pet_name = 'Cat' pet_healthy = True @@ -748,12 +753,29 @@ def test_post_pets_raises_invalid_server_error(self, spec): headers=headers, cookies=cookies, ) - with pytest.raises(InvalidServer): + with pytest.raises(ServerNotFound): validate_parameters(spec, request) - with pytest.raises(InvalidServer): + with pytest.raises(ServerNotFound): validate_body(spec, request) + data_id = 1 + data_name = 'test' + data_json = { + 'data': { + 'id': data_id, + 'name': data_name, + 'ears': { + 'healthy': True, + }, + }, + } + data = json.dumps(data_json) + response = MockResponse(data) + + with pytest.raises(ServerNotFound): + validate_data(spec, request, response) + def test_get_pet(self, spec, response_validator): host_url = 'http://petstore.swagger.io/v1' path_pattern = '/v1/pets/{petId}' @@ -1075,14 +1097,22 @@ def test_post_tags_created_datetime( message = 'Bad request' rootCause = 'Tag already exist' additionalinfo = 'Tag Dog already exist' - data_json = { + response_data_json = { 'code': code, 'message': message, 'rootCause': rootCause, 'additionalinfo': additionalinfo, } - data = json.dumps(data_json) - response = MockResponse(data, status_code=404) + response_data = json.dumps(response_data_json) + response = MockResponse(response_data, status_code=404) + + data = validate_data(spec, request, response) + + assert isinstance(data, BaseModel) + assert data.code == code + assert data.message == message + assert data.rootCause == rootCause + assert data.additionalinfo == additionalinfo response_result = response_validator.validate(request, response) diff --git a/tests/integration/validation/test_validators.py b/tests/integration/validation/test_validators.py index 30c985ab..101f2329 100644 --- a/tests/integration/validation/test_validators.py +++ b/tests/integration/validation/test_validators.py @@ -9,15 +9,15 @@ InvalidContentType, ) from openapi_core.extensions.models.models import BaseModel -from openapi_core.schema.operations.exceptions import InvalidOperation from openapi_core.schema.parameters.exceptions import MissingRequiredParameter -from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.request_bodies.exceptions import MissingRequestBody from openapi_core.schema.responses.exceptions import ( MissingResponseContent, InvalidResponse, ) -from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.shortcuts import create_spec +from openapi_core.templating.paths.exceptions import ( + PathNotFound, OperationNotFound, +) from openapi_core.testing import MockRequest, MockResponse from openapi_core.unmarshalling.schemas.exceptions import InvalidSchemaValue from openapi_core.validation.exceptions import InvalidSecurity @@ -48,7 +48,7 @@ def spec(self, spec_dict): @pytest.fixture(scope='session') def validator(self, spec): - return RequestValidator(spec) + return RequestValidator(spec, base_url=self.host_url) def test_request_server_error(self, validator): request = MockRequest('http://petstore.invalid.net/v1', 'get', '/') @@ -56,7 +56,7 @@ def test_request_server_error(self, validator): result = validator.validate(request) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidServer + assert type(result.errors[0]) == PathNotFound assert result.body is None assert result.parameters is None @@ -66,7 +66,7 @@ def test_invalid_path(self, validator): result = validator.validate(request) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidPath + assert type(result.errors[0]) == PathNotFound assert result.body is None assert result.parameters is None @@ -76,7 +76,7 @@ def test_invalid_operation(self, validator): result = validator.validate(request) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidOperation + assert type(result.errors[0]) == OperationNotFound assert result.body is None assert result.parameters is None @@ -149,7 +149,7 @@ def test_missing_body(self, validator): 'user': '123', } request = MockRequest( - self.host_url, 'post', '/v1/pets', + 'https://development.gigantic-server.com', 'post', '/v1/pets', path_pattern='/v1/pets', headers=headers, cookies=cookies, ) @@ -176,7 +176,7 @@ def test_invalid_content_type(self, validator): 'user': '123', } request = MockRequest( - self.host_url, 'post', '/v1/pets', + 'https://development.gigantic-server.com', 'post', '/v1/pets', path_pattern='/v1/pets', mimetype='text/csv', headers=headers, cookies=cookies, ) @@ -220,7 +220,7 @@ def test_post_pets(self, validator, spec_dict): 'user': '123', } request = MockRequest( - self.host_url, 'post', '/v1/pets', + 'https://development.gigantic-server.com', 'post', '/v1/pets', path_pattern='/v1/pets', data=data, headers=headers, cookies=cookies, ) @@ -326,7 +326,7 @@ def spec(self, spec_dict): @pytest.fixture(scope='session') def validator(self, spec): - return RequestValidator(spec) + return RequestValidator(spec, base_url='http://example.com') def test_request_missing_param(self, validator): request = MockRequest('http://example.com', 'get', '/resource') @@ -373,7 +373,8 @@ def test_request_override_param(self, spec_dict): }, } ] - validator = RequestValidator(create_spec(spec_dict)) + validator = RequestValidator( + create_spec(spec_dict), base_url='http://example.com') request = MockRequest('http://example.com', 'get', '/resource') result = validator.validate(request) @@ -395,7 +396,8 @@ def test_request_override_param_uniqueness(self, spec_dict): }, } ] - validator = RequestValidator(create_spec(spec_dict)) + validator = RequestValidator( + create_spec(spec_dict), base_url='http://example.com') request = MockRequest('http://example.com', 'get', '/resource') result = validator.validate(request) @@ -419,7 +421,7 @@ def spec(self, spec_dict): @pytest.fixture def validator(self, spec): - return ResponseValidator(spec) + return ResponseValidator(spec, base_url=self.host_url) def test_invalid_server(self, validator): request = MockRequest('http://petstore.invalid.net/v1', 'get', '/') @@ -428,18 +430,18 @@ def test_invalid_server(self, validator): result = validator.validate(request, response) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidServer + assert type(result.errors[0]) == PathNotFound assert result.data is None assert result.headers is None def test_invalid_operation(self, validator): - request = MockRequest(self.host_url, 'get', '/v1') + request = MockRequest(self.host_url, 'patch', '/v1/pets') response = MockResponse('Not Found', status_code=404) result = validator.validate(request, response) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidPath + assert type(result.errors[0]) == OperationNotFound assert result.data is None assert result.headers is None diff --git a/tests/unit/templating/test_paths_finders.py b/tests/unit/templating/test_paths_finders.py new file mode 100644 index 00000000..f44a4d3b --- /dev/null +++ b/tests/unit/templating/test_paths_finders.py @@ -0,0 +1,392 @@ +import pytest + +from openapi_core.schema.infos.models import Info +from openapi_core.schema.operations.models import Operation +from openapi_core.schema.parameters.models import Parameter +from openapi_core.schema.paths.models import Path +from openapi_core.schema.servers.models import Server, ServerVariable +from openapi_core.schema.specs.models import Spec +from openapi_core.templating.datatypes import TemplateResult +from openapi_core.templating.paths.exceptions import ( + PathNotFound, OperationNotFound, ServerNotFound, +) +from openapi_core.templating.paths.finders import PathFinder +from openapi_core.testing import MockRequest + + +class BaseTestSimpleServer(object): + + server_url = 'http://petstore.swagger.io' + + @pytest.fixture + def server(self): + return Server(self.server_url, {}) + + @pytest.fixture + def servers(self, server): + return [server, ] + + +class BaseTestVariableServer(BaseTestSimpleServer): + + server_url = 'http://petstore.swagger.io/{version}' + server_variable_name = 'version' + server_variable_default = 'v1' + server_variable_enum = ['v1', 'v2'] + + @pytest.fixture + def server_variable(self): + return ServerVariable( + self.server_variable_name, + default=self.server_variable_default, + enum=self.server_variable_enum, + ) + + @pytest.fixture + def server_variables(self, server_variable): + return { + self.server_variable_name: server_variable, + } + + @pytest.fixture + def server(self, server_variables): + return Server(self.server_url, server_variables) + + +class BaseTestSimplePath(object): + + path_name = '/resource' + + @pytest.fixture + def path(self, operations): + return Path(self.path_name, operations) + + @pytest.fixture + def paths(self, path): + return { + self.path_name: path, + } + + +class BaseTestVariablePath(BaseTestSimplePath): + + path_name = '/resource/{resource_id}' + path_parameter_name = 'resource_id' + + @pytest.fixture + def parameter(self): + return Parameter(self.path_parameter_name, 'path') + + @pytest.fixture + def parameters(self, parameter): + return { + self.path_parameter_name: parameter + } + + @pytest.fixture + def path(self, operations, parameters): + return Path(self.path_name, operations, parameters=parameters) + + +class BaseTestSpecServer(object): + + @pytest.fixture + def info(self): + return Info('Test schema', '1.0') + + @pytest.fixture + def operation(self): + return Operation('get', self.path_name, {}, {}) + + @pytest.fixture + def operations(self, operation): + return { + 'get': operation, + } + + @pytest.fixture + def spec(self, info, paths, servers): + return Spec(info, paths, servers) + + @pytest.fixture + def finder(self, spec): + return PathFinder(spec) + + +class BaseTestPathServer(BaseTestSpecServer): + + @pytest.fixture + def path(self, operations, servers): + return Path(self.path_name, operations, servers=servers) + + @pytest.fixture + def spec(self, info, paths): + return Spec(info, paths) + + +class BaseTestOperationServer(BaseTestSpecServer): + + @pytest.fixture + def operation(self, servers): + return Operation('get', self.path_name, {}, {}, servers=servers) + + @pytest.fixture + def spec(self, info, paths): + return Spec(info, paths) + + +class BaseTestServerNotFound(object): + + @pytest.fixture + def servers(self): + return [] + + def test_raises(self, finder): + request_uri = '/resource' + request = MockRequest( + 'http://petstore.swagger.io', 'get', request_uri) + + with pytest.raises(ServerNotFound): + finder.find(request) + + +class BaseTestOperationNotFound(object): + + @pytest.fixture + def operations(self): + return {} + + def test_raises(self, finder): + request_uri = '/resource' + request = MockRequest( + 'http://petstore.swagger.io', 'get', request_uri) + + with pytest.raises(OperationNotFound): + finder.find(request) + + +class BaseTestValid(object): + + def test_simple(self, finder, path, operation, server): + request_uri = '/resource' + request = MockRequest( + 'http://petstore.swagger.io', 'get', request_uri) + + result = finder.find(request) + + path_result = TemplateResult(self.path_name, {}) + server_result = TemplateResult(self.server_url, {}) + assert result == ( + path, operation, server, path_result, server_result, + ) + + +class BaseTestVariableValid(object): + + @pytest.mark.parametrize('version', ['v1', 'v2']) + def test_variable(self, finder, path, operation, server, version): + request_uri = '/{0}/resource'.format(version) + request = MockRequest( + 'http://petstore.swagger.io', 'get', request_uri) + + result = finder.find(request) + + path_result = TemplateResult(self.path_name, {}) + server_result = TemplateResult(self.server_url, {'version': version}) + assert result == ( + path, operation, server, path_result, server_result, + ) + + +class BaseTestPathVariableValid(object): + + @pytest.mark.parametrize('res_id', ['111', '222']) + def test_path_variable(self, finder, path, operation, server, res_id): + request_uri = '/resource/{0}'.format(res_id) + request = MockRequest( + 'http://petstore.swagger.io', 'get', request_uri) + + result = finder.find(request) + + path_result = TemplateResult(self.path_name, {'resource_id': res_id}) + server_result = TemplateResult(self.server_url, {}) + assert result == ( + path, operation, server, path_result, server_result, + ) + + +class BaseTestPathNotFound(object): + + @pytest.fixture + def paths(self): + return {} + + def test_raises(self, finder): + request_uri = '/resource' + request = MockRequest( + 'http://petstore.swagger.io', 'get', request_uri) + + with pytest.raises(PathNotFound): + finder.find(request) + + +class TestSpecSimpleServerServerNotFound( + BaseTestServerNotFound, BaseTestSpecServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestSpecSimpleServerOperationNotFound( + BaseTestOperationNotFound, BaseTestSpecServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestSpecSimpleServerPathNotFound( + BaseTestPathNotFound, BaseTestSpecServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestOperationSimpleServerServerNotFound( + BaseTestServerNotFound, BaseTestOperationServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestOperationSimpleServerOperationNotFound( + BaseTestOperationNotFound, BaseTestOperationServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestOperationSimpleServerPathNotFound( + BaseTestPathNotFound, BaseTestOperationServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestPathSimpleServerServerNotFound( + BaseTestServerNotFound, BaseTestPathServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestPathSimpleServerOperationNotFound( + BaseTestOperationNotFound, BaseTestPathServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestPathSimpleServerPathNotFound( + BaseTestPathNotFound, BaseTestPathServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestSpecSimpleServerValid( + BaseTestValid, BaseTestSpecServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestOperationSimpleServerValid( + BaseTestValid, BaseTestOperationServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestPathSimpleServerValid( + BaseTestValid, BaseTestPathServer, + BaseTestSimplePath, BaseTestSimpleServer): + pass + + +class TestSpecSimpleServerVariablePathValid( + BaseTestPathVariableValid, BaseTestSpecServer, + BaseTestVariablePath, BaseTestSimpleServer): + pass + + +class TestOperationSimpleServerVariablePathValid( + BaseTestPathVariableValid, BaseTestOperationServer, + BaseTestVariablePath, BaseTestSimpleServer): + pass + + +class TestPathSimpleServerVariablePathValid( + BaseTestPathVariableValid, BaseTestPathServer, + BaseTestVariablePath, BaseTestSimpleServer): + pass + + +class TestSpecVariableServerServerNotFound( + BaseTestServerNotFound, BaseTestSpecServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestSpecVariableServerOperationNotFound( + BaseTestOperationNotFound, BaseTestSpecServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestSpecVariableServerPathNotFound( + BaseTestPathNotFound, BaseTestSpecServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestOperationVariableServerServerNotFound( + BaseTestServerNotFound, BaseTestOperationServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestOperationVariableServerOperationNotFound( + BaseTestOperationNotFound, BaseTestOperationServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestOperationVariableServerPathNotFound( + BaseTestPathNotFound, BaseTestOperationServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestPathVariableServerServerNotFound( + BaseTestServerNotFound, BaseTestPathServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestPathVariableServerOperationNotFound( + BaseTestOperationNotFound, BaseTestPathServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestPathVariableServerPathNotFound( + BaseTestPathNotFound, BaseTestPathServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestSpecVariableServerValid( + BaseTestVariableValid, BaseTestSpecServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestOperationVariableServerValid( + BaseTestVariableValid, BaseTestOperationServer, + BaseTestSimplePath, BaseTestVariableServer): + pass + + +class TestPathVariableServerValid( + BaseTestVariableValid, BaseTestPathServer, + BaseTestSimplePath, BaseTestVariableServer): + pass diff --git a/tests/unit/templating/test_paths_util.py b/tests/unit/templating/test_paths_util.py deleted file mode 100644 index 556fdea3..00000000 --- a/tests/unit/templating/test_paths_util.py +++ /dev/null @@ -1,18 +0,0 @@ -from openapi_core.templating.paths.util import path_qs - - -class TestPathQs(object): - - def test_path(self): - url = 'https://test.com:1234/path' - - result = path_qs(url) - - assert result == '/path' - - def test_query(self): - url = 'https://test.com:1234/path?query=1' - - result = path_qs(url) - - assert result == '/path?query=1'