mirror of
https://github.com/correl/openapi-core.git
synced 2024-11-22 19:18:36 +00:00
Merge pull request #9 from p1c2u/feature/request-validator
Request validator
This commit is contained in:
commit
7249858dfc
17 changed files with 625 additions and 205 deletions
27
README.rst
27
README.rst
|
@ -51,10 +51,31 @@ Now you can use it to validate and unmarshal requests
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from openapi_core import request_parameters_factory, request_body_factory
|
||||
from openapi_core.validators import RequestValidator
|
||||
|
||||
parameters = request_parameters_factory.create(request, spec)
|
||||
body = request_body_factory.create(request, spec)
|
||||
validator = RequestValidator(spec)
|
||||
result = validator.validate(request)
|
||||
|
||||
# raise errors if request invalid
|
||||
result.validate()
|
||||
|
||||
# get parameters
|
||||
path_params = result.parameters['path']
|
||||
query_params = result.parameters['query']
|
||||
|
||||
# get body
|
||||
body = result.body
|
||||
|
||||
Request object should implement BaseOpenAPIRequest interface. You can use FlaskOpenAPIRequest a Flask/Werkzeug request wrapper implementation:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openapi_core.validators import RequestValidator
|
||||
from openapi_core.wrappers import FlaskOpenAPIRequest
|
||||
|
||||
openapi_request = FlaskOpenAPIRequest(flask_request)
|
||||
validator = RequestValidator(spec)
|
||||
result = validator.validate(openapi_request)
|
||||
|
||||
Related projects
|
||||
================
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""OpenAPI core module"""
|
||||
from openapi_core.shortcuts import create_spec
|
||||
from openapi_core.wrappers import RequestParametersFactory, RequestBodyFactory
|
||||
from openapi_core.shortcuts import (
|
||||
create_spec, validate_parameters, validate_body,
|
||||
)
|
||||
|
||||
__author__ = 'Artur Maciąg'
|
||||
__email__ = 'maciag.artur@gmail.com'
|
||||
|
@ -8,7 +9,4 @@ __version__ = '0.2.2'
|
|||
__url__ = 'https://github.com/p1c2u/openapi-core'
|
||||
__license__ = 'BSD 3-Clause License'
|
||||
|
||||
__all__ = ['create_spec', 'request_parameters_factory', 'request_body_factory']
|
||||
|
||||
request_parameters_factory = RequestParametersFactory()
|
||||
request_body_factory = RequestBodyFactory()
|
||||
__all__ = ['create_spec', 'validate_parameters', 'validate_body']
|
||||
|
|
|
@ -9,23 +9,11 @@ class OpenAPIMappingError(OpenAPIError):
|
|||
pass
|
||||
|
||||
|
||||
class MissingParameterError(OpenAPIMappingError):
|
||||
class OpenAPIServerError(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingPropertyError(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidContentTypeError(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidOperationError(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidServerError(OpenAPIMappingError):
|
||||
class OpenAPIOperationError(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
|
@ -33,13 +21,53 @@ class InvalidValueType(OpenAPIMappingError):
|
|||
pass
|
||||
|
||||
|
||||
class OpenAPIParameterError(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class OpenAPIBodyError(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidServer(OpenAPIServerError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidOperation(OpenAPIOperationError):
|
||||
pass
|
||||
|
||||
|
||||
class EmptyValue(OpenAPIParameterError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingParameter(OpenAPIParameterError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidParameterValue(OpenAPIParameterError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingBody(OpenAPIBodyError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidMediaTypeValue(OpenAPIBodyError):
|
||||
pass
|
||||
|
||||
|
||||
class UndefinedSchemaProperty(OpenAPIBodyError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingProperty(OpenAPIBodyError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidContentType(OpenAPIBodyError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidValue(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class EmptyValue(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class UndefinedSchemaProperty(OpenAPIMappingError):
|
||||
pass
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""OpenAPI core mediaTypes module"""
|
||||
from six import iteritems
|
||||
|
||||
from openapi_core.exceptions import InvalidValueType, InvalidMediaTypeValue
|
||||
|
||||
|
||||
class MediaType(object):
|
||||
"""Represents an OpenAPI MediaType."""
|
||||
|
@ -13,7 +15,10 @@ class MediaType(object):
|
|||
if not self.schema:
|
||||
return value
|
||||
|
||||
return self.schema.unmarshal(value)
|
||||
try:
|
||||
return self.schema.unmarshal(value)
|
||||
except InvalidValueType as exc:
|
||||
raise InvalidMediaTypeValue(str(exc))
|
||||
|
||||
|
||||
class MediaTypeGenerator(object):
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
import logging
|
||||
import warnings
|
||||
|
||||
from openapi_core.exceptions import EmptyValue
|
||||
from openapi_core.exceptions import (
|
||||
EmptyValue, InvalidValueType, InvalidParameterValue,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -35,12 +37,15 @@ class Parameter(object):
|
|||
if (self.location == "query" and value == "" and
|
||||
not self.allow_empty_value):
|
||||
raise EmptyValue(
|
||||
"Value of {0} parameter cannot be empty.".format(self.name))
|
||||
"Value of {0} parameter cannot be empty".format(self.name))
|
||||
|
||||
if not self.schema:
|
||||
return value
|
||||
|
||||
return self.schema.unmarshal(value)
|
||||
try:
|
||||
return self.schema.unmarshal(value)
|
||||
except InvalidValueType as exc:
|
||||
raise InvalidParameterValue(str(exc))
|
||||
|
||||
|
||||
class ParametersGenerator(object):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""OpenAPI core requestBodies module"""
|
||||
from functools import lru_cache
|
||||
|
||||
from openapi_core.exceptions import InvalidContentType
|
||||
from openapi_core.media_types import MediaTypeGenerator
|
||||
|
||||
|
||||
|
@ -12,7 +13,11 @@ class RequestBody(object):
|
|||
self.required = required
|
||||
|
||||
def __getitem__(self, mimetype):
|
||||
return self.content[mimetype]
|
||||
try:
|
||||
return self.content[mimetype]
|
||||
except KeyError:
|
||||
raise InvalidContentType(
|
||||
"Invalid mime type `{0}`".format(mimetype))
|
||||
|
||||
|
||||
class RequestBodyFactory(object):
|
||||
|
|
|
@ -10,8 +10,7 @@ from json import loads
|
|||
from six import iteritems
|
||||
|
||||
from openapi_core.exceptions import (
|
||||
InvalidValueType, UndefinedSchemaProperty, MissingPropertyError,
|
||||
InvalidValue,
|
||||
InvalidValueType, UndefinedSchemaProperty, MissingProperty, InvalidValue,
|
||||
)
|
||||
from openapi_core.models import ModelFactory
|
||||
|
||||
|
@ -59,7 +58,8 @@ class Schema(object):
|
|||
if value is None:
|
||||
if not self.nullable:
|
||||
raise InvalidValueType(
|
||||
"Failed to cast value of %s to %s", value, self.type,
|
||||
"Failed to cast value of {0} to {1}".format(
|
||||
value, self.type)
|
||||
)
|
||||
return self.default
|
||||
|
||||
|
@ -73,7 +73,7 @@ class Schema(object):
|
|||
return cast_callable(value)
|
||||
except ValueError:
|
||||
raise InvalidValueType(
|
||||
"Failed to cast value of %s to %s", value, self.type,
|
||||
"Failed to cast value of {0} to {1}".format(value, self.type)
|
||||
)
|
||||
|
||||
def unmarshal(self, value):
|
||||
|
@ -88,7 +88,8 @@ class Schema(object):
|
|||
|
||||
if self.enum and casted not in self.enum:
|
||||
raise InvalidValue(
|
||||
"Value of %s not in enum choices: %s", value, str(self.enum),
|
||||
"Value of {0} not in enum choices: {1}".format(
|
||||
value, self.enum)
|
||||
)
|
||||
|
||||
return casted
|
||||
|
@ -115,7 +116,7 @@ class Schema(object):
|
|||
prop_value = value[prop_name]
|
||||
except KeyError:
|
||||
if prop_name in self.required:
|
||||
raise MissingPropertyError(
|
||||
raise MissingProperty(
|
||||
"Missing schema property {0}".format(prop_name))
|
||||
if not prop.nullable and not prop.default:
|
||||
continue
|
||||
|
|
|
@ -3,7 +3,9 @@ from jsonschema.validators import RefResolver
|
|||
from openapi_spec_validator.validators import Dereferencer
|
||||
from openapi_spec_validator import default_handlers
|
||||
|
||||
from openapi_core.exceptions import OpenAPIParameterError, OpenAPIBodyError
|
||||
from openapi_core.specs import SpecFactory
|
||||
from openapi_core.validators import RequestValidator
|
||||
|
||||
|
||||
def create_spec(spec_dict, spec_url=''):
|
||||
|
@ -12,3 +14,25 @@ def create_spec(spec_dict, spec_url=''):
|
|||
dereferencer = Dereferencer(spec_resolver)
|
||||
spec_factory = SpecFactory(dereferencer)
|
||||
return spec_factory.create(spec_dict, spec_url=spec_url)
|
||||
|
||||
|
||||
def validate_parameters(spec, request):
|
||||
validator = RequestValidator(spec)
|
||||
result = validator.validate(request)
|
||||
try:
|
||||
result.validate()
|
||||
except OpenAPIBodyError:
|
||||
return result.parameters
|
||||
else:
|
||||
return result.parameters
|
||||
|
||||
|
||||
def validate_body(spec, request):
|
||||
validator = RequestValidator(spec)
|
||||
result = validator.validate(request)
|
||||
try:
|
||||
result.validate()
|
||||
except OpenAPIParameterError:
|
||||
return result.body
|
||||
else:
|
||||
return result.body
|
||||
|
|
|
@ -6,7 +6,7 @@ from functools import partialmethod, lru_cache
|
|||
from openapi_spec_validator import openapi_v3_spec_validator
|
||||
|
||||
from openapi_core.components import ComponentsFactory
|
||||
from openapi_core.exceptions import InvalidOperationError
|
||||
from openapi_core.exceptions import InvalidOperation, InvalidServer
|
||||
from openapi_core.infos import InfoFactory
|
||||
from openapi_core.paths import PathsGenerator
|
||||
from openapi_core.schemas import SchemaRegistry
|
||||
|
@ -32,6 +32,14 @@ class Spec(object):
|
|||
def default_url(self):
|
||||
return self.servers[0].default_url
|
||||
|
||||
def get_server(self, full_url_pattern):
|
||||
for spec_server in self.servers:
|
||||
if spec_server.default_url in full_url_pattern:
|
||||
return spec_server
|
||||
|
||||
raise InvalidServer(
|
||||
"Invalid request server {0}".format(full_url_pattern))
|
||||
|
||||
def get_server_url(self, index=0):
|
||||
return self.servers[index].default_url
|
||||
|
||||
|
@ -39,7 +47,7 @@ class Spec(object):
|
|||
try:
|
||||
return self.paths[path_pattern].operations[http_method]
|
||||
except KeyError:
|
||||
raise InvalidOperationError(
|
||||
raise InvalidOperation(
|
||||
"Unknown operation path {0} with method {1}".format(
|
||||
path_pattern, http_method))
|
||||
|
||||
|
|
122
openapi_core/validators.py
Normal file
122
openapi_core/validators.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
"""OpenAPI core validators module"""
|
||||
from six import iteritems
|
||||
|
||||
from openapi_core.exceptions import (
|
||||
OpenAPIMappingError, MissingParameter, MissingBody,
|
||||
)
|
||||
|
||||
|
||||
class RequestParameters(dict):
|
||||
|
||||
valid_locations = ['path', 'query', 'headers', 'cookies']
|
||||
|
||||
def __getitem__(self, location):
|
||||
self.validate_location(location)
|
||||
|
||||
return self.setdefault(location, {})
|
||||
|
||||
def __setitem__(self, location, value):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def validate_location(cls, location):
|
||||
if location not in cls.valid_locations:
|
||||
raise OpenAPIMappingError(
|
||||
"Unknown parameter location: {0}".format(str(location)))
|
||||
|
||||
|
||||
class BaseValidationResult(object):
|
||||
|
||||
def __init__(self, errors):
|
||||
self.errors = errors
|
||||
|
||||
def validate(self):
|
||||
for error in self.errors:
|
||||
raise error
|
||||
|
||||
|
||||
class RequestValidationResult(BaseValidationResult):
|
||||
|
||||
def __init__(self, errors, body=None, parameters=None):
|
||||
super(RequestValidationResult, self).__init__(errors)
|
||||
self.body = body
|
||||
self.parameters = parameters or RequestParameters()
|
||||
|
||||
|
||||
class RequestValidator(object):
|
||||
|
||||
def __init__(self, spec):
|
||||
self.spec = spec
|
||||
|
||||
def validate(self, request):
|
||||
errors = []
|
||||
body = None
|
||||
parameters = RequestParameters()
|
||||
|
||||
try:
|
||||
server = self.spec.get_server(request.full_url_pattern)
|
||||
# don't process if server errors
|
||||
except OpenAPIMappingError as exc:
|
||||
errors.append(exc)
|
||||
return RequestValidationResult(errors, body, parameters)
|
||||
|
||||
operation_pattern = request.full_url_pattern.replace(
|
||||
server.default_url, '')
|
||||
|
||||
try:
|
||||
operation = self.spec.get_operation(
|
||||
operation_pattern, request.method)
|
||||
# don't process if operation errors
|
||||
except OpenAPIMappingError as exc:
|
||||
errors.append(exc)
|
||||
return RequestValidationResult(errors, body, parameters)
|
||||
|
||||
for param_name, param in iteritems(operation.parameters):
|
||||
try:
|
||||
raw_value = self._get_raw_value(request, param)
|
||||
except MissingParameter as exc:
|
||||
if param.required:
|
||||
errors.append(exc)
|
||||
|
||||
if not param.schema or param.schema.default is None:
|
||||
continue
|
||||
raw_value = param.schema.default
|
||||
|
||||
try:
|
||||
value = param.unmarshal(raw_value)
|
||||
except OpenAPIMappingError as exc:
|
||||
errors.append(exc)
|
||||
else:
|
||||
parameters[param.location][param_name] = value
|
||||
|
||||
if operation.request_body is not None:
|
||||
try:
|
||||
media_type = operation.request_body[request.mimetype]
|
||||
except OpenAPIMappingError as exc:
|
||||
errors.append(exc)
|
||||
else:
|
||||
try:
|
||||
raw_body = self._get_raw_body(request)
|
||||
except MissingBody as exc:
|
||||
if operation.request_body.required:
|
||||
errors.append(exc)
|
||||
else:
|
||||
try:
|
||||
body = media_type.unmarshal(raw_body)
|
||||
except OpenAPIMappingError as exc:
|
||||
errors.append(exc)
|
||||
|
||||
return RequestValidationResult(errors, body, parameters)
|
||||
|
||||
def _get_raw_value(self, request, param):
|
||||
try:
|
||||
return request.parameters[param.location][param.name]
|
||||
except KeyError:
|
||||
raise MissingParameter(
|
||||
"Missing required `{0}` parameter".format(param.name))
|
||||
|
||||
def _get_raw_body(self, request):
|
||||
if not request.body:
|
||||
raise MissingBody("Missing required request body")
|
||||
|
||||
return request.body
|
|
@ -1,110 +1,9 @@
|
|||
"""OpenAPI core wrappers module"""
|
||||
from six import iteritems
|
||||
import warnings
|
||||
|
||||
from six.moves.urllib.parse import urljoin
|
||||
|
||||
from openapi_core.exceptions import (
|
||||
OpenAPIMappingError, MissingParameterError, InvalidContentTypeError,
|
||||
InvalidServerError,
|
||||
)
|
||||
|
||||
SPEC_LOCATION_TO_REQUEST_LOCATION_MAPPING = {
|
||||
'path': 'view_args',
|
||||
'query': 'args',
|
||||
'headers': 'headers',
|
||||
'cookies': 'cookies',
|
||||
}
|
||||
|
||||
|
||||
class RequestParameters(dict):
|
||||
|
||||
valid_locations = ['path', 'query', 'headers', 'cookies']
|
||||
|
||||
def __getitem__(self, location):
|
||||
self.validate_location(location)
|
||||
|
||||
return self.setdefault(location, {})
|
||||
|
||||
def __setitem__(self, location, value):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def validate_location(cls, location):
|
||||
if location not in cls.valid_locations:
|
||||
raise OpenAPIMappingError(
|
||||
"Unknown parameter location: {0}".format(str(location)))
|
||||
|
||||
|
||||
class BaseRequestFactory(object):
|
||||
|
||||
def get_operation(self, request, spec):
|
||||
server = self._get_server(request, spec)
|
||||
|
||||
operation_pattern = request.full_url_pattern.replace(
|
||||
server.default_url, '')
|
||||
method = request.method.lower()
|
||||
|
||||
return spec.get_operation(operation_pattern, method)
|
||||
|
||||
def _get_server(self, request, spec):
|
||||
for server in spec.servers:
|
||||
if server.default_url in request.full_url_pattern:
|
||||
return server
|
||||
|
||||
raise InvalidServerError(
|
||||
"Invalid request server {0}".format(request.full_url_pattern))
|
||||
|
||||
|
||||
class RequestParametersFactory(BaseRequestFactory):
|
||||
|
||||
def __init__(self, attr_mapping=SPEC_LOCATION_TO_REQUEST_LOCATION_MAPPING):
|
||||
self.attr_mapping = attr_mapping
|
||||
|
||||
def create(self, request, spec):
|
||||
operation = self.get_operation(request, spec)
|
||||
|
||||
params = RequestParameters()
|
||||
for param_name, param in iteritems(operation.parameters):
|
||||
try:
|
||||
raw_value = self._get_raw_value(request, param)
|
||||
except MissingParameterError:
|
||||
if param.required:
|
||||
raise
|
||||
|
||||
if not param.schema or param.schema.default is None:
|
||||
continue
|
||||
raw_value = param.schema.default
|
||||
|
||||
value = param.unmarshal(raw_value)
|
||||
|
||||
params[param.location][param_name] = value
|
||||
return params
|
||||
|
||||
def _get_raw_value(self, request, param):
|
||||
request_location = self.attr_mapping[param.location]
|
||||
request_attr = getattr(request, request_location)
|
||||
|
||||
try:
|
||||
return request_attr[param.name]
|
||||
except KeyError:
|
||||
raise MissingParameterError(
|
||||
"Missing required `{0}` parameter".format(param.name))
|
||||
|
||||
|
||||
class RequestBodyFactory(BaseRequestFactory):
|
||||
|
||||
def create(self, request, spec):
|
||||
operation = self.get_operation(request, spec)
|
||||
|
||||
if operation.request_body is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
media_type = operation.request_body[request.mimetype]
|
||||
except KeyError:
|
||||
raise InvalidContentTypeError(
|
||||
"Invalid media type `{0}`".format(request.mimetype))
|
||||
|
||||
return media_type.unmarshal(request.data)
|
||||
from openapi_core.shortcuts import validate_parameters, validate_body
|
||||
|
||||
|
||||
class BaseOpenAPIRequest(object):
|
||||
|
@ -114,12 +13,8 @@ class BaseOpenAPIRequest(object):
|
|||
path_pattern = NotImplemented
|
||||
method = NotImplemented
|
||||
|
||||
args = NotImplemented
|
||||
view_args = NotImplemented
|
||||
headers = NotImplemented
|
||||
cookies = NotImplemented
|
||||
|
||||
data = NotImplemented
|
||||
parameters = NotImplemented
|
||||
body = NotImplemented
|
||||
|
||||
mimetype = NotImplemented
|
||||
|
||||
|
@ -127,8 +22,85 @@ class BaseOpenAPIRequest(object):
|
|||
def full_url_pattern(self):
|
||||
return urljoin(self.host_url, self.path_pattern)
|
||||
|
||||
def get_parameters(self, spec):
|
||||
return RequestParametersFactory().create(self, spec)
|
||||
|
||||
def get_body(self, spec):
|
||||
return RequestBodyFactory().create(self, spec)
|
||||
warnings.warn(
|
||||
"`get_body` method is deprecated. "
|
||||
"Use RequestValidator instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
# backward compatibility
|
||||
return validate_body(spec, self)
|
||||
|
||||
def get_parameters(self, spec):
|
||||
warnings.warn(
|
||||
"`get_parameters` method is deprecated. "
|
||||
"Use RequestValidator instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
# backward compatibility
|
||||
return validate_parameters(spec, self)
|
||||
|
||||
|
||||
class MockRequest(BaseOpenAPIRequest):
|
||||
|
||||
def __init__(
|
||||
self, host_url, method, path, path_pattern=None, args=None,
|
||||
view_args=None, headers=None, cookies=None, data=None,
|
||||
mimetype='application/json'):
|
||||
self.host_url = host_url
|
||||
self.path = path
|
||||
self.path_pattern = path_pattern or path
|
||||
self.method = method.lower()
|
||||
|
||||
self.parameters = {
|
||||
'path': view_args or {},
|
||||
'query': args or {},
|
||||
'headers': headers or {},
|
||||
'cookies': cookies or {},
|
||||
}
|
||||
|
||||
self.body = data or ''
|
||||
|
||||
self.mimetype = mimetype
|
||||
|
||||
|
||||
class FlaskOpenAPIRequest(BaseOpenAPIRequest):
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
@property
|
||||
def host_url(self):
|
||||
return self.request.host_url
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self.request.path
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
return self.request.method.lower()
|
||||
|
||||
@property
|
||||
def path_pattern(self):
|
||||
if self.request.url_rule is None:
|
||||
return self.path
|
||||
|
||||
return self.request.url_rule.rule
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
return {
|
||||
'path': self.request.view_args,
|
||||
'query': self.request.args,
|
||||
'headers': self.request.headers,
|
||||
'cookies': self.request.cookies,
|
||||
}
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self.request.data
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
return self.request.mimetype
|
||||
|
|
|
@ -3,3 +3,4 @@ pytest
|
|||
pytest-pep8
|
||||
pytest-flakes
|
||||
pytest-cov
|
||||
flask
|
|
@ -73,6 +73,7 @@ paths:
|
|||
tags:
|
||||
- pets
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
|
|
@ -3,9 +3,9 @@ import pytest
|
|||
from six import iteritems
|
||||
|
||||
from openapi_core.exceptions import (
|
||||
MissingParameterError, InvalidContentTypeError, InvalidServerError,
|
||||
InvalidValueType, UndefinedSchemaProperty, MissingPropertyError,
|
||||
EmptyValue,
|
||||
MissingParameter, InvalidContentType, InvalidServer,
|
||||
UndefinedSchemaProperty, MissingProperty,
|
||||
EmptyValue, InvalidMediaTypeValue, InvalidParameterValue,
|
||||
)
|
||||
from openapi_core.media_types import MediaType
|
||||
from openapi_core.operations import Operation
|
||||
|
@ -14,27 +14,7 @@ from openapi_core.request_bodies import RequestBody
|
|||
from openapi_core.schemas import Schema
|
||||
from openapi_core.servers import Server, ServerVariable
|
||||
from openapi_core.shortcuts import create_spec
|
||||
from openapi_core.wrappers import BaseOpenAPIRequest
|
||||
|
||||
|
||||
class RequestMock(BaseOpenAPIRequest):
|
||||
|
||||
def __init__(
|
||||
self, host_url, method, path, path_pattern=None, args=None,
|
||||
view_args=None, headers=None, cookies=None, data=None,
|
||||
mimetype='application/json'):
|
||||
self.host_url = host_url
|
||||
self.path = path
|
||||
self.path_pattern = path_pattern or path
|
||||
self.method = method
|
||||
|
||||
self.args = args or {}
|
||||
self.view_args = view_args or {}
|
||||
self.headers = headers or {}
|
||||
self.cookies = cookies or {}
|
||||
self.data = data or ''
|
||||
|
||||
self.mimetype = mimetype
|
||||
from openapi_core.wrappers import MockRequest
|
||||
|
||||
|
||||
class TestPetstore(object):
|
||||
|
@ -125,7 +105,7 @@ class TestPetstore(object):
|
|||
'ids': ['12', '13'],
|
||||
}
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'GET', '/pets',
|
||||
path_pattern=path_pattern, args=query_params,
|
||||
)
|
||||
|
@ -150,12 +130,12 @@ class TestPetstore(object):
|
|||
'limit': 'twenty',
|
||||
}
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'GET', '/pets',
|
||||
path_pattern=path_pattern, args=query_params,
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidValueType):
|
||||
with pytest.raises(InvalidParameterValue):
|
||||
request.get_parameters(spec)
|
||||
|
||||
body = request.get_body(spec)
|
||||
|
@ -165,12 +145,12 @@ class TestPetstore(object):
|
|||
def test_get_pets_raises_missing_required_param(self, spec):
|
||||
host_url = 'http://petstore.swagger.io/v1'
|
||||
path_pattern = '/v1/pets'
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'GET', '/pets',
|
||||
path_pattern=path_pattern,
|
||||
)
|
||||
|
||||
with pytest.raises(MissingParameterError):
|
||||
with pytest.raises(MissingParameter):
|
||||
request.get_parameters(spec)
|
||||
|
||||
body = request.get_body(spec)
|
||||
|
@ -184,7 +164,7 @@ class TestPetstore(object):
|
|||
'limit': '',
|
||||
}
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'GET', '/pets',
|
||||
path_pattern=path_pattern, args=query_params,
|
||||
)
|
||||
|
@ -202,7 +182,7 @@ class TestPetstore(object):
|
|||
'limit': None,
|
||||
}
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'GET', '/pets',
|
||||
path_pattern=path_pattern, args=query_params,
|
||||
)
|
||||
|
@ -239,7 +219,7 @@ class TestPetstore(object):
|
|||
}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'POST', '/pets',
|
||||
path_pattern=path_pattern, data=data,
|
||||
)
|
||||
|
@ -267,7 +247,7 @@ class TestPetstore(object):
|
|||
data_json = {}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'POST', '/pets',
|
||||
path_pattern=path_pattern, data=data,
|
||||
)
|
||||
|
@ -276,7 +256,7 @@ class TestPetstore(object):
|
|||
|
||||
assert parameters == {}
|
||||
|
||||
with pytest.raises(MissingPropertyError):
|
||||
with pytest.raises(MissingProperty):
|
||||
request.get_body(spec)
|
||||
|
||||
def test_post_pets_extra_body_properties(self, spec, spec_dict):
|
||||
|
@ -290,7 +270,7 @@ class TestPetstore(object):
|
|||
}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'POST', '/pets',
|
||||
path_pattern=path_pattern, data=data,
|
||||
)
|
||||
|
@ -311,7 +291,7 @@ class TestPetstore(object):
|
|||
}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'POST', '/pets',
|
||||
path_pattern=path_pattern, data=data,
|
||||
)
|
||||
|
@ -342,7 +322,7 @@ class TestPetstore(object):
|
|||
}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'POST', '/pets',
|
||||
path_pattern=path_pattern, data=data,
|
||||
)
|
||||
|
@ -351,7 +331,7 @@ class TestPetstore(object):
|
|||
|
||||
assert parameters == {}
|
||||
|
||||
with pytest.raises(InvalidValueType):
|
||||
with pytest.raises(InvalidMediaTypeValue):
|
||||
request.get_body(spec)
|
||||
|
||||
def test_post_pets_raises_invalid_mimetype(self, spec):
|
||||
|
@ -363,7 +343,7 @@ class TestPetstore(object):
|
|||
}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'POST', '/pets',
|
||||
path_pattern=path_pattern, data=data, mimetype='text/html',
|
||||
)
|
||||
|
@ -372,7 +352,7 @@ class TestPetstore(object):
|
|||
|
||||
assert parameters == {}
|
||||
|
||||
with pytest.raises(InvalidContentTypeError):
|
||||
with pytest.raises(InvalidContentType):
|
||||
request.get_body(spec)
|
||||
|
||||
def test_post_pets_raises_invalid_server_error(self, spec):
|
||||
|
@ -384,15 +364,15 @@ class TestPetstore(object):
|
|||
}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'POST', '/pets',
|
||||
path_pattern=path_pattern, data=data, mimetype='text/html',
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidServerError):
|
||||
with pytest.raises(InvalidServer):
|
||||
request.get_parameters(spec)
|
||||
|
||||
with pytest.raises(InvalidServerError):
|
||||
with pytest.raises(InvalidServer):
|
||||
request.get_body(spec)
|
||||
|
||||
def test_get_pet(self, spec):
|
||||
|
@ -401,7 +381,7 @@ class TestPetstore(object):
|
|||
view_args = {
|
||||
'petId': '1',
|
||||
}
|
||||
request = RequestMock(
|
||||
request = MockRequest(
|
||||
host_url, 'GET', '/pets/1',
|
||||
path_pattern=path_pattern, view_args=view_args,
|
||||
)
|
||||
|
|
157
tests/integration/test_validators.py
Normal file
157
tests/integration/test_validators.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
import json
|
||||
import pytest
|
||||
|
||||
from openapi_core.exceptions import (
|
||||
InvalidServer, InvalidOperation, MissingParameter,
|
||||
MissingBody, InvalidContentType,
|
||||
)
|
||||
from openapi_core.shortcuts import create_spec
|
||||
from openapi_core.validators import RequestValidator
|
||||
from openapi_core.wrappers import MockRequest
|
||||
|
||||
|
||||
class TestRequestValidator(object):
|
||||
|
||||
host_url = 'http://petstore.swagger.io'
|
||||
|
||||
@pytest.fixture
|
||||
def spec_dict(self, factory):
|
||||
return factory.spec_from_file("data/v3.0/petstore.yaml")
|
||||
|
||||
@pytest.fixture
|
||||
def spec(self, spec_dict):
|
||||
return create_spec(spec_dict)
|
||||
|
||||
@pytest.fixture
|
||||
def validator(self, spec):
|
||||
return RequestValidator(spec)
|
||||
|
||||
def test_request_server_error(self, validator):
|
||||
request = MockRequest('http://petstore.invalid.net/v1', 'get', '/')
|
||||
|
||||
result = validator.validate(request)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == InvalidServer
|
||||
assert result.body is None
|
||||
assert result.parameters == {}
|
||||
|
||||
def test_invalid_operation(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1')
|
||||
|
||||
result = validator.validate(request)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == InvalidOperation
|
||||
assert result.body is None
|
||||
assert result.parameters == {}
|
||||
|
||||
def test_missing_parameter(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1/pets')
|
||||
|
||||
result = validator.validate(request)
|
||||
|
||||
assert type(result.errors[0]) == MissingParameter
|
||||
assert result.body is None
|
||||
assert result.parameters == {
|
||||
'query': {
|
||||
'page': 1,
|
||||
'search': '',
|
||||
},
|
||||
}
|
||||
|
||||
def test_get_pets(self, validator):
|
||||
request = MockRequest(
|
||||
self.host_url, 'get', '/v1/pets',
|
||||
path_pattern='/v1/pets', args={'limit': '10'},
|
||||
)
|
||||
|
||||
result = validator.validate(request)
|
||||
|
||||
assert result.errors == []
|
||||
assert result.body is None
|
||||
assert result.parameters == {
|
||||
'query': {
|
||||
'limit': 10,
|
||||
'page': 1,
|
||||
'search': '',
|
||||
},
|
||||
}
|
||||
|
||||
def test_missing_body(self, validator):
|
||||
request = MockRequest(
|
||||
self.host_url, 'post', '/v1/pets',
|
||||
path_pattern='/v1/pets',
|
||||
)
|
||||
|
||||
result = validator.validate(request)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == MissingBody
|
||||
assert result.body is None
|
||||
assert result.parameters == {}
|
||||
|
||||
def test_invalid_content_type(self, validator):
|
||||
request = MockRequest(
|
||||
self.host_url, 'post', '/v1/pets',
|
||||
path_pattern='/v1/pets', mimetype='text/csv',
|
||||
)
|
||||
|
||||
result = validator.validate(request)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == InvalidContentType
|
||||
assert result.body is None
|
||||
assert result.parameters == {}
|
||||
|
||||
def test_post_pets(self, validator, spec_dict):
|
||||
pet_name = 'Cat'
|
||||
pet_tag = 'cats'
|
||||
pet_street = 'Piekna'
|
||||
pet_city = 'Warsaw'
|
||||
data_json = {
|
||||
'name': pet_name,
|
||||
'tag': pet_tag,
|
||||
'position': '2',
|
||||
'address': {
|
||||
'street': pet_street,
|
||||
'city': pet_city,
|
||||
}
|
||||
}
|
||||
data = json.dumps(data_json)
|
||||
request = MockRequest(
|
||||
self.host_url, 'post', '/v1/pets',
|
||||
path_pattern='/v1/pets', data=data,
|
||||
)
|
||||
|
||||
result = validator.validate(request)
|
||||
|
||||
assert result.errors == []
|
||||
assert result.parameters == {}
|
||||
|
||||
schemas = spec_dict['components']['schemas']
|
||||
pet_model = schemas['PetCreate']['x-model']
|
||||
address_model = schemas['Address']['x-model']
|
||||
assert result.body.__class__.__name__ == pet_model
|
||||
assert result.body.name == pet_name
|
||||
assert result.body.tag == pet_tag
|
||||
assert result.body.position == 2
|
||||
assert result.body.address.__class__.__name__ == address_model
|
||||
assert result.body.address.street == pet_street
|
||||
assert result.body.address.city == pet_city
|
||||
|
||||
def test_get_pet(self, validator):
|
||||
request = MockRequest(
|
||||
self.host_url, 'get', '/v1/pets/1',
|
||||
path_pattern='/v1/pets/{petId}', view_args={'petId': '1'},
|
||||
)
|
||||
|
||||
result = validator.validate(request)
|
||||
|
||||
assert result.errors == []
|
||||
assert result.body is None
|
||||
assert result.parameters == {
|
||||
'path': {
|
||||
'petId': 1,
|
||||
},
|
||||
}
|
92
tests/integration/test_wrappers.py
Normal file
92
tests/integration/test_wrappers.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
import pytest
|
||||
|
||||
from flask.wrappers import Request
|
||||
from werkzeug.datastructures import EnvironHeaders, ImmutableMultiDict
|
||||
from werkzeug.routing import Map, Rule, Subdomain
|
||||
from werkzeug.test import create_environ
|
||||
|
||||
from openapi_core.wrappers import FlaskOpenAPIRequest
|
||||
|
||||
|
||||
class TestFlaskOpenAPIRequest(object):
|
||||
|
||||
server_name = 'localhost'
|
||||
|
||||
@pytest.fixture
|
||||
def environ(self):
|
||||
return create_environ()
|
||||
|
||||
@pytest.fixture
|
||||
def map(self):
|
||||
return Map([
|
||||
# Static URLs
|
||||
Rule('/', endpoint='static/index'),
|
||||
Rule('/about', endpoint='static/about'),
|
||||
Rule('/help', endpoint='static/help'),
|
||||
# Knowledge Base
|
||||
Subdomain('kb', [
|
||||
Rule('/', endpoint='kb/index'),
|
||||
Rule('/browse/', endpoint='kb/browse'),
|
||||
Rule('/browse/<int:id>/', endpoint='kb/browse'),
|
||||
Rule('/browse/<int:id>/<int:page>', endpoint='kb/browse')
|
||||
])
|
||||
], default_subdomain='www')
|
||||
|
||||
@pytest.fixture
|
||||
def request_factory(self, map, environ):
|
||||
def create_request(method, path, subdomain=None):
|
||||
req = Request(environ)
|
||||
urls = map.bind_to_environ(
|
||||
environ, server_name=self.server_name, subdomain=subdomain)
|
||||
req.url_rule, req.view_args = urls.match(
|
||||
path, method, return_rule=True)
|
||||
return req
|
||||
return create_request
|
||||
|
||||
@pytest.fixture
|
||||
def openapi_request(self, request):
|
||||
return FlaskOpenAPIRequest(request)
|
||||
|
||||
def test_simple(self, request_factory, environ, request):
|
||||
request = request_factory('GET', '/', subdomain='www')
|
||||
|
||||
openapi_request = FlaskOpenAPIRequest(request)
|
||||
|
||||
path = {}
|
||||
query = ImmutableMultiDict([])
|
||||
headers = EnvironHeaders(environ)
|
||||
cookies = {}
|
||||
assert openapi_request.parameters == {
|
||||
'path': path,
|
||||
'query': query,
|
||||
'headers': headers,
|
||||
'cookies': cookies,
|
||||
}
|
||||
assert openapi_request.host_url == request.host_url
|
||||
assert openapi_request.path == request.path
|
||||
assert openapi_request.method == request.method.lower()
|
||||
assert openapi_request.path_pattern == request.path
|
||||
assert openapi_request.body == request.data
|
||||
assert openapi_request.mimetype == request.mimetype
|
||||
|
||||
def test_url_rule(self, request_factory, environ, request):
|
||||
request = request_factory('GET', '/browse/12/', subdomain='kb')
|
||||
|
||||
openapi_request = FlaskOpenAPIRequest(request)
|
||||
|
||||
path = {'id': 12}
|
||||
query = ImmutableMultiDict([])
|
||||
headers = EnvironHeaders(environ)
|
||||
cookies = {}
|
||||
assert openapi_request.parameters == {
|
||||
'path': path,
|
||||
'query': query,
|
||||
'headers': headers,
|
||||
'cookies': cookies,
|
||||
}
|
||||
assert openapi_request.host_url == request.host_url
|
||||
assert openapi_request.path == request.path
|
||||
assert openapi_request.method == request.method.lower()
|
||||
assert openapi_request.path_pattern == request.url_rule.rule
|
||||
assert openapi_request.body == request.data
|
||||
assert openapi_request.mimetype == request.mimetype
|
|
@ -1,7 +1,7 @@
|
|||
import mock
|
||||
import pytest
|
||||
|
||||
from openapi_core.exceptions import InvalidOperationError
|
||||
from openapi_core.exceptions import InvalidOperation
|
||||
from openapi_core.paths import Path
|
||||
from openapi_core.specs import Spec
|
||||
|
||||
|
@ -42,9 +42,9 @@ class TestSpecs(object):
|
|||
assert operation == mock.sentinel.path1_get
|
||||
|
||||
def test_invalid_path(self, spec):
|
||||
with pytest.raises(InvalidOperationError):
|
||||
with pytest.raises(InvalidOperation):
|
||||
spec.get_operation('/path3', 'get')
|
||||
|
||||
def test_invalid_method(self, spec):
|
||||
with pytest.raises(InvalidOperationError):
|
||||
with pytest.raises(InvalidOperation):
|
||||
spec.get_operation('/path1', 'post')
|
||||
|
|
Loading…
Reference in a new issue