Response validator

This commit is contained in:
Artur Maciag 2017-11-06 13:32:31 +00:00
parent b0c4141f64
commit 08fdf7c9aa
6 changed files with 223 additions and 16 deletions

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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 == {}

View file

@ -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