diff --git a/.gitignore b/.gitignore index 05a5a88..2452aab 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ /build/ /dist/ + +docs diff --git a/.travis.yml b/.travis.yml index 1053da9..ba0f007 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,22 @@ language: python -sudo: false matrix: include: - - python: pypy - env: TOX_ENV=pypy - - python: '2.7' - env: TOX_ENV=py27 - - python: '3.4' - env: TOX_ENV=py34 - - python: '3.5' - env: TOX_ENV=py35 - - python: '3.6' - env: TOX_ENV=py36 - python: '3.7' - env: TOX_ENV=py37,import-order,flake8,mypy + env: TOX_ENV=black,flake8,mypy,py37 dist: xenial + sudo: true # required workaround for https://github.com/travis-ci/travis-ci/issues/9815 + - python: '3.6' + env: TOX_ENV=py36 + - python: '3.5' + env: TOX_ENV=py35 + - python: '3.4' + env: TOX_ENV=py34 + - python: '2.7' + env: TOX_ENV=py27 + - python: 'pypy3.5' + env: TOX_ENV=pypy3 + - python: 'pypy' + env: TOX_ENV=pypy cache: directories: - $HOME/.cache/pip diff --git a/README.md b/README.md index 4c86a5e..8a7f93c 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,13 @@ for building GraphQL servers or integrations into existing web frameworks using ## Documentation -The `graphql_server` package provides these three public helper functions: +The `graphql_server` package provides these public helper functions: * `run_http_query` * `encode_execution_results` * `laod_json_body` + * `json_encode` + * `json_encode_pretty` All functions in the package are annotated with type hints and docstrings, and you can build HTML documentation from these using `bin/build_docs`. diff --git a/README.rst b/README.rst index a9afadf..d8f2633 100644 --- a/README.rst +++ b/README.rst @@ -33,12 +33,13 @@ Django `graphene-django = 3.3) + from collections.abc import MutableMapping +except ImportError: # pragma: no cover (Python < 3.3) + # noinspection PyUnresolvedReferences,PyProtectedMember + from collections import MutableMapping + # Necessary for static type checking +# noinspection PyUnreachableCode if False: # flake8: noqa - from typing import List, Dict, Optional, Tuple, Any, Union, Callable, Type - from graphql import GraphQLSchema, GraphQLBackend + from typing import Any, Callable, Dict, List, Optional, Type, Union + from graphql import GraphQLBackend __all__ = [ "run_http_query", "encode_execution_results", "load_json_body", - "HttpQueryError"] + "json_encode", + "json_encode_pretty", + "HttpQueryError", + "RequestParams", + "ServerResults", + "ServerResponse", +] + + +# The public data structures + +RequestParams = namedtuple("RequestParams", "query variables operation_name") + +ServerResults = namedtuple("ServerResults", "results params") + +ServerResponse = namedtuple("ServerResponse", "body status_code") # The public helper functions @@ -41,7 +64,7 @@ def run_http_query( query_data=None, # type: Optional[Dict] batch_enabled=False, # type: bool catch=False, # type: bool - **execute_options # type: Dict + **execute_options # type: Any ): """Execute GraphQL coming from an HTTP query against a given schema. @@ -55,9 +78,11 @@ def run_http_query( All other keyword arguments are passed on to the GraphQL-Core function for executing GraphQL queries. - This functions returns a tuple with the list of ExecutionResults as first item + Returns a ServerResults tuple with the list of ExecutionResults as first item and the list of parameters that have been used for execution as second item. """ + if not isinstance(schema, GraphQLSchema): + raise TypeError("Expected a GraphQL schema, but received {!r}.".format(schema)) if request_method not in ("get", "post"): raise HttpQueryError( 405, @@ -78,7 +103,7 @@ def run_http_query( if not is_batch: if not isinstance(data, (dict, MutableMapping)): raise HttpQueryError( - 400, "GraphQL params should be a dict. Received {}.".format(data) + 400, "GraphQL params should be a dict. Received {!r}.".format(data) ) data = [data] elif not batch_enabled: @@ -94,12 +119,12 @@ def run_http_query( all_params = [get_graphql_params(entry, extra_data) for entry in data] - responses = [ + results = [ get_response(schema, params, catch_exc, allow_only_query, **execute_options) for params in all_params ] - return responses, all_params + return ServerResults(results, all_params) def encode_execution_results( @@ -108,7 +133,7 @@ def encode_execution_results( is_batch=False, # type: bool encode=None, # type: Callable[[Dict], Any] ): - # type: (...) -> Tuple[Any, int] + # type: (...) -> ServerResponse """Serialize the ExecutionResults. This function takes the ExecutionResults that are returned by run_http_query() @@ -117,18 +142,21 @@ def encode_execution_results( the first one will be used. You can also pass a custom function that formats the errors in the ExecutionResults, expecting a dictionary as result and another custom function that is used to serialize the output. + + Returns a ServerResponse tuple with the serialized response as the first item and + a status code of 200 or 400 in case any result was invalid as the second item. """ - responses = [ + results = [ format_execution_result(execution_result, format_error or default_format_error) for execution_result in execution_results ] - result, status_codes = zip(*responses) + result, status_codes = zip(*results) status_code = max(status_codes) if not is_batch: result = result[0] - return (encode or json_encode)(result), status_code + return ServerResponse((encode or json_encode)(result), status_code) def load_json_body(data): @@ -144,11 +172,25 @@ def load_json_body(data): raise HttpQueryError(400, "POST body sent invalid JSON.") -# Some more private helpers +def json_encode(data): + # type: (Union[Dict,List]) -> str + """Serialize the given data(a dictionary or a list) using JSON. + + The given data (a dictionary or a list) will be serialized using JSON + and returned as a string that will be nicely formatted if you set pretty=True. + """ + return json.dumps(data, separators=(",", ":")) + -GraphQLParams = namedtuple("GraphQLParams", "query variables operation_name") +def json_encode_pretty(data): + # type: (Union[Dict,List]) -> str + """Serialize the given data using JSON with nice formatting.""" + return json.dumps(data, indent=2, separators=(",", ": ")) + + +# Some more private helpers -GraphQLResponse = namedtuple("GraphQLResponse", "result status_code") +FormattedResult = namedtuple("FormattedResult", "result status_code") class _NoException(Exception): @@ -156,20 +198,20 @@ class _NoException(Exception): def get_graphql_params(data, query_data): - # type: (Dict, Dict) -> GraphQLParams + # type: (Dict, Dict) -> RequestParams """Fetch GraphQL query, variables and operation name parameters from given data. You need to pass both the data from the HTTP request body and the HTTP query string. Params from the request body will take precedence over those from the query string. - You will get a GraphQLParams object with these parameters as attributes in return. + You will get a RequestParams tuple with these parameters in return. """ query = data.get("query") or query_data.get("query") variables = data.get("variables") or query_data.get("variables") # document_id = data.get('documentId') operation_name = data.get("operationName") or query_data.get("operationName") - return GraphQLParams(query, load_json_variables(variables), operation_name) + return RequestParams(query, load_json_variables(variables), operation_name) def load_json_variables(variables): @@ -190,12 +232,12 @@ def load_json_variables(variables): def execute_graphql_request( schema, # type: GraphQLSchema - params, # type: GraphQLParams + params, # type: RequestParams allow_only_query=False, # type: bool backend=None, # type: GraphQLBackend - **kwargs # type: Dict + **kwargs # type: Any ): - """Execute a GraphQL request and return a ExecutionResult. + """Execute a GraphQL request and return an ExecutionResult. You need to pass the GraphQL schema and the GraphQLParams that you can get with the get_graphql_params() function. If you only want to allow GraphQL query @@ -235,10 +277,10 @@ def execute_graphql_request( def get_response( schema, # type: GraphQLSchema - params, # type: GraphQLParams + params, # type: RequestParams catch_exc, # type: Type[BaseException] allow_only_query=False, # type: bool - **kwargs # type: Dict + **kwargs # type: Any ): # type: (...) -> Optional[ExecutionResult] """Get an individual execution result as response, with option to catch errors. @@ -246,7 +288,7 @@ def get_response( This does the same as execute_graphql_request() except that you can catch errors that belong to an exception class that you need to pass as a parameter. """ - + # noinspection PyBroadException try: execution_result = execute_graphql_request( schema, params, allow_only_query, **kwargs @@ -261,10 +303,10 @@ def format_execution_result( execution_result, # type: Optional[ExecutionResult] format_error, # type: Optional[Callable[[Exception], Dict]] ): - # type: (...) -> GraphQLResponse + # type: (...) -> FormattedResult """Format an execution result into a GraphQLResponse. - This converts the given execution result into a GraphQLResponse that contains + This converts the given execution result into a FormattedResult that contains the ExecutionResult converted to a dictionary and an appropriate status code. """ status_code = 200 @@ -276,17 +318,4 @@ def format_execution_result( else: response = None - return GraphQLResponse(response, status_code) - - -def json_encode(data, pretty=False): - # type: (Union[Dict,List], bool) -> str - """Serialize the given data using JSON. - - The given data (a dictionary or a list) will be serialized using JSON - and returned as a string that will be nicely formatted if you set pretty=True. - """ - if not pretty: - return json.dumps(data, separators=(",", ":")) - - return json.dumps(data, indent=2, separators=(",", ": ")) + return FormattedResult(response, status_code) diff --git a/setup.py b/setup.py index 2ca3baa..64892dc 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -required_packages = ["graphql-core>=2.1", "promise"] +required_packages = ["graphql-core>=2.1,<3", "promise"] setup( name="graphql-server-core", @@ -30,7 +30,7 @@ keywords="api graphql protocol rest", packages=find_packages(exclude=["tests"]), install_requires=required_packages, - tests_require=["pytest>=2.7.3"], + tests_require=["pytest>=3.0"], include_package_data=True, zip_safe=False, platforms="any", diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..2a8fe60 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""GraphQL-Server-Core Tests""" diff --git a/tests/schema.py b/tests/schema.py index 7ea803f..c60b0ed 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -1,38 +1,52 @@ -from graphql.type.definition import GraphQLArgument, GraphQLField, GraphQLNonNull, GraphQLObjectType +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!") +def resolve_error(*_args): + raise ValueError("Throws!") + + +def resolve_request(_obj, info): + return info.context.get("q") + + +def resolve_context(_obj, info): + return str(info.context) +def resolve_test(_obj, _info, who="World"): + return "Hello {}".format(who) + + +NonNullString = GraphQLNonNull(GraphQLString) + QueryRootType = GraphQLObjectType( - name='QueryRoot', + name="QueryRoot", fields={ - 'thrower': GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_raises), - 'request': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, info: context.args.get('q')), - 'context': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, info: context), - 'test': GraphQLField( - type=GraphQLString, - args={ - 'who': GraphQLArgument(GraphQLString) - }, - resolver=lambda obj, info, who='World': 'Hello %s' % who - ) - } + "error": GraphQLField(NonNullString, resolver=resolve_error), + "request": GraphQLField(NonNullString, resolver=resolve_request), + "context": GraphQLField(NonNullString, resolver=resolve_context), + "test": GraphQLField( + GraphQLString, + {"who": GraphQLArgument(GraphQLString)}, + resolver=resolve_test, + ), + }, ) MutationRootType = GraphQLObjectType( - name='MutationRoot', + name="MutationRoot", fields={ - 'writeTest': GraphQLField( - type=QueryRootType, - resolver=lambda *_: QueryRootType + "writeTest": GraphQLField( + type=QueryRootType, resolver=lambda *_args: QueryRootType ) - } + }, ) schema = GraphQLSchema(QueryRootType, MutationRootType) diff --git a/tests/test_base.py b/tests/test_base.py deleted file mode 100644 index 52ad6e5..0000000 --- a/tests/test_base.py +++ /dev/null @@ -1,517 +0,0 @@ -from pytest import raises -import json - -from graphql.execution import ExecutionResult -from graphql_server import run_http_query, GraphQLParams, HttpQueryError, default_format_error -from .schema import schema - - -def execution_to_dict(execution_result): - result = {} - if execution_result.invalid: - result["errors"] = [default_format_error(e) for e in execution_result.errors] - if execution_result.data: - result["data"] = execution_result.data - - return result - - -def executions_to_dict(execution_results): - return list(map(execution_to_dict, execution_results)) - - -def test_allows_get_with_query_param(): - query = '{test}' - results, params = run_http_query(schema, 'get', {}, query_data=dict(query=query)) - - assert executions_to_dict(results) == [{ - 'data': {'test': "Hello World"}, - }] - assert params == [GraphQLParams(query=query,variables=None,operation_name=None)] - - -def test_allows_get_with_variable_values(): - query = '{test}' - results, params = run_http_query(schema, 'get', {}, query_data=dict( - query='query helloWho($who: String){ test(who: $who) }', - variables=json.dumps({'who': "Dolly"}) - )) - - assert executions_to_dict(results) == [{ - 'data': {'test': "Hello Dolly"}, - }] - - -def test_allows_get_with_operation_name(): - results, params = run_http_query(schema, 'get', {}, query_data=dict( - 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 executions_to_dict(results) == [{ - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - }, - }] - - -def test_reports_validation_errors(): - results, params = run_http_query(schema, 'get', {}, query_data=dict( - query='{ test, unknownOne, unknownTwo }' - )) - - assert executions_to_dict(results) == [{ - 'errors': [ - { - 'message': 'Cannot query field "unknownOne" on type "QueryRoot".', - 'locations': [{'line': 1, 'column': 9}] - }, - { - 'message': 'Cannot query field "unknownTwo" on type "QueryRoot".', - 'locations': [{'line': 1, 'column': 21}] - } - ] - }] - - -def test_errors_when_missing_operation_name(): - results, params = run_http_query(schema, 'get', {}, query_data=dict( - query=''' - query TestQuery { test } - mutation TestMutation { writeTest { test } } - ''' - )) - - assert executions_to_dict(results) == [{ - 'errors': [ - { - 'message': 'Must provide operation name if query contains multiple operations.' - } - ] - }] - - -# def test_errors_when_sending_a_mutation_via_get(): -# results, params = run_http_query(schema, 'get', {}, query_data=dict( -# query=''' -# mutation TestMutation { writeTest { test } } -# ''' -# )) - -# assert executions_to_dict(results) == [{ -# 'errors': [ -# { -# 'message': 'Can only perform a mutation operation from a POST request.' -# } -# ] -# }] - - -# def test_errors_when_selecting_a_mutation_within_a_get(client): -# response = client.get(url_string( -# 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.' -# } -# ] -# } - - -# def test_allows_mutation_to_exist_within_a_get(client): -# response = client.get(url_string( -# 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(client): -# response = client.post(url_string(), data=j(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(client): -# response = client.post(url_string(), data=j(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(client): -# response = client.post(url_string(), 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(client): -# response = client.post(url_string(), data=j( -# 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(client): -# response = client.post(url_string(), data=j( -# 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(client): -# response = client.post(url_string(), 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(client): -# response = client.post(url_string( -# variables=json.dumps({'who': "Dolly"}) -# ), data=j( -# 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(client): -# response = client.post(url_string( -# 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(client): -# response = client.post(url_string( -# 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(client): -# response = client.post(url_string(), data=j( -# 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(client): -# response = client.post(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") -# } -# ''', -# 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(client): -# response = client.get(url_string(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(client): -# response = client.get(url_string(query='{test}')) - -# assert response.data.decode() == ( -# '{"data":{"test":"Hello World"}}' -# ) - - -# def test_supports_pretty_printing_by_request(client): -# response = client.get(url_string(query='{test}', pretty='1')) - -# assert response.data.decode() == ( -# '{\n' -# ' "data": {\n' -# ' "test": "Hello World"\n' -# ' }\n' -# '}' -# ) - - -# def test_handles_field_errors_caught_by_graphql(client): -# response = client.get(url_string(query='{thrower}')) -# assert response.status_code == 200 -# assert response_json(response) == { -# 'data': None, -# 'errors': [{'locations': [{'column': 2, 'line': 1}], 'message': 'Throws!'}] -# } - - -# def test_handles_syntax_errors_caught_by_graphql(client): -# response = client.get(url_string(query='syntaxerror')) -# assert response.status_code == 400 -# assert response_json(response) == { -# 'errors': [{'locations': [{'column': 1, 'line': 1}], -# 'message': 'Syntax Error GraphQL request (1:1) ' -# 'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n'}] -# } - - -# def test_handles_errors_caused_by_a_lack_of_query(client): -# response = client.get(url_string()) - -# assert response.status_code == 400 -# assert response_json(response) == { -# 'errors': [{'message': 'Must provide query string.'}] -# } - - -# def test_handles_batch_correctly_if_is_disabled(client): -# response = client.post(url_string(), data='[]', content_type='application/json') - -# assert response.status_code == 400 -# assert response_json(response) == { -# 'errors': [{'message': 'Batch GraphQL requests are not enabled.'}] -# } - - -# def test_handles_incomplete_json_bodies(client): -# response = client.post(url_string(), data='{"query":', content_type='application/json') - -# assert response.status_code == 400 -# assert response_json(response) == { -# 'errors': [{'message': 'POST body sent invalid JSON.'}] -# } - - -# def test_handles_plain_post_text(client): -# response = client.post(url_string( -# 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.'}] -# } - - -# def test_handles_poorly_formed_variables(client): -# response = client.get(url_string( -# 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.'}] -# } - - -def test_handles_unsupported_http_methods(): - with raises(HttpQueryError) as exc_info: - run_http_query(schema, 'put', None) - - assert exc_info.value == HttpQueryError( - 405, - 'GraphQL only supports GET and POST requests.', - headers={ - 'Allow': 'GET, POST' - } - ) - -# def test_passes_request_into_request_context(client): -# response = client.get(url_string(query='{request}', q='testing')) - -# assert response.status_code == 200 -# assert response_json(response) == { -# 'data': { -# 'request': 'testing' -# } -# } - - -# @pytest.mark.parametrize('app', [create_app(get_context=lambda:"CUSTOM CONTEXT")]) -# def test_supports_pretty_printing(client): -# response = client.get(url_string(query='{context}')) - - -# assert response.status_code == 200 -# assert response_json(response) == { -# 'data': { -# 'context': 'CUSTOM CONTEXT' -# } -# } - - -# def test_post_multipart_data(client): -# query = 'mutation TestMutation { writeTest { test } }' -# response = client.post( -# url_string(), -# 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(client): -# response = client.post( -# url_string(), -# data=jl( -# # 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(client): -# response = client.post( -# url_string(), -# data=jl( -# # 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(client): -# response = client.post( -# url_string(), -# data=jl( -# # 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' -# } -# }] diff --git a/tests/test_error.py b/tests/test_error.py new file mode 100644 index 0000000..a0f7017 --- /dev/null +++ b/tests/test_error.py @@ -0,0 +1,28 @@ +from graphql_server import HttpQueryError + + +def test_create_http_query_error(): + + error = HttpQueryError(420, "Some message", headers={"SomeHeader": "SomeValue"}) + assert error.status_code == 420 + assert error.message == "Some message" + assert error.headers == {"SomeHeader": "SomeValue"} + + +def test_compare_http_query_errors(): + + error = HttpQueryError(400, "Message", headers={"Header": "Value"}) + assert error == HttpQueryError(400, "Message", headers={"Header": "Value"}) + assert error != HttpQueryError(420, "Message", headers={"Header": "Value"}) + assert error != HttpQueryError(400, "Other Message", headers={"Header": "Value"}) + assert error != HttpQueryError(400, "Message", headers={"Header": "OtherValue"}) + + +def test_hash_http_query_errors(): + + error = HttpQueryError(400, "Foo", headers={"Bar": "Baz"}) + + assert hash(error) == hash(HttpQueryError(400, "Foo", headers={"Bar": "Baz"})) + assert hash(error) != hash(HttpQueryError(420, "Foo", headers={"Bar": "Baz"})) + assert hash(error) != hash(HttpQueryError(400, "Boo", headers={"Bar": "Baz"})) + assert hash(error) != hash(HttpQueryError(400, "Foo", headers={"Bar": "Faz"})) diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..9f99669 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,316 @@ +import json + +from graphql.error import GraphQLError +from graphql.execution import ExecutionResult +from graphql.language.location import SourceLocation + +from graphql_server import ( + HttpQueryError, + ServerResponse, + encode_execution_results, + json_encode, + json_encode_pretty, + load_json_body, +) +from pytest import raises + + +def test_json_encode(): + result = json_encode({"query": "{test}"}) + assert result == '{"query":"{test}"}' + + +def test_json_encode_pretty(): + result = json_encode_pretty({"query": "{test}"}) + assert result == '{\n "query": "{test}"\n}' + + +def test_load_json_body_as_dict(): + result = load_json_body('{"query": "{test}"}') + assert result == {"query": "{test}"} + + +def test_load_json_body_with_variables(): + result = load_json_body( + """ + { + "query": "query helloWho($who: String){ test(who: $who) }", + "variables": {"who": "Dolly"} + } + """ + ) + + assert result["variables"] == {"who": "Dolly"} + + +def test_load_json_body_as_list(): + result = load_json_body('[{"query": "{test}"}]') + assert result == [{"query": "{test}"}] + + +def test_load_invalid_json_body(): + with raises(HttpQueryError) as exc_info: + load_json_body('{"query":') + assert exc_info.value == HttpQueryError(400, "POST body sent invalid JSON.") + + +def test_graphql_server_response(): + assert issubclass(ServerResponse, tuple) + # noinspection PyUnresolvedReferences + assert ServerResponse._fields == ("body", "status_code") + + +def test_encode_execution_results_without_error(): + execution_results = [ + ExecutionResult({"result": 1}, None), + ExecutionResult({"result": 2}, None), + ExecutionResult({"result": 3}, None), + ] + + output = encode_execution_results(execution_results) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert json.loads(output.body) == {"data": {"result": 1}} + assert output.status_code == 200 + + +def test_encode_execution_results_with_error(): + execution_results = [ + ExecutionResult( + None, + [ + GraphQLError( + "Some error", locations=[SourceLocation(1, 2)], path=["somePath"] + ) + ], + ), + ExecutionResult({"result": 42}, None), + ] + + output = encode_execution_results(execution_results) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert json.loads(output.body) == { + "data": None, + "errors": [ + { + "message": "Some error", + "locations": [{"line": 1, "column": 2}], + "path": ["somePath"], + } + ], + } + assert output.status_code == 200 + + +def test_encode_execution_results_with_invalid(): + execution_results = [ + ExecutionResult( + None, + [GraphQLError("SyntaxError", locations=[SourceLocation(1, 2)])], + invalid=True, + ), + ExecutionResult({"result": 42}, None), + ] + + output = encode_execution_results(execution_results) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert json.loads(output.body) == { + "errors": [{"message": "SyntaxError", "locations": [{"line": 1, "column": 2}]}] + } + assert output.status_code == 400 + + +def test_encode_execution_results_with_empty_result(): + execution_results = [None] + + output = encode_execution_results(execution_results) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert output.body == "null" + assert output.status_code == 200 + + +def test_encode_execution_results_with_format_error(): + execution_results = [ + ExecutionResult( + None, + [ + GraphQLError( + "Some msg", locations=[SourceLocation(1, 2)], path=["some", "path"] + ) + ], + ) + ] + + def format_error(error): + return { + "msg": str(error), + "loc": "{}:{}".format(error.locations[0].line, error.locations[0].column), + "pth": "/".join(error.path), + } + + output = encode_execution_results(execution_results, format_error=format_error) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert json.loads(output.body) == { + "data": None, + "errors": [{"msg": "Some msg", "loc": "1:2", "pth": "some/path"}], + } + assert output.status_code == 200 + + +def test_encode_execution_results_with_batch(): + execution_results = [ + ExecutionResult({"result": 1}, None), + ExecutionResult({"result": 2}, None), + ExecutionResult({"result": 3}, None), + ] + + output = encode_execution_results(execution_results, is_batch=True) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert json.loads(output.body) == [ + {"data": {"result": 1}}, + {"data": {"result": 2}}, + {"data": {"result": 3}}, + ] + assert output.status_code == 200 + + +def test_encode_execution_results_with_batch_and_empty_result(): + execution_results = [ + ExecutionResult({"result": 1}, None), + None, + ExecutionResult({"result": 3}, None), + ] + + output = encode_execution_results(execution_results, is_batch=True) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert json.loads(output.body) == [ + {"data": {"result": 1}}, + None, + {"data": {"result": 3}}, + ] + assert output.status_code == 200 + + +def test_encode_execution_results_with_batch_and_error(): + execution_results = [ + ExecutionResult({"result": 1}, None), + ExecutionResult( + None, + [ + GraphQLError( + "No data here", locations=[SourceLocation(1, 2)], path=["somePath"] + ) + ], + ), + ExecutionResult({"result": 3}, None), + ] + + output = encode_execution_results(execution_results, is_batch=True) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert json.loads(output.body) == [ + {"data": {"result": 1}}, + { + "data": None, + "errors": [ + { + "message": "No data here", + "locations": [{"line": 1, "column": 2}], + "path": ["somePath"], + } + ], + }, + {"data": {"result": 3}}, + ] + assert output.status_code == 200 + + +def test_encode_execution_results_with_batch_and_invalid(): + execution_results = [ + ExecutionResult({"result": 1}, None), + ExecutionResult( + None, + [ + GraphQLError( + "No data here", locations=[SourceLocation(1, 2)], path=["somePath"] + ) + ], + ), + ExecutionResult({"result": 3}, None), + ExecutionResult( + None, + [GraphQLError("SyntaxError", locations=[SourceLocation(1, 2)])], + invalid=True, + ), + ExecutionResult({"result": 5}, None), + ] + + output = encode_execution_results(execution_results, is_batch=True) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert json.loads(output.body) == [ + {"data": {"result": 1}}, + { + "data": None, + "errors": [ + { + "message": "No data here", + "locations": [{"line": 1, "column": 2}], + "path": ["somePath"], + } + ], + }, + {"data": {"result": 3}}, + { + "errors": [ + {"message": "SyntaxError", "locations": [{"line": 1, "column": 2}]} + ] + }, + {"data": {"result": 5}}, + ] + assert output.status_code == 400 + + +def test_encode_execution_results_with_encode(): + execution_results = [ExecutionResult({"result": None}, None)] + + def encode(result): + return repr(dict(result)) + + output = encode_execution_results(execution_results, encode=encode) + assert isinstance(output, ServerResponse) + assert isinstance(output.body, str) + assert isinstance(output.status_code, int) + assert output.body == "{'data': {'result': None}}" + assert output.status_code == 200 + + +def test_encode_execution_results_with_pretty(): + execution_results = [ExecutionResult({"test": "Hello World"}, None)] + + output = encode_execution_results(execution_results, encode=json_encode_pretty) + body = output.body + assert body == "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" + + +def test_encode_execution_results_not_pretty_by_default(): + execution_results = [ExecutionResult({"test": "Hello World"}, None)] + # execution_results = [ExecutionResult({"result": None}, None)] + + output = encode_execution_results(execution_results) + assert output.body == '{"data":{"test":"Hello World"}}' diff --git a/tests/test_query.py b/tests/test_query.py new file mode 100644 index 0000000..07c9e57 --- /dev/null +++ b/tests/test_query.py @@ -0,0 +1,532 @@ +import json + +from graphql.error import GraphQLError + +from graphql_server import ( + HttpQueryError, + RequestParams, + ServerResults, + encode_execution_results, + json_encode, + json_encode_pretty, + load_json_body, + run_http_query, +) +from pytest import raises + +from .schema import schema + + +def as_dicts(results): + """Convert execution results to a list of tuples of dicts for better comparison.""" + return [result.to_dict(dict_class=dict) for result in results] + + +def test_request_params(): + assert issubclass(RequestParams, tuple) + # noinspection PyUnresolvedReferences + assert RequestParams._fields == ("query", "variables", "operation_name") + + +def test_server_results(): + assert issubclass(ServerResults, tuple) + # noinspection PyUnresolvedReferences + assert ServerResults._fields == ("results", "params") + + +def test_allows_get_with_query_param(): + query = "{test}" + results, params = run_http_query(schema, "get", {}, dict(query=query)) + + assert as_dicts(results) == [{"data": {"test": "Hello World"}}] + assert params == [RequestParams(query=query, variables=None, operation_name=None)] + + +def test_allows_get_with_variable_values(): + results, params = run_http_query( + schema, + "get", + {}, + dict( + query="query helloWho($who: String){ test(who: $who) }", + variables=json.dumps({"who": "Dolly"}), + ), + ) + + assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + + +def test_allows_get_with_operation_name(): + results, params = run_http_query( + schema, + "get", + {}, + query_data=dict( + 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 as_dicts(results) == [ + {"data": {"test": "Hello World", "shared": "Hello Everyone"}} + ] + + +def test_reports_validation_errors(): + results, params = run_http_query( + schema, "get", {}, query_data=dict(query="{ test, unknownOne, unknownTwo }") + ) + + assert as_dicts(results) == [ + { + "errors": [ + { + "message": 'Cannot query field "unknownOne" on type "QueryRoot".', + "locations": [{"line": 1, "column": 9}], + }, + { + "message": 'Cannot query field "unknownTwo" on type "QueryRoot".', + "locations": [{"line": 1, "column": 21}], + }, + ] + } + ] + + +def test_non_dict_params_in_non_batch_query(): + with raises(HttpQueryError) as exc_info: + # noinspection PyTypeChecker + run_http_query(schema, "get", "not a dict") # type: ignore + + assert exc_info.value == HttpQueryError( + 400, "GraphQL params should be a dict. Received 'not a dict'." + ) + + +def test_empty_batch_in_batch_query(): + with raises(HttpQueryError) as exc_info: + run_http_query(schema, "get", [], batch_enabled=True) + + assert exc_info.value == HttpQueryError( + 400, "Received an empty list in the batch request." + ) + + +def test_errors_when_missing_operation_name(): + results, params = run_http_query( + schema, + "get", + {}, + query_data=dict( + query=""" + query TestQuery { test } + mutation TestMutation { writeTest { test } } + """ + ), + ) + + assert as_dicts(results) == [ + { + "errors": [ + { + "message": ( + "Must provide operation name" + " if query contains multiple operations." + ) + } + ] + } + ] + assert isinstance(results[0].errors[0], GraphQLError) + + +def test_errors_when_sending_a_mutation_via_get(): + with raises(HttpQueryError) as exc_info: + run_http_query( + schema, + "get", + {}, + query_data=dict( + query=""" + mutation TestMutation { writeTest { test } } + """ + ), + ) + + assert exc_info.value == HttpQueryError( + 405, + "Can only perform a mutation operation from a POST request.", + headers={"Allow": "POST"}, + ) + + +def test_catching_errors_when_sending_a_mutation_via_get(): + results, params = run_http_query( + schema, + "get", + {}, + query_data=dict( + query=""" + mutation TestMutation { writeTest { test } } + """ + ), + catch=True, + ) + + assert results == [None] + + +def test_errors_when_selecting_a_mutation_within_a_get(): + with raises(HttpQueryError) as exc_info: + run_http_query( + schema, + "get", + {}, + query_data=dict( + query=""" + query TestQuery { test } + mutation TestMutation { writeTest { test } } + """, + operationName="TestMutation", + ), + ) + + assert exc_info.value == HttpQueryError( + 405, + "Can only perform a mutation operation from a POST request.", + headers={"Allow": "POST"}, + ) + + +def test_allows_mutation_to_exist_within_a_get(): + results, params = run_http_query( + schema, + "get", + {}, + query_data=dict( + query=""" + query TestQuery { test } + mutation TestMutation { writeTest { test } } + """, + operationName="TestQuery", + ), + ) + + assert as_dicts(results) == [{"data": {"test": "Hello World"}}] + + +def test_allows_sending_a_mutation_via_post(): + results, params = run_http_query( + schema, + "post", + {}, + query_data=dict(query="mutation TestMutation { writeTest { test } }"), + ) + + assert as_dicts(results) == [{"data": {"writeTest": {"test": "Hello World"}}}] + + +def test_allows_post_with_url_encoding(): + results, params = run_http_query( + schema, "post", {}, query_data=dict(query="{test}") + ) + + assert as_dicts(results) == [{"data": {"test": "Hello World"}}] + + +def test_supports_post_json_query_with_string_variables(): + results, params = run_http_query( + schema, + "post", + {}, + query_data=dict( + query="query helloWho($who: String){ test(who: $who) }", + variables='{"who": "Dolly"}', + ), + ) + + assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + + +def test_supports_post_url_encoded_query_with_string_variables(): + results, params = run_http_query( + schema, + "post", + {}, + query_data=dict( + query="query helloWho($who: String){ test(who: $who) }", + variables='{"who": "Dolly"}', + ), + ) + + assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + + +def test_supports_post_json_query_with_get_variable_values(): + results, params = run_http_query( + schema, + "post", + data=dict(query="query helloWho($who: String){ test(who: $who) }"), + query_data=dict(variables={"who": "Dolly"}), + ) + + assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + + +def test_post_url_encoded_query_with_get_variable_values(): + results, params = run_http_query( + schema, + "get", + data=dict(query="query helloWho($who: String){ test(who: $who) }"), + query_data=dict(variables='{"who": "Dolly"}'), + ) + + assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + + +def test_supports_post_raw_text_query_with_get_variable_values(): + results, params = run_http_query( + schema, + "get", + data=dict(query="query helloWho($who: String){ test(who: $who) }"), + query_data=dict(variables='{"who": "Dolly"}'), + ) + + assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + + +def test_allows_post_with_operation_name(): + results, params = run_http_query( + schema, + "get", + data=dict( + 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 as_dicts(results) == [ + {"data": {"test": "Hello World", "shared": "Hello Everyone"}} + ] + + +def test_allows_post_with_get_operation_name(): + results, params = run_http_query( + schema, + "get", + data=dict( + 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") + } + """ + ), + query_data=dict(operationName="helloWorld"), + ) + + assert as_dicts(results) == [ + {"data": {"test": "Hello World", "shared": "Hello Everyone"}} + ] + + +def test_supports_pretty_printing_data(): + results, params = run_http_query(schema, "get", dict(query="{test}")) + body = encode_execution_results(results, encode=json_encode_pretty).body + + assert body == "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" + + +def test_not_pretty_data_by_default(): + results, params = run_http_query(schema, "get", dict(query="{test}")) + body = encode_execution_results(results).body + + assert body == '{"data":{"test":"Hello World"}}' + + +def test_handles_field_errors_caught_by_graphql(): + results, params = run_http_query(schema, "get", dict(query="{error}")) + + assert as_dicts(results) == [ + { + "data": None, + "errors": [ + { + "message": "Throws!", + "locations": [{"line": 1, "column": 2}], + "path": ["error"], + } + ], + } + ] + + +def test_handles_syntax_errors_caught_by_graphql(): + results, params = run_http_query(schema, "get", dict(query="syntaxerror")) + + assert as_dicts(results) == [ + { + "errors": [ + { + "locations": [{"line": 1, "column": 1}], + "message": "Syntax Error GraphQL (1:1)" + ' Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n', + } + ] + } + ] + + +def test_handles_errors_caused_by_a_lack_of_query(): + with raises(HttpQueryError) as exc_info: + run_http_query(schema, "get", {}) + + assert exc_info.value == HttpQueryError(400, "Must provide query string.") + + +def test_handles_errors_caused_by_invalid_query_type(): + results, params = run_http_query(schema, "get", dict(query=42)) + + assert as_dicts(results) == [ + {"errors": [{"message": "The query must be a string"}]} + ] + + +def test_handles_batch_correctly_if_is_disabled(): + with raises(HttpQueryError) as exc_info: + run_http_query(schema, "post", []) + + assert exc_info.value == HttpQueryError( + 400, "Batch GraphQL requests are not enabled." + ) + + +def test_handles_incomplete_json_bodies(): + with raises(HttpQueryError) as exc_info: + run_http_query(schema, "post", load_json_body('{"query":')) + + assert exc_info.value == HttpQueryError(400, "POST body sent invalid JSON.") + + +def test_handles_plain_post_text(): + with raises(HttpQueryError) as exc_info: + run_http_query(schema, "post", {}) + + assert exc_info.value == HttpQueryError(400, "Must provide query string.") + + +def test_handles_poorly_formed_variables(): + with raises(HttpQueryError) as exc_info: + run_http_query( + schema, + "get", + {}, + dict( + query="query helloWho($who: String){ test(who: $who) }", + variables="who:You", + ), + ) + + assert exc_info.value == HttpQueryError(400, "Variables are invalid JSON.") + + +def test_handles_bad_schema(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + run_http_query("not a schema", "get", {"query": "{error}"}) # type: ignore + + msg = str(exc_info.value) + assert msg == "Expected a GraphQL schema, but received 'not a schema'." + + +def test_handles_unsupported_http_methods(): + with raises(HttpQueryError) as exc_info: + run_http_query(schema, "put", {}) + + assert exc_info.value == HttpQueryError( + 405, + "GraphQL only supports GET and POST requests.", + headers={"Allow": "GET, POST"}, + ) + + +def test_passes_request_into_request_context(): + results, params = run_http_query( + schema, "get", {}, dict(query="{request}"), context_value={"q": "testing"} + ) + + assert as_dicts(results) == [{"data": {"request": "testing"}}] + + +def test_supports_pretty_printing_context(): + class Context: + def __str__(self): + return "CUSTOM CONTEXT" + + results, params = run_http_query( + schema, "get", {}, dict(query="{context}"), context_value=Context() + ) + + assert as_dicts(results) == [{"data": {"context": "CUSTOM CONTEXT"}}] + + +def test_post_multipart_data(): + query = "mutation TestMutation { writeTest { test } }" + results, params = run_http_query(schema, "post", {}, query_data=dict(query=query)) + + assert as_dicts(results) == [{"data": {"writeTest": {"test": "Hello World"}}}] + + +def test_batch_allows_post_with_json_encoding(): + data = load_json_body('[{"query": "{test}"}]') + results, params = run_http_query(schema, "post", data, batch_enabled=True) + + assert as_dicts(results) == [{"data": {"test": "Hello World"}}] + + +def test_batch_supports_post_json_query_with_json_variables(): + data = load_json_body( + '[{"query":"query helloWho($who: String){ test(who: $who) }",' + '"variables":{"who":"Dolly"}}]' + ) + results, params = run_http_query(schema, "post", data, batch_enabled=True) + + assert as_dicts(results) == [{"data": {"test": "Hello Dolly"}}] + + +def test_batch_allows_post_with_operation_name(): + data = [ + dict( + 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", + ) + ] + data = load_json_body(json_encode(data)) + results, params = run_http_query(schema, "post", data, batch_enabled=True) + + assert as_dicts(results) == [ + {"data": {"test": "Hello World", "shared": "Hello Everyone"}} + ] diff --git a/tox.ini b/tox.ini index 0de7495..2c8640b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,33 +1,40 @@ [tox] -envlist = flake8,import-order,py37,py36,py35,py34,py33,py27,pypy +envlist = black,flake8,mypy,py37,py36,py35,py34,py33,py27,pypy3,pypy skipsdist = true [testenv] setenv = PYTHONPATH = {toxinidir} deps = - pytest>=2.7.2 + pytest>=3.0 graphql-core>=2.1 pytest-cov commands = py{py,27,33,34,35,36,37}: py.test tests {posargs} -[testenv:flake8] +[testenv:black] basepython=python3.7 -deps = flake8 +deps = black commands = - flake8 graphql_server + black --check graphql_server tests -[testenv:mypy] +[testenv:flake8] basepython=python3.7 -deps = mypy +deps = flake8 commands = - mypy graphql_server --ignore-missing-imports + flake8 graphql_server tests -[testenv:import-order] +[testenv:isort] basepython=python3.7 deps = isort graphql-core>=2.1 commands = - isort --check-only graphql_server/ -rc + isort -rc graphql_server/ tests/ + +[testenv:mypy] +basepython=python3.7 +deps = mypy +commands = + mypy graphql_server tests --ignore-missing-imports +