From e045b57e30b404b389f1a9fdd5c59c9ce0942da8 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Fri, 7 Aug 2020 13:46:47 -0500 Subject: [PATCH] chore: use graphql-server to re-export webob --- .gitignore | 208 ++++++++++- .travis.yml | 30 +- MANIFEST.in | 11 +- Makefile | 5 + README.md | 64 ++-- README.rst | 74 ---- bin/autolinter | 7 - bin/convert_documentation | 3 - setup.cfg | 6 +- setup.py | 41 ++- tests/app.py | 46 +++ tests/schema.py | 54 +-- tests/test_graphiql.py | 65 ---- tests/test_graphiqlview.py | 43 +++ tests/test_graphql.py | 529 ---------------------------- tests/test_graphqlview.py | 585 +++++++++++++++++++++++++++++++ tests/utils.py | 53 --- tox.ini | 41 +-- webob_graphql/__init__.py | 4 +- webob_graphql/base.py | 112 ------ webob_graphql/mako.py | 143 -------- webob_graphql/render_graphiql.py | 141 -------- webob_graphql/utils.py | 56 --- 23 files changed, 1020 insertions(+), 1301 deletions(-) create mode 100644 Makefile delete mode 100644 README.rst delete mode 100755 bin/autolinter delete mode 100755 bin/convert_documentation create mode 100644 tests/app.py delete mode 100644 tests/test_graphiql.py create mode 100644 tests/test_graphiqlview.py delete mode 100644 tests/test_graphql.py create mode 100644 tests/test_graphqlview.py delete mode 100644 tests/utils.py delete mode 100644 webob_graphql/base.py delete mode 100644 webob_graphql/mako.py delete mode 100644 webob_graphql/render_graphiql.py delete mode 100644 webob_graphql/utils.py diff --git a/.gitignore b/.gitignore index 89b1585..642f015 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,204 @@ -*.pyc -.idea -.cache -.tox + +# 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 + +# 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/ +.venv/ .coverage -/build/ +.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 + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history -/dist/ +# End of https://www.gitignore.io/api/python,intellij+all,visualstudiocode diff --git a/.travis.yml b/.travis.yml index 5779b9a..52554b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,21 @@ language: python sudo: false -matrix: - include: - - python: pypy - env: TOX_ENV=pypy - - python: '2.7' - env: TOX_ENV=py27 - - python: '3.5' - env: TOX_ENV=py35 - - python: '3.6' - env: TOX_ENV=py36,import-order,flake8 -cache: - directories: - - $HOME/.cache/pip - - $TRAVIS_BUILD_DIR/.tox +python: + - 3.6 + - 3.7 + - 3.8 +cache: pip + install: -- pip install tox coveralls + - pip install tox-travis + script: -- tox -e $TOX_ENV -- --cov=webob_graphql + - tox + after_success: -- coveralls + - pip install coveralls + - coveralls + deploy: provider: pypi user: syrusakbary diff --git a/MANIFEST.in b/MANIFEST.in index 31ac6ee..0de3d9e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,10 @@ +include LICENSE include README.md -recursive-include webob_graphql/static * -recursive-include webob_graphql/templates * + +include tox.ini +include Makefile + +recursive-include webob_graphql *.py +recursive-include tests *.py + +global-exclude *.py[co] __pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a634216 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +dev-setup: + python pip install -e ".[test]" + +tests: + py.test tests --cov=webob_graphql -vv \ No newline at end of file diff --git a/README.md b/README.md index dad332c..9c8beb5 100644 --- a/README.md +++ b/README.md @@ -4,49 +4,53 @@ Adds GraphQL support to your WebOb (Pyramid, Pylons, ...) application. -## Installation - -For instaling WebOb-GraphQL, just run this command in your shell - -```bash -pip install "webob-graphql>=1.0.dev" -``` - - ## Usage -Just use the `serve_graphql_request` function from `webob_graphql` - +Use the `GraphQLView` view from `webob_graphql` ### Pyramid ```python +from wsgiref.simple_server import make_server from pyramid.view import view_config from webob_graphql import serve_graphql_request +from schema import schema -@view_config( - route_name='graphql', - # The serve_graphql_request method will detect what's the best renderer - # to use, so it will do the json render automatically. - # In summary, don't use the renderer='json' here :) -) def graphql_view(request): - context = {'session': request.session} - return serve_graphql_request(request, schema, context_value=context) - - # Optional, for adding batch query support (used in Apollo-Client) - return serve_graphql_request(request, schema, batch_enabled=True, context_value=context) + return GraphQLView(request=request, schema=schema, graphiql=True).dispatch_request(request) + +if __name__ == '__main__': + with Configurator() as config: + config.add_route('graphql', '/graphql') + config.add_view(graphql_view, route_name='graphql') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() ``` +This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. + +### Supported options for GraphQLView -### Supported options * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. - * `context_value`: A value to pass as the `context` to the `graphql()` function. - * `root_value`: The `root_value` you want to provide to `executor.execute`. - * `format_error`: If you want to use a custom error formatter. + * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. + * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. - * `executor`: The `Executor` that you want to use to execute queries. - * `graphiql_enabled`: If `True` (default), may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `render_graphiql`: A custom function for rendering GraphiQL (this function should have the arguments `result` and `params`). - * `batch_enabled`: Enable batch support (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) + * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). + * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. + * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. + * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) + * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). + * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). + * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. + * `enable_async`: whether `async` mode will be enabled. + * `subscriptions`: The GraphiQL socket endpoint for using subscriptions in graphql-ws. + * `headers`: An optional GraphQL string to use as the initial displayed request headers, if not provided, the stored headers will be used. + * `default_query`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If not provided, GraphiQL will use its own default query. +* `header_editor_enabled`: An optional boolean which enables the header editor when true. Defaults to **false**. +* `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**. + +## Contributing +Since v3, `webob-graphql` code lives at [graphql-server](https://github.com/graphql-python/graphql-server) repository to keep any breaking change on the base package on sync with all other integrations. In order to contribute, please take a look at [CONTRIBUTING.md](https://github.com/graphql-python/graphql-server/blob/master/CONTRIBUTING.md). diff --git a/README.rst b/README.rst deleted file mode 100644 index 41d7769..0000000 --- a/README.rst +++ /dev/null @@ -1,74 +0,0 @@ -WebOb-GraphQL -============= - -|Build Status| |Coverage Status| |PyPI version| - -Adds GraphQL support to your WebOb (Pyramid, Pylons, ...) application. - -Installation ------------- - -For instaling WebOb-GraphQL, just run this command in your shell - -.. code:: bash - - pip install "webob-graphql>=1.0.dev" - -Usage ------ - -Just use the ``serve_graphql_request`` function from ``webob_graphql`` - -Pyramid -~~~~~~~ - -.. code:: python - - from pyramid.view import view_config - - from webob_graphql import serve_graphql_request - - - @view_config( - route_name='graphql', - # The serve_graphql_request method will detect what's the best renderer - # to use, so it will do the json render automatically. - # In summary, don't use the renderer='json' here :) - ) - def graphql_view(request): - return serve_graphql_request(request, schema) - - # Optional, for adding batch query support (used in Apollo-Client) - return serve_graphql_request(request, schema, batch_enabled=True) - -Supported options -~~~~~~~~~~~~~~~~~ - -- ``schema``: The ``GraphQLSchema`` object that you want the view to - execute when it gets a valid request. -- ``context``: A value to pass as the ``context`` to the ``graphql()`` - function. -- ``root_value``: The ``root_value`` you want to provide to - ``executor.execute``. -- ``format_error``: If you want to use a custom error formatter. -- ``pretty``: Whether or not you want the response to be pretty printed - JSON. -- ``executor``: The ``Executor`` that you want to use to execute - queries. -- ``graphiql_enabled``: If ``True`` (default), may present - `GraphiQL `__ when loaded - directly from a browser (a useful tool for debugging and - exploration). -- ``render_graphiql``: A custom function for rendering GraphiQL (this - function should have the arguments ``result`` and ``params``). -- ``batch_enabled``: Enable batch support (for using in - `Apollo-Client `__ - or - `ReactRelayNetworkLayer `__) - -.. |Build Status| image:: https://travis-ci.org/graphql-python/webob-graphql.svg?branch=master - :target: https://travis-ci.org/graphql-python/webob-graphql -.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/webob-graphql/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/graphql-python/webob-graphql?branch=master -.. |PyPI version| image:: https://badge.fury.io/py/webob-graphql.svg - :target: https://badge.fury.io/py/webob-graphql diff --git a/bin/autolinter b/bin/autolinter deleted file mode 100755 index 00fb00f..0000000 --- a/bin/autolinter +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Install the required scripts with -# pip install autoflake autopep8 isort -autoflake ./webob_graphql/ -r --remove-unused-variables --remove-all-unused-imports --in-place -autopep8 ./webob_graphql/ -r --in-place --experimental --aggressive --max-line-length 120 -isort -rc ./webob_graphql/ diff --git a/bin/convert_documentation b/bin/convert_documentation deleted file mode 100755 index b55d5da..0000000 --- a/bin/convert_documentation +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -pandoc README.md --from markdown --to rst -s -o README.rst diff --git a/setup.cfg b/setup.cfg index bccff8a..b6ff204 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [flake8] exclude = tests,scripts,setup.py,docs -max-line-length = 160 +max-line-length = 88 [isort] known_first_party=graphql -[pytest] -norecursedirs = venv .tox .cache +[tool:pytest] +norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache diff --git a/setup.py b/setup.py index 4ec2bce..48a24e8 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,30 @@ from setuptools import setup, find_packages -required_packages = [ - 'graphql-core>=2.1rc2', 'webob', 'graphql-server-core>=1.1rc0' +install_requires = [ + "graphql-server[webob]>=3.0.0b1", ] -tests_require = ['pytest>=2.7.3', 'mako'] +tests_requires = [ + "pytest>=5.4,<5.5", + "pytest-cov>=2.8,<3", + "Jinja2>=2.10.1,<3", +] + +dev_requires = [ + "flake8>=3.7,<4", + "isort>=4,<5", + "check-manifest>=0.40,<1", +] + tests_requires + +with open("README.md", encoding="utf-8") as readme_file: + readme = readme_file.read() setup( name='WebOb-GraphQL', version='2.0rc0', description= 'Adds GraphQL support to your WebOb (Pyramid/Pylons/...) application', - long_description=open('README.rst').read(), + long_description=readme, url='https://github.com/graphql-python/webob-graphql', download_url='https://github.com/graphql-python/webob-graphql/releases', author='Syrus Akbary', @@ -21,22 +34,20 @@ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: Implementation :: PyPy', + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", 'License :: OSI Approved :: MIT License', ], keywords='api graphql protocol rest webob', packages=find_packages(exclude=['tests']), - install_requires=required_packages, + install_requires=install_requires, + tests_require=tests_requires, extras_require={ - 'test': tests_require, + 'test': tests_requires, + 'dev': dev_requires, }, - tests_require=tests_require, include_package_data=True, zip_safe=False, - platforms='any', ) + platforms='any', +) diff --git a/tests/app.py b/tests/app.py new file mode 100644 index 0000000..39a036a --- /dev/null +++ b/tests/app.py @@ -0,0 +1,46 @@ +from urllib.parse import urlencode + +from webob import Request + +from tests.schema import Schema +from webob_graphql import GraphQLView + + +def url_string(**url_params): + string = "/graphql" + + if url_params: + string += "?" + urlencode(url_params) + + return string + + +class Client(object): + def __init__(self, **kwargs): + self.schema = kwargs.pop("schema", None) or Schema + self.settings = kwargs.pop("settings", None) or {} + + def get(self, url, **extra): + request = Request.blank(url, method="GET", **extra) + context = self.settings.pop("context", request) + response = GraphQLView( + request=request, schema=self.schema, context=context, **self.settings + ) + return response.dispatch_request(request) + + def post(self, url, **extra): + extra["POST"] = extra.pop("data") + request = Request.blank(url, method="POST", **extra) + context = self.settings.pop("context", request) + response = GraphQLView( + request=request, schema=self.schema, context=context, **self.settings + ) + return response.dispatch_request(request) + + def put(self, url, **extra): + request = Request.blank(url, method="PUT", **extra) + context = self.settings.pop("context", request) + response = GraphQLView( + request=request, schema=self.schema, context=context, **self.settings + ) + return response.dispatch_request(request) diff --git a/tests/schema.py b/tests/schema.py index 488f711..8785c3a 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -1,4 +1,5 @@ -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 @@ -7,32 +8,41 @@ def resolve_raises(*_): raise Exception("Throws!") +# Sync schema QueryRootType = GraphQLObjectType( - name='QueryRoot', + name="QueryRoot", fields={ - 'thrower': GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_raises), - 'request': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, info: info.context.params.get('q')), - 'context': GraphQLField(GraphQLNonNull(GraphQLString), - resolver=lambda obj, info: info.context), - 'test': GraphQLField( - type=GraphQLString, - args={ - 'who': GraphQLArgument(GraphQLString) - }, - resolver=lambda obj, info, who='World': 'Hello %s' % who - ) - } + "thrower": GraphQLField(GraphQLNonNull(GraphQLString), resolve=resolve_raises), + "request": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=lambda obj, info: info.context["request"].params.get("q"), + ), + "context": GraphQLField( + GraphQLObjectType( + name="context", + fields={ + "session": GraphQLField(GraphQLString), + "request": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=lambda obj, info: info.context["request"], + ), + }, + ), + 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', + name="MutationRoot", fields={ - 'writeTest': GraphQLField( - type=QueryRootType, - resolver=lambda *_: QueryRootType - ) - } + "writeTest": GraphQLField(type_=QueryRootType, resolve=lambda *_: QueryRootType) + }, ) -schema = GraphQLSchema(QueryRootType, MutationRootType) +Schema = GraphQLSchema(QueryRootType, MutationRootType) diff --git a/tests/test_graphiql.py b/tests/test_graphiql.py deleted file mode 100644 index 8e3a989..0000000 --- a/tests/test_graphiql.py +++ /dev/null @@ -1,65 +0,0 @@ -import pytest - -from .schema import schema -from .utils import url_string, response_json, j, jl, Client - -from webob_graphql.mako import render_graphiql as mako_render_graphiql - - -@pytest.fixture -def settings(): - return {} - - -@pytest.fixture -def client(settings): - return Client(schema, settings) - - -@pytest.fixture -def pretty_response(): - return ( - '{\n' - ' "data": {\n' - ' "test": "Hello World"\n' - ' }\n' - '}' - ).replace('\"','\\\"').replace('\n', '\\n') - - -def test_graphiql_is_enabled_by_default(client): - response = client.get(url_string(), headers={'Accept': 'text/html'}) - assert response.status_code == 200 - assert response.content_type == 'text/html' - - -def test_graphiql_simple_renderer(client, pretty_response): - response = client.get(url_string(query='{test}'), headers={'Accept': 'text/html'}) - assert response.status_code == 200 - assert pretty_response in response.body.decode('utf-8') - - -@pytest.mark.parametrize('settings', [dict(render_graphiql=mako_render_graphiql)]) -def test_graphiql_mako_renderer(client, pretty_response): - response = client.get(url_string(query='{test}'), headers={'Accept': 'text/html'}) - assert response.status_code == 200 - assert pretty_response in response.body.decode('utf-8') - - -def test_graphiql_html_is_not_accepted(client): - response = client.get(url_string(), headers={'Accept': 'application/json'}) - assert response.status_code == 400 - - -def test_graphiql_get_mutation(client, pretty_response): - query = 'mutation TestMutation { writeTest { test } }' - response = client.get(url_string(query=query), headers={'Accept': 'text/html'}) - assert response.status_code == 200 - assert 'response: null' in response.body.decode('utf-8') - - -@pytest.mark.parametrize('settings', [dict(graphiql_enabled=False)]) -def test_graphiql_is_enabled_by_default(client): - response = client.get(url_string(query='{test}'), headers={'Accept': 'text/html'}) - assert response.status_code == 200 - assert response.content_type == 'application/json' diff --git a/tests/test_graphiqlview.py b/tests/test_graphiqlview.py new file mode 100644 index 0000000..dbfe627 --- /dev/null +++ b/tests/test_graphiqlview.py @@ -0,0 +1,43 @@ +import pytest + +from .app import Client, url_string + + +@pytest.fixture +def settings(): + return {} + + +@pytest.fixture +def client(settings): + return Client(settings=settings) + + +@pytest.fixture +def pretty_response(): + return ( + "{\n" + ' "data": {\n' + ' "test": "Hello World"\n' + " }\n" + "}".replace('"', '\\"').replace("\n", "\\n") + ) + + +@pytest.mark.parametrize("settings", [dict(graphiql=True)]) +def test_graphiql_is_enabled(client, settings): + response = client.get(url_string(query="{test}"), headers={"Accept": "text/html"}) + assert response.status_code == 200 + + +@pytest.mark.parametrize("settings", [dict(graphiql=True)]) +def test_graphiql_simple_renderer(client, settings, pretty_response): + response = client.get(url_string(query="{test}"), headers={"Accept": "text/html"}) + assert response.status_code == 200 + assert pretty_response in response.body.decode("utf-8") + + +@pytest.mark.parametrize("settings", [dict(graphiql=True)]) +def test_graphiql_html_is_not_accepted(client, settings): + response = client.get(url_string(), headers={"Accept": "application/json"}) + assert response.status_code == 400 diff --git a/tests/test_graphql.py b/tests/test_graphql.py deleted file mode 100644 index 1859b77..0000000 --- a/tests/test_graphql.py +++ /dev/null @@ -1,529 +0,0 @@ -import pytest -import json - -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode - -from .schema import schema -from .utils import url_string, response_json, j, jl, Client - - -@pytest.fixture -def settings(): - return {} - - -@pytest.fixture -def client(settings): - return Client(schema, settings) - - -def test_allows_get_with_query_param(client): - response = client.get(url_string(query='{test}')) - assert response.status_code == 200, response.status - assert response_json(response) == { - 'data': {'test': "Hello World"} - } - - -def test_allows_get_with_variable_values(client): - response = client.get(url_string( - 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(client): - response = client.get(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_code == 200 - assert response_json(response) == { - 'data': { - 'test': 'Hello World', - 'shared': 'Hello Everyone' - } - } - - -def test_reports_validation_errors(client): - response = client.get(url_string( - 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}] - }, - { - 'message': 'Cannot query field "unknownTwo" on type "QueryRoot".', - 'locations': [{'line': 1, 'column': 21}] - } - ] - } - - -def test_errors_when_missing_operation_name(client): - response = client.get(url_string( - 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.' - } - ] - } - - -def test_errors_when_sending_a_mutation_via_get(client): - response = client.get(url_string( - 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.' - } - ] - } - - -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('settings', [dict(pretty=True)]) -def test_supports_pretty_printing(client): - response = client.get(url_string(query='{test}')) - - assert response.body.decode() == ( - '{\n' - ' "data": {\n' - ' "test": "Hello World"\n' - ' }\n' - '}' - ) - - -@pytest.mark.parametrize('settings', [dict(pretty=False)]) -def test_not_pretty_by_default(client): - response = client.get(url_string(query='{test}')) - - assert response.body.decode() == ( - '{"data":{"test":"Hello World"}}' - ) - - -def test_supports_pretty_printing_by_request(client): - response = client.get(url_string(query='{test}', pretty='1')) - - assert response.body.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!', 'path': ['thrower']}] - } - - -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 (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(client): - response = client.put(url_string(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.'}] - } - - -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('settings', [dict(context_value="CUSTOM CONTEXT")]) -def test_supports_custom_context(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 } }' - data = ('------webobgraphql\r\n' + - 'Content-Disposition: form-data; name="query"\r\n' + - '\r\n' + - query + '\r\n' + - '------webobgraphql--\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' + - '------webobgraphql--\r\n' - ) - - response = client.post( - url_string(), - data=data, - content_type='multipart/form-data; boundary=----webobgraphql' - ) - - assert response.status_code == 200 - assert response_json(response) == {'data': {u'writeTest': {u'test': u'Hello World'}}} - - -@pytest.mark.parametrize('settings', [dict(batch_enabled=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('settings', [dict(batch_enabled=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('settings', [dict(batch_enabled=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_graphqlview.py b/tests/test_graphqlview.py new file mode 100644 index 0000000..456b5f1 --- /dev/null +++ b/tests/test_graphqlview.py @@ -0,0 +1,585 @@ +import json +from urllib.parse import urlencode + +import pytest + +from .app import Client, url_string + + +@pytest.fixture +def settings(): + return {} + + +@pytest.fixture +def client(settings): + return Client(settings=settings) + + +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]) + + +def test_allows_get_with_query_param(client): + response = client.get(url_string(query="{test}")) + assert response.status_code == 200, response.status + assert response_json(response) == {"data": {"test": "Hello World"}} + + +def test_allows_get_with_variable_values(client): + response = client.get( + url_string( + 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(client): + response = client.get( + 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_code == 200 + assert response_json(response) == { + "data": {"test": "Hello World", "shared": "Hello Everyone"} + } + + +def test_reports_validation_errors(client): + response = client.get(url_string(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(client): + response = client.get( + url_string( + 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.", + "locations": None, + "path": None, + } + ] + } + + +def test_errors_when_sending_a_mutation_via_get(client): + response = client.get( + url_string( + 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(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.", + "locations": None, + "path": None, + } + ] + } + + +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=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(client): + response = client.post( + url_string(), + 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(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=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(client): + response = client.post( + url_string(), + 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(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=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(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=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(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("settings", [dict(pretty=True)]) +def test_supports_pretty_printing(client, settings): + response = client.get(url_string(query="{test}")) + + assert response.body.decode() == ( + "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" + ) + + +@pytest.mark.parametrize("settings", [dict(pretty=False)]) +def test_not_pretty_by_default(client, settings): + response = client.get(url_string(query="{test}")) + + assert response.body.decode() == '{"data":{"test":"Hello World"}}' + + +def test_supports_pretty_printing_by_request(client): + response = client.get(url_string(query="{test}", pretty="1")) + + assert response.body.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": [ + { + "message": "Throws!", + "locations": [{"column": 2, "line": 1}], + "path": ["thrower"], + } + ], + } + + +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": [ + { + "message": "Syntax Error: Unexpected Name 'syntaxerror'.", + "locations": [{"column": 1, "line": 1}], + "path": None, + } + ] + } + + +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.", "locations": None, "path": None} + ] + } + + +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.", + "locations": None, + "path": None, + } + ] + } + + +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.", "locations": None, "path": None} + ] + } + + +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.", "locations": None, "path": None} + ] + } + + +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.", "locations": None, "path": None} + ] + } + + +def test_handles_unsupported_http_methods(client): + response = client.put(url_string(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(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("settings", [dict(context={"session": "CUSTOM CONTEXT"})]) +def test_passes_custom_context_into_context(client, settings): + response = client.get(url_string(query="{context { session request }}")) + + assert response.status_code == 200 + res = response_json(response) + assert "data" in res + assert "session" in res["data"]["context"] + assert "request" in res["data"]["context"] + assert "CUSTOM CONTEXT" in res["data"]["context"]["session"] + assert "request" in res["data"]["context"]["request"] + + +@pytest.mark.parametrize("settings", [dict(context="CUSTOM CONTEXT")]) +def test_context_remapped_if_not_mapping(client, settings): + response = client.get(url_string(query="{context { session request }}")) + + assert response.status_code == 200 + res = response_json(response) + assert "data" in res + assert "session" in res["data"]["context"] + assert "request" in res["data"]["context"] + assert "CUSTOM CONTEXT" not in res["data"]["context"]["request"] + assert "request" in res["data"]["context"]["request"] + + +def test_post_multipart_data(client): + query = "mutation TestMutation { writeTest { test } }" + data = ( + "------webobgraphql\r\n" + + 'Content-Disposition: form-data; name="query"\r\n' + + "\r\n" + + query + + "\r\n" + + "------webobgraphql--\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" + + "------webobgraphql--\r\n" + ) + + response = client.post( + url_string(), + data=data, + content_type="multipart/form-data; boundary=----webobgraphql", + ) + + assert response.status_code == 200 + assert response_json(response) == { + "data": {u"writeTest": {u"test": u"Hello World"}} + } + + +@pytest.mark.parametrize("settings", [dict(batch=True)]) +def test_batch_allows_post_with_json_encoding(client, settings): + response = client.post( + url_string(), + 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("settings", [dict(batch=True)]) +def test_batch_supports_post_json_query_with_json_variables(client, settings): + response = client.post( + url_string(), + 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("settings", [dict(batch=True)]) +def test_batch_allows_post_with_operation_name(client, settings): + response = client.post( + 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", + ), + 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/utils.py b/tests/utils.py deleted file mode 100644 index 5190418..0000000 --- a/tests/utils.py +++ /dev/null @@ -1,53 +0,0 @@ -from webob import Request, Response - - -from webob_graphql import serve_graphql_request - -import json - -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode - - -def url_string(**url_params): - string = '/graphql' - - if url_params: - string += '?' + urlencode(url_params) - - return string - - -def response_json(response): - return json.loads(response.body.decode()) - - -j = lambda **kwargs: json.dumps(kwargs) -jl = lambda **kwargs: json.dumps([kwargs]) - - -class Client(object): - def __init__(self, schema, settings): - self.schema = schema - self.settings = settings or {} - - def get(self, url, **extra): - request = Request.blank(url, method='GET', **extra) - context_value = self.settings.pop('context_value',request) - response = serve_graphql_request(request, self.schema, context_value=context_value, **self.settings) - return response - - def post(self, url, **extra): - extra['POST'] = extra.pop('data') - request = Request.blank(url, method='POST', **extra) - context_value = self.settings.pop('context_value',request) - response = serve_graphql_request(request, self.schema, context_value=context_value, **self.settings) - return response - - def put(self, url, **extra): - request = Request.blank(url, method='PUT', **extra) - context_value = self.settings.pop('context_value',request) - response = serve_graphql_request(request, self.schema, context_value=context_value, **self.settings) - return response \ No newline at end of file diff --git a/tox.ini b/tox.ini index ce4338c..3c0e429 100644 --- a/tox.ini +++ b/tox.ini @@ -1,31 +1,32 @@ [tox] -envlist = flake8,import-order,py27,py35,py36,pypy -skipsdist = true +envlist = + py{36,37,38} + flake8,import-order,manifest +; requires = tox-conda [testenv] +passenv = * setenv = PYTHONPATH = {toxinidir} -deps = - pytest>=2.7.2 - graphql-core>=2.1rc2 - graphql-server-core>=1.1rc0 - WebOb - mako - pytest-cov -commands = - py{py,27,35,36}: py.test tests {posargs} +install_command = python -m pip install --pre --ignore-installed {opts} {packages} +deps = -e.[test] +commands = + pytest tests --cov-report=term-missing --cov=webob_graphql {posargs} [testenv:flake8] -basepython=python3.6 -deps = flake8 +basepython=python3.8 +deps = -e.[dev] commands = - flake8 webob_graphql + flake8 setup.py webob_graphql tests [testenv:import-order] -basepython=python3.6 -deps = - isort - graphql-core>=1.0 - WebOb +basepython=python3.8 +deps = -e.[dev] +commands = + isort -rc webob_graphql/ tests/ + +[testenv:manifest] +basepython = python3.8 +deps = -e.[dev] commands = - isort --check-only webob_graphql/ -rc + check-manifest -v \ No newline at end of file diff --git a/webob_graphql/__init__.py b/webob_graphql/__init__.py index ae2a96a..5ad91db 100644 --- a/webob_graphql/__init__.py +++ b/webob_graphql/__init__.py @@ -1,3 +1,3 @@ -from .base import serve_graphql_request +from graphql_server.webob import GraphQLView -__all__ = ['serve_graphql_request'] +__all__ = ["GraphQLView"] diff --git a/webob_graphql/base.py b/webob_graphql/base.py deleted file mode 100644 index 3b6710c..0000000 --- a/webob_graphql/base.py +++ /dev/null @@ -1,112 +0,0 @@ -from functools import partial - -from webob import Response - -from graphql_server import (HttpQueryError, default_format_error, - encode_execution_results, json_encode, - load_json_body, run_http_query) - -from .render_graphiql import render_graphiql as default_render_graphiql - - -def serve_graphql_request(request, schema, pretty=None, response_class=None, graphiql_enabled=True, - batch_enabled=False, render_graphiql=None, format_error=None, encode=None, charset=None, **execute_options): - if format_error is None: - format_error = default_format_error - - if encode is None: - encode = json_encode - - if response_class is None: - response_class = Response - - if render_graphiql is None: - render_graphiql = default_render_graphiql - - if charset is None: - charset = 'UTF-8' - - try: - request_method = request.method.lower() - data = parse_body(request) - - show_graphiql = graphiql_enabled and request_method == 'get' and should_display_graphiql(request) - catch = show_graphiql - - pretty = pretty or show_graphiql or request.params.get('pretty') - - execution_results, all_params = run_http_query( - schema, - request_method, - data, - query_data=request.params, - batch_enabled=batch_enabled, - catch=catch, - # Execute options - **execute_options - ) - - result, status_code = encode_execution_results( - execution_results, - is_batch=isinstance(data, list), - format_error=format_error, - encode=partial(encode, pretty=pretty) - ) - - if show_graphiql: - return response_class( - render_graphiql( - params=all_params[0], - result=result - ), - charset=charset, - content_type='text/html' - ) - - return response_class( - result, - status=status_code, - charset=charset, - content_type='application/json' - ) - - except HttpQueryError as e: - return response_class( - encode({ - 'errors': [format_error(e)] - }), - status=e.status_code, - charset=charset, - headers=e.headers or {}, - content_type='application/json' - ) - - -# noinspection PyBroadException -def parse_body(request): - # We use mimetype here since we don't need the other - # information provided by content_type - content_type = request.content_type - 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.params - - return {} - - -def should_display_graphiql(request): - if 'raw' in request.params: - return False - - return request_wants_html(request) - - -def request_wants_html(request): - best = request.accept \ - .best_match(['application/json', 'text/html']) - return best == 'text/html' diff --git a/webob_graphql/mako.py b/webob_graphql/mako.py deleted file mode 100644 index 0fab204..0000000 --- a/webob_graphql/mako.py +++ /dev/null @@ -1,143 +0,0 @@ -from __future__ import absolute_import - -from mako.template import Template - -from .utils import tojson - -GRAPHIQL_VERSION = '0.10.2' - -TEMPLATE = Template(''' - - - - - - - - - - - - - -''') - - -def render_graphiql(params, result, graphiql_version=None, graphiql_template=None, graphql_url=None): - graphiql_version = graphiql_version or GRAPHIQL_VERSION - template = graphiql_template or TEMPLATE - - if result != "null": - result = tojson(result) - - return template.render( - graphiql_version=graphiql_version, - graphql_url=tojson(graphql_url or ''), - result=result, - query=tojson(params and params.query or None), - variables=tojson(params and params.variables or None), - operation_name=tojson(params and params.operation_name or None), - ) diff --git a/webob_graphql/render_graphiql.py b/webob_graphql/render_graphiql.py deleted file mode 100644 index 2b8ef17..0000000 --- a/webob_graphql/render_graphiql.py +++ /dev/null @@ -1,141 +0,0 @@ -from string import Template - -from .utils import tojson - -GRAPHIQL_VERSION = '0.10.2' - -TEMPLATE = Template(''' - - - - - - - - - - - - - -''') - - -def render_graphiql(params, result, graphiql_version=None, graphiql_template=None, graphql_url=None): - graphiql_version = graphiql_version or GRAPHIQL_VERSION - template = graphiql_template or TEMPLATE - - if result != "null": - result = tojson(result) - - return template.substitute( - graphiql_version=graphiql_version, - graphql_url=tojson(graphql_url or ''), - result=result, - query=tojson(params and params.query or None), - variables=tojson(params and params.variables or None), - operation_name=tojson(params and params.operation_name or None), - ) diff --git a/webob_graphql/utils.py b/webob_graphql/utils.py deleted file mode 100644 index 8310bb6..0000000 --- a/webob_graphql/utils.py +++ /dev/null @@ -1,56 +0,0 @@ -import json - -import six - -_slash_escape = '\\/' not in json.dumps('/') - - -def dumps(obj, **kwargs): - """Serialize ``obj`` to a JSON formatted ``str`` by using the application's - configured encoder (:attr:`~webob.WebOb.json_encoder`) if there is an - application on the stack. - This function can return ``unicode`` strings or ascii-only bytestrings by - default which coerce into unicode strings automatically. That behavior by - default is controlled by the ``JSON_AS_ASCII`` configuration variable - and can be overridden by the simplejson ``ensure_ascii`` parameter. - """ - encoding = kwargs.pop('encoding', None) - rv = json.dumps(obj, **kwargs) - if encoding is not None and isinstance(rv, six.text_type): - rv = rv.encode(encoding) - return rv - - -def htmlsafe_dumps(obj, **kwargs): - """Works exactly like :func:`dumps` but is safe for use in ``