From 8b243ee565054a7bbf8d8837f4321bb6fa90f645 Mon Sep 17 00:00:00 2001 From: Correl Roush Date: Thu, 25 Feb 2021 22:55:07 -0500 Subject: [PATCH] Add a test case base class Add a unittest test case class based on Tornado's AsyncHTTPTestCase which validates responses against an OpenAPI 3 specification. --- tests/test_testing.py | 107 ++++++++++++++++++++++++++++++++++++ tornado_openapi3/testing.py | 29 ++++++++++ 2 files changed, 136 insertions(+) create mode 100644 tests/test_testing.py create mode 100644 tornado_openapi3/testing.py diff --git a/tests/test_testing.py b/tests/test_testing.py new file mode 100644 index 0000000..9f30742 --- /dev/null +++ b/tests/test_testing.py @@ -0,0 +1,107 @@ +import json +from openapi_core.schema.responses.exceptions import InvalidResponse # type: ignore +import tornado.web + +from tornado_openapi3.handler import OpenAPIRequestHandler +from tornado_openapi3.testing import AsyncOpenAPITestCase + + +def spec(responses: dict = dict()) -> dict: + return { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0", + }, + "components": { + "schemas": { + "resource": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + }, + }, + "paths": { + "/resource": { + "get": { + "responses": responses, + } + } + }, + } + + +class BaseTestCase(AsyncOpenAPITestCase): + spec = spec() + custom_media_type_deserializers = { + "application/vnd.example.resource+json": json.loads, + } + + def get_app(self) -> tornado.web.Application: + testcase = self + + class ResourceHandler(OpenAPIRequestHandler): + spec = self.spec + custom_media_type_deserializers = self.custom_media_type_deserializers + + async def get(self) -> None: + await testcase.get(self) + + return tornado.web.Application([(r"/resource", ResourceHandler)]) + + async def get(self, handler: tornado.web.RequestHandler) -> None: + ... + + +class SuccessTests(BaseTestCase): + spec = spec( + responses={ + "200": { + "description": "Success", + "content": { + "application/vnd.example.resource+json": { + "schema": {"$ref": "#/components/schemas/resource"} + } + }, + } + } + ) + + async def get(self, handler: tornado.web.RequestHandler) -> None: + handler.set_header("Content-Type", "application/vnd.example.resource+json") + handler.finish(json.dumps({"name": "Name"})) + + def test_success(self) -> None: + response = self.fetch("/resource") + self.assertEqual(200, response.code) + + +class IncorrectResponseTests(BaseTestCase): + spec = spec(responses={"200": {"description": "Success"}}) + + async def get(self, handler: tornado.web.RequestHandler) -> None: + handler.set_status(418) + + def test_unexpected_response_code(self) -> None: + with self.assertRaises(InvalidResponse) as context: + self.fetch("/resource") + self.assertEqual("418", context.exception.http_status) + + +class RaiseErrorTests(BaseTestCase): + spec = spec( + responses={ + "418": { + "description": "I'm a teapot", + } + } + ) + + async def get(self, handler: tornado.web.RequestHandler) -> None: + handler.set_status(418) + + def test_fetch_throws_error_on_expected_failure(self) -> None: + with self.assertRaises(tornado.httpclient.HTTPError) as context: + self.fetch("/resource", raise_error=True) + self.assertEqual(418, context.exception.code) diff --git a/tornado_openapi3/testing.py b/tornado_openapi3/testing.py new file mode 100644 index 0000000..8d3e28d --- /dev/null +++ b/tornado_openapi3/testing.py @@ -0,0 +1,29 @@ +from typing import Any + +import tornado.httpclient +import tornado.testing + +from openapi_core import create_spec # type: ignore +from tornado_openapi3.responses import ResponseValidator + + +class AsyncOpenAPITestCase(tornado.testing.AsyncHTTPTestCase): + spec: dict = {} + custom_media_type_deserializers: dict = {} + + def setUp(self) -> None: + super().setUp() + self.validator = ResponseValidator( + create_spec(self.spec), + custom_media_type_deserializers=self.custom_media_type_deserializers, + ) + + def fetch( + self, path: str, raise_error: bool = False, **kwargs: Any + ) -> tornado.httpclient.HTTPResponse: + response = super().fetch(path, raise_error=False, **kwargs) + result = self.validator.validate(response) + result.raise_for_errors() + if raise_error: + response.rethrow() + return response