Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9e9e3cd
tried to add support for messagepack
arthurprioli Oct 18, 2024
8deb751
added documentation
arthurprioli Oct 20, 2024
97a5a2a
fixed ruff formatting
arthurprioli Oct 20, 2024
58e251d
Merge branch 'master' into 1026-add-messsagepack-support
arthurprioli Oct 28, 2024
2cea4f6
tests added
arthurprioli Oct 30, 2024
f911793
inserted changes asked by code review
arthurprioli Oct 30, 2024
aa6edf2
fixed overidented docstrings
arthurprioli Oct 30, 2024
37d702e
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 Nov 6, 2024
4db15eb
Merge branch 'master' into 1026-add-messsagepack-support
arthurprioli Nov 10, 2024
918ba30
added more tests, fixed documentation and wrote better client.py
arthurprioli Nov 20, 2024
ef7a39d
Merge branch '1026-add-messsagepack-support' of https://github.com/ar…
arthurprioli Nov 20, 2024
f226b5b
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 Nov 22, 2024
f562383
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 Dec 30, 2024
41965f6
removed diffs from newsfragment
arthurprioli Jan 3, 2025
eab724a
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 Jan 4, 2025
9a4aded
added new test function for msgpack parameters and fixed small bugs
arthurprioli Jan 7, 2025
03bb7c0
added and rendered towncrier docs
arthurprioli Jan 7, 2025
8fdf4aa
added tests on content body
arthurprioli Jan 8, 2025
bdb36e2
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 Feb 8, 2025
0f5bb82
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 Mar 14, 2025
87e8e2d
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 Mar 18, 2025
7993bad
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 May 17, 2025
87f984d
Merge branch 'master' into 1026-add-messsagepack-support
vytas7 Aug 13, 2025
6403d7b
Merge branch 'master' into 1026-add-messsagepack-support
arthurprioli Nov 20, 2025
46708ca
Moved msgpack to the end of the parameter list. And fixed conflicts.
arthurprioli Nov 20, 2025
97b52c8
Merge branch 'master' into 1026-add-messsagepack-support
arthurprioli Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/_newsfragments/1026.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The :func:`~falcon.testing.simulate_request` now suports ``msgpack``
and returns Content-Type as ``MEDIA_MSGPACK`` in a similar way that
was made to JSON parameters.
57 changes: 57 additions & 0 deletions falcon/testing/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@
from falcon.asgi_spec import ScopeType
from falcon.constants import COMBINED_METHODS
from falcon.constants import MEDIA_JSON
from falcon.constants import MEDIA_MSGPACK
from falcon.errors import CompatibilityError
from falcon.media import MessagePackHandler
from falcon.testing import helpers
from falcon.testing.srmock import StartResponseMock
from falcon.typing import Headers
Expand Down Expand Up @@ -490,6 +492,7 @@ def simulate_request(
cookies: CookieArg | None = None,
asgi_chunk_size: int = 4096,
asgi_disconnect_ttl: int = 300,
msgpack: Any | None = None,
) -> Result:
"""Simulate a request to a WSGI or ASGI application.

Expand Down Expand Up @@ -597,6 +600,13 @@ def simulate_request(
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -625,6 +635,7 @@ def simulate_request(
asgi_chunk_size=asgi_chunk_size,
asgi_disconnect_ttl=asgi_disconnect_ttl,
cookies=cookies,
msgpack=msgpack,
)

path, query_string, headers, body, extras = _prepare_sim_args(
Expand All @@ -637,6 +648,7 @@ def simulate_request(
body,
json,
extras,
msgpack,
)

env = helpers.create_environ(
Expand Down Expand Up @@ -702,6 +714,7 @@ async def _simulate_request_asgi(
cookies: CookieArg | None = ...,
_one_shot: Literal[False] = ...,
_stream_result: Literal[True] = ...,
msgpack: Any | None = ...,
) -> StreamedResult: ...


Expand Down Expand Up @@ -729,6 +742,7 @@ async def _simulate_request_asgi(
cookies: CookieArg | None = ...,
_one_shot: Literal[True] = ...,
_stream_result: bool = ...,
msgpack: Any | None = ...,
) -> Result: ...


Expand Down Expand Up @@ -764,6 +778,7 @@ async def _simulate_request_asgi(
# don't want these kwargs to be documented.
_one_shot: bool = True,
_stream_result: bool = False,
msgpack: Any | None = None,
) -> Result | StreamedResult:
"""Simulate a request to an ASGI application.

Expand Down Expand Up @@ -853,6 +868,13 @@ async def _simulate_request_asgi(
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as `
`'application/msgpack'``.

Returns:
:class:`~.Result`: The result of the request
Expand All @@ -868,6 +890,7 @@ async def _simulate_request_asgi(
body,
json,
extras,
msgpack,
)

# ---------------------------------------------------------------------
Expand Down Expand Up @@ -1604,6 +1627,13 @@ def simulate_post(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -1715,6 +1745,13 @@ def simulate_put(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -1910,6 +1947,13 @@ def simulate_patch(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -2016,6 +2060,13 @@ def simulate_delete(app: Callable[..., Any], path: str, **kwargs: Any) -> Result
iterable yielding a series of two-member (*name*, *value*)
iterables. Each pair of items provides the name and value
for the 'Set-Cookie' header.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments. If msgpack and json
are both specified, the Content-Type header will be set as
``'application/msgpack'``.

Returns:
:class:`~.Result`: The result of the request
Expand Down Expand Up @@ -2270,6 +2321,7 @@ def _prepare_sim_args(
body: str | bytes | None,
json: Any | None,
extras: Mapping[str, Any] | None,
msgpack: Any | None,
) -> tuple[str, str, HeaderArg | None, str | bytes | None, Mapping[str, Any]]:
if not path.startswith('/'):
raise ValueError("path must start with '/'")
Expand Down Expand Up @@ -2303,6 +2355,11 @@ def _prepare_sim_args(
headers = dict(headers or {})
headers['Content-Type'] = MEDIA_JSON

if msgpack is not None:
body = MessagePackHandler().serialize(content_type=None, media=msgpack)
headers = dict(headers or {})
headers['Content-Type'] = MEDIA_MSGPACK

return path, query_string, headers, body, extras


Expand Down
89 changes: 89 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
from falcon import testing
from falcon.util.sync import async_to_sync

try:
import msgpack
except ImportError:
msgpack = None

SAMPLE_BODY = testing.rand_string(0, 128 * 1024)


class CustomCookies:
def items(self):
Expand Down Expand Up @@ -106,6 +113,88 @@ def on_post(self, req, resp):
assert result.text == falcon.MEDIA_JSON


@pytest.mark.skipif(not msgpack, reason='msgpack not installed')
@pytest.mark.parametrize(
'json,msgpack,response',
[
({}, None, falcon.MEDIA_JSON),
(None, {}, falcon.MEDIA_MSGPACK),
({}, {}, falcon.MEDIA_MSGPACK),
],
)
def test_simulate_request_msgpack_content_type(json, msgpack, response):
class Foo:
def on_post(self, req, resp):
resp.text = req.content_type

app = App()
app.add_route('/', Foo())

headers = {'Content-Type': falcon.MEDIA_TEXT}

result = testing.simulate_post(app, '/', json=json, msgpack=msgpack)
assert result.text == response

result = testing.simulate_post(
app, '/', json=json, msgpack=msgpack, content_type=falcon.MEDIA_HTML
)
assert result.text == response

result = testing.simulate_post(
app, '/', json=json, msgpack=msgpack, headers=headers
)
assert result.text == response

result = testing.simulate_post(
app,
'/',
json=json,
msgpack=msgpack,
headers=headers,
content_type=falcon.MEDIA_HTML,
)
assert result.text == response


@pytest.mark.skipif(not msgpack, reason='msgpack not installed')
@pytest.mark.parametrize(
'value',
(
'd\xff\xff\x00',
'quick fox jumps over the lazy dog',
'{"hello": "WORLD!"}',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praese',
'{"hello": "WORLD!", "greetings": "fellow traveller"}',
'\xe9\xe8',
),
)
def test_simulate_request_msgpack_different_bodies(value):
value = bytes(value, 'UTF-8')

resource = testing.SimpleTestResource(body=value)

app = App()
app.add_route('/', resource)

result = testing.simulate_post(app, '/', msgpack={})
captured_resp = resource.captured_resp
content = captured_resp.text

if len(value) > 40:
content = value[:20] + b'...' + value[-20:]
else:
content = value

args = [
captured_resp.status,
captured_resp.headers['content-type'],
str(content),
]

expected_content = 'Result<{}>'.format(' '.join(filter(None, args)))
assert str(result) == expected_content


@pytest.mark.parametrize('mode', ['wsgi', 'asgi', 'asgi-stream'])
def test_content_type(util, mode):
class Responder:
Expand Down