Add response validation

This commit is contained in:
Correl Roush 2020-11-26 00:05:02 -05:00
parent 53471f3fce
commit bcf128e69d
4 changed files with 165 additions and 1 deletions

View file

@ -30,7 +30,7 @@ Adding validation to request handlers
) )
from openapi_core.unmarshalling.schemas.exceptions import ValidateError # type: ignore from openapi_core.unmarshalling.schemas.exceptions import ValidateError # type: ignore
from tornado.web import RequestHandler from tornado.web import RequestHandler
from tornado_openapi3.requests import RequestValidator from tornado_openapi3 import RequestValidator
import yaml import yaml
@ -53,3 +53,24 @@ Adding validation to request handlers
self.finish() self.finish()
except OpenAPIError: except OpenAPIError:
raise raise
Validating a response
=====================
.. code:: python
from tornado.testing import AsyncHTTPTestCase
from tornado_openapi3 import ResponseValidator
from myapplication import create_app, spec
class TestResponses(AsyncHTTPTestCase):
def get_app(self) -> Application:
return create_app()
def test_status(self) -> None:
validator = ResponseValidator(spec)
response = self.fetch("/status")
result = validator.validate(response)
result.raise_for_errors()

110
tests/test_responses.py Normal file
View file

@ -0,0 +1,110 @@
from dataclasses import dataclass
from typing import Any, Dict, Optional
import unittest
import attr
from hypothesis import given, settings # type: ignore
import hypothesis.strategies as s # type: ignore
from openapi_core import create_spec # type: ignore
from openapi_core.validation.response.datatypes import OpenAPIResponse # type: ignore
from tornado.httpclient import HTTPRequest, HTTPResponse # type: ignore
from tornado.testing import AsyncHTTPTestCase # type: ignore
from tornado.web import Application, RequestHandler # type: ignore
from tornado_openapi3 import (
ResponseValidator,
TornadoResponseFactory,
)
settings(deadline=None)
@dataclass
class Responses:
code: int
headers: Dict[str, str]
def as_openapi(self) -> Dict[str, Any]:
return {
str(self.code): {
"description": "Response",
"headers": {
name: {"schema": {"type": "string", "enum": [value]}}
for name, value in self.headers.items()
},
}
}
@s.composite
def responses(draw, min_headers=0) -> Responses:
field_name = s.text(
s.characters(
min_codepoint=33,
max_codepoint=126,
blacklist_categories=("Lu",),
blacklist_characters=":",
),
min_size=1,
)
field_value = s.text(
s.characters(
min_codepoint=0x20, max_codepoint=0x7E, blacklist_characters=" \r\n"
),
min_size=1,
)
return Responses(
code=draw(s.sampled_from([200, 304, 400, 500])),
headers=draw(s.dictionaries(field_name, field_value, min_size=min_headers)),
)
class TestResponseFactory(unittest.TestCase):
def test_response(self) -> None:
tornado_request = HTTPRequest(url="http://example.com")
tornado_response = HTTPResponse(request=tornado_request, code=200)
expected = OpenAPIResponse(
data="",
status_code=200,
mimetype="text/html",
)
openapi_response = TornadoResponseFactory.create(tornado_response)
self.assertEqual(attr.asdict(expected), attr.asdict(openapi_response))
class ResponsesHandler(RequestHandler):
responses: Optional[Responses] = None
def get(self) -> None:
if ResponsesHandler.responses:
self.set_status(ResponsesHandler.responses.code)
for name, value in ResponsesHandler.responses.headers.items():
self.add_header(name, value)
class TestResponse(AsyncHTTPTestCase):
def get_app(self) -> Application:
return Application([(r"/.*", ResponsesHandler)])
@given(responses())
def test_simple_request(self, responses: Responses) -> None:
spec = create_spec(
{
"openapi": "3.0.0",
"info": {"title": "Test specification", "version": "0.1"},
"paths": {
"/": {
"get": {
"responses": responses.as_openapi(),
}
}
},
}
)
ResponsesHandler.responses = responses
validator = ResponseValidator(spec)
response = self.fetch("/")
result = validator.validate(response)
result.raise_for_errors()

View file

@ -1,6 +1,9 @@
from tornado_openapi3.requests import RequestValidator, TornadoRequestFactory from tornado_openapi3.requests import RequestValidator, TornadoRequestFactory
from tornado_openapi3.responses import ResponseValidator, TornadoResponseFactory
__all__ = [ __all__ = [
"RequestValidator", "RequestValidator",
"ResponseValidator",
"TornadoRequestFactory", "TornadoRequestFactory",
"TornadoResponseFactory",
] ]

View file

@ -0,0 +1,30 @@
from openapi_core.validation.response.datatypes import ( # type: ignore
OpenAPIResponse,
ResponseValidationResult,
)
from openapi_core.validation.response import validators # type: ignore
from tornado.httpclient import HTTPResponse # type: ignore
from .requests import TornadoRequestFactory
class TornadoResponseFactory:
@classmethod
def create(cls, response: HTTPResponse) -> OpenAPIResponse:
mimetype = response.headers.get("Content-Type", "text/html")
return OpenAPIResponse(
data=response.body.decode("utf-8"),
status_code=response.code,
mimetype=mimetype,
)
class ResponseValidator(validators.ResponseValidator):
def validate(self, response: HTTPResponse) -> ResponseValidationResult:
return super().validate(
TornadoRequestFactory.create(response.request),
TornadoResponseFactory.create(response),
)
__all__ = ["ResponseValidator", "TornadoResponseFactory"]