From 64178bf6cf4d950ca5c509086284f495b2e16869 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 4 Dec 2021 20:10:38 -0500 Subject: [PATCH 01/12] Added a framework independent top level testing module. --- README.md | 59 +++--- README.rst | 266 ------------------------- graphene_file_upload/django/testing.py | 139 +------------ graphene_file_upload/flask/testing.py | 12 ++ graphene_file_upload/testing.py | 134 +++++++++++++ setup.py | 66 +++--- tests/test_django.py | 89 ++++----- tests/test_flask.py | 66 ++++-- tox.ini | 2 +- 9 files changed, 315 insertions(+), 518 deletions(-) delete mode 100644 README.rst create mode 100644 graphene_file_upload/flask/testing.py create mode 100644 graphene_file_upload/testing.py diff --git a/README.md b/README.md index 4624ece..651b1e0 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,7 @@ It currently supports Python 2.7 and 3.4+. ## Installation: ```shell script - pip install graphene-file-upload - ``` ## Usage @@ -24,6 +22,7 @@ To add an upload type to your mutation, import and use `Upload`. Upload is a scalar type. ```python +import graphene from graphene_file_upload.scalars import Upload class UploadMutation(graphene.Mutation): @@ -44,10 +43,11 @@ To use, import the view, then add to your list of urls (replace previous GraphQL view). ```python +from django.urls import path from graphene_file_upload.django import FileUploadGraphQLView urlpatterns = [ - url(r'^graphql', FileUploadGraphQLView.as_view(graphiql=True)), + path(r'^graphql', FileUploadGraphQLView.as_view(graphiql=True)), ] ``` @@ -60,8 +60,11 @@ writing this README, you must install `flask-graphql` with Simply import the modified view and create a new url rule on your app: ```python +from flask import Flask from graphene_file_upload.flask import FileUploadGraphQLView +app = Flask(__name__) +app.debug = True app.add_url_rule( '/graphql', view_func=FileUploadGraphQLView.as_view( @@ -76,7 +79,7 @@ app.add_url_rule( https://flask.palletsprojects.com/en/1.1.x/testing/#the-testing-skeleton -```py +```python # Create a fixture using the file_graphql_query helper and `client` fixture. import os import json @@ -84,7 +87,7 @@ import tempfile from flaskr import flaskr import pytest -from graphene_file_upload.flask.testing import file_graphql_query +from graphene_file_upload.testing import file_graphql_query @pytest.fixture @@ -110,22 +113,27 @@ def client_query(client): # Test your query using the client_query fixture def test_some_query(client_query): - test_file = SimpleUploadedFile(name='test.txt', content=file_text.encode('utf-8')) + with tempfile.NamedTemporaryFile() as test_file: + test_file.write(b"test") + test_file.seek(0) - response = client_query( - ''' - mutation testMutation($file: Upload!) { - myUpload(fileIn: $file) { - ok + query = ''' + mutation testMutation($file: Upload!) { + myUpload(fileIn: $file) { + ok + } } - } - ''', - op_name='testMutation' - files={'file': test_file}, - ) - - content = json.loads(response.content) - assert 'errors' not in content + ''' + + response = file_graphql_query( + query, + op_name='testMutation', + files={'file': test_file}, + client=client, + ) + + content = json.loads(response.content) + assert 'errors' not in content ``` ### Django @@ -141,7 +149,9 @@ To use pytest define a simple fixture using the query helper below import json import pytest -from graphene_file_upload.django.testing import file_graphql_query + +from django.core.files.uploadedfile import SimpleUploadedFile +from graphene_file_upload.testing import file_graphql_query @pytest.fixture def client_query(client): @@ -152,7 +162,7 @@ def client_query(client): # Test your query using the client_query fixture def test_some_query(client_query): - test_file = SimpleUploadedFile(name='test.txt', content=file_text.encode('utf-8')) + test_file = SimpleUploadedFile(name='test.txt', content=b"test") response = client_query( ''' @@ -162,7 +172,7 @@ def test_some_query(client_query): } } ''', - op_name='testMutation' + op_name='testMutation', files={'file': test_file}, ) @@ -175,13 +185,12 @@ def test_some_query(client_query): Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLFileUploadTestCase`. The default endpoint is `GRAPHQL_URL = “/graphql/”`. ```py -import json - +from django.core.files.uploadedfile import SimpleUploadedFile from graphene_file_upload.django.testing import GraphQLFileUploadTestCase class MutationTestCase(GraphQLFileUploadTestCase): def test_some_mutation(self): - test_file = SimpleUploadedFile(name='test.txt', content=file_text.encode('utf-8')) + test_file = SimpleUploadedFile(name='test.txt', content=b"test") response = self.file_query( ''' diff --git a/README.rst b/README.rst deleted file mode 100644 index 101c274..0000000 --- a/README.rst +++ /dev/null @@ -1,266 +0,0 @@ -.. image:: https://travis-ci.com/lmcgartland/graphene-file-upload.svg?branch=master - :target: https://travis-ci.com/lmcgartland/graphene-file-upload - -.. image:: https://badge.fury.io/py/graphene-file-upload.svg - :target: https://badge.fury.io/py/graphene-file-upload - -.. image:: https://static.pepy.tech/personalized-badge/graphene-file-upload?period=month&units=international_system&left_color=grey&right_color=blue&left_text=downloads/month - :target: https://pepy.tech/project/graphene-file-upload - -.. image:: https://img.shields.io/pypi/pyversions/graphene-file-upload - :alt: PyPI - Python Version - -.. image:: https://img.shields.io/pypi/djversions/graphene-file-upload - :alt: PyPI - Django Version - -.. image:: https://img.shields.io/badge/flask%20-%23000.svg?&style=flat&logo=flask&logoColor=white - -graphene-file-upload -==================== - -``graphene-file-upload`` is a drop in replacement for the the GraphQL -view in Graphene for Django, and for Flask-Graphql. - -It supports multi-part file uploads that adhere to the `Multipart Request Spec `_. - -It currently supports Python 2.7 and 3.4+. - -Installation: -------------- - -.. code:: bash - - $ pip install graphene-file-upload - -Usage ------ - -To add an upload type to your mutation, import and use ``Upload``. -Upload is a scalar type. - -.. code:: python - - from graphene_file_upload.scalars import Upload - - class UploadMutation(graphene.Mutation): - class Arguments: - file = Upload(required=True) - - success = graphene.Boolean() - - def mutate(self, info, file, **kwargs): - # do something with your file - - return UploadMutation(success=True) - -Django Integration: -~~~~~~~~~~~~~~~~~~~ - -To use, import the view, then add to your list of urls (replace previous -GraphQL view). - -.. code:: python - - from graphene_file_upload.django import FileUploadGraphQLView - - urlpatterns = [ - url(r'^graphql', FileUploadGraphQLView.as_view(graphiql=True)), - ] - -Flask Integration: -~~~~~~~~~~~~~~~~~~ - -Note that ``flask-graphql`` version ``<2.0`` is not supported. At the -time of writing this README, you must install ``flask-graphql`` with -``pip install --pre flask-graphql`` - -Simply import the modified view and create a new url rule on your app: - -.. code:: python - - from graphene_file_upload.flask import FileUploadGraphQLView - - app.add_url_rule( - '/graphql', - view_func=FileUploadGraphQLView.as_view( - ... - ) - ) - -Testing -------- - -Flask -~~~~~ - -``_ - -.. code:: python - - # Create a fixture using the file_graphql_query helper and `client` fixture. - import os - import json - import tempfile - - from flaskr import flaskr - import pytest - from graphene_file_upload.flask.testing import file_graphql_query - - - @pytest.fixture - def client(): - db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - flaskr.app.config['TESTING'] = True - - with flaskr.app.test_client() as client: - with flaskr.app.app_context(): - flaskr.init_db() - yield client - - os.close(db_fd) - os.unlink(flaskr.app.config['DATABASE']) - - @pytest.fixture - def client_query(client): - def func(*args, **kwargs): - return file_graphql_query(*args, **kwargs, client=client) - - return func - - # Test your query using the client_query fixture - def test_some_query(client_query): - test_file = SimpleUploadedFile(name='test.txt', content=file_text.encode('utf-8')) - - response = client_query( - ''' - mutation testMutation($file: Upload!) { - myUpload(fileIn: $file) { - ok - } - } - ''', - op_name='testMutation' - files={'file': test_file}, - ) - - content = json.loads(response.content) - assert 'errors' not in content - - -Django -~~~~~~ - -Writing test using `django's test client `_ - -Using pytest -############ - -To use pytest define a simple fixture using the query helper below - -.. code:: python - - # Create a fixture using the file_graphql_query helper and ``client` fixture from ``pytest-django``. - - import json - import pytest - from graphene_file_upload.django.testing import file_graphql_query - - @pytest.fixture - def client_query(client): - def func(*args, **kwargs): - return file_graphql_query(*args, **kwargs, client=client) - - return func - - # Test your query using the client_query fixture - def test_some_query(client_query): - test_file = SimpleUploadedFile(name='test.txt', content=file_text.encode('utf-8')) - - response = client_query( - ''' - mutation testMutation($file: Upload!) { - myUpload(fileIn: $file) { - ok - } - } - ''', - op_name='testMutation' - files={'file': test_file}, - ) - - content = json.loads(response.content) - assert 'errors' not in content - - -Using unittest -############## - -Your endpoint is set through the ``GRAPHQL_URL`` attribute on ``GraphQLFileUploadTestCase``. - -The default endpoint is ``GRAPHQL_URL = “/graphql/”``. - -.. code:: python - - import json - - from graphene_file_upload.django.testing import GraphQLFileUploadTestCase - - class MutationTestCase(GraphQLFileUploadTestCase): - def test_some_mutation(self): - test_file = SimpleUploadedFile(name='test.txt', content=file_text.encode('utf-8')) - - response = self.file_query( - ''' - mutation testMutation($file: Upload!) { - myUpload(fileIn: $file) { - ok - } - } - ''', - op_name='testMutation', - files={'file': test_file}, - ) - - # This validates the status code and if you get errors - self.assertResponseNoErrors(response) - - -Contributing: -------------- - -If you'd like to contribute, please run the test suite prior to sending a PR. - -In order to run the testing environment, create a virtual environment, install -tox, and run the tox commands: - -.. code:: bash - - $ python3 -m venv venv - $ source venv/bin/activate - $ make install - # You may have to deactivate and reactivate to have access to the tox command, - # depending on your system. - - # Run the test suite with the versions of python you have installed - $ tox - # Alternatively, if you're using something like pyenv and can easily install - # Multiple versions of python, then try running the following command - $ tox - - # If for some reason you need to recreate the tox environment (e.g. a new - # dependency has been added since you last ran it, add the -r flag to the - # tox command) - $ tox -r {...additional flags...} - -Check out `pyenv -`_ if you'd like a simple way of -installing multiple python versions to test out. - -Packaging for PyPi: -------------------- - -Run - -.. code:: bash - - $ make deploy diff --git a/graphene_file_upload/django/testing.py b/graphene_file_upload/django/testing.py index 94e35a0..27315de 100644 --- a/graphene_file_upload/django/testing.py +++ b/graphene_file_upload/django/testing.py @@ -1,137 +1,14 @@ -"""Use django's Test client to run file based graphql test.""" -import json +""" +Django testing module +""" -from django.test import Client, TestCase +from django.test import TestCase, SimpleTestCase +from graphene_file_upload.testing import GraphQLFileUploadTestMixin -DEFAULT_GRAPHQL_URL = "/graphql/" - -def file_graphql_query( - query, op_name=None, input_data=None, variables=None, - headers=None, files=None, client=None, graphql_url=None, -): - """ - Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/ - - Perform file based mutation. - - :param str query: GraphQL query to run - :param str op_name: If the query is a mutation or named query, you must - supply the op_name. For annon queries ("{ ... }"), should be None (default). - :param dict input_data: If provided, the $input variable in GraphQL will be set - to this value. If both ``input_data`` and ``variables``, - are provided, the ``input`` field in the ``variables`` - dict will be overwritten with this value. Defaults to None. - :param dict variables: If provided, the "variables" field in GraphQL will be - set to this value. Defaults to None. - :param dict headers: If provided, the headers in POST request to GRAPHQL_URL - will be set to this value. Defaults to None - :param dict files: Files to be sent via request.FILES. Defaults to None. - :param django.test.Client client: Test client. Defaults to django.test.Client. - :param str graphql_url: URL to graphql endpoint. Defaults to "/graphql" - :return: Response object from client - """ - - if not files: - raise ValueError('Missing required argument "files".') - - client = client or Client() - headers = headers or {} - variables = variables or {} - graphql_url = graphql_url or DEFAULT_GRAPHQL_URL - map_ = {} - - for key in files.keys(): - map_[key] = ['variables.{key}'.format(key=key)] - if key not in variables: - variables[key] = None - - body = {'query': query} - if op_name: - body['operationName'] = op_name - if variables: - body['variables'] = variables - if input_data: - if 'variables' in body: - body['variables']['input'] = input_data - else: - body['variables'] = {'input': input_data} - - data = { - 'operations': json.dumps(body), - 'map': json.dumps(map_), - } - - data.update(files) - if headers: - resp = client.post(graphql_url, data, **headers) - else: - resp = client.post(graphql_url, data) - - return resp - - -class GraphQLFileUploadTestMixin: - """GraphQL file upload test mixin.""" - - # URL to graphql endpoint - GRAPHQL_URL = DEFAULT_GRAPHQL_URL - - def file_query( - self, query, op_name=None, input_data=None, files=None, - variables=None, headers=None, - ): - """ - Perform file based mutation. - - :param str query: GraphQL query to run - :param str op_name: If the query is a mutation or named query, you must - supply the op_name. For annon queries ("{ ... }"), should be None (default). - :param dict input_data: If provided, the $input variable in GraphQL will be set - to this value. If both ``input_data`` and ``variables``, - are provided, the ``input`` field in the ``variables`` - dict will be overwritten with this value. Defaults to None. - :param dict variables: If provided, the "variables" field in GraphQL will be - set to this value. Defaults to None. - :param dict headers: If provided, the headers in POST request to GRAPHQL_URL - will be set to this value. Defaults to None - :param dict files: Files to be sent via request.FILES. Defaults to None. - :param django.test.Client client: Test client. Defaults to django.test.Client. - :param str graphql_url: URL to graphql endpoint. Defaults to "/graphql" - :return: Response object from client - """ - return file_graphql_query( - query, - op_name=op_name, - input_data=input_data, - variables=variables, - headers=headers, - files=files, - client=self.client, - graphql_url=self.GRAPHQL_URL, - ) - - def assertResponseNoErrors(self, resp, msg=None): # pylint: disable=C0103 - """ - Assert that the call went through correctly. 200 means the syntax is ok, - if there are no `errors`, the call was fine. - :param Response resp: HttpResponse - :param str msg: Error message. - """ - content = json.loads(resp.content) - self.assertEqual(resp.status_code, 200, msg or content) - self.assertNotIn("errors", list(content.keys()), msg or content) - - def assertResponseHasErrors(self, resp, msg=None): # pylint: disable=C0103 - """ - Assert that the call was failing. Take care: Even with errors, - GraphQL returns status 200! - :param Response resp: HttpResponse - :param str msg: Error message. - """ - content = json.loads(resp.content) - self.assertIn("errors", list(content.keys()), msg or content) +class GraphQLFileUploadTestCase(GraphQLFileUploadTestMixin, TestCase): + pass -class GraphQLFileUploadTestCase(GraphQLFileUploadTestMixin, TestCase): +class GraphQLFileUploadSimpleTestCase(GraphQLFileUploadTestMixin, SimpleTestCase): pass diff --git a/graphene_file_upload/flask/testing.py b/graphene_file_upload/flask/testing.py new file mode 100644 index 0000000..7328710 --- /dev/null +++ b/graphene_file_upload/flask/testing.py @@ -0,0 +1,12 @@ +""" +Flask testing module +""" +from flask.testing import FlaskClient +from flask_unittest import ClientTestCase + +from graphene_file_upload.testing import GraphQLFileUploadTestMixin + + +class GraphQLFileUploadTestCase(GraphQLFileUploadTestMixin, ClientTestCase): + def setUp(self, client: FlaskClient) -> None: + self.client = client diff --git a/graphene_file_upload/testing.py b/graphene_file_upload/testing.py new file mode 100644 index 0000000..cfad105 --- /dev/null +++ b/graphene_file_upload/testing.py @@ -0,0 +1,134 @@ +"""Base module for file based graphql test shared by both django and flask.""" + +import json + +DEFAULT_GRAPHQL_URL = "/graphql" + + +def file_graphql_query( + query, op_name=None, input_data=None, variables=None, + headers=None, files=None, client=None, graphql_url=None, +): + """ + Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/ + + Perform file based mutation. + + :param str query: GraphQL query to run + :param str op_name: If the query is a mutation or named query, you must + supply the op_name. For annon queries ("{ ... }"), should be None (default). + :param dict input_data: If provided, the $input variable in GraphQL will be set + to this value. If both ``input_data`` and ``variables``, + are provided, the ``input`` field in the ``variables`` + dict will be overwritten with this value. Defaults to None. + :param dict variables: If provided, the "variables" field in GraphQL will be + set to this value. Defaults to None. + :param dict headers: If provided, the headers in POST request to GRAPHQL_URL + will be set to this value. Defaults to None + :param dict files: Files to be sent via request.FILES. Defaults to None. + :param django.test.Client client: Test client. Defaults to django.test.Client. + :param str graphql_url: URL to graphql endpoint. Defaults to "/graphql" + :return: Response object from client + """ + + if not files: + raise ValueError('Missing required argument: "files".') + + if not client: + raise ValueError('Missing required argument: "client".') + + headers = headers or {} + variables = variables or {} + graphql_url = graphql_url or DEFAULT_GRAPHQL_URL + map_ = {} + + for key in files.keys(): + map_[key] = ['variables.{key}'.format(key=key)] + if key not in variables: + variables[key] = None + + body = {'query': query} + if op_name: + body['operationName'] = op_name + if variables: + body['variables'] = variables + if input_data: + if 'variables' in body: + body['variables']['input'] = input_data + else: + body['variables'] = {'input': input_data} + + data = { + 'operations': json.dumps(body), + 'map': json.dumps(map_), + } + + data.update(files) + if headers: + resp = client.post(graphql_url, data=data, **headers) + else: + resp = client.post(graphql_url, data=data) + + return resp + + +class GraphQLFileUploadTestMixin: + """GraphQL file upload test mixin.""" + + # URL to graphql endpoint + GRAPHQL_URL = DEFAULT_GRAPHQL_URL + + def file_query( + self, query, op_name=None, input_data=None, files=None, + variables=None, headers=None, + ): + """ + Perform file based mutation. + + :param str query: GraphQL query to run + :param str op_name: If the query is a mutation or named query, you must + supply the op_name. For annon queries ("{ ... }"), should be None (default). + :param dict input_data: If provided, the $input variable in GraphQL will be set + to this value. If both ``input_data`` and ``variables``, + are provided, the ``input`` field in the ``variables`` + dict will be overwritten with this value. Defaults to None. + :param dict variables: If provided, the "variables" field in GraphQL will be + set to this value. Defaults to None. + :param dict headers: If provided, the headers in POST request to GRAPHQL_URL + will be set to this value. Defaults to None + :param dict files: Files to be sent via request.FILES. Defaults to None. + :param django.test.Client client: Test client. Defaults to django.test.Client. + :param str graphql_url: URL to graphql endpoint. Defaults to "/graphql" + :return: Response object from client + """ + return file_graphql_query( + query, + op_name=op_name, + input_data=input_data, + variables=variables, + headers=headers, + files=files, + client=self.client, + graphql_url=self.GRAPHQL_URL, + ) + + def assertResponseNoErrors(self, resp, msg=None): # pylint: disable=C0103 + """ + Assert that the call went through correctly. 200 means the syntax is ok, + if there are no `errors`, the call was fine. + :param Response resp: HttpResponse + :param str msg: Error message. + """ + content = json.loads(resp.content) + self.assertEqual(resp.status_code, 200, msg or content) + self.assertNotIn("errors", list(content.keys()), msg or content) + + def assertResponseHasErrors(self, resp, msg=None): # pylint: disable=C0103 + """ + Assert that the call was failing. Take care: Even with errors, + GraphQL returns status 200! + :param Response resp: HttpResponse + :param str msg: Error message. + """ + content = json.loads(resp.content) + self.assertIn("errors", list(content.keys()), msg or content) diff --git a/setup.py b/setup.py index 02a3ed2..e8498f2 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,6 @@ # Always prefer setuptools over distutils +import io + from setuptools import setup, find_packages # To use a consistent encoding from codecs import open @@ -6,47 +8,51 @@ here = path.abspath(path.dirname(__file__)) -# Get the long description from the README file -# try: - # import pypandoc - # long_description = pypandoc.convert('README.md', 'rst') -# except(IOError, ImportError): -# long_description = open('README.md').read() +BASE_DIR = path.dirname(__file__) +README_PATH = path.join(BASE_DIR, "README.md") +LONG_DESCRIPTION_TYPE = "text/markdown" -long_description = open('README.rst').read() +if path.isfile(README_PATH): + with io.open(README_PATH, encoding="utf-8") as f: + LONG_DESCRIPTION = f.read() +else: + LONG_DESCRIPTION = "" flask_requires = [ - 'Flask>=1.0.2', - 'graphene>=2.1.2', - 'Flask-Graphql>=2.0.0', + "Flask>=1.0.2", + "graphene>=2.1.2", + "Flask-Graphql>=2.0.0", + "flask_unittest>=0.1.2", ] django_requires = [ - 'graphene-django>=2.0.0', + "django", + "graphene-django>=2.0.0", ] all_requires = flask_requires + django_requires tests_require = [ - 'coverage', - 'pytest', - 'pytest-cov', - 'pytest-django' + "coverage", + "pytest", + "pytest-cov", + "pytest-django", + "parameterized", ] setup( - name='graphene_file_upload', + name="graphene_file_upload", packages=find_packages(exclude=["tests"]), - version='1.3.0', - description='Lib for adding file upload functionality to GraphQL mutations in Graphene Django and Flask-Graphql', - long_description=long_description, - long_description_content_type='text/x-rst', - author='Lucas McGartland', - author_email='lucasmcgartland@gmail.com', - url='https://github.com/lmcgartland/graphene-file-upload', # use the URL to the github repo - # download_url = 'https://github.com/lmcgartland/graphene-file-upload/archive/0.1.0.tar.gz', - keywords=['graphql', 'graphene', 'apollo', 'upload'], # arbitrary keywords + version="1.3.0", + description="Lib for adding file upload functionality to GraphQL mutations in Graphene Django and Flask-Graphql", + long_description=LONG_DESCRIPTION, + long_description_content_type=LONG_DESCRIPTION_TYPE, + author="Lucas McGartland", + author_email="lucasmcgartland@gmail.com", + url="https://github.com/lmcgartland/graphene-file-upload", # use the URL to the github repo + # download_url = "https://github.com/lmcgartland/graphene-file-upload/archive/0.1.0.tar.gz", + keywords=["graphql", "graphene", "apollo", "upload"], # arbitrary keywords install_requires=[ - 'six>=1.11.0', + "six>=1.11.0", ], tests_require=tests_require, classifiers=[ @@ -75,9 +81,9 @@ "Framework :: Flask" ], extras_require={ - 'flask': flask_requires, - 'django': django_requires, - 'all': all_requires, - 'tests': tests_require, + "flask": flask_requires, + "django": django_requires, + "all": all_requires, + "tests": tests_require, }, ) diff --git a/tests/test_django.py b/tests/test_django.py index 3e800cf..283d0c3 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -2,8 +2,11 @@ import pytest from django.core.files.uploadedfile import SimpleUploadedFile +from parameterized import parameterized from graphene_file_upload.django import FileUploadGraphQLView +from graphene_file_upload.django.testing import GraphQLFileUploadSimpleTestCase +from graphene_file_upload.testing import file_graphql_query from tests.schema import schema try: @@ -20,52 +23,6 @@ def response_utf8_json(resp): path('graphql', FileUploadGraphQLView.as_view(schema=schema), name='graphql'), ] -@pytest.mark.parametrize( - 'client,file_text,expected_first_line', - ( - (None, u'Fake Data\nLine2\n', u'Fake Data'), - # Try the fire emoji - (None, u'\U0001F525\nLine2\nLine3\n', u'\U0001F525'), - ), - indirect=['client'] -) -def test_upload(client, file_text, expected_first_line): - query = ''' - mutation testMutation($file: Upload!) { - myUpload(fileIn: $file) { - ok - firstLine - } - } - ''' - - t_file = SimpleUploadedFile(name='test.txt', content=file_text.encode('utf-8')) - - response = client.post( - '/graphql', - data={ - 'operations': json.dumps({ - 'query': query, - 'variables': { - 'file': None, - }, - }), - 't_file': t_file, - 'map': json.dumps({ - 't_file': ['variables.file'], - }), - } - ) - assert response.status_code == 200 - assert response_utf8_json(response) == { - 'data': { - 'myUpload': { - 'ok': True, - 'firstLine': expected_first_line, - }, - } - } - @pytest.mark.parametrize( 'client,file_text,expected_first_line', @@ -76,9 +33,7 @@ def test_upload(client, file_text, expected_first_line): ), indirect=['client'] ) -def test_file_graphql_query(client, file_text, expected_first_line): - from graphene_file_upload.django.testing import file_graphql_query - +def test_upload(client, file_text, expected_first_line): query = ''' mutation testMutation($file: Upload!) { myUpload(fileIn: $file) { @@ -95,7 +50,6 @@ def test_file_graphql_query(client, file_text, expected_first_line): op_name='testMutation', files={'file': t_file}, client=client, - graphql_url='/graphql', ) assert response.status_code == 200 @@ -107,3 +61,38 @@ def test_file_graphql_query(client, file_text, expected_first_line): }, } } + + +class MyUploadTestCase(GraphQLFileUploadSimpleTestCase): + @parameterized.expand([ + (u'Fake Data\nLine2\n', u'Fake Data'), + # Try the fire emoji + (u'\U0001F525\nLine2\nLine3\n', u'\U0001F525'), + ]) + def test_upload(self, file_text, expected_first_line): + query = ''' + mutation testMutation($file: Upload!) { + myUpload(fileIn: $file) { + ok + firstLine + } + } + ''' + + t_file = SimpleUploadedFile(name='test.txt', content=file_text.encode('utf-8')) + + response = self.file_query( + query, + op_name='testMutation', + files={'file': t_file}, + ) + + assert response.status_code == 200 + assert response_utf8_json(response) == { + 'data': { + 'myUpload': { + 'ok': True, + 'firstLine': expected_first_line, + }, + } + } diff --git a/tests/test_flask.py b/tests/test_flask.py index 81d04ad..113457c 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -2,7 +2,11 @@ from tempfile import NamedTemporaryFile import pytest +from flask.testing import FlaskClient +from parameterized import parameterized +from graphene_file_upload.flask.testing import GraphQLFileUploadTestCase +from graphene_file_upload.testing import file_graphql_query from .flask_app import create_app from .schema import schema @@ -27,7 +31,7 @@ def client(): ), indirect=['client'] ) -def test_single_file(client, file_text, expected_first_line): +def test_upload(client, file_text, expected_first_line): query = ''' mutation testMutation($file: Upload!) { myUpload(fileIn: $file) { @@ -39,21 +43,14 @@ def test_single_file(client, file_text, expected_first_line): with NamedTemporaryFile() as t_file: t_file.write(file_text.encode('utf-8')) t_file.seek(0) - response = client.post( - '/graphql', - data={ - 'operations': json.dumps({ - 'query': query, - 'variables': { - 'file': None, - }, - }), - 't_file': t_file, - 'map': json.dumps({ - 't_file': ['variables.file'], - }), - } + + response = file_graphql_query( + query, + op_name='testMutation', + files={'file': t_file}, + client=client, ) + assert response.status_code == 200 assert response_utf8_json(response) == { 'data': { @@ -63,3 +60,42 @@ def test_single_file(client, file_text, expected_first_line): }, } } + + +class MyUploadTestCase(GraphQLFileUploadTestCase): + app = create_app(schema=schema) + + @parameterized.expand([ + (u'Fake Data\nLine2\n', u'Fake Data'), + # Try the fire emoji + (u'\U0001F525\nLine2\nLine3\n', u'\U0001F525'), + ]) + def test_upload(self, client, file_text, expected_first_line): + query = ''' + mutation testMutation($file: Upload!) { + myUpload(fileIn: $file) { + ok + firstLine + } + } + ''' + + with NamedTemporaryFile() as t_file: + t_file.write(file_text.encode('utf-8')) + t_file.seek(0) + + response = self.file_query( + query, + op_name='testMutation', + files={'file': t_file}, + ) + + assert response.status_code == 200 + assert response_utf8_json(response) == { + 'data': { + 'myUpload': { + 'ok': True, + 'firstLine': expected_first_line, + }, + } + } diff --git a/tox.ini b/tox.ini index 147ba21..44ac021 100644 --- a/tox.ini +++ b/tox.ini @@ -28,5 +28,5 @@ deps = py3{4,5,6,7,8,9}: pylint commands = -coverage erase - py3{4,5,6,7,8,9}: pylint graphene_file_upload + py3{4,5,6,7,8,9}: pylint graphene_file_upload --disable=C0209 py.test --cov=graphene_file_upload -v tests/ From c9520b6ed3cb78f4dc608eed5df00d40116d3242 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 4 Dec 2021 20:25:02 -0500 Subject: [PATCH 02/12] Updated README.md --- README.md | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 651b1e0..8882c79 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Upload is a scalar type. import graphene from graphene_file_upload.scalars import Upload + class UploadMutation(graphene.Mutation): class Arguments: file = Upload(required=True) @@ -46,6 +47,7 @@ GraphQL view). from django.urls import path from graphene_file_upload.django import FileUploadGraphQLView + urlpatterns = [ path(r'^graphql', FileUploadGraphQLView.as_view(graphiql=True)), ] @@ -63,6 +65,7 @@ Simply import the modified view and create a new url rule on your app: from flask import Flask from graphene_file_upload.flask import FileUploadGraphQLView + app = Flask(__name__) app.debug = True app.add_url_rule( @@ -104,15 +107,7 @@ def client(): os.unlink(flaskr.app.config['DATABASE']) -@pytest.fixture -def client_query(client): - def func(*args, **kwargs): - return file_graphql_query(*args, **kwargs, client=client) - - return func - -# Test your query using the client_query fixture -def test_some_query(client_query): +def test_some_query(client): with tempfile.NamedTemporaryFile() as test_file: test_file.write(b"test") test_file.seek(0) @@ -148,23 +143,15 @@ To use pytest define a simple fixture using the query helper below # Create a fixture using the file_graphql_query helper and `client` fixture from `pytest-django`. import json -import pytest from django.core.files.uploadedfile import SimpleUploadedFile from graphene_file_upload.testing import file_graphql_query -@pytest.fixture -def client_query(client): - def func(*args, **kwargs): - return file_graphql_query(*args, **kwargs, client=client) - - return func -# Test your query using the client_query fixture -def test_some_query(client_query): +def test_some_query(client): test_file = SimpleUploadedFile(name='test.txt', content=b"test") - response = client_query( + response = file_graphql_query( ''' mutation testMutation($file: Upload!) { myUpload(fileIn: $file) { @@ -174,6 +161,7 @@ def test_some_query(client_query): ''', op_name='testMutation', files={'file': test_file}, + client=client, ) content = json.loads(response.content) @@ -182,13 +170,13 @@ def test_some_query(client_query): #### Using unittest -Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLFileUploadTestCase`. The default endpoint is `GRAPHQL_URL = “/graphql/”`. +Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLFileUploadTestCase`. The default endpoint is `GRAPHQL_URL = “/graphql”`. ```py from django.core.files.uploadedfile import SimpleUploadedFile -from graphene_file_upload.django.testing import GraphQLFileUploadTestCase +from graphene_file_upload.django.testing import GraphQLFileUploadSimpleTestCase -class MutationTestCase(GraphQLFileUploadTestCase): +class MutationTestCase(GraphQLFileUploadSimpleTestCase): def test_some_mutation(self): test_file = SimpleUploadedFile(name='test.txt', content=b"test") From 374f8f2678aae7897c2236bcfcb9e30ce18b5e48 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 4 Dec 2021 21:20:13 -0500 Subject: [PATCH 03/12] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 8882c79..fd2c500 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,6 @@ Writing test using [django's test client](https://docs.djangoproject.com/en/3.1/ To use pytest define a simple fixture using the query helper below ```py -# Create a fixture using the file_graphql_query helper and `client` fixture from `pytest-django`. - import json from django.core.files.uploadedfile import SimpleUploadedFile From 987a3b750dacd89cb8f64c9650dda78300bda513 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 4 Dec 2021 21:21:46 -0500 Subject: [PATCH 04/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd2c500..ca1a34a 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ from graphene_file_upload.django import FileUploadGraphQLView urlpatterns = [ - path(r'^graphql', FileUploadGraphQLView.as_view(graphiql=True)), + path('graphql', FileUploadGraphQLView.as_view(graphiql=True)), ] ``` From 05b95ebf155a5e6293102bd42eb2ca85a8e9f76c Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sat, 29 Jan 2022 23:51:49 -0500 Subject: [PATCH 05/12] Delete .travis.yml --- .travis.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 00a4d54..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -matrix: - include: - - python: 2.7 - env: TOXENV=py27 - - python: 3.4 - env: TOXENV=py34 - - python: 3.5 - env: TOXENV=py35 - - python: 3.6 - env: TOXENV=py36 - - python: 3.7 - env: TOXENV=py37 - - python: 3.8 - env: TOXENV=py38 - - python: 3.9 - env: TOXENV=py39 - - python: pypy - env: TOXENV=pypy -install: - - pip install tox -script: - - tox From e1796906461a69d2eb6309f104f03f655ebed4b7 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sun, 30 Jan 2022 00:00:58 -0500 Subject: [PATCH 06/12] Create python-app.yml --- .github/workflows/python-app.yml | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/python-app.yml diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..3d939a6 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,51 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: true + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10.0-beta.1'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/cache@v2 + id: pip-cache + with: + path: ~/.cache/pip + key: ${{ runner.os }}-${{ matrix.platform }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.platform }}-pip-${{ matrix.python-version }}- + + - name: Install dependencies + run: | + pip install tox + + - name: Run test + run: | + tox + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + PLATFORM: ${{ matrix.platform }} + + - name: "Upload coverage to Codecov" + uses: codecov/codecov-action@v2.1.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true From c0c642f6a6043cfb6bc412cfd434e0e9e8548f1d Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sun, 30 Jan 2022 00:01:51 -0500 Subject: [PATCH 07/12] Create test.yml --- .github/workflows/test.yml | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3d939a6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: true + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10.0-beta.1'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2.3.1 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/cache@v2 + id: pip-cache + with: + path: ~/.cache/pip + key: ${{ runner.os }}-${{ matrix.platform }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.platform }}-pip-${{ matrix.python-version }}- + + - name: Install dependencies + run: | + pip install tox + + - name: Run test + run: | + tox + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + PLATFORM: ${{ matrix.platform }} + + - name: "Upload coverage to Codecov" + uses: codecov/codecov-action@v2.1.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true From be4dd336e18e061f0fb255cbe213c7bd609a216c Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sun, 30 Jan 2022 00:02:04 -0500 Subject: [PATCH 08/12] Delete python-app.yml --- .github/workflows/python-app.yml | 51 -------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 .github/workflows/python-app.yml diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml deleted file mode 100644 index 3d939a6..0000000 --- a/.github/workflows/python-app.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Test - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - build: - runs-on: ${{ matrix.platform }} - strategy: - fail-fast: true - matrix: - platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10.0-beta.1'] - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.3.1 - with: - python-version: ${{ matrix.python-version }} - - - uses: actions/cache@v2 - id: pip-cache - with: - path: ~/.cache/pip - key: ${{ runner.os }}-${{ matrix.platform }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.platform }}-pip-${{ matrix.python-version }}- - - - name: Install dependencies - run: | - pip install tox - - - name: Run test - run: | - tox - env: - CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} - PLATFORM: ${{ matrix.platform }} - - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v2.1.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true From e2512f4044e9c9bfbdbb58ea24213967ba004ca6 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sun, 30 Jan 2022 00:04:35 -0500 Subject: [PATCH 09/12] Update test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d939a6..33f8dd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10.0-beta.1'] + python-version: [2.7.x, 3.4.x, 3.5.x, 3.6.x, 3.7.x, 3.8.x, 3.9.x, '3.10.0-beta.1'] steps: - uses: actions/checkout@v2 From 8b026b88dc4839c1b1768fa460566e249bdb0fc2 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sun, 30 Jan 2022 00:10:43 -0500 Subject: [PATCH 10/12] Update test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33f8dd9..9f19135 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: [2.7.x, 3.4.x, 3.5.x, 3.6.x, 3.7.x, 3.8.x, 3.9.x, '3.10.0-beta.1'] + python-version: [2.7.18, 3.4.10, 3.5.10, 3.6.15, 3.7.12, 3.8.12, 3.9.10, 3.10.2, '3.11.0-alpha.4'] steps: - uses: actions/checkout@v2 From 34f62e7635a5b8bc059051aaf7ba47656f153781 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sun, 30 Jan 2022 00:14:29 -0500 Subject: [PATCH 11/12] Update test.yml --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f19135..13633bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,11 +10,10 @@ on: jobs: build: - runs-on: ${{ matrix.platform }} + runs-on: ubuntu-latest strategy: fail-fast: true matrix: - platform: [ubuntu-latest, macos-latest, windows-latest] python-version: [2.7.18, 3.4.10, 3.5.10, 3.6.15, 3.7.12, 3.8.12, 3.9.10, 3.10.2, '3.11.0-alpha.4'] steps: From 055e02a84d67463e4f81e7446337dabf13f55cea Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Sun, 30 Jan 2022 00:15:12 -0500 Subject: [PATCH 12/12] Update test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 13633bb..4a28ea2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: build: runs-on: ubuntu-latest strategy: - fail-fast: true + fail-fast: false matrix: python-version: [2.7.18, 3.4.10, 3.5.10, 3.6.15, 3.7.12, 3.8.12, 3.9.10, 3.10.2, '3.11.0-alpha.4']