diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..d879e7212d --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,4 @@ +Release type: patch + +Fix bug where files would be converted into io.BytesIO when using the sanic GraphQLView +instead of using the sanic File type diff --git a/strawberry/sanic/utils.py b/strawberry/sanic/utils.py index 1d78c09118..885a02c406 100644 --- a/strawberry/sanic/utils.py +++ b/strawberry/sanic/utils.py @@ -1,6 +1,5 @@ from __future__ import annotations -from io import BytesIO from typing import TYPE_CHECKING, Any, Optional, Union, cast from sanic.request import File @@ -10,7 +9,7 @@ def convert_request_to_files_dict(request: Request) -> dict[str, Any]: - """Converts the request.files dictionary to a dictionary of BytesIO objects. + """Converts the request.files dictionary to a dictionary of sanic Request objects. `request.files` has the following format, even if only a single file is uploaded: @@ -29,12 +28,12 @@ def convert_request_to_files_dict(request: Request) -> dict[str, Any]: if not request_files: return {} - files_dict: dict[str, Union[BytesIO, list[BytesIO]]] = {} + files_dict: dict[str, Union[File, list[File]]] = {} for field_name, file_list in request_files.items(): assert len(file_list) == 1 - files_dict[field_name] = BytesIO(file_list[0].body) + files_dict[field_name] = file_list[0] return files_dict diff --git a/tests/sanic/__init__.py b/tests/sanic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/sanic/test_file_upload.py b/tests/sanic/test_file_upload.py new file mode 100644 index 0000000000..24c3a38d1d --- /dev/null +++ b/tests/sanic/test_file_upload.py @@ -0,0 +1,94 @@ +from io import BytesIO +from typing import cast + +import pytest +from sanic_testing.testing import SanicTestClient + +import strawberry +from sanic import Sanic +from sanic.request import File +from strawberry.file_uploads import Upload +from strawberry.sanic import utils +from strawberry.sanic.views import GraphQLView + + +@strawberry.type +class Query: + @strawberry.field + def index(self) -> str: + return "Hello there" + + +@strawberry.type +class Mutation: + @strawberry.mutation + def file_upload(self, file: Upload) -> str: + return cast(File, file).name + + +@pytest.fixture +def app(): + sanic_app = Sanic("sanic_testing") + + sanic_app.add_route( + GraphQLView.as_view( + schema=strawberry.Schema(query=Query, mutation=Mutation), + multipart_uploads_enabled=True, + ), + "/graphql", + ) + + return sanic_app + + +def test_file_cast(app: Sanic): + """Tests that the list of files in a sanic Request gets correctly turned into a dictionary""" + file_name = "test.txt" + + file_content = b"Hello, there!." + in_memory_file = BytesIO(file_content) + in_memory_file.name = file_name + + form_data = { + "operations": '{ "query": "mutation($file: Upload!){ fileUpload(file: $file) }", "variables": { "file": null } }', + "map": '{ "file": ["variables.file"] }', + } + + files = { + "file": in_memory_file, + } + + request, _ = cast(SanicTestClient, app.test_client).post( + "/graphql", data=form_data, files=files + ) + + files = utils.convert_request_to_files_dict(request) # type: ignore + file = files["file"] + + assert isinstance(file, File) + assert file.name == file_name + assert file.body == file_content + + +def test_endpoint(app: Sanic): + """Tests that the graphql api correctly handles file upload and processing""" + file_name = "test.txt" + + file_content = b"Hello, there!" + in_memory_file = BytesIO(file_content) + in_memory_file.name = file_name + + form_data = { + "operations": '{ "query": "mutation($file: Upload!){ fileUpload(file: $file) }", "variables": { "file": null } }', + "map": '{ "file": ["variables.file"] }', + } + + files = { + "file": in_memory_file, + } + + _, response = cast(SanicTestClient, app.test_client).post( + "/graphql", data=form_data, files=files + ) + + assert response.json["data"]["fileUpload"] == file_name # type: ignore