mirror of
https://github.com/correl/openapi-core.git
synced 2024-11-22 03:00:10 +00:00
Response validator
This commit is contained in:
parent
b0c4141f64
commit
08fdf7c9aa
6 changed files with 223 additions and 16 deletions
|
@ -39,7 +39,6 @@ class Operation(object):
|
|||
if http_status_range in self.responses:
|
||||
return self.responses[http_status_range]
|
||||
|
||||
|
||||
if 'default' not in self.responses:
|
||||
raise InvalidResponse(
|
||||
"Unknown response http status {0}".format(http_status))
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from six import iteritems
|
||||
|
||||
from openapi_core.exceptions import (
|
||||
OpenAPIMappingError, MissingParameter, MissingBody,
|
||||
OpenAPIMappingError, MissingParameter, MissingBody, InvalidResponse,
|
||||
)
|
||||
|
||||
|
||||
|
@ -43,6 +43,14 @@ class RequestValidationResult(BaseValidationResult):
|
|||
self.parameters = parameters or RequestParameters()
|
||||
|
||||
|
||||
class ResponseValidationResult(BaseValidationResult):
|
||||
|
||||
def __init__(self, errors, body=None, headers=None):
|
||||
super(ResponseValidationResult, self).__init__(errors)
|
||||
self.body = body
|
||||
self.headers = headers
|
||||
|
||||
|
||||
class RequestValidator(object):
|
||||
|
||||
def __init__(self, spec):
|
||||
|
@ -120,3 +128,63 @@ class RequestValidator(object):
|
|||
raise MissingBody("Missing required request body")
|
||||
|
||||
return request.body
|
||||
|
||||
|
||||
class ResponseValidator(object):
|
||||
|
||||
def __init__(self, spec):
|
||||
self.spec = spec
|
||||
|
||||
def validate(self, request, response):
|
||||
errors = []
|
||||
body = None
|
||||
headers = {}
|
||||
|
||||
try:
|
||||
server = self.spec.get_server(request.full_url_pattern)
|
||||
# don't process if server errors
|
||||
except OpenAPIMappingError as exc:
|
||||
errors.append(exc)
|
||||
return ResponseValidationResult(errors, body, headers)
|
||||
|
||||
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 ResponseValidationResult(errors, body, headers)
|
||||
|
||||
try:
|
||||
operation_response = operation.get_response(str(response.status))
|
||||
# don't process if invalid response status code
|
||||
except InvalidResponse as exc:
|
||||
errors.append(exc)
|
||||
return ResponseValidationResult(errors, body, headers)
|
||||
|
||||
if operation_response.content:
|
||||
try:
|
||||
media_type = operation_response.content[response.mimetype]
|
||||
except OpenAPIMappingError as exc:
|
||||
errors.append(exc)
|
||||
else:
|
||||
try:
|
||||
raw_body = self._get_raw_body(response)
|
||||
except MissingBody as exc:
|
||||
errors.append(exc)
|
||||
else:
|
||||
try:
|
||||
body = media_type.unmarshal(raw_body)
|
||||
except OpenAPIMappingError as exc:
|
||||
errors.append(exc)
|
||||
|
||||
return ResponseValidationResult(errors, body, headers)
|
||||
|
||||
def _get_raw_body(self, response):
|
||||
if not response.body:
|
||||
raise MissingBody("Missing required response body")
|
||||
|
||||
return response.body
|
||||
|
|
|
@ -104,3 +104,38 @@ class FlaskOpenAPIRequest(BaseOpenAPIRequest):
|
|||
@property
|
||||
def mimetype(self):
|
||||
return self.request.mimetype
|
||||
|
||||
|
||||
class BaseOpenAPIResponse(object):
|
||||
|
||||
body = NotImplemented
|
||||
status = NotImplemented
|
||||
|
||||
mimetype = NotImplemented
|
||||
|
||||
|
||||
class MockResponse(BaseOpenAPIRequest):
|
||||
|
||||
def __init__(self, body, status=200, mimetype='application/json'):
|
||||
self.body = body
|
||||
|
||||
self.status = status
|
||||
self.mimetype = mimetype
|
||||
|
||||
|
||||
class FlaskOpenAPIResponse(BaseOpenAPIResponse):
|
||||
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self.response.text
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.response.status
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
return self.response.mimetype
|
||||
|
|
|
@ -61,12 +61,6 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pets"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
post:
|
||||
summary: Create a pet
|
||||
operationId: createPets
|
||||
|
@ -164,9 +158,12 @@ components:
|
|||
position:
|
||||
$ref: "#/components/schemas/Position"
|
||||
Pets:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
|
|
|
@ -3,11 +3,11 @@ import pytest
|
|||
|
||||
from openapi_core.exceptions import (
|
||||
InvalidServer, InvalidOperation, MissingParameter,
|
||||
MissingBody, InvalidContentType,
|
||||
MissingBody, InvalidContentType, InvalidResponse, InvalidMediaTypeValue,
|
||||
)
|
||||
from openapi_core.shortcuts import create_spec
|
||||
from openapi_core.validators import RequestValidator
|
||||
from openapi_core.wrappers import MockRequest
|
||||
from openapi_core.validators import RequestValidator, ResponseValidator
|
||||
from openapi_core.wrappers import MockRequest, MockResponse
|
||||
|
||||
|
||||
class TestRequestValidator(object):
|
||||
|
@ -155,3 +155,93 @@ class TestRequestValidator(object):
|
|||
'petId': 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestResponseValidator(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 ResponseValidator(spec)
|
||||
|
||||
def test_invalid_server(self, validator):
|
||||
request = MockRequest('http://petstore.invalid.net/v1', 'get', '/')
|
||||
response = MockResponse('Not Found', status=404)
|
||||
|
||||
result = validator.validate(request, response)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == InvalidServer
|
||||
assert result.body is None
|
||||
assert result.headers == {}
|
||||
|
||||
def test_invalid_operation(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1')
|
||||
response = MockResponse('Not Found', status=404)
|
||||
|
||||
result = validator.validate(request, response)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == InvalidOperation
|
||||
assert result.body is None
|
||||
assert result.headers == {}
|
||||
|
||||
def test_invalid_response(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1/pets')
|
||||
response = MockResponse('Not Found', status=409)
|
||||
|
||||
result = validator.validate(request, response)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == InvalidResponse
|
||||
assert result.body is None
|
||||
assert result.headers == {}
|
||||
|
||||
def test_missing_body(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1/pets')
|
||||
response = MockResponse(None)
|
||||
|
||||
result = validator.validate(request, response)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == MissingBody
|
||||
assert result.body is None
|
||||
assert result.headers == {}
|
||||
|
||||
def test_invalid_media_type_value(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1/pets')
|
||||
response = MockResponse('\{\}')
|
||||
|
||||
result = validator.validate(request, response)
|
||||
|
||||
assert len(result.errors) == 1
|
||||
assert type(result.errors[0]) == InvalidMediaTypeValue
|
||||
assert result.body is None
|
||||
assert result.headers == {}
|
||||
|
||||
def test_get_pets(self, validator):
|
||||
request = MockRequest(self.host_url, 'get', '/v1/pets')
|
||||
response_json = {
|
||||
'data': [
|
||||
{
|
||||
'id': 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
response_data = json.dumps(response_json)
|
||||
response = MockResponse(response_data)
|
||||
|
||||
result = validator.validate(request, response)
|
||||
|
||||
assert result.errors == []
|
||||
assert result.body == response_json
|
||||
assert result.headers == {}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import pytest
|
||||
|
||||
from flask.wrappers import Request
|
||||
from flask.wrappers import Request, Response
|
||||
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
|
||||
from openapi_core.wrappers import FlaskOpenAPIRequest, FlaskOpenAPIResponse
|
||||
|
||||
|
||||
class TestFlaskOpenAPIRequest(object):
|
||||
|
@ -90,3 +90,21 @@ class TestFlaskOpenAPIRequest(object):
|
|||
assert openapi_request.path_pattern == request.url_rule.rule
|
||||
assert openapi_request.body == request.data
|
||||
assert openapi_request.mimetype == request.mimetype
|
||||
|
||||
|
||||
class TetsFlaskOpenAPIResponse(object):
|
||||
|
||||
@pytest.fixture
|
||||
def response_factory(self):
|
||||
def create_response(body, status=200):
|
||||
return Response('Not Found', status=404)
|
||||
return create_response
|
||||
|
||||
def test_invalid_server(self, response_factory):
|
||||
response = response_factory('Not Found', status=404)
|
||||
|
||||
openapi_response = FlaskOpenAPIResponse(response)
|
||||
|
||||
assert openapi_response.body == response.text
|
||||
assert openapi_response.status == response.status
|
||||
assert openapi_response.mimetype == response.mimetype
|
||||
|
|
Loading…
Reference in a new issue