Source code for falcon.testing.client

# 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 warnings
import wsgiref.validate

from falcon.constants import MEDIA_JSON
from falcon.testing import helpers
from falcon.testing.srmock import StartResponseMock
from falcon.util import CaseInsensitiveDict, compat, http_date_to_dt, to_query_str
from falcon.util import json as util_json
from falcon.util.compat import http_cookies


warnings.filterwarnings(
    'ignore',
    (
        'Unknown REQUEST_METHOD: '
        "'(CONNECT|CHECKIN|CHECKOUT|UNCHECKIN|UPDATE|VERSION-CONTROL|REPORT|SETECASTRONOMY)'"
    ),
    wsgiref.validate.WSGIWarning,
    '',
    0,
)


[docs]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, except for cookies, which may be accessed via the `cookies` attribute. Note: Multiple instances of a header in the response are currently not supported; it is unspecified which value will "win" and be represented in `headers`. cookies (dict): A dictionary of :py:class:`falcon.testing.Cookie` values parsed from the response, by name. 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.7, and of type ``str`` otherwise. If the content type does not specify an encoding, UTF-8 is assumed. json (JSON serializable): Deserialized JSON body. Will be ``None`` if the body has no content to deserialize. Otherwise, raises an error if the response is not valid JSON. """ def __init__(self, iterable, status, headers): self._text = None self._content = b''.join(iterable) self._status = status self._status_code = int(status[:3]) self._headers = CaseInsensitiveDict(headers) cookies = http_cookies.SimpleCookie() for name, value in headers: if name.lower() == 'set-cookie': cookies.load(value) self._cookies = dict( (morsel.key, Cookie(morsel)) for morsel in cookies.values() ) 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 cookies(self): return self._cookies @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): if not self.text: return None return util_json.loads(self.text)
[docs]def simulate_request(app, method='GET', path='/', query_string=None, headers=None, body=None, json=None, file_wrapper=None, wsgierrors=None, params=None, params_csv=True, protocol='http', host=helpers.DEFAULT_HOST, remote_addr=None, extras=None): """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: '/'). Note: The path may contain a query string. However, neither `query_string` nor `params` may be specified in this case. protocol: The protocol to use for the URL scheme (default: 'http') 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. json(JSON serializable): A JSON document to serialize as the body of the request (default: ``None``). If specified, overrides `body` and the Content-Type header in `headers`. 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. 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 request (default: '127.0.0.1') wsgierrors (io): The stream to use as *wsgierrors* (default ``sys.stderr``) extras (dict): Additional CGI variables to add to the WSGI ``environ`` dictionary for the request (default: ``None``) Returns: :py:class:`~.Result`: The result of the request """ if not path.startswith('/'): raise ValueError("path must start with '/'") if '?' in path: if query_string or params: raise ValueError( 'path may not contain a query string in combination with ' 'the query_string or params parameters. Please use only one ' 'way of specifying the query string.' ) path, query_string = path.split('?', 1) elif query_string and query_string.startswith('?'): raise ValueError("query_string should not start with '?'") extras = extras or {} if 'REQUEST_METHOD' in extras and extras['REQUEST_METHOD'] != method: # NOTE(vytas): Even given the duct tape nature of overriding # arbitrary environ variables, changing the method can potentially # be very confusing, particularly when using specialized # simulate_get/post/patch etc methods. raise ValueError( 'environ extras may not override the request method. Please ' 'use the method parameter.' ) if query_string is None: query_string = to_query_str( params, comma_delimited_lists=params_csv, prefix=False, ) if json is not None: body = util_json.dumps(json, ensure_ascii=False) headers = headers or {} headers['Content-Type'] = MEDIA_JSON env = helpers.create_environ( method=method, scheme=protocol, path=path, query_string=(query_string or ''), headers=headers, body=body, file_wrapper=file_wrapper, host=host, remote_addr=remote_addr, wsgierrors=wsgierrors, ) if extras: env.update(extras) srmock = StartResponseMock() validator = wsgiref.validate.validator(app) iterable = validator(env, srmock) result = Result(helpers.closed_wsgi_iterable(iterable), srmock.status, srmock.headers) return result
[docs]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. Note: The path may contain a query string. However, neither `query_string` nor `params` may be specified in this case. 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. protocol: The protocol to use for the URL scheme (default: 'http') 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 request (default: '127.0.0.1') extras (dict): Additional CGI variables to add to the WSGI ``environ`` dictionary for the request (default: ``None``) """ return simulate_request(app, 'GET', path, **kwargs)
[docs]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. Note: The path may contain a query string. However, neither `query_string` nor `params` may be specified in this case. 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``) protocol: The protocol to use for the URL scheme (default: 'http') 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 request (default: '127.0.0.1') extras (dict): Additional CGI variables to add to the WSGI ``environ`` dictionary for the request (default: ``None``) """ return simulate_request(app, 'HEAD', path, **kwargs)
[docs]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. json(JSON serializable): A JSON document to serialize as the body of the request (default: ``None``). If specified, overrides `body` and the Content-Type header in `headers`. protocol: The protocol to use for the URL scheme (default: 'http') 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 request (default: '127.0.0.1') extras (dict): Additional CGI variables to add to the WSGI ``environ`` dictionary for the request (default: ``None``) """ return simulate_request(app, 'POST', path, **kwargs)
[docs]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. json(JSON serializable): A JSON document to serialize as the body of the request (default: ``None``). If specified, overrides `body` and the Content-Type header in `headers`. protocol: The protocol to use for the URL scheme (default: 'http') 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 request (default: '127.0.0.1') extras (dict): Additional CGI variables to add to the WSGI ``environ`` dictionary for the request (default: ``None``) """ return simulate_request(app, 'PUT', path, **kwargs)
[docs]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``) protocol: The protocol to use for the URL scheme (default: 'http') 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 request (default: '127.0.0.1') extras (dict): Additional CGI variables to add to the WSGI ``environ`` dictionary for the request (default: ``None``) """ return simulate_request(app, 'OPTIONS', path, **kwargs)
[docs]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. json(JSON serializable): A JSON document to serialize as the body of the request (default: ``None``). If specified, overrides `body` and the Content-Type header in `headers`. protocol: The protocol to use for the URL scheme (default: 'http') 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 request (default: '127.0.0.1') extras (dict): Additional CGI variables to add to the WSGI ``environ`` dictionary for the request (default: ``None``) """ return simulate_request(app, 'PATCH', path, **kwargs)
[docs]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``) protocol: The protocol to use for the URL scheme (default: 'http') 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 request (default: '127.0.0.1') extras (dict): Additional CGI variables to add to the WSGI ``environ`` dictionary for the request (default: ``None``) """ return simulate_request(app, 'DELETE', path, **kwargs)
[docs]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') Note: The methods all call ``self.simulate_request()`` for convenient overriding of request preparation by child classes. Args: app (callable): A WSGI application to target when simulating requests Keyword Arguments: headers (dict): Default headers to set on every request (default ``None``). These defaults may be overridden by passing values for the same headers to one of the `simulate_*()` methods. """ def __init__(self, app, headers=None): self.app = app self._default_headers = headers
[docs] def simulate_get(self, path='/', **kwargs): """Simulates a GET request to a WSGI application. (See also: :py:meth:`falcon.testing.simulate_get`) """ return self.simulate_request('GET', path, **kwargs)
[docs] def simulate_head(self, path='/', **kwargs): """Simulates a HEAD request to a WSGI application. (See also: :py:meth:`falcon.testing.simulate_head`) """ return self.simulate_request('HEAD', path, **kwargs)
[docs] def simulate_post(self, path='/', **kwargs): """Simulates a POST request to a WSGI application. (See also: :py:meth:`falcon.testing.simulate_post`) """ return self.simulate_request('POST', path, **kwargs)
[docs] def simulate_put(self, path='/', **kwargs): """Simulates a PUT request to a WSGI application. (See also: :py:meth:`falcon.testing.simulate_put`) """ return self.simulate_request('PUT', path, **kwargs)
[docs] def simulate_options(self, path='/', **kwargs): """Simulates an OPTIONS request to a WSGI application. (See also: :py:meth:`falcon.testing.simulate_options`) """ return self.simulate_request('OPTIONS', path, **kwargs)
[docs] def simulate_patch(self, path='/', **kwargs): """Simulates a PATCH request to a WSGI application. (See also: :py:meth:`falcon.testing.simulate_patch`) """ return self.simulate_request('PATCH', path, **kwargs)
[docs] def simulate_delete(self, path='/', **kwargs): """Simulates a DELETE request to a WSGI application. (See also: :py:meth:`falcon.testing.simulate_delete`) """ return self.simulate_request('DELETE', path, **kwargs)
[docs] 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) """ if self._default_headers: # NOTE(kgriffs): Handle the case in which headers is explicitly # set to None. additional_headers = kwargs.get('headers', {}) or {} merged_headers = self._default_headers.copy() merged_headers.update(additional_headers) kwargs['headers'] = merged_headers if compat.PY2 and 'headers' in kwargs: for key, value in kwargs['headers'].items(): kwargs['headers'][key] = str(value) if isinstance(value, unicode) else value return simulate_request(self.app, *args, **kwargs)