tornado-openapi3/tests/test_requests.py

258 lines
8.6 KiB
Python
Raw Normal View History

2020-09-18 04:01:10 +00:00
from dataclasses import dataclass
2020-12-05 02:01:22 +00:00
from typing import Any, Callable, Dict, List, Optional, Tuple
2020-09-17 03:04:11 +00:00
import unittest
from urllib.parse import urlencode, urlparse
2020-09-17 03:04:11 +00:00
import attr
from hypothesis import given
2020-11-23 16:29:28 +00:00
import hypothesis.strategies as s # type: ignore
2020-09-18 03:39:35 +00:00
from openapi_core import create_spec # type: ignore
from openapi_core.exceptions import ( # type: ignore
2020-09-18 04:01:10 +00:00
MissingRequiredParameter,
OpenAPIError,
2020-09-18 04:01:10 +00:00
)
2020-09-17 03:04:11 +00:00
from openapi_core.validation.request.datatypes import ( # type: ignore
RequestParameters,
OpenAPIRequest,
)
from tornado.httpclient import HTTPRequest
from tornado.httputil import HTTPHeaders, HTTPServerRequest
from tornado.testing import AsyncHTTPTestCase
from tornado.web import Application, RequestHandler
2020-09-17 03:04:11 +00:00
from werkzeug.datastructures import ImmutableMultiDict
from tornado_openapi3 import RequestValidator, TornadoRequestFactory
2020-09-17 03:04:11 +00:00
2020-09-18 04:01:10 +00:00
@dataclass
class Parameters:
headers: Dict[str, str]
query_parameters: Dict[str, str]
def as_openapi(self) -> List[dict]:
headers = [
{
"name": name.lower(),
"in": "header",
"required": True,
"schema": {"type": "string", "enum": [value]},
}
for name, value in self.headers.items()
]
qargs = [
{
"name": name.lower(),
"in": "query",
"required": True,
"schema": {"type": "string", "enum": [value]},
}
for name, value in self.query_parameters.items()
]
return headers + qargs
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,
)
2020-12-05 02:01:22 +00:00
def headers(min_size: int = 0) -> s.SearchStrategy[Dict[str, str]]:
return s.dictionaries(field_name, field_value, min_size=min_size)
2020-12-05 02:01:22 +00:00
def query_parameters(min_size: int = 0) -> s.SearchStrategy[Dict[str, str]]:
return s.dictionaries(field_name, field_value, min_size=min_size)
2020-09-18 03:39:35 +00:00
@s.composite
2020-12-05 02:01:22 +00:00
def parameters(
draw: Callable[[Any], Any], min_headers: int = 0, min_query_parameters: int = 0
) -> Parameters:
2020-09-18 04:01:10 +00:00
return Parameters(
headers=draw(headers(min_size=min_headers)),
query_parameters=draw(query_parameters(min_size=min_query_parameters)),
2020-09-18 04:01:10 +00:00
)
2020-09-18 03:39:35 +00:00
2020-09-17 03:04:11 +00:00
class TestRequestFactory(unittest.TestCase):
@given(
s.one_of(
s.tuples(s.just(""), s.just(dict())),
s.tuples(s.just("http://example.com/foo"), query_parameters()),
)
)
2020-12-05 02:01:22 +00:00
def test_http_request(self, opts: Tuple[str, Dict[str, str]]) -> None:
url, parameters = opts
request_url = f"{url}?{urlencode(parameters)}" if url else ""
tornado_request = HTTPRequest(method="GET", url=request_url)
expected = OpenAPIRequest(
full_url_pattern=url,
method="get",
parameters=RequestParameters(query=ImmutableMultiDict(parameters)),
body=None,
mimetype="application/x-www-form-urlencoded",
)
openapi_request = TornadoRequestFactory.create(tornado_request)
self.assertEqual(attr.asdict(expected), attr.asdict(openapi_request))
@given(
s.one_of(
s.tuples(s.just(""), s.just(dict())),
s.tuples(s.just("http://example.com/foo"), query_parameters()),
2020-09-17 03:04:11 +00:00
)
)
2020-12-05 02:01:22 +00:00
def test_http_server_request(self, opts: Tuple[str, Dict[str, str]]) -> None:
url, parameters = opts
request_url = f"{url}?{urlencode(parameters)}" if url else ""
parsed = urlparse(request_url)
tornado_request = HTTPServerRequest(
2020-12-04 18:37:25 +00:00
method="GET",
uri=f"{parsed.path}?{parsed.query}",
)
tornado_request.protocol = parsed.scheme
2020-12-04 18:37:25 +00:00
tornado_request.host = parsed.netloc.split(":")[0]
2020-09-17 03:04:11 +00:00
expected = OpenAPIRequest(
full_url_pattern=url,
2020-09-17 03:04:11 +00:00
method="get",
parameters=RequestParameters(
query=ImmutableMultiDict(parameters), path={}, cookie={}
),
body=None,
2020-11-20 00:37:58 +00:00
mimetype="application/x-www-form-urlencoded",
2020-09-17 03:04:11 +00:00
)
openapi_request = TornadoRequestFactory.create(tornado_request)
self.assertEqual(attr.asdict(expected), attr.asdict(openapi_request))
2020-09-18 03:39:35 +00:00
class TestRequest(AsyncHTTPTestCase):
def setUp(self) -> None:
super(TestRequest, self).setUp()
self.request: Optional[HTTPServerRequest] = None
2020-09-18 03:39:35 +00:00
def get_app(self) -> Application:
testcase = self
class TestHandler(RequestHandler):
def get(self) -> None:
nonlocal testcase
testcase.request = self.request
2020-09-18 03:39:35 +00:00
2020-09-20 00:38:08 +00:00
return Application([(r"/.*", TestHandler)])
2020-09-18 03:39:35 +00:00
@given(parameters())
2020-09-18 04:01:10 +00:00
def test_simple_request(self, parameters: Parameters) -> None:
2020-09-18 03:39:35 +00:00
spec = create_spec(
{
"openapi": "3.0.0",
"info": {"title": "Test specification", "version": "0.1"},
"paths": {
"/": {
"get": {
2020-09-18 04:01:10 +00:00
"parameters": parameters.as_openapi(),
2020-09-18 03:39:35 +00:00
"responses": {"default": {"description": "Root response"}},
}
}
},
}
)
validator = RequestValidator(spec)
self.fetch(
2020-09-18 04:01:10 +00:00
"/?" + urlencode(parameters.query_parameters),
headers=HTTPHeaders(parameters.headers),
2020-09-18 03:39:35 +00:00
)
assert self.request is not None
2020-09-18 03:39:35 +00:00
result = validator.validate(self.request)
result.raise_for_errors()
2020-09-18 04:01:10 +00:00
@given(parameters(min_headers=1) | parameters(min_query_parameters=1))
def test_simple_request_fails_without_parameters(
self, parameters: Parameters
) -> None:
spec = create_spec(
{
"openapi": "3.0.0",
"info": {"title": "Test specification", "version": "0.1"},
"paths": {
"/": {
"get": {
"parameters": parameters.as_openapi(),
"responses": {"default": {"description": "Root response"}},
}
}
},
}
)
validator = RequestValidator(spec)
self.fetch("/")
assert self.request is not None
2020-09-18 04:01:10 +00:00
result = validator.validate(self.request)
with self.assertRaises(MissingRequiredParameter):
result.raise_for_errors()
2020-09-20 00:38:08 +00:00
def test_url_parameters(self) -> None:
spec = create_spec(
{
"openapi": "3.0.0",
"info": {"title": "Test specification", "version": "0.1"},
"paths": {
"/{id}": {
"get": {
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"schema": {"type": "integer"},
}
],
"responses": {"default": {"description": "Root response"}},
}
}
},
}
)
validator = RequestValidator(spec)
self.fetch("/1234")
assert self.request is not None
2020-09-20 00:38:08 +00:00
result = validator.validate(self.request)
result.raise_for_errors()
def test_bad_url_parameters(self) -> None:
spec = create_spec(
{
"openapi": "3.0.0",
"info": {"title": "Test specification", "version": "0.1"},
"paths": {
"/{id}": {
"get": {
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"schema": {"type": "integer"},
}
],
"responses": {"default": {"description": "Root response"}},
}
}
},
}
)
validator = RequestValidator(spec)
self.fetch("/abcd")
assert self.request is not None
2020-09-20 00:38:08 +00:00
result = validator.validate(self.request)
with self.assertRaises(OpenAPIError):
result.raise_for_errors()