Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(testing): add msgpack support #2394

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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.
50 changes: 50 additions & 0 deletions falcon/testing/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,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 @@ -455,6 +457,7 @@ def simulate_request(
content_type: Optional[str] = None,
body: Optional[Union[str, bytes]] = None,
json: Optional[Any] = None,
msgpack: Optional[Any] = None,
file_wrapper: Optional[Callable[..., Any]] = None,
wsgierrors: Optional[TextIO] = None,
params: Optional[Mapping[str, Any]] = None,
Expand Down Expand Up @@ -592,6 +595,7 @@ def simulate_request(
content_type=content_type,
body=body,
json=json,
msgpack=msgpack,
params=params,
params_csv=params_csv,
protocol=protocol,
Expand All @@ -615,6 +619,7 @@ def simulate_request(
headers,
body,
json,
msgpack,
extras,
)

Expand Down Expand Up @@ -667,6 +672,7 @@ async def _simulate_request_asgi(
content_type: Optional[str] = ...,
body: Optional[Union[str, bytes]] = ...,
json: Optional[Any] = ...,
msgpack: Optional[Any] = ...,
params: Optional[Mapping[str, Any]] = ...,
params_csv: bool = ...,
protocol: str = ...,
Expand Down Expand Up @@ -694,6 +700,7 @@ async def _simulate_request_asgi(
content_type: Optional[str] = ...,
body: Optional[Union[str, bytes]] = ...,
json: Optional[Any] = ...,
msgpack: Optional[Any] = ...,
params: Optional[Mapping[str, Any]] = ...,
params_csv: bool = ...,
protocol: str = ...,
Expand Down Expand Up @@ -724,6 +731,7 @@ async def _simulate_request_asgi(
content_type: Optional[str] = None,
body: Optional[Union[str, bytes]] = None,
json: Optional[Any] = None,
msgpack: Optional[Any] = None,
params: Optional[Mapping[str, Any]] = None,
params_csv: bool = False,
protocol: str = 'http',
Expand Down Expand Up @@ -808,6 +816,13 @@ async def _simulate_request_asgi(
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
vytas7 marked this conversation as resolved.
Show resolved Hide resolved
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'``.
host(str): A string to use for the hostname part of the fully
qualified request URL (default: 'falconframework.org')
remote_addr (str): A string to use as the remote IP address for the
Expand Down Expand Up @@ -846,6 +861,7 @@ async def _simulate_request_asgi(
headers,
body,
json,
msgpack,
extras,
)

Expand Down Expand Up @@ -1551,6 +1567,13 @@ def simulate_post(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
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'``.
file_wrapper (callable): Callable that returns an iterable,
to be used as the value for *wsgi.file_wrapper* in the
WSGI environ (default: ``None``). This can be used to test
Expand Down Expand Up @@ -1662,6 +1685,13 @@ def simulate_put(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
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'``.
file_wrapper (callable): Callable that returns an iterable,
to be used as the value for *wsgi.file_wrapper* in the
WSGI environ (default: ``None``). This can be used to test
Expand Down Expand Up @@ -1862,6 +1892,13 @@ def simulate_patch(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
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'``.
host(str): A string to use for the hostname part of the fully
qualified request URL (default: 'falconframework.org')
remote_addr (str): A string to use as the remote IP address for the
Expand Down Expand Up @@ -1968,6 +2005,13 @@ def simulate_delete(app: Callable[..., Any], path: str, **kwargs: Any) -> Result
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
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'``.
host(str): A string to use for the hostname part of the fully
qualified request URL (default: 'falconframework.org')
remote_addr (str): A string to use as the remote IP address for the
Expand Down Expand Up @@ -2248,6 +2292,7 @@ def _prepare_sim_args(
headers: Optional[HeaderArg],
body: Optional[Union[str, bytes]],
json: Optional[Any],
msgpack: Optional[Any],
extras: Optional[Mapping[str, Any]],
) -> Tuple[
str, str, Optional[HeaderArg], Optional[Union[str, bytes]], Mapping[str, Any]
Expand Down Expand Up @@ -2284,6 +2329,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
arthurprioli marked this conversation as resolved.
Show resolved Hide resolved
arthurprioli marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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 @@ -104,6 +111,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
Loading