Skip to content

Commit

Permalink
Fix multipart/form-data requests
Browse files Browse the repository at this point in the history
Multipart/form-data wasn't working because `boundary` wasn't included in
`content type`. Openapi-core needs `boundary`[1][2] to parse the body.

[1] https://github.com/python-openapi/openapi-core/blob/0.19.0/openapi_core/protocols.py#L50
[2] https://github.com/python-openapi/openapi-core/blob/0.19.0/openapi_core/deserializing/media_types/util.py#L54
  • Loading branch information
am-on authored and zupo committed Mar 11, 2024
1 parent dd15e96 commit 6d47141
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 26 deletions.
85 changes: 77 additions & 8 deletions pyramid_openapi3/tests/test_contenttypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,47 @@
from pyramid.config import Configurator
from pyramid.request import Request
from pyramid.router import Router
from webob.multidict import MultiDict
from webtest.app import TestApp

import tempfile
import typing as t
import unittest


def app(spec: str, view: t.Callable, route: str) -> Router:
def app(spec: str) -> Router:
"""Prepare a Pyramid app."""

def foo_view(request: Request) -> t.Dict[str, str]:
"""Return reversed string."""
return {"bar": request.openapi_validated.body["bar"][::-1]}

def multipart_view(request: Request) -> t.Dict[str, t.Union[str, t.List[str]]]:
"""Return reversed string."""
body = request.openapi_validated.body
return {
"key1": body["key1"][::-1],
"key2": [x[::-1] for x in body["key2"]],
"key3": body["key3"].decode("utf-8")[::-1],
}

with Configurator() as config:
config.include("pyramid_openapi3")
config.pyramid_openapi3_spec(spec)
config.add_route("foo", route)
config.add_view(openapi=True, renderer="json", view=view, route_name="foo")
config.add_route("foo", "/foo")
config.add_view(
openapi=True,
renderer="json",
view=foo_view,
route_name="foo",
)
config.add_route("multipart", "/multipart")
config.add_view(
openapi=True,
renderer="json",
view=multipart_view,
route_name="multipart",
)
return config.make_wsgi_app()


Expand All @@ -32,6 +59,18 @@ def app(spec: str, view: t.Callable, route: str) -> Router:
properties:
bar:
type: string
BarObject:
type: object
properties:
key1:
type: string
key2:
type: array
items:
type: string
key3:
type: string
format: binary
paths:
/foo:
post:
Expand All @@ -46,6 +85,16 @@ def app(spec: str, view: t.Callable, route: str) -> Router:
responses:
200:
description: OK
/multipart:
post:
requestBody:
content:
multipart/form-data:
schema:
$ref: "#/components/schemas/BarObject"
responses:
200:
description: OK
"""


Expand All @@ -56,15 +105,11 @@ def _testapp(self) -> TestApp:
"""Start up the app so that tests can send requests to it."""
from webtest import TestApp

def foo_view(request: Request) -> t.Dict[str, str]:
"""Return reversed string."""
return {"bar": request.openapi_validated.body["bar"][::-1]}

with tempfile.NamedTemporaryFile() as document:
document.write(OPENAPI_YAML.encode())
document.seek(0)

return TestApp(app(document.name, foo_view, "/foo"))
return TestApp(app(document.name))

def test_post_json(self) -> None:
"""Post with `application/json`."""
Expand All @@ -77,3 +122,27 @@ def test_post_form(self) -> None: # pragma: no cover

res = self._testapp().post("/foo", params={"bar": "baz"}, status=200)
self.assertEqual(res.json, {"bar": "zab"})

def test_post_multipart(self) -> None:
"""Post with `multipart/form-data`."""

multi_dict = MultiDict()
multi_dict.add("key1", "value1")
multi_dict.add("key2", "value2.1")
multi_dict.add("key2", "value2.2")
multi_dict.add("key3", b"value3")

res = self._testapp().post(
"/multipart",
multi_dict,
content_type="multipart/form-data",
status=200,
)
self.assertEqual(
res.json,
{
"key1": "1eulav",
"key2": ["1.2eulav", "2.2eulav"],
"key3": "3eulav",
},
)
16 changes: 0 additions & 16 deletions pyramid_openapi3/tests/test_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from pyramid.testing import DummyRequest
from pyramid_openapi3.wrappers import PyramidOpenAPIRequest
from pyramid_openapi3.wrappers import PyramidOpenAPIResponse
from webob.multidict import MultiDict


@dataclass
Expand Down Expand Up @@ -77,21 +76,6 @@ def test_relative_app_request() -> None:
assert openapi_request.content_type == "text/html"


def test_form_data_request() -> None:
"""Test that request.POST is used as the body in case of form-data."""
multi_dict = MultiDict()
multi_dict.add("key1", "value1")
multi_dict.add("key2", "value2.1")
multi_dict.add("key2", "value2.2")
pyramid_request = DummyRequest(path="/foo", post=multi_dict)
pyramid_request.matched_route = DummyRoute(name="foo", pattern="/foo")
pyramid_request.content_type = "multipart/form-data"

openapi_request = PyramidOpenAPIRequest(pyramid_request)

assert openapi_request.body == {"key1": "value1", "key2": ["value2.1", "value2.2"]}


def test_no_matched_route() -> None:
"""Test path_pattern when no route is matched."""
pyramid_request = DummyRequest(path="/foo")
Expand Down
8 changes: 6 additions & 2 deletions pyramid_openapi3/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ def method(self) -> str:
@property
def body(self) -> t.Optional[t.Union[bytes, str, t.Dict]]:
"""The request body.""" # noqa D401
if "multipart/form-data" == self.request.content_type:
return self.request.POST.mixed()
return self.request.body

@property
def content_type(self) -> str:
"""The content type of the request.""" # noqa D401
if "multipart/form-data" == self.request.content_type:
# Pyramid does not include boundary in request.content_type, but
# openapi-core needs it to parse the request body.
return self.request.headers.environ.get(
"CONTENT_TYPE", "multipart/form-data"
)
return self.request.content_type

@property
Expand Down

0 comments on commit 6d47141

Please sign in to comment.