A developer wishing to test high-performance file transmission may wish to pass a mock for wsgi.file_wrapper to simulate_get(). This kwarg is not documented for other simulate_*() functions since they are unlikely to use it.
518 lines
19 KiB
Python
518 lines
19 KiB
Python
# Copyright 2016 by Rackspace Hosting, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# Copyright 2016 by Rackspace Hosting, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""WSGI test client utilities.
|
|
|
|
This package includes utilities for simulating HTTP requests against a
|
|
WSGI callable, without having to stand up a WSGI server.
|
|
"""
|
|
|
|
import json
|
|
import wsgiref.validate
|
|
|
|
from falcon.testing import helpers
|
|
from falcon.testing.srmock import StartResponseMock
|
|
from falcon.util import CaseInsensitiveDict, to_query_str
|
|
|
|
|
|
class Result(object):
|
|
"""Encapsulates the result of a simulated WSGI request.
|
|
|
|
Args:
|
|
iterable (iterable): An iterable that yields zero or more
|
|
bytestrings, per PEP-3333
|
|
status (str): An HTTP status string, including status code and
|
|
reason string
|
|
headers (list): A list of (header_name, header_value) tuples,
|
|
per PEP-3333
|
|
|
|
Attributes:
|
|
status (str): HTTP status string given in the response
|
|
status_code (int): The code portion of the HTTP status string
|
|
headers (CaseInsensitiveDict): A case-insensitive dictionary
|
|
containing all the headers in the response
|
|
encoding (str): Text encoding of the response body, or ``None``
|
|
if the encoding can not be determined.
|
|
content (bytes): Raw response body, or ``bytes`` if the
|
|
response body was empty.
|
|
text (str): Decoded response body of type ``unicode``
|
|
under Python 2.6 and 2.7, and of type ``str`` otherwise.
|
|
If the content type does not specify an encoding, UTF-8 is
|
|
assumed.
|
|
json (dict): Deserialized JSON body. Raises an error if the
|
|
response is not JSON.
|
|
"""
|
|
|
|
def __init__(self, iterable, status, headers):
|
|
self._text = None
|
|
|
|
self._content = b''.join(iterable)
|
|
if hasattr(iterable, 'close'):
|
|
iterable.close()
|
|
|
|
self._status = status
|
|
self._status_code = int(status[:3])
|
|
self._headers = CaseInsensitiveDict(headers)
|
|
|
|
self._encoding = helpers.get_encoding_from_headers(self._headers)
|
|
|
|
@property
|
|
def status(self):
|
|
return self._status
|
|
|
|
@property
|
|
def status_code(self):
|
|
return self._status_code
|
|
|
|
@property
|
|
def headers(self):
|
|
return self._headers
|
|
|
|
@property
|
|
def encoding(self):
|
|
return self._encoding
|
|
|
|
@property
|
|
def content(self):
|
|
return self._content
|
|
|
|
@property
|
|
def text(self):
|
|
if self._text is None:
|
|
if not self.content:
|
|
self._text = u''
|
|
else:
|
|
if self.encoding is None:
|
|
encoding = 'UTF-8'
|
|
else:
|
|
encoding = self.encoding
|
|
|
|
self._text = self.content.decode(encoding)
|
|
|
|
return self._text
|
|
|
|
@property
|
|
def json(self):
|
|
return json.loads(self.text)
|
|
|
|
|
|
def simulate_request(app, method='GET', path='/', query_string=None,
|
|
headers=None, body=None, file_wrapper=None,
|
|
params=None, params_csv=True):
|
|
"""Simulates a request to a WSGI application.
|
|
|
|
Performs a request against a WSGI application. Uses
|
|
:any:`wsgiref.validate` to ensure the response is valid
|
|
WSGI.
|
|
|
|
Keyword Args:
|
|
app (callable): The WSGI application to call
|
|
method (str): An HTTP method to use in the request
|
|
(default: 'GET')
|
|
path (str): The URL path to request (default: '/')
|
|
params (dict): A dictionary of query string parameters,
|
|
where each key is a parameter name, and each value is
|
|
either a ``str`` or something that can be converted
|
|
into a ``str``, or a list of such values. If a ``list``,
|
|
the value will be converted to a comma-delimited string
|
|
of values (e.g., 'thing=1,2,3').
|
|
params_csv (bool): Set to ``False`` to encode list values
|
|
in query string params by specifying multiple instances
|
|
of the parameter (e.g., 'thing=1&thing=2&thing=3').
|
|
Otherwise, parameters will be encoded as comma-separated
|
|
values (e.g., 'thing=1,2,3'). Defaults to ``True``.
|
|
query_string (str): A raw query string to include in the
|
|
request (default: ``None``). If specified, overrides
|
|
`params`.
|
|
headers (dict): Additional headers to include in the request
|
|
(default: ``None``)
|
|
body (str): A string to send as the body of the request.
|
|
Accepts both byte strings and Unicode strings
|
|
(default: ``None``). If a Unicode string is provided,
|
|
it will be encoded as UTF-8 in the request.
|
|
file_wrapper (callable): Callable that returns an iterable,
|
|
to be used as the value for *wsgi.file_wrapper* in the
|
|
environ (default: ``None``). This can be used to test
|
|
high-performance file transmission when `resp.stream` is
|
|
set to a file-like object.
|
|
|
|
Returns:
|
|
:py:class:`~.Result`: The result of the request
|
|
"""
|
|
|
|
if not path.startswith('/'):
|
|
raise ValueError("path must start with '/'")
|
|
|
|
if query_string and query_string.startswith('?'):
|
|
raise ValueError("query_string should not start with '?'")
|
|
|
|
if '?' in path:
|
|
# NOTE(kgriffs): We could allow this, but then we'd need
|
|
# to define semantics regarding whether the path takes
|
|
# precedence over the query_string. Also, it would make
|
|
# tests less consistent, since there would be "more than
|
|
# one...way to do it."
|
|
raise ValueError(
|
|
'path may not contain a query string. Please use the '
|
|
'query_string parameter instead.'
|
|
)
|
|
|
|
if query_string is None:
|
|
query_string = to_query_str(
|
|
params,
|
|
comma_delimited_lists=params_csv,
|
|
prefix=False,
|
|
)
|
|
|
|
env = helpers.create_environ(
|
|
method=method,
|
|
path=path,
|
|
query_string=(query_string or ''),
|
|
headers=headers,
|
|
body=body,
|
|
file_wrapper=file_wrapper,
|
|
)
|
|
|
|
srmock = StartResponseMock()
|
|
validator = wsgiref.validate.validator(app)
|
|
iterable = validator(env, srmock)
|
|
|
|
result = Result(iterable, srmock.status, srmock.headers)
|
|
|
|
return result
|
|
|
|
|
|
def simulate_get(app, path, **kwargs):
|
|
"""Simulates a GET request to a WSGI application.
|
|
|
|
Equivalent to::
|
|
|
|
simulate_request(app, 'GET', path, **kwargs)
|
|
|
|
Args:
|
|
app (callable): The WSGI application to call
|
|
path (str): The URL path to request
|
|
|
|
Keyword Args:
|
|
params (dict): A dictionary of query string parameters,
|
|
where each key is a parameter name, and each value is
|
|
either a ``str`` or something that can be converted
|
|
into a ``str``, or a list of such values. If a ``list``,
|
|
the value will be converted to a comma-delimited string
|
|
of values (e.g., 'thing=1,2,3').
|
|
params_csv (bool): Set to ``False`` to encode list values
|
|
in query string params by specifying multiple instances
|
|
of the parameter (e.g., 'thing=1&thing=2&thing=3').
|
|
Otherwise, parameters will be encoded as comma-separated
|
|
values (e.g., 'thing=1,2,3'). Defaults to ``True``.
|
|
query_string (str): A raw query string to include in the
|
|
request (default: ``None``). If specified, overrides
|
|
`params`.
|
|
headers (dict): Additional headers to include in the request
|
|
(default: ``None``)
|
|
file_wrapper (callable): Callable that returns an iterable,
|
|
to be used as the value for *wsgi.file_wrapper* in the
|
|
environ (default: ``None``). This can be used to test
|
|
high-performance file transmission when `resp.stream` is
|
|
set to a file-like object.
|
|
"""
|
|
return simulate_request(app, 'GET', path, **kwargs)
|
|
|
|
|
|
def simulate_head(app, path, **kwargs):
|
|
"""Simulates a HEAD request to a WSGI application.
|
|
|
|
Equivalent to::
|
|
|
|
simulate_request(app, 'HEAD', path, **kwargs)
|
|
|
|
Args:
|
|
app (callable): The WSGI application to call
|
|
path (str): The URL path to request
|
|
|
|
Keyword Args:
|
|
params (dict): A dictionary of query string parameters,
|
|
where each key is a parameter name, and each value is
|
|
either a ``str`` or something that can be converted
|
|
into a ``str``, or a list of such values. If a ``list``,
|
|
the value will be converted to a comma-delimited string
|
|
of values (e.g., 'thing=1,2,3').
|
|
params_csv (bool): Set to ``False`` to encode list values
|
|
in query string params by specifying multiple instances
|
|
of the parameter (e.g., 'thing=1&thing=2&thing=3').
|
|
Otherwise, parameters will be encoded as comma-separated
|
|
values (e.g., 'thing=1,2,3'). Defaults to ``True``.
|
|
query_string (str): A raw query string to include in the
|
|
request (default: ``None``). If specified, overrides
|
|
`params`.
|
|
headers (dict): Additional headers to include in the request
|
|
(default: ``None``)
|
|
"""
|
|
return simulate_request(app, 'HEAD', path, **kwargs)
|
|
|
|
|
|
def simulate_post(app, path, **kwargs):
|
|
"""Simulates a POST request to a WSGI application.
|
|
|
|
Equivalent to::
|
|
|
|
simulate_request(app, 'POST', path, **kwargs)
|
|
|
|
Args:
|
|
app (callable): The WSGI application to call
|
|
path (str): The URL path to request
|
|
|
|
Keyword Args:
|
|
params (dict): A dictionary of query string parameters,
|
|
where each key is a parameter name, and each value is
|
|
either a ``str`` or something that can be converted
|
|
into a ``str``, or a list of such values. If a ``list``,
|
|
the value will be converted to a comma-delimited string
|
|
of values (e.g., 'thing=1,2,3').
|
|
params_csv (bool): Set to ``False`` to encode list values
|
|
in query string params by specifying multiple instances
|
|
of the parameter (e.g., 'thing=1&thing=2&thing=3').
|
|
Otherwise, parameters will be encoded as comma-separated
|
|
values (e.g., 'thing=1,2,3'). Defaults to ``True``.
|
|
headers (dict): Additional headers to include in the request
|
|
(default: ``None``)
|
|
body (str): A string to send as the body of the request.
|
|
Accepts both byte strings and Unicode strings
|
|
(default: ``None``). If a Unicode string is provided,
|
|
it will be encoded as UTF-8 in the request.
|
|
"""
|
|
return simulate_request(app, 'POST', path, **kwargs)
|
|
|
|
|
|
def simulate_put(app, path, **kwargs):
|
|
"""Simulates a PUT request to a WSGI application.
|
|
|
|
Equivalent to::
|
|
|
|
simulate_request(app, 'PUT', path, **kwargs)
|
|
|
|
Args:
|
|
app (callable): The WSGI application to call
|
|
path (str): The URL path to request
|
|
|
|
Keyword Args:
|
|
params (dict): A dictionary of query string parameters,
|
|
where each key is a parameter name, and each value is
|
|
either a ``str`` or something that can be converted
|
|
into a ``str``, or a list of such values. If a ``list``,
|
|
the value will be converted to a comma-delimited string
|
|
of values (e.g., 'thing=1,2,3').
|
|
params_csv (bool): Set to ``False`` to encode list values
|
|
in query string params by specifying multiple instances
|
|
of the parameter (e.g., 'thing=1&thing=2&thing=3').
|
|
Otherwise, parameters will be encoded as comma-separated
|
|
values (e.g., 'thing=1,2,3'). Defaults to ``True``.
|
|
headers (dict): Additional headers to include in the request
|
|
(default: ``None``)
|
|
body (str): A string to send as the body of the request.
|
|
Accepts both byte strings and Unicode strings
|
|
(default: ``None``). If a Unicode string is provided,
|
|
it will be encoded as UTF-8 in the request.
|
|
"""
|
|
return simulate_request(app, 'PUT', path, **kwargs)
|
|
|
|
|
|
def simulate_options(app, path, **kwargs):
|
|
"""Simulates an OPTIONS request to a WSGI application.
|
|
|
|
Equivalent to::
|
|
|
|
simulate_request(app, 'OPTIONS', path, **kwargs)
|
|
|
|
Args:
|
|
app (callable): The WSGI application to call
|
|
path (str): The URL path to request
|
|
|
|
Keyword Args:
|
|
params (dict): A dictionary of query string parameters,
|
|
where each key is a parameter name, and each value is
|
|
either a ``str`` or something that can be converted
|
|
into a ``str``, or a list of such values. If a ``list``,
|
|
the value will be converted to a comma-delimited string
|
|
of values (e.g., 'thing=1,2,3').
|
|
params_csv (bool): Set to ``False`` to encode list values
|
|
in query string params by specifying multiple instances
|
|
of the parameter (e.g., 'thing=1&thing=2&thing=3').
|
|
Otherwise, parameters will be encoded as comma-separated
|
|
values (e.g., 'thing=1,2,3'). Defaults to ``True``.
|
|
headers (dict): Additional headers to include in the request
|
|
(default: ``None``)
|
|
"""
|
|
return simulate_request(app, 'OPTIONS', path, **kwargs)
|
|
|
|
|
|
def simulate_patch(app, path, **kwargs):
|
|
"""Simulates a PATCH request to a WSGI application.
|
|
|
|
Equivalent to::
|
|
|
|
simulate_request(app, 'PATCH', path, **kwargs)
|
|
|
|
Args:
|
|
app (callable): The WSGI application to call
|
|
path (str): The URL path to request
|
|
|
|
Keyword Args:
|
|
params (dict): A dictionary of query string parameters,
|
|
where each key is a parameter name, and each value is
|
|
either a ``str`` or something that can be converted
|
|
into a ``str``, or a list of such values. If a ``list``,
|
|
the value will be converted to a comma-delimited string
|
|
of values (e.g., 'thing=1,2,3').
|
|
params_csv (bool): Set to ``False`` to encode list values
|
|
in query string params by specifying multiple instances
|
|
of the parameter (e.g., 'thing=1&thing=2&thing=3').
|
|
Otherwise, parameters will be encoded as comma-separated
|
|
values (e.g., 'thing=1,2,3'). Defaults to ``True``.
|
|
headers (dict): Additional headers to include in the request
|
|
(default: ``None``)
|
|
body (str): A string to send as the body of the request.
|
|
Accepts both byte strings and Unicode strings
|
|
(default: ``None``). If a Unicode string is provided,
|
|
it will be encoded as UTF-8 in the request.
|
|
"""
|
|
return simulate_request(app, 'PATCH', path, **kwargs)
|
|
|
|
|
|
def simulate_delete(app, path, **kwargs):
|
|
"""Simulates a DELETE request to a WSGI application.
|
|
|
|
Equivalent to::
|
|
|
|
simulate_request(app, 'DELETE', path, **kwargs)
|
|
|
|
Args:
|
|
app (callable): The WSGI application to call
|
|
path (str): The URL path to request
|
|
|
|
Keyword Args:
|
|
params (dict): A dictionary of query string parameters,
|
|
where each key is a parameter name, and each value is
|
|
either a ``str`` or something that can be converted
|
|
into a ``str``, or a list of such values. If a ``list``,
|
|
the value will be converted to a comma-delimited string
|
|
of values (e.g., 'thing=1,2,3').
|
|
params_csv (bool): Set to ``False`` to encode list values
|
|
in query string params by specifying multiple instances
|
|
of the parameter (e.g., 'thing=1&thing=2&thing=3').
|
|
Otherwise, parameters will be encoded as comma-separated
|
|
values (e.g., 'thing=1,2,3'). Defaults to ``True``.
|
|
headers (dict): Additional headers to include in the request
|
|
(default: ``None``)
|
|
"""
|
|
return simulate_request(app, 'DELETE', path, **kwargs)
|
|
|
|
|
|
class TestClient(object):
|
|
""""Simulates requests to a WSGI application.
|
|
|
|
This class provides a contextual wrapper for Falcon's simulate_*
|
|
test functions. It lets you replace this::
|
|
|
|
simulate_get(app, '/messages')
|
|
simulate_head(app, '/messages')
|
|
|
|
with this::
|
|
|
|
client = TestClient(app)
|
|
client.simulate_get('/messages')
|
|
client.simulate_head('/messages')
|
|
|
|
Args:
|
|
app (callable): A WSGI application to target when simulating
|
|
requests
|
|
"""
|
|
|
|
def __init__(self, app):
|
|
self.app = app
|
|
|
|
def simulate_get(self, path='/', **kwargs):
|
|
"""Simulates a GET request to a WSGI application.
|
|
|
|
See also: :py:meth:`falcon.testing.simulate_get`.
|
|
"""
|
|
return simulate_get(self.app, path, **kwargs)
|
|
|
|
def simulate_head(self, path='/', **kwargs):
|
|
"""Simulates a HEAD request to a WSGI application.
|
|
|
|
See also: :py:meth:`falcon.testing.simulate_head`.
|
|
"""
|
|
return simulate_head(self.app, path, **kwargs)
|
|
|
|
def simulate_post(self, path='/', **kwargs):
|
|
"""Simulates a POST request to a WSGI application.
|
|
|
|
See also: :py:meth:`falcon.testing.simulate_post`.
|
|
"""
|
|
return simulate_post(self.app, path, **kwargs)
|
|
|
|
def simulate_put(self, path='/', **kwargs):
|
|
"""Simulates a PUT request to a WSGI application.
|
|
|
|
See also: :py:meth:`falcon.testing.simulate_put`.
|
|
"""
|
|
return simulate_put(self.app, path, **kwargs)
|
|
|
|
def simulate_options(self, path='/', **kwargs):
|
|
"""Simulates an OPTIONS request to a WSGI application.
|
|
|
|
See also: :py:meth:`falcon.testing.simulate_options`.
|
|
"""
|
|
return simulate_options(self.app, path, **kwargs)
|
|
|
|
def simulate_patch(self, path='/', **kwargs):
|
|
"""Simulates a PATCH request to a WSGI application.
|
|
|
|
See also: :py:meth:`falcon.testing.simulate_patch`.
|
|
"""
|
|
return simulate_patch(self.app, path, **kwargs)
|
|
|
|
def simulate_delete(self, path='/', **kwargs):
|
|
"""Simulates a DELETE request to a WSGI application.
|
|
|
|
See also: :py:meth:`falcon.testing.simulate_delete`.
|
|
"""
|
|
return simulate_delete(self.app, path, **kwargs)
|
|
|
|
def simulate_request(self, *args, **kwargs):
|
|
"""Simulates a request to a WSGI application.
|
|
|
|
Wraps :py:meth:`falcon.testing.simulate_request` to perform a
|
|
WSGI request directly against ``self.app``. Equivalent to::
|
|
|
|
falcon.testing.simulate_request(self.app, *args, **kwargs)
|
|
"""
|
|
|
|
return simulate_request(self.app, *args, **kwargs)
|