From 5c79d33518865337ee208a58a04fabc6d10a1306 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Wed, 6 May 2020 23:46:09 -0500 Subject: [PATCH 1/3] refactor: add sanic-graphql as optional feature --- graphql_server/sanic/__init__.py | 3 + graphql_server/sanic/graphqlview.py | 198 ++++++++ graphql_server/sanic/render_graphiql.py | 185 +++++++ setup.cfg | 1 + setup.py | 12 +- tests/sanic/__init__.py | 0 tests/sanic/app.py | 58 +++ tests/sanic/schema.py | 71 +++ tests/sanic/test_graphiqlview.py | 88 ++++ tests/sanic/test_graphqlview.py | 644 ++++++++++++++++++++++++ 10 files changed, 1259 insertions(+), 1 deletion(-) create mode 100644 graphql_server/sanic/__init__.py create mode 100644 graphql_server/sanic/graphqlview.py create mode 100644 graphql_server/sanic/render_graphiql.py create mode 100644 tests/sanic/__init__.py create mode 100644 tests/sanic/app.py create mode 100644 tests/sanic/schema.py create mode 100644 tests/sanic/test_graphiqlview.py create mode 100644 tests/sanic/test_graphqlview.py diff --git a/graphql_server/sanic/__init__.py b/graphql_server/sanic/__init__.py new file mode 100644 index 0000000..8f5beaf --- /dev/null +++ b/graphql_server/sanic/__init__.py @@ -0,0 +1,3 @@ +from .graphqlview import GraphQLView + +__all__ = ["GraphQLView"] diff --git a/graphql_server/sanic/graphqlview.py b/graphql_server/sanic/graphqlview.py new file mode 100644 index 0000000..e05f734 --- /dev/null +++ b/graphql_server/sanic/graphqlview.py @@ -0,0 +1,198 @@ +import copy +from cgi import parse_header +from collections.abc import MutableMapping +from functools import partial + +from graphql import GraphQLError +from graphql.type.schema import GraphQLSchema +from sanic.response import HTTPResponse +from sanic.views import HTTPMethodView + +from graphql_server import ( + HttpQueryError, + encode_execution_results, + format_error_default, + json_encode, + load_json_body, + run_http_query, +) + +from .render_graphiql import render_graphiql + + +class GraphQLView(HTTPMethodView): + schema = None + executor = None + root_value = None + context = None + pretty = False + graphiql = False + graphiql_version = None + graphiql_template = None + middleware = None + batch = False + jinja_env = None + max_age = 86400 + enable_async = True + + methods = ["GET", "POST", "PUT", "DELETE"] + + def __init__(self, **kwargs): + super(GraphQLView, self).__init__() + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + + assert isinstance( + self.schema, GraphQLSchema + ), "A Schema is required to be provided to GraphQLView." + + def get_root_value(self): + return self.root_value + + def get_context(self, request): + context = ( + copy.copy(self.context) + if self.context and isinstance(self.context, MutableMapping) + else {} + ) + if isinstance(context, MutableMapping) and "request" not in context: + context.update({"request": request}) + return context + + def get_middleware(self): + return self.middleware + + def get_executor(self): + return self.executor + + def render_graphiql(self, params, result): + return render_graphiql( + jinja_env=self.jinja_env, + params=params, + result=result, + graphiql_version=self.graphiql_version, + graphiql_template=self.graphiql_template, + ) + + format_error = staticmethod(format_error_default) + encode = staticmethod(json_encode) + + async def dispatch_request(self, request, *args, **kwargs): + try: + request_method = request.method.lower() + data = self.parse_body(request) + + show_graphiql = request_method == "get" and self.should_display_graphiql( + request + ) + catch = show_graphiql + + pretty = self.pretty or show_graphiql or request.args.get("pretty") + + extra_options = {} + executor = self.get_executor() + if executor: + # We only include it optionally since + # executor is not a valid argument in all backends + extra_options["executor"] = executor + + if request_method != "options": + execution_results, all_params = run_http_query( + self.schema, + request_method, + data, + query_data=request.args, + batch_enabled=self.batch, + catch=catch, + # Execute options + # This check was previously using an isinstance of AsyncioExecutor + check_sync=not (self.enable_async and self.executor is not None), + root_value=self.get_root_value(), + context_value=self.get_context(request), + middleware=self.get_middleware(), + **extra_options + ) + result, status_code = encode_execution_results( + execution_results, + is_batch=isinstance(data, list), + format_error=self.format_error, + encode=partial(self.encode, pretty=pretty), # noqa: ignore + ) + + if show_graphiql: + return await self.render_graphiql( + params=all_params[0], result=result + ) + + return HTTPResponse( + result, status=status_code, content_type="application/json" + ) + + else: + return self.process_preflight(request) + + except HttpQueryError as e: + parsed_error = GraphQLError(e.message) + return HTTPResponse( + self.encode(dict(errors=[self.format_error(parsed_error)])), + status=e.status_code, + headers=e.headers, + content_type="application/json", + ) + + # noinspection PyBroadException + def parse_body(self, request): + content_type = self.get_mime_type(request) + if content_type == "application/graphql": + return {"query": request.body.decode("utf8")} + + elif content_type == "application/json": + return load_json_body(request.body.decode("utf8")) + + elif content_type in ( + "application/x-www-form-urlencoded", + "multipart/form-data", + ): + return request.form + + return {} + + @staticmethod + def get_mime_type(request): + # We use mime type here since we don't need the other + # information provided by content_type + if "content-type" not in request.headers: + return None + + mime_type, _ = parse_header(request.headers["content-type"]) + return mime_type + + def should_display_graphiql(self, request): + if not self.graphiql or "raw" in request.args: + return False + + return self.request_wants_html(request) + + @staticmethod + def request_wants_html(request): + accept = request.headers.get("accept", {}) + return "text/html" in accept or "*/*" in accept + + def process_preflight(self, request): + """ Preflight request support for apollo-client + https://www.w3.org/TR/cors/#resource-preflight-requests """ + origin = request.headers.get("Origin", "") + method = request.headers.get("Access-Control-Request-Method", "").upper() + + if method and method in self.methods: + return HTTPResponse( + status=200, + headers={ + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Methods": ", ".join(self.methods), + "Access-Control-Max-Age": str(self.max_age), + }, + ) + else: + return HTTPResponse(status=400) diff --git a/graphql_server/sanic/render_graphiql.py b/graphql_server/sanic/render_graphiql.py new file mode 100644 index 0000000..ca21ee3 --- /dev/null +++ b/graphql_server/sanic/render_graphiql.py @@ -0,0 +1,185 @@ +import json +import re + +from sanic.response import html + +GRAPHIQL_VERSION = "0.7.1" + +TEMPLATE = """ + + + + + + + + + + + + + + +""" + + +def escape_js_value(value): + quotation = False + if value.startswith('"') and value.endswith('"'): + quotation = True + value = value[1 : len(value) - 1] + + value = value.replace("\\\\n", "\\\\\\n").replace("\\n", "\\\\n") + if quotation: + value = '"' + value.replace('\\\\"', '"').replace('"', '\\"') + '"' + + return value + + +def process_var(template, name, value, jsonify=False): + pattern = r"{{\s*" + name + r"(\s*|[^}]+)*\s*}}" + if jsonify and value not in ["null", "undefined"]: + value = json.dumps(value) + value = escape_js_value(value) + + return re.sub(pattern, value, template) + + +def simple_renderer(template, **values): + replace = ["graphiql_version"] + replace_jsonify = ["query", "result", "variables", "operation_name"] + + for r in replace: + template = process_var(template, r, values.get(r, "")) + + for r in replace_jsonify: + template = process_var(template, r, values.get(r, ""), True) + + return template + + +async def render_graphiql( + jinja_env=None, + graphiql_version=None, + graphiql_template=None, + params=None, + result=None, +): + graphiql_version = graphiql_version or GRAPHIQL_VERSION + template = graphiql_template or TEMPLATE + template_vars = { + "graphiql_version": graphiql_version, + "query": params and params.query, + "variables": params and params.variables, + "operation_name": params and params.operation_name, + "result": result, + } + + if jinja_env: + template = jinja_env.from_string(template) + if jinja_env.is_async: + source = await template.render_async(**template_vars) + else: + source = template.render(**template_vars) + else: + source = simple_renderer(template, **template_vars) + + return html(source) diff --git a/setup.cfg b/setup.cfg index 78bddbd..b943008 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [flake8] exclude = docs max-line-length = 88 +ignore = E203, E501, W503 [isort] known_first_party=graphql_server diff --git a/setup.py b/setup.py index 15397cc..fbf8637 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,8 @@ tests_requires = [ "pytest>=5.3,<5.4", "pytest-cov>=2.8,<3", + "aiohttp>=3.5.0,<4", + "Jinja2>=2.10.1,<3", ] dev_requires = [ @@ -21,7 +23,14 @@ "flask>=0.7.0", ] -install_all_requires = install_requires + install_flask_requires +install_sanic_requires = [ + "sanic>=19.9.0,<20", +] + +install_all_requires = \ + install_requires + \ + install_flask_requires + \ + install_sanic_requires setup( name="graphql-server-core", @@ -52,6 +61,7 @@ "test": install_all_requires + tests_requires, "dev": install_all_requires + dev_requires, "flask": install_flask_requires, + "sanic": install_sanic_requires, }, include_package_data=True, zip_safe=False, diff --git a/tests/sanic/__init__.py b/tests/sanic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/sanic/app.py b/tests/sanic/app.py new file mode 100644 index 0000000..73db85d --- /dev/null +++ b/tests/sanic/app.py @@ -0,0 +1,58 @@ +from urllib.parse import urlencode + +import pytest +from sanic import Sanic +from sanic.testing import SanicTestClient + +from graphql_server.sanic import GraphQLView + +from .schema import Schema + + +def create_app(path="/graphql", **kwargs): + app = Sanic(__name__) + app.debug = True + + schema = kwargs.pop("schema", None) or Schema + # async_executor = kwargs.pop("async_executor", False) + # + # if async_executor: + # + # @app.listener("before_server_start") + # def init_async_executor(app, loop): + # executor = AsyncioExecutor(loop) + # app.add_route( + # GraphQLView.as_view(schema=schema, executor=executor, **kwargs), path + # ) + # + # @app.listener("before_server_stop") + # def remove_graphql_endpoint(app, loop): + # app.remove_route(path) + # + # else: + # app.add_route(GraphQLView.as_view(schema=schema, **kwargs), path) + + app.add_route(GraphQLView.as_view(schema=schema, **kwargs), path) + + app.client = SanicTestClient(app) + return app + + +def url_string(uri="/graphql", **url_params): + string = "/graphql" + + if url_params: + string += "?" + urlencode(url_params) + + return string + + +def parametrize_sync_async_app_test(arg, **extra_options): + def decorator(test): + apps = [] + for ae in [False, True]: + apps.append(create_app(async_executor=ae, **extra_options)) + + return pytest.mark.parametrize("app", apps)(test) + + return decorator diff --git a/tests/sanic/schema.py b/tests/sanic/schema.py new file mode 100644 index 0000000..2a5b1da --- /dev/null +++ b/tests/sanic/schema.py @@ -0,0 +1,71 @@ +import asyncio + +from graphql.type.definition import ( + GraphQLArgument, + GraphQLField, + GraphQLNonNull, + GraphQLObjectType, +) +from graphql.type.scalars import GraphQLString +from graphql.type.schema import GraphQLSchema + + +def resolve_raises(*_): + raise Exception("Throws!") + + +# Sync schema +QueryRootType = GraphQLObjectType( + name="QueryRoot", + fields={ + "thrower": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_raises), + "request": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=lambda obj, info: info.context["request"].args.get("q"), + ), + "context": GraphQLField( + GraphQLNonNull(GraphQLString), resolve=lambda obj, info: info.context + ), + "test": GraphQLField( + type_=GraphQLString, + args={"who": GraphQLArgument(GraphQLString)}, + resolve=lambda obj, info, who=None: "Hello %s" % (who or "World"), + ), + }, +) + +MutationRootType = GraphQLObjectType( + name="MutationRoot", + fields={ + "writeTest": GraphQLField(type_=QueryRootType, resolve=lambda *_: QueryRootType) + }, +) + +Schema = GraphQLSchema(QueryRootType, MutationRootType) + + +# Schema with async methods +async def resolver(context, *_, **__): + await asyncio.sleep(0.001) + return "hey" + + +async def resolver_2(context, *_, **__): + await asyncio.sleep(0.003) + return "hey2" + + +def resolver_3(context, *_, **__): + return "hey3" + + +AsyncQueryType = GraphQLObjectType( + "AsyncQueryType", + { + "a": GraphQLField(GraphQLString, resolve=resolver), + "b": GraphQLField(GraphQLString, resolve=resolver_2), + "c": GraphQLField(GraphQLString, resolve=resolver_3), + }, +) + +AsyncSchema = GraphQLSchema(AsyncQueryType) diff --git a/tests/sanic/test_graphiqlview.py b/tests/sanic/test_graphiqlview.py new file mode 100644 index 0000000..9e98ed6 --- /dev/null +++ b/tests/sanic/test_graphiqlview.py @@ -0,0 +1,88 @@ +import pytest +from jinja2 import Environment + +from .app import create_app, parametrize_sync_async_app_test, url_string +from .schema import AsyncSchema + + +@pytest.fixture +def pretty_response(): + return ( + "{\n" + ' "data": {\n' + ' "test": "Hello World"\n' + " }\n" + "}".replace('"', '\\"').replace("\n", "\\n") + ) + + +@parametrize_sync_async_app_test("app", graphiql=True) +def test_graphiql_is_enabled(app): + _, response = app.client.get( + uri=url_string(query="{test}"), headers={"Accept": "text/html"} + ) + assert response.status == 200 + + +@parametrize_sync_async_app_test("app", graphiql=True) +def test_graphiql_simple_renderer(app, pretty_response): + _, response = app.client.get( + uri=url_string(query="{test}"), headers={"Accept": "text/html"} + ) + assert response.status == 200 + assert pretty_response in response.body.decode("utf-8") + + +@parametrize_sync_async_app_test("app", graphiql=True, jinja_env=Environment()) +def test_graphiql_jinja_renderer(app, pretty_response): + _, response = app.client.get( + uri=url_string(query="{test}"), headers={"Accept": "text/html"} + ) + assert response.status == 200 + assert pretty_response in response.body.decode("utf-8") + + +@parametrize_sync_async_app_test( + "app", graphiql=True, jinja_env=Environment(enable_async=True) +) +def test_graphiql_jinja_async_renderer(app, pretty_response): + _, response = app.client.get( + uri=url_string(query="{test}"), headers={"Accept": "text/html"} + ) + assert response.status == 200 + assert pretty_response in response.body.decode("utf-8") + + +@parametrize_sync_async_app_test("app", graphiql=True) +def test_graphiql_html_is_not_accepted(app): + _, response = app.client.get( + uri=url_string(), headers={"Accept": "application/json"} + ) + assert response.status == 400 + + +@pytest.mark.parametrize( + "app", [create_app(async_executor=True, graphiql=True, schema=AsyncSchema)] +) +def test_graphiql_asyncio_schema(app): + query = "{a,b,c}" + _, response = app.client.get( + uri=url_string(query=query), headers={"Accept": "text/html"} + ) + + expected_response = ( + ( + "{\n" + ' "data": {\n' + ' "a": "hey",\n' + ' "b": "hey2",\n' + ' "c": "hey3"\n' + " }\n" + "}" + ) + .replace('"', '\\"') + .replace("\n", "\\n") + ) + + assert response.status == 200 + assert expected_response in response.body.decode("utf-8") diff --git a/tests/sanic/test_graphqlview.py b/tests/sanic/test_graphqlview.py new file mode 100644 index 0000000..b9d1693 --- /dev/null +++ b/tests/sanic/test_graphqlview.py @@ -0,0 +1,644 @@ +import json +from urllib.parse import urlencode + +import pytest +from aiohttp.formdata import FormData + +from graphql_server.sanic import GraphQLView + +from .app import create_app, parametrize_sync_async_app_test, url_string +from .schema import AsyncSchema, Schema + + +def response_json(response): + return json.loads(response.body.decode()) + + +def json_dump_kwarg(**kwargs): + return json.dumps(kwargs) + + +def json_dump_kwarg_list(**kwargs): + return json.dumps([kwargs]) + + +# This test requires an executor +# @pytest.mark.parametrize( +# "view,expected", +# [ +# (GraphQLView(schema=Schema), False), +# (GraphQLView(schema=Schema, executor=SyncExecutor()), False), +# (GraphQLView(schema=Schema, executor=AsyncioExecutor()), True), +# ], +# ) +# def test_eval(view, expected): +# assert view.enable_async == expected + + +@pytest.mark.parametrize( + "app", [create_app()] +) +def test_allows_get_with_query_param(app): + _, response = app.client.get(uri=url_string(query="{test}")) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello World"}} + + +@parametrize_sync_async_app_test("app") +def test_allows_get_with_variable_values(app): + _, response = app.client.get( + uri=url_string( + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ) + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello Dolly"}} + + +@parametrize_sync_async_app_test("app") +def test_allows_get_with_operation_name(app): + _, response = app.client.get( + uri=url_string( + query=""" + query helloYou { test(who: "You"), ...shared } + query helloWorld { test(who: "World"), ...shared } + query helloDolly { test(who: "Dolly"), ...shared } + fragment shared on QueryRoot { + shared: test(who: "Everyone") + } + """, + operationName="helloWorld", + ) + ) + + assert response.status == 200 + assert response_json(response) == { + "data": {"test": "Hello World", "shared": "Hello Everyone"} + } + + +@parametrize_sync_async_app_test("app") +def test_reports_validation_errors(app): + _, response = app.client.get( + uri=url_string(query="{ test, unknownOne, unknownTwo }") + ) + + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", + "locations": [{"line": 1, "column": 9}], + "path": None, + }, + { + "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", + "locations": [{"line": 1, "column": 21}], + "path": None, + }, + ] + } + + +@parametrize_sync_async_app_test("app") +def test_errors_when_missing_operation_name(app): + _, response = app.client.get( + uri=url_string( + query=""" + query TestQuery { test } + mutation TestMutation { writeTest { test } } + """ + ) + ) + + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "Must provide operation name if query contains multiple operations.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_errors_when_sending_a_mutation_via_get(app): + _, response = app.client.get( + uri=url_string( + query=""" + mutation TestMutation { writeTest { test } } + """ + ) + ) + assert response.status == 405 + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "Can only perform a mutation operation from a POST request.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_errors_when_selecting_a_mutation_within_a_get(app): + _, response = app.client.get( + uri=url_string( + query=""" + query TestQuery { test } + mutation TestMutation { writeTest { test } } + """, + operationName="TestMutation", + ) + ) + + assert response.status == 405 + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "Can only perform a mutation operation from a POST request.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_allows_mutation_to_exist_within_a_get(app): + _, response = app.client.get( + uri=url_string( + query=""" + query TestQuery { test } + mutation TestMutation { writeTest { test } } + """, + operationName="TestQuery", + ) + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello World"}} + + +@parametrize_sync_async_app_test("app") +def test_allows_post_with_json_encoding(app): + _, response = app.client.post( + uri=url_string(), + data=json_dump_kwarg(query="{test}"), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello World"}} + + +@parametrize_sync_async_app_test("app") +def test_allows_sending_a_mutation_via_post(app): + _, response = app.client.post( + uri=url_string(), + data=json_dump_kwarg(query="mutation TestMutation { writeTest { test } }"), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} + + +@parametrize_sync_async_app_test("app") +def test_allows_post_with_url_encoding(app): + data = FormData() + data.add_field("query", "{test}") + _, response = app.client.post( + uri=url_string(), + data=data, + headers={"content-type": "application/x-www-form-urlencoded"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello World"}} + + +@parametrize_sync_async_app_test("app") +def test_supports_post_json_query_with_string_variables(app): + _, response = app.client.post( + uri=url_string(), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello Dolly"}} + + +@parametrize_sync_async_app_test("app") +def test_supports_post_json_query_with_json_variables(app): + _, response = app.client.post( + uri=url_string(), + data=json_dump_kwarg( + query="query helloWho($who: String){ test(who: $who) }", + variables={"who": "Dolly"}, + ), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello Dolly"}} + + +@parametrize_sync_async_app_test("app") +def test_supports_post_url_encoded_query_with_string_variables(app): + _, response = app.client.post( + uri=url_string(), + data=urlencode( + dict( + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ) + ), + headers={"content-type": "application/x-www-form-urlencoded"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello Dolly"}} + + +@parametrize_sync_async_app_test("app") +def test_supports_post_json_query_with_get_variable_values(app): + _, response = app.client.post( + uri=url_string(variables=json.dumps({"who": "Dolly"})), + data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello Dolly"}} + + +@parametrize_sync_async_app_test("app") +def test_post_url_encoded_query_with_get_variable_values(app): + _, response = app.client.post( + uri=url_string(variables=json.dumps({"who": "Dolly"})), + data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)), + headers={"content-type": "application/x-www-form-urlencoded"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello Dolly"}} + + +@parametrize_sync_async_app_test("app") +def test_supports_post_raw_text_query_with_get_variable_values(app): + _, response = app.client.post( + uri=url_string(variables=json.dumps({"who": "Dolly"})), + data="query helloWho($who: String){ test(who: $who) }", + headers={"content-type": "application/graphql"}, + ) + + assert response.status == 200 + assert response_json(response) == {"data": {"test": "Hello Dolly"}} + + +@parametrize_sync_async_app_test("app") +def test_allows_post_with_operation_name(app): + _, response = app.client.post( + uri=url_string(), + data=json_dump_kwarg( + query=""" + query helloYou { test(who: "You"), ...shared } + query helloWorld { test(who: "World"), ...shared } + query helloDolly { test(who: "Dolly"), ...shared } + fragment shared on QueryRoot { + shared: test(who: "Everyone") + } + """, + operationName="helloWorld", + ), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == { + "data": {"test": "Hello World", "shared": "Hello Everyone"} + } + + +@parametrize_sync_async_app_test("app") +def test_allows_post_with_get_operation_name(app): + _, response = app.client.post( + uri=url_string(operationName="helloWorld"), + data=""" + query helloYou { test(who: "You"), ...shared } + query helloWorld { test(who: "World"), ...shared } + query helloDolly { test(who: "Dolly"), ...shared } + fragment shared on QueryRoot { + shared: test(who: "Everyone") + } + """, + headers={"content-type": "application/graphql"}, + ) + + assert response.status == 200 + assert response_json(response) == { + "data": {"test": "Hello World", "shared": "Hello Everyone"} + } + + +@parametrize_sync_async_app_test("app", pretty=True) +def test_supports_pretty_printing(app): + _, response = app.client.get(uri=url_string(query="{test}")) + + assert response.body.decode() == ( + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" + ) + + +@parametrize_sync_async_app_test("app", pretty=False) +def test_not_pretty_by_default(app): + _, response = app.client.get(url_string(query="{test}")) + + assert response.body.decode() == '{"data":{"test":"Hello World"}}' + + +@parametrize_sync_async_app_test("app") +def test_supports_pretty_printing_by_request(app): + _, response = app.client.get(uri=url_string(query="{test}", pretty="1")) + + assert response.body.decode() == ( + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" + ) + + +@parametrize_sync_async_app_test("app") +def test_handles_field_errors_caught_by_graphql(app): + _, response = app.client.get(uri=url_string(query="{thrower}")) + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "locations": [{"column": 2, "line": 1}], + "message": "Throws!", + "path": ["thrower"], + } + ], + } + + +@parametrize_sync_async_app_test("app") +def test_handles_syntax_errors_caught_by_graphql(app): + _, response = app.client.get(uri=url_string(query="syntaxerror")) + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "locations": [{"column": 1, "line": 1}], + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_handles_errors_caused_by_a_lack_of_query(app): + _, response = app.client.get(uri=url_string()) + + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "Must provide query string.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_handles_batch_correctly_if_is_disabled(app): + _, response = app.client.post( + uri=url_string(), data="[]", headers={"content-type": "application/json"} + ) + + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "Batch GraphQL requests are not enabled.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_handles_incomplete_json_bodies(app): + _, response = app.client.post( + uri=url_string(), data='{"query":', headers={"content-type": "application/json"} + ) + + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "POST body sent invalid JSON.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_handles_plain_post_text(app): + _, response = app.client.post( + uri=url_string(variables=json.dumps({"who": "Dolly"})), + data="query helloWho($who: String){ test(who: $who) }", + headers={"content-type": "text/plain"}, + ) + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "Must provide query string.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_handles_poorly_formed_variables(app): + _, response = app.client.get( + uri=url_string( + query="query helloWho($who: String){ test(who: $who) }", variables="who:You" + ) + ) + assert response.status == 400 + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "Variables are invalid JSON.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_handles_unsupported_http_methods(app): + _, response = app.client.put(uri=url_string(query="{test}")) + assert response.status == 405 + assert response.headers["Allow"] in ["GET, POST", "HEAD, GET, POST, OPTIONS"] + assert response_json(response) == { + "errors": [ + { + "locations": None, + "message": "GraphQL only supports GET and POST requests.", + "path": None + } + ] + } + + +@parametrize_sync_async_app_test("app") +def test_passes_request_into_request_context(app): + _, response = app.client.get(uri=url_string(query="{request}", q="testing")) + + assert response.status == 200 + assert response_json(response) == {"data": {"request": "testing"}} + + +@parametrize_sync_async_app_test("app", context="CUSTOM CONTEXT") +def test_supports_pretty_printing_on_custom_context_response(app): + _, response = app.client.get(uri=url_string(query="{context}")) + + assert response.status == 200 + assert "data" in response_json(response) + assert ( + response_json(response)["data"]["context"] + == "{'request': }" + ) + + +@parametrize_sync_async_app_test("app") +def test_post_multipart_data(app): + query = "mutation TestMutation { writeTest { test } }" + + data = ( + "------sanicgraphql\r\n" + + 'Content-Disposition: form-data; name="query"\r\n' + + "\r\n" + + query + + "\r\n" + + "------sanicgraphql--\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + 'Content-Disposition: form-data; name="file"; filename="text1.txt"; filename*=utf-8\'\'text1.txt\r\n' + + "\r\n" + + "\r\n" + + "------sanicgraphql--\r\n" + ) + + _, response = app.client.post( + uri=url_string(), + data=data, + headers={"content-type": "multipart/form-data; boundary=----sanicgraphql"}, + ) + + assert response.status == 200 + assert response_json(response) == { + "data": {u"writeTest": {u"test": u"Hello World"}} + } + + +@parametrize_sync_async_app_test("app", batch=True) +def test_batch_allows_post_with_json_encoding(app): + _, response = app.client.post( + uri=url_string(), + data=json_dump_kwarg_list(id=1, query="{test}"), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == [{"data": {"test": "Hello World"}}] + + +@parametrize_sync_async_app_test("app", batch=True) +def test_batch_supports_post_json_query_with_json_variables(app): + _, response = app.client.post( + uri=url_string(), + data=json_dump_kwarg_list( + id=1, + query="query helloWho($who: String){ test(who: $who) }", + variables={"who": "Dolly"}, + ), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == [{"data": {"test": "Hello Dolly"}}] + + +@parametrize_sync_async_app_test("app", batch=True) +def test_batch_allows_post_with_operation_name(app): + _, response = app.client.post( + uri=url_string(), + data=json_dump_kwarg_list( + id=1, + query=""" + query helloYou { test(who: "You"), ...shared } + query helloWorld { test(who: "World"), ...shared } + query helloDolly { test(who: "Dolly"), ...shared } + fragment shared on QueryRoot { + shared: test(who: "Everyone") + } + """, + operationName="helloWorld", + ), + headers={"content-type": "application/json"}, + ) + + assert response.status == 200 + assert response_json(response) == [ + {"data": {"test": "Hello World", "shared": "Hello Everyone"}} + ] + + +@pytest.mark.parametrize("app", [create_app(async_executor=True, schema=AsyncSchema)]) +def test_async_schema(app): + query = "{a,b,c}" + _, response = app.client.get(uri=url_string(query=query)) + + assert response.status == 200 + assert response_json(response) == {"data": {"a": "hey", "b": "hey2", "c": "hey3"}} + + +@parametrize_sync_async_app_test("app") +def test_preflight_request(app): + _, response = app.client.options( + uri=url_string(), headers={"Access-Control-Request-Method": "POST"} + ) + + assert response.status == 200 + + +@parametrize_sync_async_app_test("app") +def test_preflight_incorrect_request(app): + _, response = app.client.options( + uri=url_string(), headers={"Access-Control-Request-Method": "OPTIONS"} + ) + + assert response.status == 400 From dd3d9cda07d16150ddb500fffeea2e83a9dbce41 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Sun, 10 May 2020 16:43:48 -0500 Subject: [PATCH 2/3] refactor: sanic tests and remove executor parameter --- graphql_server/sanic/graphqlview.py | 30 ++------ tests/sanic/app.py | 29 ------- tests/sanic/schema.py | 18 ++--- tests/sanic/test_graphiqlview.py | 19 +++-- tests/sanic/test_graphqlview.py | 115 ++++++++++++---------------- 5 files changed, 76 insertions(+), 135 deletions(-) diff --git a/graphql_server/sanic/graphqlview.py b/graphql_server/sanic/graphqlview.py index e05f734..678992a 100644 --- a/graphql_server/sanic/graphqlview.py +++ b/graphql_server/sanic/graphqlview.py @@ -22,7 +22,6 @@ class GraphQLView(HTTPMethodView): schema = None - executor = None root_value = None context = None pretty = False @@ -33,7 +32,7 @@ class GraphQLView(HTTPMethodView): batch = False jinja_env = None max_age = 86400 - enable_async = True + enable_async = False methods = ["GET", "POST", "PUT", "DELETE"] @@ -63,11 +62,8 @@ def get_context(self, request): def get_middleware(self): return self.middleware - def get_executor(self): - return self.executor - - def render_graphiql(self, params, result): - return render_graphiql( + async def render_graphiql(self, params, result): + return await render_graphiql( jinja_env=self.jinja_env, params=params, result=result, @@ -90,13 +86,6 @@ async def dispatch_request(self, request, *args, **kwargs): pretty = self.pretty or show_graphiql or request.args.get("pretty") - extra_options = {} - executor = self.get_executor() - if executor: - # We only include it optionally since - # executor is not a valid argument in all backends - extra_options["executor"] = executor - if request_method != "options": execution_results, all_params = run_http_query( self.schema, @@ -106,24 +95,21 @@ async def dispatch_request(self, request, *args, **kwargs): batch_enabled=self.batch, catch=catch, # Execute options - # This check was previously using an isinstance of AsyncioExecutor - check_sync=not (self.enable_async and self.executor is not None), + run_sync=not self.enable_async, root_value=self.get_root_value(), context_value=self.get_context(request), - middleware=self.get_middleware(), - **extra_options + middleware=self.get_middleware() ) + exec_res = [await ex for ex in execution_results] if self.enable_async else execution_results result, status_code = encode_execution_results( - execution_results, + exec_res, is_batch=isinstance(data, list), format_error=self.format_error, encode=partial(self.encode, pretty=pretty), # noqa: ignore ) if show_graphiql: - return await self.render_graphiql( - params=all_params[0], result=result - ) + return await self.render_graphiql(params=all_params[0], result=result) return HTTPResponse( result, status=status_code, content_type="application/json" diff --git a/tests/sanic/app.py b/tests/sanic/app.py index 73db85d..7ea9e8b 100644 --- a/tests/sanic/app.py +++ b/tests/sanic/app.py @@ -14,24 +14,6 @@ def create_app(path="/graphql", **kwargs): app.debug = True schema = kwargs.pop("schema", None) or Schema - # async_executor = kwargs.pop("async_executor", False) - # - # if async_executor: - # - # @app.listener("before_server_start") - # def init_async_executor(app, loop): - # executor = AsyncioExecutor(loop) - # app.add_route( - # GraphQLView.as_view(schema=schema, executor=executor, **kwargs), path - # ) - # - # @app.listener("before_server_stop") - # def remove_graphql_endpoint(app, loop): - # app.remove_route(path) - # - # else: - # app.add_route(GraphQLView.as_view(schema=schema, **kwargs), path) - app.add_route(GraphQLView.as_view(schema=schema, **kwargs), path) app.client = SanicTestClient(app) @@ -45,14 +27,3 @@ def url_string(uri="/graphql", **url_params): string += "?" + urlencode(url_params) return string - - -def parametrize_sync_async_app_test(arg, **extra_options): - def decorator(test): - apps = [] - for ae in [False, True]: - apps.append(create_app(async_executor=ae, **extra_options)) - - return pytest.mark.parametrize("app", apps)(test) - - return decorator diff --git a/tests/sanic/schema.py b/tests/sanic/schema.py index 2a5b1da..a0be485 100644 --- a/tests/sanic/schema.py +++ b/tests/sanic/schema.py @@ -24,7 +24,7 @@ def resolve_raises(*_): resolve=lambda obj, info: info.context["request"].args.get("q"), ), "context": GraphQLField( - GraphQLNonNull(GraphQLString), resolve=lambda obj, info: info.context + GraphQLNonNull(GraphQLString), resolve=lambda obj, info: info.context["request"] ), "test": GraphQLField( type_=GraphQLString, @@ -45,26 +45,26 @@ def resolve_raises(*_): # Schema with async methods -async def resolver(context, *_, **__): +async def resolver_field_async_1(_obj, info): await asyncio.sleep(0.001) return "hey" -async def resolver_2(context, *_, **__): +async def resolver_field_async_2(_obj, info): await asyncio.sleep(0.003) return "hey2" -def resolver_3(context, *_, **__): +def resolver_field_sync(_obj, info): return "hey3" AsyncQueryType = GraphQLObjectType( - "AsyncQueryType", - { - "a": GraphQLField(GraphQLString, resolve=resolver), - "b": GraphQLField(GraphQLString, resolve=resolver_2), - "c": GraphQLField(GraphQLString, resolve=resolver_3), + name="AsyncQueryType", + fields={ + "a": GraphQLField(GraphQLString, resolve=resolver_field_async_1), + "b": GraphQLField(GraphQLString, resolve=resolver_field_async_2), + "c": GraphQLField(GraphQLString, resolve=resolver_field_sync), }, ) diff --git a/tests/sanic/test_graphiqlview.py b/tests/sanic/test_graphiqlview.py index 9e98ed6..a4e190d 100644 --- a/tests/sanic/test_graphiqlview.py +++ b/tests/sanic/test_graphiqlview.py @@ -1,7 +1,7 @@ import pytest from jinja2 import Environment -from .app import create_app, parametrize_sync_async_app_test, url_string +from .app import create_app, url_string from .schema import AsyncSchema @@ -16,7 +16,7 @@ def pretty_response(): ) -@parametrize_sync_async_app_test("app", graphiql=True) +@pytest.mark.parametrize("app", [create_app(graphiql=True)]) def test_graphiql_is_enabled(app): _, response = app.client.get( uri=url_string(query="{test}"), headers={"Accept": "text/html"} @@ -24,7 +24,7 @@ def test_graphiql_is_enabled(app): assert response.status == 200 -@parametrize_sync_async_app_test("app", graphiql=True) +@pytest.mark.parametrize("app", [create_app(graphiql=True)]) def test_graphiql_simple_renderer(app, pretty_response): _, response = app.client.get( uri=url_string(query="{test}"), headers={"Accept": "text/html"} @@ -33,7 +33,7 @@ def test_graphiql_simple_renderer(app, pretty_response): assert pretty_response in response.body.decode("utf-8") -@parametrize_sync_async_app_test("app", graphiql=True, jinja_env=Environment()) +@pytest.mark.parametrize("app", [create_app(graphiql=True, jinja_env=Environment())]) def test_graphiql_jinja_renderer(app, pretty_response): _, response = app.client.get( uri=url_string(query="{test}"), headers={"Accept": "text/html"} @@ -42,9 +42,7 @@ def test_graphiql_jinja_renderer(app, pretty_response): assert pretty_response in response.body.decode("utf-8") -@parametrize_sync_async_app_test( - "app", graphiql=True, jinja_env=Environment(enable_async=True) -) +@pytest.mark.parametrize("app", [create_app(graphiql=True, jinja_env=Environment(enable_async=True))]) def test_graphiql_jinja_async_renderer(app, pretty_response): _, response = app.client.get( uri=url_string(query="{test}"), headers={"Accept": "text/html"} @@ -53,7 +51,7 @@ def test_graphiql_jinja_async_renderer(app, pretty_response): assert pretty_response in response.body.decode("utf-8") -@parametrize_sync_async_app_test("app", graphiql=True) +@pytest.mark.parametrize("app", [create_app(graphiql=True)]) def test_graphiql_html_is_not_accepted(app): _, response = app.client.get( uri=url_string(), headers={"Accept": "application/json"} @@ -62,12 +60,13 @@ def test_graphiql_html_is_not_accepted(app): @pytest.mark.parametrize( - "app", [create_app(async_executor=True, graphiql=True, schema=AsyncSchema)] + "app", [create_app(graphiql=True, schema=AsyncSchema, enable_async=True)] ) def test_graphiql_asyncio_schema(app): query = "{a,b,c}" _, response = app.client.get( - uri=url_string(query=query), headers={"Accept": "text/html"} + uri=url_string(query=query), + headers={"Accept": "text/html"} ) expected_response = ( diff --git a/tests/sanic/test_graphqlview.py b/tests/sanic/test_graphqlview.py index b9d1693..5ff2cb5 100644 --- a/tests/sanic/test_graphqlview.py +++ b/tests/sanic/test_graphqlview.py @@ -2,12 +2,9 @@ from urllib.parse import urlencode import pytest -from aiohttp.formdata import FormData -from graphql_server.sanic import GraphQLView - -from .app import create_app, parametrize_sync_async_app_test, url_string -from .schema import AsyncSchema, Schema +from .app import create_app, url_string +from .schema import AsyncSchema def response_json(response): @@ -22,22 +19,7 @@ def json_dump_kwarg_list(**kwargs): return json.dumps([kwargs]) -# This test requires an executor -# @pytest.mark.parametrize( -# "view,expected", -# [ -# (GraphQLView(schema=Schema), False), -# (GraphQLView(schema=Schema, executor=SyncExecutor()), False), -# (GraphQLView(schema=Schema, executor=AsyncioExecutor()), True), -# ], -# ) -# def test_eval(view, expected): -# assert view.enable_async == expected - - -@pytest.mark.parametrize( - "app", [create_app()] -) +@pytest.mark.parametrize("app", [create_app()]) def test_allows_get_with_query_param(app): _, response = app.client.get(uri=url_string(query="{test}")) @@ -45,7 +27,7 @@ def test_allows_get_with_query_param(app): assert response_json(response) == {"data": {"test": "Hello World"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_allows_get_with_variable_values(app): _, response = app.client.get( uri=url_string( @@ -58,7 +40,7 @@ def test_allows_get_with_variable_values(app): assert response_json(response) == {"data": {"test": "Hello Dolly"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_allows_get_with_operation_name(app): _, response = app.client.get( uri=url_string( @@ -80,7 +62,7 @@ def test_allows_get_with_operation_name(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_reports_validation_errors(app): _, response = app.client.get( uri=url_string(query="{ test, unknownOne, unknownTwo }") @@ -103,7 +85,7 @@ def test_reports_validation_errors(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_errors_when_missing_operation_name(app): _, response = app.client.get( uri=url_string( @@ -126,7 +108,7 @@ def test_errors_when_missing_operation_name(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_errors_when_sending_a_mutation_via_get(app): _, response = app.client.get( uri=url_string( @@ -147,7 +129,7 @@ def test_errors_when_sending_a_mutation_via_get(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_errors_when_selecting_a_mutation_within_a_get(app): _, response = app.client.get( uri=url_string( @@ -171,7 +153,7 @@ def test_errors_when_selecting_a_mutation_within_a_get(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_allows_mutation_to_exist_within_a_get(app): _, response = app.client.get( uri=url_string( @@ -187,7 +169,7 @@ def test_allows_mutation_to_exist_within_a_get(app): assert response_json(response) == {"data": {"test": "Hello World"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_allows_post_with_json_encoding(app): _, response = app.client.post( uri=url_string(), @@ -199,7 +181,7 @@ def test_allows_post_with_json_encoding(app): assert response_json(response) == {"data": {"test": "Hello World"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_allows_sending_a_mutation_via_post(app): _, response = app.client.post( uri=url_string(), @@ -211,21 +193,23 @@ def test_allows_sending_a_mutation_via_post(app): assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_allows_post_with_url_encoding(app): - data = FormData() - data.add_field("query", "{test}") + # Example of how sanic does send data using url enconding + # can be found at their repo. + # https://github.com/huge-success/sanic/blob/master/tests/test_requests.py#L927 + payload = "query={test}" _, response = app.client.post( uri=url_string(), - data=data, - headers={"content-type": "application/x-www-form-urlencoded"}, + data=payload, + headers={"content-type": "application/x-www-form-urlencoded"} ) assert response.status == 200 assert response_json(response) == {"data": {"test": "Hello World"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_supports_post_json_query_with_string_variables(app): _, response = app.client.post( uri=url_string(), @@ -240,7 +224,7 @@ def test_supports_post_json_query_with_string_variables(app): assert response_json(response) == {"data": {"test": "Hello Dolly"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_supports_post_json_query_with_json_variables(app): _, response = app.client.post( uri=url_string(), @@ -255,7 +239,7 @@ def test_supports_post_json_query_with_json_variables(app): assert response_json(response) == {"data": {"test": "Hello Dolly"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_supports_post_url_encoded_query_with_string_variables(app): _, response = app.client.post( uri=url_string(), @@ -272,7 +256,7 @@ def test_supports_post_url_encoded_query_with_string_variables(app): assert response_json(response) == {"data": {"test": "Hello Dolly"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_supports_post_json_query_with_get_variable_values(app): _, response = app.client.post( uri=url_string(variables=json.dumps({"who": "Dolly"})), @@ -284,7 +268,7 @@ def test_supports_post_json_query_with_get_variable_values(app): assert response_json(response) == {"data": {"test": "Hello Dolly"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_post_url_encoded_query_with_get_variable_values(app): _, response = app.client.post( uri=url_string(variables=json.dumps({"who": "Dolly"})), @@ -296,7 +280,7 @@ def test_post_url_encoded_query_with_get_variable_values(app): assert response_json(response) == {"data": {"test": "Hello Dolly"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_supports_post_raw_text_query_with_get_variable_values(app): _, response = app.client.post( uri=url_string(variables=json.dumps({"who": "Dolly"})), @@ -308,7 +292,7 @@ def test_supports_post_raw_text_query_with_get_variable_values(app): assert response_json(response) == {"data": {"test": "Hello Dolly"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_allows_post_with_operation_name(app): _, response = app.client.post( uri=url_string(), @@ -332,7 +316,7 @@ def test_allows_post_with_operation_name(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_allows_post_with_get_operation_name(app): _, response = app.client.post( uri=url_string(operationName="helloWorld"), @@ -353,7 +337,7 @@ def test_allows_post_with_get_operation_name(app): } -@parametrize_sync_async_app_test("app", pretty=True) +@pytest.mark.parametrize("app", [create_app(pretty=True)]) def test_supports_pretty_printing(app): _, response = app.client.get(uri=url_string(query="{test}")) @@ -362,14 +346,14 @@ def test_supports_pretty_printing(app): ) -@parametrize_sync_async_app_test("app", pretty=False) +@pytest.mark.parametrize("app", [create_app(pretty=False)]) def test_not_pretty_by_default(app): _, response = app.client.get(url_string(query="{test}")) assert response.body.decode() == '{"data":{"test":"Hello World"}}' -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_supports_pretty_printing_by_request(app): _, response = app.client.get(uri=url_string(query="{test}", pretty="1")) @@ -378,11 +362,12 @@ def test_supports_pretty_printing_by_request(app): ) -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_handles_field_errors_caught_by_graphql(app): _, response = app.client.get(uri=url_string(query="{thrower}")) - assert response.status == 400 + assert response.status == 200 assert response_json(response) == { + "data": None, "errors": [ { "locations": [{"column": 2, "line": 1}], @@ -393,7 +378,7 @@ def test_handles_field_errors_caught_by_graphql(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_handles_syntax_errors_caught_by_graphql(app): _, response = app.client.get(uri=url_string(query="syntaxerror")) assert response.status == 400 @@ -408,7 +393,7 @@ def test_handles_syntax_errors_caught_by_graphql(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_handles_errors_caused_by_a_lack_of_query(app): _, response = app.client.get(uri=url_string()) @@ -424,7 +409,7 @@ def test_handles_errors_caused_by_a_lack_of_query(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_handles_batch_correctly_if_is_disabled(app): _, response = app.client.post( uri=url_string(), data="[]", headers={"content-type": "application/json"} @@ -442,7 +427,7 @@ def test_handles_batch_correctly_if_is_disabled(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_handles_incomplete_json_bodies(app): _, response = app.client.post( uri=url_string(), data='{"query":', headers={"content-type": "application/json"} @@ -460,7 +445,7 @@ def test_handles_incomplete_json_bodies(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_handles_plain_post_text(app): _, response = app.client.post( uri=url_string(variables=json.dumps({"who": "Dolly"})), @@ -479,7 +464,7 @@ def test_handles_plain_post_text(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_handles_poorly_formed_variables(app): _, response = app.client.get( uri=url_string( @@ -498,7 +483,7 @@ def test_handles_poorly_formed_variables(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_handles_unsupported_http_methods(app): _, response = app.client.put(uri=url_string(query="{test}")) assert response.status == 405 @@ -514,7 +499,7 @@ def test_handles_unsupported_http_methods(app): } -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_passes_request_into_request_context(app): _, response = app.client.get(uri=url_string(query="{request}", q="testing")) @@ -522,7 +507,7 @@ def test_passes_request_into_request_context(app): assert response_json(response) == {"data": {"request": "testing"}} -@parametrize_sync_async_app_test("app", context="CUSTOM CONTEXT") +@pytest.mark.parametrize("app", [create_app(context="CUSTOM CONTEXT")]) def test_supports_pretty_printing_on_custom_context_response(app): _, response = app.client.get(uri=url_string(query="{context}")) @@ -530,11 +515,11 @@ def test_supports_pretty_printing_on_custom_context_response(app): assert "data" in response_json(response) assert ( response_json(response)["data"]["context"] - == "{'request': }" + == "" ) -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_post_multipart_data(app): query = "mutation TestMutation { writeTest { test } }" @@ -564,7 +549,7 @@ def test_post_multipart_data(app): } -@parametrize_sync_async_app_test("app", batch=True) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_allows_post_with_json_encoding(app): _, response = app.client.post( uri=url_string(), @@ -576,7 +561,7 @@ def test_batch_allows_post_with_json_encoding(app): assert response_json(response) == [{"data": {"test": "Hello World"}}] -@parametrize_sync_async_app_test("app", batch=True) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_supports_post_json_query_with_json_variables(app): _, response = app.client.post( uri=url_string(), @@ -592,7 +577,7 @@ def test_batch_supports_post_json_query_with_json_variables(app): assert response_json(response) == [{"data": {"test": "Hello Dolly"}}] -@parametrize_sync_async_app_test("app", batch=True) +@pytest.mark.parametrize("app", [create_app(batch=True)]) def test_batch_allows_post_with_operation_name(app): _, response = app.client.post( uri=url_string(), @@ -617,7 +602,7 @@ def test_batch_allows_post_with_operation_name(app): ] -@pytest.mark.parametrize("app", [create_app(async_executor=True, schema=AsyncSchema)]) +@pytest.mark.parametrize("app", [create_app(schema=AsyncSchema, enable_async=True)]) def test_async_schema(app): query = "{a,b,c}" _, response = app.client.get(uri=url_string(query=query)) @@ -626,7 +611,7 @@ def test_async_schema(app): assert response_json(response) == {"data": {"a": "hey", "b": "hey2", "c": "hey3"}} -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_preflight_request(app): _, response = app.client.options( uri=url_string(), headers={"Access-Control-Request-Method": "POST"} @@ -635,7 +620,7 @@ def test_preflight_request(app): assert response.status == 200 -@parametrize_sync_async_app_test("app") +@pytest.mark.parametrize("app", [create_app()]) def test_preflight_incorrect_request(app): _, response = app.client.options( uri=url_string(), headers={"Access-Control-Request-Method": "OPTIONS"} From 2b658df58b8e5779b32a5318673ba79630b4d7e4 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Sun, 10 May 2020 16:48:51 -0500 Subject: [PATCH 3/3] styles: apply black and flake8 formatting --- graphql_server/sanic/graphqlview.py | 12 ++++++-- tests/sanic/app.py | 1 - tests/sanic/schema.py | 3 +- tests/sanic/test_graphiqlview.py | 7 +++-- tests/sanic/test_graphqlview.py | 43 ++++++++--------------------- 5 files changed, 27 insertions(+), 39 deletions(-) diff --git a/graphql_server/sanic/graphqlview.py b/graphql_server/sanic/graphqlview.py index 678992a..fd22af2 100644 --- a/graphql_server/sanic/graphqlview.py +++ b/graphql_server/sanic/graphqlview.py @@ -98,9 +98,13 @@ async def dispatch_request(self, request, *args, **kwargs): run_sync=not self.enable_async, root_value=self.get_root_value(), context_value=self.get_context(request), - middleware=self.get_middleware() + middleware=self.get_middleware(), + ) + exec_res = ( + [await ex for ex in execution_results] + if self.enable_async + else execution_results ) - exec_res = [await ex for ex in execution_results] if self.enable_async else execution_results result, status_code = encode_execution_results( exec_res, is_batch=isinstance(data, list), @@ -109,7 +113,9 @@ async def dispatch_request(self, request, *args, **kwargs): ) if show_graphiql: - return await self.render_graphiql(params=all_params[0], result=result) + return await self.render_graphiql( + params=all_params[0], result=result + ) return HTTPResponse( result, status=status_code, content_type="application/json" diff --git a/tests/sanic/app.py b/tests/sanic/app.py index 7ea9e8b..f5a74cf 100644 --- a/tests/sanic/app.py +++ b/tests/sanic/app.py @@ -1,6 +1,5 @@ from urllib.parse import urlencode -import pytest from sanic import Sanic from sanic.testing import SanicTestClient diff --git a/tests/sanic/schema.py b/tests/sanic/schema.py index a0be485..a129d92 100644 --- a/tests/sanic/schema.py +++ b/tests/sanic/schema.py @@ -24,7 +24,8 @@ def resolve_raises(*_): resolve=lambda obj, info: info.context["request"].args.get("q"), ), "context": GraphQLField( - GraphQLNonNull(GraphQLString), resolve=lambda obj, info: info.context["request"] + GraphQLNonNull(GraphQLString), + resolve=lambda obj, info: info.context["request"], ), "test": GraphQLField( type_=GraphQLString, diff --git a/tests/sanic/test_graphiqlview.py b/tests/sanic/test_graphiqlview.py index a4e190d..60ecc75 100644 --- a/tests/sanic/test_graphiqlview.py +++ b/tests/sanic/test_graphiqlview.py @@ -42,7 +42,9 @@ def test_graphiql_jinja_renderer(app, pretty_response): assert pretty_response in response.body.decode("utf-8") -@pytest.mark.parametrize("app", [create_app(graphiql=True, jinja_env=Environment(enable_async=True))]) +@pytest.mark.parametrize( + "app", [create_app(graphiql=True, jinja_env=Environment(enable_async=True))] +) def test_graphiql_jinja_async_renderer(app, pretty_response): _, response = app.client.get( uri=url_string(query="{test}"), headers={"Accept": "text/html"} @@ -65,8 +67,7 @@ def test_graphiql_html_is_not_accepted(app): def test_graphiql_asyncio_schema(app): query = "{a,b,c}" _, response = app.client.get( - uri=url_string(query=query), - headers={"Accept": "text/html"} + uri=url_string(query=query), headers={"Accept": "text/html"} ) expected_response = ( diff --git a/tests/sanic/test_graphqlview.py b/tests/sanic/test_graphqlview.py index 5ff2cb5..7325e6d 100644 --- a/tests/sanic/test_graphqlview.py +++ b/tests/sanic/test_graphqlview.py @@ -102,7 +102,7 @@ def test_errors_when_missing_operation_name(app): { "locations": None, "message": "Must provide operation name if query contains multiple operations.", - "path": None + "path": None, } ] } @@ -123,7 +123,7 @@ def test_errors_when_sending_a_mutation_via_get(app): { "locations": None, "message": "Can only perform a mutation operation from a POST request.", - "path": None + "path": None, } ] } @@ -147,7 +147,7 @@ def test_errors_when_selecting_a_mutation_within_a_get(app): { "locations": None, "message": "Can only perform a mutation operation from a POST request.", - "path": None + "path": None, } ] } @@ -202,7 +202,7 @@ def test_allows_post_with_url_encoding(app): _, response = app.client.post( uri=url_string(), data=payload, - headers={"content-type": "application/x-www-form-urlencoded"} + headers={"content-type": "application/x-www-form-urlencoded"}, ) assert response.status == 200 @@ -387,7 +387,7 @@ def test_handles_syntax_errors_caught_by_graphql(app): { "locations": [{"column": 1, "line": 1}], "message": "Syntax Error: Unexpected Name 'syntaxerror'.", - "path": None + "path": None, } ] } @@ -400,11 +400,7 @@ def test_handles_errors_caused_by_a_lack_of_query(app): assert response.status == 400 assert response_json(response) == { "errors": [ - { - "locations": None, - "message": "Must provide query string.", - "path": None - } + {"locations": None, "message": "Must provide query string.", "path": None} ] } @@ -421,7 +417,7 @@ def test_handles_batch_correctly_if_is_disabled(app): { "locations": None, "message": "Batch GraphQL requests are not enabled.", - "path": None + "path": None, } ] } @@ -436,11 +432,7 @@ def test_handles_incomplete_json_bodies(app): assert response.status == 400 assert response_json(response) == { "errors": [ - { - "locations": None, - "message": "POST body sent invalid JSON.", - "path": None - } + {"locations": None, "message": "POST body sent invalid JSON.", "path": None} ] } @@ -455,11 +447,7 @@ def test_handles_plain_post_text(app): assert response.status == 400 assert response_json(response) == { "errors": [ - { - "locations": None, - "message": "Must provide query string.", - "path": None - } + {"locations": None, "message": "Must provide query string.", "path": None} ] } @@ -474,11 +462,7 @@ def test_handles_poorly_formed_variables(app): assert response.status == 400 assert response_json(response) == { "errors": [ - { - "locations": None, - "message": "Variables are invalid JSON.", - "path": None - } + {"locations": None, "message": "Variables are invalid JSON.", "path": None} ] } @@ -493,7 +477,7 @@ def test_handles_unsupported_http_methods(app): { "locations": None, "message": "GraphQL only supports GET and POST requests.", - "path": None + "path": None, } ] } @@ -513,10 +497,7 @@ def test_supports_pretty_printing_on_custom_context_response(app): assert response.status == 200 assert "data" in response_json(response) - assert ( - response_json(response)["data"]["context"] - == "" - ) + assert response_json(response)["data"]["context"] == "" @pytest.mark.parametrize("app", [create_app()])