diff --git a/.gitignore b/.gitignore
index 608847c..1789e38 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,19 +1,203 @@
-*.pyc
-*.pyo
+# Created by https://www.gitignore.io/api/python,intellij+all,visualstudiocode
+# Edit at https://www.gitignore.io/?templates=python,intellij+all,visualstudiocode
+
+### Intellij+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij+all Patch ###
+# Ignores the whole .idea folder and all .iml files
+# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
+
+.idea/
+
+# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
+
+*.iml
+modules.xml
+.idea/misc.xml
+*.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
*.egg
-*.egg-info
+MANIFEST
-.cache
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
.coverage
-.idea
-.mypy_cache
-.pytest_cache
-.tox
-.venv
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+### VisualStudioCode ###
.vscode
-/build/
-/dist/
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
-docs
+# End of https://www.gitignore.io/api/python,intellij+all,visualstudiocode
diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py
index 29efffa..c4685c0 100644
--- a/graphql_server/__init__.py
+++ b/graphql_server/__init__.py
@@ -30,6 +30,7 @@
"GraphQLResponse",
"ServerResponse",
"format_execution_result",
+ "format_error_default",
]
@@ -230,11 +231,11 @@ def get_response(
as a parameter.
"""
- if not params.query:
- raise HttpQueryError(400, "Must provide query string.")
-
# noinspection PyBroadException
try:
+ if not params.query:
+ raise HttpQueryError(400, "Must provide query string.")
+
# Parse document to trigger a new HttpQueryError if allow_only_query is True
try:
document = parse(params.query)
diff --git a/graphql_server/flask/__init__.py b/graphql_server/flask/__init__.py
new file mode 100644
index 0000000..8f5beaf
--- /dev/null
+++ b/graphql_server/flask/__init__.py
@@ -0,0 +1,3 @@
+from .graphqlview import GraphQLView
+
+__all__ = ["GraphQLView"]
diff --git a/graphql_server/flask/graphqlview.py b/graphql_server/flask/graphqlview.py
new file mode 100644
index 0000000..d1d971a
--- /dev/null
+++ b/graphql_server/flask/graphqlview.py
@@ -0,0 +1,151 @@
+from functools import partial
+
+from flask import Response, request
+from flask.views import View
+from graphql.error import GraphQLError
+from graphql.type.schema import GraphQLSchema
+
+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(View):
+ schema = None
+ executor = None
+ root_value = None
+ pretty = False
+ graphiql = False
+ graphiql_version = None
+ graphiql_template = None
+ graphiql_html_title = None
+ middleware = None
+ batch = False
+
+ 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."
+
+ # noinspection PyUnusedLocal
+ def get_root_value(self):
+ return self.root_value
+
+ def get_context_value(self):
+ return request
+
+ def get_middleware(self):
+ return self.middleware
+
+ def get_executor(self):
+ return self.executor
+
+ def render_graphiql(self, params, result):
+ return render_graphiql(
+ params=params,
+ result=result,
+ graphiql_version=self.graphiql_version,
+ graphiql_template=self.graphiql_template,
+ graphiql_html_title=self.graphiql_html_title,
+ )
+
+ format_error = staticmethod(format_error_default)
+ encode = staticmethod(json_encode)
+
+ def dispatch_request(self):
+ try:
+ request_method = request.method.lower()
+ data = self.parse_body()
+
+ show_graphiql = request_method == "get" and self.should_display_graphiql()
+ 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
+
+ execution_results, all_params = run_http_query(
+ self.schema,
+ request_method,
+ data,
+ query_data=request.args,
+ batch_enabled=self.batch,
+ catch=catch,
+ # Execute options
+ root_value=self.get_root_value(),
+ context_value=self.get_context_value(),
+ 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),
+ )
+
+ if show_graphiql:
+ return self.render_graphiql(params=all_params[0], result=result)
+
+ return Response(result, status=status_code, content_type="application/json")
+
+ except HttpQueryError as e:
+ parsed_error = GraphQLError(e.message)
+ return Response(
+ self.encode(dict(errors=[self.format_error(parsed_error)])),
+ status=e.status_code,
+ headers=e.headers,
+ content_type="application/json",
+ )
+
+ # Flask
+ def parse_body(self):
+ # We use mimetype here since we don't need the other
+ # information provided by content_type
+ content_type = request.mimetype
+ if content_type == "application/graphql":
+ return {"query": request.data.decode("utf8")}
+
+ elif content_type == "application/json":
+ return load_json_body(request.data.decode("utf8"))
+
+ elif content_type in (
+ "application/x-www-form-urlencoded",
+ "multipart/form-data",
+ ):
+ return request.form
+
+ return {}
+
+ def should_display_graphiql(self):
+ if not self.graphiql or "raw" in request.args:
+ return False
+
+ return self.request_wants_html()
+
+ def request_wants_html(self):
+ best = request.accept_mimetypes.best_match(["application/json", "text/html"])
+ return (
+ best == "text/html"
+ and request.accept_mimetypes[best]
+ > request.accept_mimetypes["application/json"]
+ )
diff --git a/graphql_server/flask/render_graphiql.py b/graphql_server/flask/render_graphiql.py
new file mode 100644
index 0000000..d395d44
--- /dev/null
+++ b/graphql_server/flask/render_graphiql.py
@@ -0,0 +1,148 @@
+from flask import render_template_string
+
+GRAPHIQL_VERSION = "0.11.11"
+
+TEMPLATE = """
+
+
+
+ {{graphiql_html_title|default("GraphiQL", true)}}
+
+
+
+
+
+
+
+
+
+
+
+"""
+
+
+def render_graphiql(
+ params,
+ result,
+ graphiql_version=None,
+ graphiql_template=None,
+ graphiql_html_title=None,
+):
+ graphiql_version = graphiql_version or GRAPHIQL_VERSION
+ template = graphiql_template or TEMPLATE
+
+ return render_template_string(
+ template,
+ graphiql_version=graphiql_version,
+ graphiql_html_title=graphiql_html_title,
+ result=result,
+ params=params,
+ )
diff --git a/setup.py b/setup.py
index 2bedab1..d8568a9 100644
--- a/setup.py
+++ b/setup.py
@@ -17,6 +17,12 @@
"check-manifest>=0.40,<1",
] + tests_requires
+install_flask_requires = [
+ "flask>=0.7.0",
+]
+
+install_all_requires = install_requires + install_flask_requires
+
setup(
name="graphql-server-core",
version="2.0.0",
@@ -40,10 +46,12 @@
keywords="api graphql protocol rest",
packages=find_packages(exclude=["tests"]),
install_requires=install_requires,
- tests_require=tests_requires,
+ tests_require=install_all_requires + tests_requires,
extras_require={
- 'test': tests_requires,
- 'dev': dev_requires,
+ "all": install_all_requires,
+ "test": install_all_requires + tests_requires,
+ "dev": dev_requires,
+ "flask": install_flask_requires,
},
include_package_data=True,
zip_safe=False,
diff --git a/tests/flask/__init__.py b/tests/flask/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/flask/app.py b/tests/flask/app.py
new file mode 100644
index 0000000..01f6fa8
--- /dev/null
+++ b/tests/flask/app.py
@@ -0,0 +1,18 @@
+from flask import Flask
+
+from graphql_server.flask import GraphQLView
+from tests.flask.schema import Schema
+
+
+def create_app(path="/graphql", **kwargs):
+ app = Flask(__name__)
+ app.debug = True
+ app.add_url_rule(
+ path, view_func=GraphQLView.as_view("graphql", schema=Schema, **kwargs)
+ )
+ return app
+
+
+if __name__ == "__main__":
+ app = create_app(graphiql=True)
+ app.run()
diff --git a/tests/flask/schema.py b/tests/flask/schema.py
new file mode 100644
index 0000000..5d4c52c
--- /dev/null
+++ b/tests/flask/schema.py
@@ -0,0 +1,41 @@
+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!")
+
+
+QueryRootType = GraphQLObjectType(
+ name="QueryRoot",
+ fields={
+ "thrower": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_raises),
+ "request": GraphQLField(
+ GraphQLNonNull(GraphQLString),
+ resolve=lambda obj, info: info.context.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="World": "Hello %s" % who,
+ ),
+ },
+)
+
+MutationRootType = GraphQLObjectType(
+ name="MutationRoot",
+ fields={
+ "writeTest": GraphQLField(type_=QueryRootType, resolve=lambda *_: QueryRootType)
+ },
+)
+
+Schema = GraphQLSchema(QueryRootType, MutationRootType)
diff --git a/tests/flask/test_graphiqlview.py b/tests/flask/test_graphiqlview.py
new file mode 100644
index 0000000..4a55710
--- /dev/null
+++ b/tests/flask/test_graphiqlview.py
@@ -0,0 +1,60 @@
+import pytest
+from flask import url_for
+
+from .app import create_app
+
+
+@pytest.fixture
+def app():
+ # import app factory pattern
+ app = create_app(graphiql=True)
+
+ # pushes an application context manually
+ ctx = app.app_context()
+ ctx.push()
+ return app
+
+
+@pytest.fixture
+def client(app):
+ return app.test_client()
+
+
+def test_graphiql_is_enabled(app, client):
+ with app.test_request_context():
+ response = client.get(
+ url_for("graphql", externals=False), headers={"Accept": "text/html"}
+ )
+ assert response.status_code == 200
+
+
+def test_graphiql_renders_pretty(app, client):
+ with app.test_request_context():
+ response = client.get(
+ url_for("graphql", query="{test}"), headers={"Accept": "text/html"}
+ )
+ assert response.status_code == 200
+ pretty_response = (
+ "{\n"
+ ' "data": {\n'
+ ' "test": "Hello World"\n'
+ " }\n"
+ "}".replace('"', '\\"').replace("\n", "\\n")
+ )
+
+ assert pretty_response in response.data.decode("utf-8")
+
+
+def test_graphiql_default_title(app, client):
+ with app.test_request_context():
+ response = client.get(url_for("graphql"), headers={"Accept": "text/html"})
+ assert "GraphiQL" in response.data.decode("utf-8")
+
+
+@pytest.mark.parametrize(
+ "app", [create_app(graphiql=True, graphiql_html_title="Awesome")]
+)
+def test_graphiql_custom_title(app, client):
+ with app.test_request_context():
+ response = client.get(url_for("graphql"), headers={"Accept": "text/html"})
+ assert "Awesome" in response.data.decode("utf-8")
diff --git a/tests/flask/test_graphqlview.py b/tests/flask/test_graphqlview.py
new file mode 100644
index 0000000..0f65072
--- /dev/null
+++ b/tests/flask/test_graphqlview.py
@@ -0,0 +1,581 @@
+import json
+from io import StringIO
+from urllib.parse import urlencode
+
+import pytest
+from flask import url_for
+
+from .app import create_app
+
+
+@pytest.fixture
+def app(request):
+ # import app factory pattern
+ app = create_app()
+
+ # pushes an application context manually
+ ctx = app.app_context()
+ ctx.push()
+ return app
+
+
+@pytest.fixture
+def client(app):
+ return app.test_client()
+
+
+def url_string(app, **url_params):
+ with app.test_request_context():
+ string = url_for("graphql")
+
+ if url_params:
+ string += "?" + urlencode(url_params)
+
+ return string
+
+
+def response_json(response):
+ return json.loads(response.data.decode())
+
+
+def json_dump_kwarg(**kwargs):
+ return json.dumps(kwargs)
+
+
+def json_dump_kwarg_list(**kwargs):
+ return json.dumps([kwargs])
+
+
+def test_allows_get_with_query_param(app, client):
+ response = client.get(url_string(app, query="{test}"))
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello World"}}
+
+
+def test_allows_get_with_variable_values(app, client):
+ response = client.get(
+ url_string(
+ app,
+ query="query helloWho($who: String){ test(who: $who) }",
+ variables=json.dumps({"who": "Dolly"}),
+ )
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello Dolly"}}
+
+
+def test_allows_get_with_operation_name(app, client):
+ response = client.get(
+ url_string(
+ app,
+ 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_code == 200
+ assert response_json(response) == {
+ "data": {"test": "Hello World", "shared": "Hello Everyone"}
+ }
+
+
+def test_reports_validation_errors(app, client):
+ response = client.get(url_string(app, query="{ test, unknownOne, unknownTwo }"))
+
+ assert response.status_code == 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,
+ },
+ ]
+ }
+
+
+def test_errors_when_missing_operation_name(app, client):
+ response = client.get(
+ url_string(
+ app,
+ query="""
+ query TestQuery { test }
+ mutation TestMutation { writeTest { test } }
+ """,
+ )
+ )
+
+ assert response.status_code == 400
+ assert response_json(response) == {
+ "errors": [
+ {
+ "message": "Must provide operation name if query contains multiple operations.", # noqa: E501
+ "locations": None,
+ "path": None,
+ }
+ ]
+ }
+
+
+def test_errors_when_sending_a_mutation_via_get(app, client):
+ response = client.get(
+ url_string(
+ app,
+ query="""
+ mutation TestMutation { writeTest { test } }
+ """,
+ )
+ )
+ assert response.status_code == 405
+ assert response_json(response) == {
+ "errors": [
+ {
+ "message": "Can only perform a mutation operation from a POST request.",
+ "locations": None,
+ "path": None,
+ }
+ ]
+ }
+
+
+def test_errors_when_selecting_a_mutation_within_a_get(app, client):
+ response = client.get(
+ url_string(
+ app,
+ query="""
+ query TestQuery { test }
+ mutation TestMutation { writeTest { test } }
+ """,
+ operationName="TestMutation",
+ )
+ )
+
+ assert response.status_code == 405
+ assert response_json(response) == {
+ "errors": [
+ {
+ "message": "Can only perform a mutation operation from a POST request.",
+ "locations": None,
+ "path": None,
+ }
+ ]
+ }
+
+
+def test_allows_mutation_to_exist_within_a_get(app, client):
+ response = client.get(
+ url_string(
+ app,
+ query="""
+ query TestQuery { test }
+ mutation TestMutation { writeTest { test } }
+ """,
+ operationName="TestQuery",
+ )
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello World"}}
+
+
+def test_allows_post_with_json_encoding(app, client):
+ response = client.post(
+ url_string(app),
+ data=json_dump_kwarg(query="{test}"),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello World"}}
+
+
+def test_allows_sending_a_mutation_via_post(app, client):
+ response = client.post(
+ url_string(app),
+ data=json_dump_kwarg(query="mutation TestMutation { writeTest { test } }"),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}}
+
+
+def test_allows_post_with_url_encoding(app, client):
+ response = client.post(
+ url_string(app),
+ data=urlencode(dict(query="{test}")),
+ content_type="application/x-www-form-urlencoded",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello World"}}
+
+
+def test_supports_post_json_query_with_string_variables(app, client):
+ response = client.post(
+ url_string(app),
+ data=json_dump_kwarg(
+ query="query helloWho($who: String){ test(who: $who) }",
+ variables=json.dumps({"who": "Dolly"}),
+ ),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello Dolly"}}
+
+
+def test_supports_post_json_query_with_json_variables(app, client):
+ response = client.post(
+ url_string(app),
+ data=json_dump_kwarg(
+ query="query helloWho($who: String){ test(who: $who) }",
+ variables={"who": "Dolly"},
+ ),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello Dolly"}}
+
+
+def test_supports_post_url_encoded_query_with_string_variables(app, client):
+ response = client.post(
+ url_string(app),
+ data=urlencode(
+ dict(
+ query="query helloWho($who: String){ test(who: $who) }",
+ variables=json.dumps({"who": "Dolly"}),
+ )
+ ),
+ content_type="application/x-www-form-urlencoded",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello Dolly"}}
+
+
+def test_supports_post_json_quey_with_get_variable_values(app, client):
+ response = client.post(
+ url_string(app, variables=json.dumps({"who": "Dolly"})),
+ data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello Dolly"}}
+
+
+def test_post_url_encoded_query_with_get_variable_values(app, client):
+ response = client.post(
+ url_string(app, variables=json.dumps({"who": "Dolly"})),
+ data=urlencode(dict(query="query helloWho($who: String){ test(who: $who) }",)),
+ content_type="application/x-www-form-urlencoded",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello Dolly"}}
+
+
+def test_supports_post_raw_text_query_with_get_variable_values(app, client):
+ response = client.post(
+ url_string(app, variables=json.dumps({"who": "Dolly"})),
+ data="query helloWho($who: String){ test(who: $who) }",
+ content_type="application/graphql",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"test": "Hello Dolly"}}
+
+
+def test_allows_post_with_operation_name(app, client):
+ response = client.post(
+ url_string(app),
+ 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",
+ ),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {
+ "data": {"test": "Hello World", "shared": "Hello Everyone"}
+ }
+
+
+def test_allows_post_with_get_operation_name(app, client):
+ response = client.post(
+ url_string(app, 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")
+ }
+ """,
+ content_type="application/graphql",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {
+ "data": {"test": "Hello World", "shared": "Hello Everyone"}
+ }
+
+
+@pytest.mark.parametrize("app", [create_app(pretty=True)])
+def test_supports_pretty_printing(app, client):
+ response = client.get(url_string(app, query="{test}"))
+
+ assert response.data.decode() == (
+ "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}"
+ )
+
+
+@pytest.mark.parametrize("app", [create_app(pretty=False)])
+def test_not_pretty_by_default(app, client):
+ response = client.get(url_string(app, query="{test}"))
+
+ assert response.data.decode() == '{"data":{"test":"Hello World"}}'
+
+
+def test_supports_pretty_printing_by_request(app, client):
+ response = client.get(url_string(app, query="{test}", pretty="1"))
+
+ assert response.data.decode() == (
+ "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}"
+ )
+
+
+def test_handles_field_errors_caught_by_graphql(app, client):
+ response = client.get(url_string(app, query="{thrower}"))
+ assert response.status_code == 400
+ assert response_json(response) == {
+ "errors": [
+ {
+ "locations": [{"column": 2, "line": 1}],
+ "path": ["thrower"],
+ "message": "Throws!",
+ }
+ ]
+ }
+
+
+def test_handles_syntax_errors_caught_by_graphql(app, client):
+ response = client.get(url_string(app, query="syntaxerror"))
+ assert response.status_code == 400
+ assert response_json(response) == {
+ "errors": [
+ {
+ "locations": [{"column": 1, "line": 1}],
+ "message": "Syntax Error: Unexpected Name 'syntaxerror'.",
+ "path": None,
+ }
+ ]
+ }
+
+
+def test_handles_errors_caused_by_a_lack_of_query(app, client):
+ response = client.get(url_string(app))
+
+ assert response.status_code == 400
+ assert response_json(response) == {
+ "errors": [
+ {"message": "Must provide query string.", "locations": None, "path": None}
+ ]
+ }
+
+
+def test_handles_batch_correctly_if_is_disabled(app, client):
+ response = client.post(url_string(app), data="[]", content_type="application/json")
+
+ assert response.status_code == 400
+ assert response_json(response) == {
+ "errors": [
+ {
+ "message": "Batch GraphQL requests are not enabled.",
+ "locations": None,
+ "path": None,
+ }
+ ]
+ }
+
+
+def test_handles_incomplete_json_bodies(app, client):
+ response = client.post(
+ url_string(app), data='{"query":', content_type="application/json"
+ )
+
+ assert response.status_code == 400
+ assert response_json(response) == {
+ "errors": [
+ {"message": "POST body sent invalid JSON.", "locations": None, "path": None}
+ ]
+ }
+
+
+def test_handles_plain_post_text(app, client):
+ response = client.post(
+ url_string(app, variables=json.dumps({"who": "Dolly"})),
+ data="query helloWho($who: String){ test(who: $who) }",
+ content_type="text/plain",
+ )
+ assert response.status_code == 400
+ assert response_json(response) == {
+ "errors": [
+ {"message": "Must provide query string.", "locations": None, "path": None}
+ ]
+ }
+
+
+def test_handles_poorly_formed_variables(app, client):
+ response = client.get(
+ url_string(
+ app,
+ query="query helloWho($who: String){ test(who: $who) }",
+ variables="who:You",
+ )
+ )
+ assert response.status_code == 400
+ assert response_json(response) == {
+ "errors": [
+ {"message": "Variables are invalid JSON.", "locations": None, "path": None}
+ ]
+ }
+
+
+def test_handles_unsupported_http_methods(app, client):
+ response = client.put(url_string(app, query="{test}"))
+ assert response.status_code == 405
+ assert response.headers["Allow"] in ["GET, POST", "HEAD, GET, POST, OPTIONS"]
+ assert response_json(response) == {
+ "errors": [
+ {
+ "message": "GraphQL only supports GET and POST requests.",
+ "locations": None,
+ "path": None,
+ }
+ ]
+ }
+
+
+def test_passes_request_into_request_context(app, client):
+ response = client.get(url_string(app, query="{request}", q="testing"))
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"request": "testing"}}
+
+
+@pytest.mark.parametrize(
+ "app", [create_app(get_context_value=lambda: "CUSTOM CONTEXT")]
+)
+def test_passes_custom_context_into_context(app, client):
+ response = client.get(url_string(app, query="{context}"))
+
+ assert response.status_code == 200
+ assert response_json(response) == {"data": {"context": "CUSTOM CONTEXT"}}
+
+
+def test_post_multipart_data(app, client):
+ query = "mutation TestMutation { writeTest { test } }"
+ response = client.post(
+ url_string(app),
+ data={"query": query, "file": (StringIO(), "text1.txt")},
+ content_type="multipart/form-data",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == {
+ "data": {u"writeTest": {u"test": u"Hello World"}}
+ }
+
+
+@pytest.mark.parametrize("app", [create_app(batch=True)])
+def test_batch_allows_post_with_json_encoding(app, client):
+ response = client.post(
+ url_string(app),
+ data=json_dump_kwarg_list(
+ # id=1,
+ query="{test}"
+ ),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == [
+ {
+ # 'id': 1,
+ "data": {"test": "Hello World"}
+ }
+ ]
+
+
+@pytest.mark.parametrize("app", [create_app(batch=True)])
+def test_batch_supports_post_json_query_with_json_variables(app, client):
+ response = client.post(
+ url_string(app),
+ data=json_dump_kwarg_list(
+ # id=1,
+ query="query helloWho($who: String){ test(who: $who) }",
+ variables={"who": "Dolly"},
+ ),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == [
+ {
+ # 'id': 1,
+ "data": {"test": "Hello Dolly"}
+ }
+ ]
+
+
+@pytest.mark.parametrize("app", [create_app(batch=True)])
+def test_batch_allows_post_with_operation_name(app, client):
+ response = client.post(
+ url_string(app),
+ 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",
+ ),
+ content_type="application/json",
+ )
+
+ assert response.status_code == 200
+ assert response_json(response) == [
+ {
+ # 'id': 1,
+ "data": {"test": "Hello World", "shared": "Hello Everyone"}
+ }
+ ]