├── uncertainty ├── tests │ ├── __init__.py │ ├── settings.py │ ├── runtests.py │ ├── test_middleware.py │ ├── test_conditions.py │ └── test_behaviours.py ├── __init__.py ├── middleware.py ├── conditions.py └── behaviours.py ├── .gitignore ├── setup.cfg ├── CHANGELOG.rst ├── setup.py ├── LICENSE.txt └── README.rst /uncertainty/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | *.pyc 4 | *.egg-info 5 | dist/ -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [flake8] 5 | max-line-length=100 6 | exclude=tests/* 7 | -------------------------------------------------------------------------------- /uncertainty/tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(__file__) 4 | SECRET_KEY = 'some_secret_key' 5 | DEBUG = True 6 | ALLOWED_HOSTS = [] 7 | INSTALLED_APPS = ( 8 | 'django.contrib.auth', 9 | 'django.contrib.contenttypes', 10 | 'uncertainty', 11 | 'uncertainty.tests', 12 | ) 13 | DATABASES = { 14 | 'default': { 15 | 'ENGINE': 'django.db.backends.sqlite3', 16 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /uncertainty/tests/runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django.test.utils import get_runner 6 | from django.conf import settings 7 | 8 | 9 | def runtests(): 10 | os.environ['DJANGO_SETTINGS_MODULE'] = 'uncertainty.tests.settings' 11 | django.setup() 12 | sys.path.insert(0, os.path.dirname(__file__)) 13 | test_runner = get_runner(settings)() 14 | failures = test_runner.run_tests([], verbosity=1, interactive=True) 15 | sys.exit(bool(failures)) 16 | 17 | 18 | if __name__ == '__main__': 19 | runtests() 20 | -------------------------------------------------------------------------------- /uncertainty/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .behaviours import (default, bad_request, case, cond, conditional, delay, # noqa 4 | delay_request, forbidden, html, json, multi_conditional, not_allowed, ok, 5 | random_choice, server_error, status, slowdown, random_stop) 6 | from .conditions import (has_param, has_parameter, is_authenticated, is_delete, is_get, # noqa 7 | is_method, is_post, is_put, path_matches, path_is, user_is) 8 | from .middleware import UncertaintyMiddleware # noqa 9 | 10 | __all__ = ('html', 'bad_request', 'forbidden', 'not_allowed', 'server_error', 'status', 'json', 11 | 'delay', 'delay_request', 'random_choice', 'conditional', 'is_method', 'is_get', 12 | 'is_delete', 'is_post', 'is_put', 'has_parameter', 'path_matches', 'path_is', 13 | 'is_authenticated', 'user_is', 'slowdown', random_stop) 14 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 1.7 (Feb 12 2017) 5 | ------------------------- 6 | 7 | * Update README. 8 | 9 | Version 1.6 (Feb 11 2017) 10 | ------------------------- 11 | 12 | * Added changelog. 13 | * Added support for streaming responses. 14 | * Added unit tests. 15 | 16 | Version 1.5 (Aug 23 2016) 17 | ------------------------- 18 | 19 | * Renamed behaviour classes. 20 | * Added and fixed unit tests. 21 | 22 | Version 1.4 (Aug 9 2016) 23 | ------------------------ 24 | 25 | * Fixed setup script. 26 | * Added flake8 setup. 27 | * Added and fixed unit tests. 28 | 29 | Version 1.3 (Aug 9 2016) 30 | ------------------------ 31 | 32 | * Fixed README formatting. 33 | * Fixed ``Predicate`` negation. 34 | * Made settings detection more robust. 35 | * Added unit tests. 36 | 37 | Version 1.2 (Aug 8 2016) 38 | ------------------------ 39 | 40 | * Fixed README formatting. 41 | * Added ``not_found`` behaviour. 42 | 43 | Version 1.1 (Aug 5 2016) 44 | ------------------------ 45 | 46 | * Switched README from Markdown to reStructuredText. 47 | 48 | Version 1.0 (Aug 5 2016) 49 | ------------------------ 50 | 51 | * Initial public release -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os import path 4 | from setuptools import setup 5 | 6 | 7 | with open(path.join(path.abspath(path.dirname(__file__)), 'README.rst')) as f: 8 | long_description = f.read() 9 | 10 | 11 | setup(name='django_uncertainty', 12 | version='1.7', 13 | description='A Django middeware to generate predictable errors on sites', 14 | long_description=long_description, 15 | author='Agustin Barto', 16 | author_email='abarto@gmail.com', 17 | url='https://github.com/abarto/django_uncertainty', 18 | license='BSD', 19 | install_requires=[], 20 | tests_require=['Django>=1.10'], 21 | test_suite='uncertainty.tests.runtests.runtests', 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Environment :: Web Environment', 25 | 'Framework :: Django', 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Programming Language :: Python :: 3', 31 | 'Topic :: Utilities', 32 | ], 33 | packages=['uncertainty']) 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Agustin Barto, Machinalis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /uncertainty/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | class UncertaintyMiddleware(object): 5 | def __init__(self, get_response): 6 | """A Django middleware to introduced controlled uncertainty into the stack. It is controlled 7 | by the DJANGO_UNCERTAINTY setting, were the developers can specify (using the provided 8 | behaviours or their own) the way Django should respond depending on certain conditions. For 9 | example, if you want Django to respond with a server_error 30% of the time, but only 10 | if the requests are POST or PUT, you can use the following specification: 11 | 12 | DJANGO_UNCERTAINTY = random_choice([(conditional(is_post or is_put, server_error()), 0.3)]) 13 | 14 | :param get_response: The get_response method provided by the Django stack 15 | """ 16 | self.get_response = get_response 17 | 18 | def __call__(self, request): 19 | """Controls the middleware behaviour using the specification given by the DJANGO_UNCERTAINTY 20 | setting. 21 | :param request: The request provided by the Django stack 22 | :return: The result of running the uncertainty specification if the DJANGO_UNCERTAINTY is 23 | present, or the default response if it's not. 24 | """ 25 | if hasattr(settings, 'DJANGO_UNCERTAINTY') and settings.DJANGO_UNCERTAINTY is not None: 26 | return settings.DJANGO_UNCERTAINTY(self.get_response, request) 27 | 28 | return self.get_response(request) 29 | -------------------------------------------------------------------------------- /uncertainty/tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import TestCase, override_settings 3 | from unittest.mock import MagicMock 4 | 5 | from uncertainty.middleware import UncertaintyMiddleware 6 | 7 | 8 | class UncertaintyMiddlewareTests(TestCase): 9 | def setUp(self): 10 | self.get_response_mock = MagicMock() 11 | self.request_mock = MagicMock() 12 | self.uncertainty_middleware = UncertaintyMiddleware(self.get_response_mock) 13 | 14 | @override_settings() 15 | def test_calls_get_response_if_setting_is_missing(self): 16 | """Tests that the middleware calls the given get_response if the DJANGO_UNCERTAINTY has not 17 | been set""" 18 | del settings.DJANGO_UNCERTAINTY 19 | self.uncertainty_middleware(self.request_mock) 20 | self.get_response_mock.assert_called_once_with(self.request_mock) 21 | 22 | @override_settings() 23 | def test_returns_get_response_result_if_setting_is_missing(self): 24 | """Tests that the middleware returns the result of calling get_response if the 25 | DJANGO_UNCERTAINTY has not been set""" 26 | del settings.DJANGO_UNCERTAINTY 27 | self.assertEqual(self.get_response_mock.return_value, 28 | self.uncertainty_middleware(self.request_mock)) 29 | 30 | @override_settings(DJANGO_UNCERTAINTY=None) 31 | def test_calls_get_response_if_setting_is_none(self): 32 | """Tests that the middleware calls the given get_response if the DJANGO_UNCERTAINTY setting 33 | is None""" 34 | self.uncertainty_middleware(self.request_mock) 35 | self.get_response_mock.assert_called_once_with(self.request_mock) 36 | 37 | @override_settings(DJANGO_UNCERTAINTY=None) 38 | def test_returns_get_response_result_if_setting_is_none(self): 39 | """Tests that the middleware returns the result of calling get_response if the 40 | DJANGO_UNCERTAINTY setting is None""" 41 | self.assertEqual(self.get_response_mock.return_value, 42 | self.uncertainty_middleware(self.request_mock)) 43 | 44 | def test_calls_django_uncertainty_setting(self): 45 | """Test that the middleware calls the DJANGO_UNCERTAINTY setting""" 46 | django_uncertainty = MagicMock() 47 | with self.settings(DJANGO_UNCERTAINTY=django_uncertainty): 48 | self.uncertainty_middleware(self.request_mock) 49 | django_uncertainty.assert_called_once_with(self.get_response_mock, self.request_mock) 50 | 51 | def test_returns_django_uncertainty_setting_result(self): 52 | """Test that the middleware returns the result of calling the DJANGO_UNCERTAINTY setting""" 53 | django_uncertainty = MagicMock() 54 | with self.settings(DJANGO_UNCERTAINTY=django_uncertainty): 55 | self.assertEqual(django_uncertainty.return_value, 56 | self.uncertainty_middleware(self.request_mock)) 57 | -------------------------------------------------------------------------------- /uncertainty/conditions.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class Predicate: 5 | """Represents a condition that a Django request must meet. It is used in conjunction with 6 | ConditionalBehaviour to control if behaviours are invoked depending on the result of the 7 | Predicate invocation. Multiple predicates can be combined with or and and. 8 | """ 9 | def __call__(self, get_response, request): 10 | """Returns True for all calls. 11 | :param get_response: The get_response method provided by the Django stack 12 | :param request: The request that triggered the middleware 13 | :return: True for all calls. 14 | """ 15 | return True 16 | 17 | def __neg__(self): 18 | """The negation with another predicate 19 | :return: The negation of this predicate 20 | """ 21 | return NotPredicate(self) 22 | 23 | def __or__(self, other): 24 | """The disjunction with another predicate 25 | :param other: The other predicate 26 | :return: A disjunction between this predicate and another 27 | """ 28 | return OrPredicate(self, other) 29 | 30 | def __and__(self, other): 31 | """The conjunction with another predicate 32 | :param other: The other predicate 33 | :return: A conjunction between this predicate and another 34 | """ 35 | return AndPredicate(self, other) 36 | 37 | def __str__(self): 38 | return 'Predicate(True)' 39 | 40 | 41 | class NotPredicate(Predicate): 42 | def __init__(self, predicate): 43 | """The negation of a predicate. 44 | :param predicate: The predicate to negate 45 | """ 46 | self._predicate = predicate 47 | 48 | def __call__(self, get_response, request): 49 | """Returns True if the predicate returns False 50 | :param get_response: The get_response method provided by the Django stack 51 | :param request: The request that triggered the middleware 52 | :return: True if the predicate returns False and False otherwise 53 | """ 54 | return not self._predicate(get_response, request) 55 | 56 | def __str__(self): 57 | return ('NotPredicate(' 58 | 'predicate={predicate})').format(predicate=self._predicate) 59 | 60 | 61 | class OrPredicate(Predicate): 62 | def __init__(self, left, right): 63 | """The disjunction of two predicates. 64 | :param left: The left predicate 65 | :param right: The right predicate 66 | """ 67 | self._left = left 68 | self._right = right 69 | 70 | def __call__(self, get_response, request): 71 | """Returns True if either predicate calls return True 72 | :param get_response: The get_response method provided by the Django stack 73 | :param request: The request that triggered the middleware 74 | :return: True if either predicate calls is True, False otherwise 75 | """ 76 | return self._left(get_response, request) or self._right(get_response, request) 77 | 78 | def __str__(self): 79 | return ('OrPredicate(' 80 | 'left={left}, ' 81 | 'right={right})').format(left=self._left, right=self._right) 82 | 83 | 84 | class AndPredicate(Predicate): 85 | def __init__(self, left, right): 86 | """The conjunction of two predicates. 87 | :param left: The left predicate 88 | :param right: The right predicate 89 | """ 90 | self._left = left 91 | self._right = right 92 | 93 | def __call__(self, get_response, request): 94 | """Returns True if both predicate calls return True 95 | :param get_response: The get_response method provided by the Django stack 96 | :param request: The request that triggered the middleware 97 | :return: True if both predicate calls is True, False otherwise 98 | """ 99 | return self._left(get_response, request) and self._right(get_response, request) 100 | 101 | def __str__(self): 102 | return ('AndPredicate(' 103 | 'left={left}, ' 104 | 'right={right})').format(left=self._left, right=self._right) 105 | 106 | 107 | class IsMethodPredicate(Predicate): 108 | def __init__(self, method): 109 | """Checks if the request method is the same as the one provided. 110 | :param method: The HTTP method (GET, POST, etc.) that request.method ought to be equal to 111 | """ 112 | self._method = method 113 | 114 | def __call__(self, get_response, request): 115 | """Returns True if the request uses the encapsulated HTTP method. 116 | :param get_response: The get_response method provided by the Django stack 117 | :param request: The request that triggered the middleware 118 | :return: True if the request uses the encapuslated method, False otherwise. 119 | """ 120 | return request.method == self._method 121 | 122 | def __str__(self): 123 | return ('IsMethodPredicate(' 124 | 'method={method})').format(method=self._method) 125 | is_method = IsMethodPredicate 126 | is_get = IsMethodPredicate('GET') 127 | is_delete = IsMethodPredicate('DELETE') 128 | is_post = IsMethodPredicate('POST') 129 | is_put = IsMethodPredicate('PUT') 130 | 131 | 132 | class HasRequestParameterPredicate(Predicate): 133 | def __init__(self, parameter): 134 | """Checks if the request contains a parameter. 135 | :param parameter: The name of the parameters 136 | """ 137 | self._parameter = parameter 138 | 139 | def __call__(self, get_response, request): 140 | """Returns True if the request has a parameter 141 | :param get_response: The get_response method provided by the Django stack 142 | :param request: The request that triggered the middleware 143 | :return: True if the request has a parameter, False otherwise 144 | """ 145 | return self._parameter in request.GET or self._parameter in request.POST 146 | 147 | def __str__(self): 148 | return ('HasRequestParameterPredicate(' 149 | 'parameter={parameter})').format(parameter=self._parameter) 150 | has_parameter = HasRequestParameterPredicate 151 | has_param = HasRequestParameterPredicate 152 | 153 | 154 | class PathMatchesRegexpPredicate(Predicate): 155 | def __init__(self, regexp): 156 | """Checks if the request path matches the given regexp 157 | :param regexp: The regexp that the request path should match 158 | """ 159 | self._regexp = re.compile(regexp) 160 | 161 | def __call__(self, get_response, request): 162 | """Returns True if the request path matches the regexp. 163 | :param get_response: The get_response method provided by the Django stack 164 | :param request: The request that triggered the middleware 165 | :return: True if the request matches the regexp, False otherwise 166 | """ 167 | return bool(self._regexp.match(request.path)) 168 | 169 | def __str__(self): 170 | return 'PathMatchesRegexpPredicate(regexp={regexp})'.format(regexp=self._regexp) 171 | path_matches = PathMatchesRegexpPredicate 172 | path_is = path_matches 173 | 174 | 175 | class IsAuthenticatedPredicate(Predicate): 176 | def __call__(self, get_response, request): 177 | """Returns True if the request is authenticated 178 | :param get_response: The get_response method provided by the Django stack 179 | :param request: The request that triggered the middleware 180 | :return: True if the request is authenticated, False otherwise 181 | """ 182 | return hasattr(request, 'user') and request.user.is_authenticated 183 | 184 | def __str__(self): 185 | return 'IsAuthenticatedPredicate()' 186 | is_authenticated = IsAuthenticatedPredicate 187 | 188 | 189 | class IsUserPredicate(Predicate): 190 | def __init__(self, username): 191 | """Checks if the request user username matches the given username 192 | :param username: The username that the request user should have 193 | """ 194 | self._username = username 195 | 196 | def __call__(self, get_response, request): 197 | """Returns True if the request user username matches the given username 198 | :param get_response: The get_response method provided by the Django stack 199 | :param request: The request that triggered the middleware 200 | :return: True if the request user username matches the username, False otherwise 201 | """ 202 | return hasattr(request, 'user') and request.user.username == self._username 203 | 204 | def __str__(self): 205 | return 'IsUser(username={username})'.format(username=self._username) 206 | user_is = IsUserPredicate 207 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django\_uncertainty 2 | =================== 3 | 4 | Introduction 5 | ------------ 6 | 7 | ``django_uncertainty`` is a `Django `_ middleware that allows the 8 | developer to introduce controlled uncertainty into his or her site. The main purpose is providing a 9 | tool to reproduce less-than-ideal conditions in a local development environment to evaluate 10 | external actors might react when a Django site starts misbehaving. 11 | 12 | It requires `Django 1.10 `_ or later as it 13 | uses the new middleware framework. 14 | 15 | Installation 16 | ------------ 17 | 18 | You can get ``django_uncertainty`` using pip: 19 | 20 | $ pip install django\_uncertainty 21 | 22 | If you want to install it from source, grab the git repository from 23 | GitHub and run setup.py: 24 | 25 | :: 26 | 27 | $ git clone git://github.com/abarto/django_uncertainty.git 28 | $ cd django_uncertainty 29 | $ python setup.py install 30 | 31 | Once the package has been installed, you need to add the middleware to 32 | your Django settings file: 33 | 34 | :: 35 | 36 | MIDDLEWARE = [ 37 | 'django.middleware.security.SecurityMiddleware', 38 | ... 39 | 'uncertainty.UncertaintyMiddleware' 40 | ] 41 | 42 | Usage 43 | ----- 44 | 45 | The middleware behaviour is controlled by the ``DJANGO_UNCERTAINTY`` Django setting. For example: 46 | 47 | :: 48 | 49 | import uncertainty as u 50 | DJANGO_UNCERTAINTY = u.cond( 51 | u.path_matches('^/api'), u.random_choice([ 52 | (u.delay(u.default(), 5), 0.3), (u.server_error(), 0.2)])) 53 | 54 | This tells the middleware that if the request path starts with "/api", 30% of the time the request 55 | is going to be delayed by 5 seconds, 20% of the time the site is going to respond with a status 500 56 | (Server Error), and the rest of the time the site is going to function normally. 57 | 58 | The next section describes all the available behaviours and conditions. 59 | 60 | Behaviours 61 | ---------- 62 | 63 | All behaviours are implemented as sub-classes of the ``Behaviour`` 64 | class: 65 | 66 | :: 67 | 68 | class Behaviour: 69 | """Base of all behaviours. It is also the default implementation which just just returns the 70 | result of calling get_response.""" 71 | def __call__(self, get_response, request): 72 | """Returns the result of calling get_response (as given by the UncertaintyMiddleware 73 | middleware with request as argument. It returns the same response that would have been 74 | created by the Django stack without the introduction of UncertaintyMiddleware. 75 | :param get_response: The get_response method provided by the Django stack 76 | :param request: The request that triggered the middleware 77 | :return: The result of calling get_response with the request parameter 78 | """ 79 | response = get_response(request) 80 | return response 81 | 82 | Behaviours work like functions that take the same parameters given the 83 | the Django middleware. 84 | 85 | default 86 | ~~~~~~~ 87 | 88 | As the name implies, this is the default behaviour. It just makes the requests continue as usual 89 | through the Django stack. Using ``default`` is the same as omitting the ``DJANGO_UNCERTAINTY`` 90 | setting altogether. 91 | 92 | :: 93 | 94 | import uncertainty as u 95 | DJANGO_UNCERTAINTY = u.default() 96 | 97 | html 98 | ~~~~ 99 | 100 | Overrides the site's response with an arbitrary HTTP response. Without any arguments it returns a 101 | response with status code 200 (Ok). ``html`` takes the same arguments as Django's 102 | `HttpResponse `_. 103 | 104 | :: 105 | 106 | import uncertainty as u 107 | DJANGO_UNCERTAINTY = u.html('

Hello World!

') 108 | 109 | ok 110 | ~~ 111 | 112 | An alias for ``html``. 113 | 114 | bad\_request 115 | ~~~~~~~~~~~~ 116 | 117 | Overrides the site's response with an HTTP response with status code 400 (Bad Request). 118 | ``bad_request`` takes the same arguments as Django's 119 | `HttpResponseBadRequest `_. 120 | 121 | :: 122 | 123 | import uncertainty as u 124 | DJANGO_UNCERTAINTY = u.bad_request('Oops!') 125 | 126 | forbidden 127 | ~~~~~~~~~ 128 | 129 | Overrides the site's response with an HTTP response with status code 403 (Forbidden). ``forbidden`` 130 | takes the same arguments as Django's 131 | `HttpResponseForbidden `_. 132 | 133 | :: 134 | 135 | import uncertainty as u 136 | DJANGO_UNCERTAINTY = u.forbidden('NOPE') 137 | 138 | not\_allowed 139 | ~~~~~~~~~~~~ 140 | 141 | Overrides the site's response with an HTTP response with status code 405 (Not Allowed). 142 | ``not_allowed`` takes the same arguments as Django's 143 | `HttpResponseNotAllowed `_. 144 | 145 | :: 146 | 147 | import uncertainty as u 148 | DJANGO_UNCERTAINTY = u.not_allowed(permitted_methods=['PUT'], content='NOPE') 149 | 150 | not\_found 151 | ~~~~~~~~~~ 152 | 153 | Overrides the site's response with an HTTP response with status code 404 (Not Found). 154 | ``not_found`` takes the same arguments as Django's 155 | `HttpResponse `_. 156 | 157 | :: 158 | 159 | import uncertainty as u 160 | DJANGO_UNCERTAINTY = u.not_found(permitted_methods=['PUT'], content='Who?') 161 | 162 | server\_error 163 | ~~~~~~~~~~~~~ 164 | 165 | Overrides the site's response with an HTTP response with status code 500 (Internal Server Error). 166 | ``server_error`` takes the same arguments as Django's 167 | `HttpResponseServerError `_. 168 | 169 | :: 170 | 171 | import uncertainty as u 172 | DJANGO_UNCERTAINTY = u.server_error('BOOM') 173 | 174 | status 175 | ~~~~~~ 176 | 177 | Overrides the site's response with an HTTP response with a given status code. 178 | 179 | :: 180 | 181 | import uncertainty as u 182 | DJANGO_UNCERTAINTY = u.status(201, content='

Created

') 183 | 184 | json 185 | ~~~~ 186 | 187 | Overrides the site's response with an arbitrary HTTP response with content type 188 | ``application/json``. Without any arguments it returns a response with status code 200 (Ok). 189 | ``json`` takes the same arguments as Django's 190 | `JsonResponse `_. 191 | 192 | :: 193 | 194 | import uncertainty as u 195 | DJANGO_UNCERTAINTY = u.json({'foo': 1, 'bar': True}) 196 | 197 | delay 198 | ~~~~~ 199 | 200 | Introduces a delay after invoking another behaviour. For example, this specifies a delay of half a 201 | second into the actual site responses: 202 | 203 | :: 204 | 205 | import uncertainty as u 206 | DJANGO_UNCERTAINTY = u.delay(u.default(), 0.5) 207 | 208 | You can replace the first argument with any other valid behaviour. 209 | 210 | delay\_request 211 | ~~~~~~~~~~~~~~ 212 | 213 | It is similar to ``delay``, but the delay is introduced *before* the specified behaviour is invoked. 214 | 215 | random\_choice 216 | ~~~~~~~~~~~~~~ 217 | 218 | This is the work horse of ``django_uncertainty``. ``random_choice`` allows you to specify different 219 | behaviours that are going to be chosen at random (following the give proportions) when a request is 220 | received. It takes a list of behaviours or tuples of behaviours and proportions, 221 | 222 | For example, let's say we want 30% of the request to be responded with an Internal Server Error 223 | response, 20% with a Bad Request response, and the rest with the actual response but with a 1 224 | second delay. This can be specified as follows: 225 | 226 | :: 227 | 228 | import uncertainty as u 229 | DJANGO_UNCERTAINTY = u.random_choice([(u.server_error(), 0.3), (u.bad_request(), 0.2), u.delay(u.default(), 1)]) 230 | 231 | If proportions are specified, the total sum of them must be less than 1. If no proportions are 232 | specified, the behaviours are chosen with an even chance between them: 233 | 234 | :: 235 | 236 | import uncertainty as u 237 | DJANGO_UNCERTAINTY = u.random_choice([u.server_error(), u.default()]) 238 | 239 | This specifies that approximetly half the request are going to be responded with an Internal Server 240 | Error, and half will work normally. 241 | 242 | conditional 243 | ~~~~~~~~~~~ 244 | 245 | It allows you to specify that a certain behaviour should be invoked only if a certain condition is 246 | met. If the condition is not met, the alternative behvaiour (which is ``default`` by default) is 247 | executed. 248 | 249 | :: 250 | 251 | import uncertainty as u 252 | DJANGO_UNCERTAINTY = u.conditional(u.is_post, u.server_error()) 253 | 254 | The specification above states that if the request uses the POST method, the site should respond 255 | with an Internal Server Error. If you want to specify an alternative behaviour other than the 256 | default, use the ``alternative_behaviour`` argument: 257 | 258 | :: 259 | 260 | import uncertainty as u 261 | DJANGO_UNCERTAINTY = u.conditional(u.is_post, u.server_error(), alternative_behaviour=u.delay(u.default(), 0.3) 262 | 263 | Conditions can be combined using boolean operators. For instance, 264 | 265 | :: 266 | 267 | import uncertainty as u 268 | DJANGO_UNCERTAINTY = u.conditional(u.is_authenticated | -u.is_get, u.bad_request()) 269 | 270 | specifies that if the request is authenticated or if it uses the GET method, a Bad Request response 271 | should be used. 272 | 273 | In the next section, all the predefined conditions are presented. 274 | 275 | cond 276 | ~~~~ 277 | 278 | An alias for ``conditional``. 279 | 280 | multi\_conditional 281 | ~~~~~~~~~~~~~~~~~~ 282 | 283 | ``multi_conditional`` takes a list of condition/behaviour pairs, and when a request is received, it 284 | iterates over the conditions until one is met, and the corresponding behaviour is invoked. If no 285 | condition is met, the default behaviour is invoked. 286 | 287 | :: 288 | 289 | import uncertainty as u 290 | DJANGO_UNCERTAINTY = u.multi_conditional([(u.is_get, u.delay(u.default(), 0.5), (u.is_post, u.server_error())]) 291 | 292 | The specification above states that if the request uses the GET method, it should be delayed by 293 | half a second, if it uses POST, it should respond with an Internal Server Error, and if neither of 294 | those conditions are met, the request should go through as usual. 295 | 296 | The default behaviour to be used when no conditions are met can be specified with the ``default_behaviour`` argument: 297 | 298 | :: 299 | 300 | import uncertainty as u 301 | DJANGO_UNCERTAINTY = u.multi_conditional( 302 | [ 303 | (u.is_get, u.delay(u.default(), 0.5), 304 | (u.is_post, u.server_error()) 305 | ], default_behaviour=u.not_found()) 306 | 307 | multi\_cond 308 | ~~~~~~~~~~~ 309 | 310 | An alias for ``multi_conditional``. 311 | 312 | case 313 | ~~~~ 314 | 315 | An alias for ``multi_conditional``. 316 | 317 | slowdown 318 | ~~~~~~~~ 319 | 320 | ``slowdown`` slows down a streaming response by introducing a set delay (in seconds) in between the 321 | chunks. If the response is not a streaming one, ``slowdown`` does nothing. 322 | 323 | :: 324 | 325 | import uncertainty as u 326 | DJANGO_UNCERTAINTY = u.slowdown(u.default(), 0.5) # 0.5 seconds delay between each chunk 327 | 328 | random_stop 329 | ~~~~~~~~~~~ 330 | 331 | ``random_stop`` stops a streaming response randomly with a given probability. If the response is 332 | not a streaming one, ``random_stop`` does nothing. 333 | 334 | :: 335 | 336 | import uncertainty as u 337 | DJANGO_UNCERTAINTY = u.random_stop(u.default(), 0.2) # 0.2 chance of stoping the stream 338 | 339 | Custom behaviours 340 | ~~~~~~~~~~~~~~~~~ 341 | 342 | We've done our best to implement behaviours that make sense in the context of introducing 343 | uncertainty into a Django site, however, if you need to implement your own behaviours, all you need 344 | to do is derive the ``Behaviour`` class. Let's say you want a Behaviour that adds a header to the 345 | response generated by another behaviour. Here's one possible implementation of such behaviour: 346 | 347 | :: 348 | 349 | class AddHeaderBehaviour(Behaviour): 350 | def __init__(self, behaviour, header_name, header_value): 351 | self._behaviour = behaviour 352 | self._header_name = header_name 353 | self._header_value = header_value 354 | 355 | def __call__(self, get_response, request): 356 | response = self._behaviour(get_response, request) 357 | response[self._header_name] = self._header_value 358 | 359 | return response 360 | 361 | For streaming responses, create a new subclass of ``StreamBehaviour`` overriding the 362 | ``wrap_streaming_content`` method. For example, if you need to drop every other chunk from the 363 | response stream, here's what you can do: 364 | 365 | :: 366 | 367 | class DropEvenStreamBehaviour(StreamBehaviour): 368 | def wrap_streaming_content(self, streaming_content): 369 | """Drops every other chunk of streaming_content. 370 | :param streaming_content: The streaming_content field of the response. 371 | """ 372 | for i, chunk in enumerate(streaming_content): 373 | if i % 2 == 0: 374 | yield chunk 375 | 376 | 377 | If you think that there's a use case that we haven't covered that might be useful for other users, 378 | feel free to create an issue on `GitHub `__. 379 | 380 | Conditions 381 | ---------- 382 | 383 | Conditions are subclasses of the ``Predicate`` class: 384 | 385 | :: 386 | 387 | class Predicate: 388 | """Represents a condition that a Django request must meet. It is used in conjunction with 389 | ConditionalBehaviour to control if behaviours are invoked depending on the result of the 390 | Predicate invocation. Multiple predicates can be combined with or and and. 391 | """ 392 | def __call__(self, get_response, request): 393 | """Returns True for all calls. 394 | :param get_response: The get_response method provided by the Django stack 395 | :param request: The request that triggered the middleware 396 | :return: True for all calls. 397 | """ 398 | return True 399 | 400 | Whenever a conditional behaviour is used, the predicate is invoked with the same parameters that 401 | would be given the the behaviour. 402 | 403 | is\_method 404 | ~~~~~~~~~~ 405 | 406 | The condition is met if the request uses the specified method. 407 | 408 | :: 409 | 410 | import uncertainty as u 411 | DJANGO_UNCERTAINTY = u.cond(u.is_method('PATCH'), u.not_allowed()) 412 | 413 | is\_get 414 | ~~~~~~~ 415 | 416 | The condition is met if the request uses the GET HTTP method. 417 | 418 | :: 419 | 420 | import uncertainty as u 421 | DJANGO_UNCERTAINTY = u.cond(u.is_get, u.not_allowed()) 422 | 423 | is\_delete 424 | ~~~~~~~~~~ 425 | 426 | The condition is met if the request uses the DELETE HTTP method. 427 | 428 | :: 429 | 430 | import uncertainty as u 431 | DJANGO_UNCERTAINTY = u.cond(u.is_delete, u.not_allowed()) 432 | 433 | is\_post 434 | ~~~~~~~~ 435 | 436 | The condition is met if the request uses the POST HTTP method. 437 | 438 | :: 439 | 440 | import uncertainty as u 441 | DJANGO_UNCERTAINTY = u.cond(u.is_post, u.not_allowed()) 442 | 443 | is\_put 444 | ~~~~~~~ 445 | 446 | The condition is met if the request uses the PUT HTTP method. 447 | 448 | :: 449 | 450 | import uncertainty as u 451 | DJANGO_UNCERTAINTY = u.cond(u.is_put, u.not_allowed()) 452 | 453 | has\_parameter 454 | ~~~~~~~~~~~~~~ 455 | 456 | The condition is met if the request has the given parameter. 457 | 458 | :: 459 | 460 | import uncertainty as u 461 | DJANGO_UNCERTAINTY = u.cond(u.has_parameter('q'), u.server_error()) 462 | 463 | has\_param 464 | ~~~~~~~~~~ 465 | 466 | An alias for ``has_parameter`` 467 | 468 | path\_matches 469 | ~~~~~~~~ 470 | 471 | The condition is met if the request path matches the given regular expression. 472 | 473 | :: 474 | 475 | import uncertainty as u 476 | DJANGO_UNCERTAINTY = u.cond(u.path_matches('^/api'), u.delay(u.default(), 0.2)) 477 | 478 | is\_authenticated 479 | ~~~~~~~~~~~~~~~~~ 480 | 481 | The condition is met if the user has authenticated itself. 482 | 483 | :: 484 | 485 | import uncertainty as u 486 | DJANGO_UNCERTAINTY = u.cond(u.is_authenticated, u.not_found()) 487 | 488 | user\_is 489 | ~~~~~~~~ 490 | 491 | The condition is met if the authenticated user has the given username. 492 | 493 | :: 494 | 495 | import uncertainty as u 496 | DJANGO_UNCERTAINTY = u.cond(u.user_is('admin', u.forbidden()) 497 | 498 | Custom conditions 499 | ~~~~~~~~~~~~~~~~~ 500 | 501 | As with behaviours, custom conditions are creating deriving the ``Predicate`` class. Let's say you 502 | want a condition that checks the presence of a header in the request. Here's one possible 503 | implementation of such condition: 504 | 505 | :: 506 | 507 | class HasHeaderPredicate(Predicate): 508 | def __index__(self, header_name): 509 | self._header_name = header_name 510 | 511 | def __call__(self, get_response, request): 512 | return self._header_name in request 513 | 514 | Feedback 515 | -------- 516 | 517 | All feedback is appreciated, so if you found problems or have ides for new features, just create an 518 | issue on `GitHub `_. 519 | -------------------------------------------------------------------------------- /uncertainty/tests/test_conditions.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from uncertainty.conditions import (Predicate, NotPredicate, OrPredicate, AndPredicate, 5 | IsMethodPredicate, is_get, is_delete, is_post, is_put, 6 | has_parameter, is_authenticated, user_is, path_matches) 7 | 8 | 9 | class PredicateTests(TestCase): 10 | def setUp(self): 11 | self.predicate = Predicate() 12 | self.other_predicate = MagicMock() 13 | 14 | def test_call_returns_true(self): 15 | """Test that invoking return True""" 16 | get_response_mock = MagicMock() 17 | request_mock = MagicMock() 18 | self.assertTrue(self.predicate(get_response_mock, request_mock)) 19 | 20 | def test_negate_calls_not_predicate(self): 21 | """Test that negating the predicate calls NotPredicate""" 22 | with patch('uncertainty.conditions.NotPredicate') as not_predicate_mock: 23 | -self.predicate 24 | not_predicate_mock.assert_called_once_with(self.predicate) 25 | 26 | def test_negate_returns_not_predicate(self): 27 | """Test that negating the predicate returns a NotPredicate""" 28 | with patch('uncertainty.conditions.NotPredicate') as not_predicate_mock: 29 | self.assertEqual(not_predicate_mock.return_value, -self.predicate) 30 | 31 | def test_or_calls_or_predicate(self): 32 | """Test that using the bitwise or (|) operator with the predicate calls OrPredicate""" 33 | with patch('uncertainty.conditions.OrPredicate') as or_predicate_mock: 34 | self.predicate | self.other_predicate 35 | or_predicate_mock.assert_called_once_with(self.predicate, self.other_predicate) 36 | 37 | def test_or_returns_or_predicate(self): 38 | """Test that using the bitwise or (|) operator with the predicate a NotPredicate""" 39 | with patch('uncertainty.conditions.OrPredicate') as or_predicate_mock: 40 | self.assertEqual(or_predicate_mock.return_value, self.predicate | self.other_predicate) 41 | 42 | def test_and_calls_and_predicate(self): 43 | """Test that using the bitwise and (&) operator with the predicate calls OrPredicate""" 44 | with patch('uncertainty.conditions.AndPredicate') as and_predicate_mock: 45 | self.predicate & self.other_predicate 46 | and_predicate_mock.assert_called_once_with(self.predicate, self.other_predicate) 47 | 48 | def test_and_returns_and_predicate(self): 49 | """Test that using the bitwise and (&) operator with the predicate a NotPredicate""" 50 | with patch('uncertainty.conditions.AndPredicate') as and_predicate_mock: 51 | self.assertEqual(and_predicate_mock.return_value, self.predicate & self.other_predicate) 52 | 53 | 54 | class NotPredicateTests(TestCase): 55 | def setUp(self): 56 | self.some_predicate = MagicMock() 57 | self.not_predicate = NotPredicate(self.some_predicate) 58 | self.get_response_mock = MagicMock() 59 | self.request_mock = MagicMock() 60 | 61 | def test_calls_encapsulated_predicate(self): 62 | """Tests that calling the NotPredicate calls the encapsulated predicate""" 63 | self.not_predicate(self.get_response_mock, self.request_mock) 64 | self.some_predicate.assert_called_once_with(self.get_response_mock, self.request_mock) 65 | 66 | def test_returns_false_when_encapsulated_predicate_returns_true(self): 67 | """Tests that calling the NotPredicate returns False when the encapsulated predicate 68 | returns True""" 69 | self.some_predicate.return_value = True 70 | self.assertFalse(self.not_predicate(self.get_response_mock, self.request_mock)) 71 | 72 | def test_returns_true_when_encapsulated_predicate_returns_false(self): 73 | """Tests that calling the NotPredicate returns True when the encapsulated predicate 74 | returns False""" 75 | self.some_predicate.return_value = False 76 | self.assertTrue(self.not_predicate(self.get_response_mock, self.request_mock)) 77 | 78 | 79 | class OrPredicateTests(TestCase): 80 | def setUp(self): 81 | self.some_left_predicate = MagicMock() 82 | self.some_right_predicate = MagicMock() 83 | self.or_predicate = OrPredicate(self.some_left_predicate, self.some_right_predicate) 84 | self.get_response_mock = MagicMock() 85 | self.request_mock = MagicMock() 86 | 87 | def test_calls_left_encapsulated_predicate(self): 88 | """Tests that calling the OrPredicate calls the left encapsulated predicate""" 89 | self.or_predicate(self.get_response_mock, self.request_mock) 90 | self.some_left_predicate.assert_called_once_with(self.get_response_mock, self.request_mock) 91 | 92 | def test_doesnt_call_right_encapsulated_predicate_if_left_is_true(self): 93 | """Tests that calling the OrPredicate doesn't call the right encapsulated predicate if 94 | calling the left predicate returns True""" 95 | self.some_left_predicate.return_value = True 96 | self.or_predicate(self.get_response_mock, self.request_mock) 97 | self.some_right_predicate.assert_not_called() 98 | 99 | def test_calls_right_encapsulated_predicate_if_left_is_false(self): 100 | """Tests that calling the OrPredicate calls the right encapsulated predicate if 101 | calling the left predicate returns False""" 102 | self.some_left_predicate.return_value = False 103 | self.or_predicate(self.get_response_mock, self.request_mock) 104 | self.some_left_predicate.assert_called_once_with(self.get_response_mock, self.request_mock) 105 | 106 | def test_returns_true_if_left_encapsulated_predicate_is_true(self): 107 | """Tests that calling the OrPredicate returns True if calling the left encapsulated 108 | predicate is True""" 109 | self.some_left_predicate.return_value = True 110 | self.assertTrue(self.or_predicate(self.get_response_mock, self.request_mock)) 111 | 112 | def test_returns_true_if_right_encapsulated_predicate_is_true(self): 113 | """Tests that calling the OrPredicate returns True if calling the right encapsulated 114 | predicate is True""" 115 | self.some_right_predicate.return_value = True 116 | self.assertTrue(self.or_predicate(self.get_response_mock, self.request_mock)) 117 | 118 | def test_returns_false_if_encapsulated_predicates_are_false(self): 119 | """Tests that calling the OrPredicate returns False if calling both encapsulated predicates 120 | return False""" 121 | self.some_left_predicate.return_value = False 122 | self.some_right_predicate.return_value = False 123 | self.assertFalse(self.or_predicate(self.get_response_mock, self.request_mock)) 124 | 125 | 126 | class AndPredicateTests(TestCase): 127 | def setUp(self): 128 | self.some_left_predicate = MagicMock() 129 | self.some_right_predicate = MagicMock() 130 | self.and_predicate = AndPredicate(self.some_left_predicate, self.some_right_predicate) 131 | self.get_response_mock = MagicMock() 132 | self.request_mock = MagicMock() 133 | 134 | def test_calls_left_encapsulated_predicate(self): 135 | """Tests that calling the AndPredicate calls the left encapsulated predicate""" 136 | self.and_predicate(self.get_response_mock, self.request_mock) 137 | self.some_left_predicate.assert_called_once_with(self.get_response_mock, 138 | self.request_mock) 139 | 140 | def test_doesnt_call_right_encapsulated_predicate_if_left_is_false(self): 141 | """Tests that calling the AndPredicate doesn't call the right encapsulated predicate if 142 | calling the left predicate returns False""" 143 | self.some_left_predicate.return_value = False 144 | self.and_predicate(self.get_response_mock, self.request_mock) 145 | self.some_right_predicate.assert_not_called() 146 | 147 | def test_calls_right_encapsulated_predicate_if_left_is_true(self): 148 | """Tests that calling the AndPredicate calls the right encapsulated predicate if 149 | calling the left predicate returns False""" 150 | self.some_left_predicate.return_value = True 151 | self.and_predicate(self.get_response_mock, self.request_mock) 152 | self.some_left_predicate.assert_called_once_with(self.get_response_mock, 153 | self.request_mock) 154 | 155 | def test_returns_false_if_left_encapsulated_predicate_is_false(self): 156 | """Tests that calling the AndPredicate returns False if calling the left encapsulated 157 | predicate is False""" 158 | self.some_left_predicate.return_value = False 159 | self.assertFalse(self.and_predicate(self.get_response_mock, self.request_mock)) 160 | 161 | def test_returns_false_if_right_encapsulated_predicate_is_false(self): 162 | """Tests that calling the AndPredicate returns True if calling the right encapsulated 163 | predicate is True""" 164 | self.some_right_predicate.return_value = False 165 | self.assertFalse(self.and_predicate(self.get_response_mock, self.request_mock)) 166 | 167 | def test_returns_true_if_encapsulated_predicates_are_true(self): 168 | """Tests that calling the AndPredicate returns True if calling both encapsulated predicates 169 | return True""" 170 | self.some_left_predicate.return_value = True 171 | self.some_right_predicate.return_value = True 172 | self.assertTrue(self.and_predicate(self.get_response_mock, self.request_mock)) 173 | 174 | 175 | class IsMethodPredicateTests(TestCase): 176 | def setUp(self): 177 | self.some_method = MagicMock() 178 | self.is_method_predicate = IsMethodPredicate(self.some_method) 179 | self.get_response_mock = MagicMock() 180 | self.request_mock = MagicMock() 181 | 182 | def test_returns_true_if_request_method_is_encapsulated_method(self): 183 | """Tests that calling IsMethodPredicate returns True if the request method is the same as 184 | the encapsulated method""" 185 | self.request_mock.method = self.some_method 186 | self.assertTrue(self.is_method_predicate(self.get_response_mock, self.request_mock)) 187 | 188 | def test_returns_false_if_request_method_is_nt_encapsulated_method(self): 189 | """Tests that calling IsMethodPredicate returns False if the request method is not the same 190 | as the encapsulated method""" 191 | self.request_mock.method = MagicMock() 192 | self.assertFalse(self.is_method_predicate(self.get_response_mock, self.request_mock)) 193 | 194 | 195 | class IsGetTests(TestCase): 196 | def setUp(self): 197 | self.get_response_mock = MagicMock() 198 | self.request_mock = MagicMock() 199 | 200 | def test_returns_true_if_request_method_is_get(self): 201 | """Tests that calling is_get returns True if the request method is 'GET'""" 202 | self.request_mock.method = 'GET' 203 | self.assertTrue(is_get(self.get_response_mock, self.request_mock)) 204 | 205 | def test_returns_false_if_request_method_is_not_get(self): 206 | """Tests that calling is_get returns False if the request method is not 'GET'""" 207 | self.request_mock.method = 'FOOBAR' 208 | self.assertFalse(is_get(self.get_response_mock, self.request_mock)) 209 | 210 | 211 | class IsDeleteTests(TestCase): 212 | def setUp(self): 213 | self.get_response_mock = MagicMock() 214 | self.request_mock = MagicMock() 215 | 216 | def test_returns_true_if_request_method_is_delete(self): 217 | """Tests that calling is_delete returns True if the request method is 'DELETE'""" 218 | self.request_mock.method = 'DELETE' 219 | self.assertTrue(is_delete(self.get_response_mock, self.request_mock)) 220 | 221 | def test_returns_false_if_request_method_is_not_delete(self): 222 | """Tests that calling is_delete returns False if the request method is not 'DELETE'""" 223 | self.request_mock.method = 'FOOBAR' 224 | self.assertFalse(is_delete(self.get_response_mock, self.request_mock)) 225 | 226 | 227 | class IsPostTests(TestCase): 228 | def setUp(self): 229 | self.get_response_mock = MagicMock() 230 | self.request_mock = MagicMock() 231 | 232 | def test_returns_true_if_request_method_is_post(self): 233 | """Tests that calling is_post returns True if the request method is 'POST'""" 234 | self.request_mock.method = 'POST' 235 | self.assertTrue(is_post(self.get_response_mock, self.request_mock)) 236 | 237 | def test_returns_false_if_request_method_is_not_post(self): 238 | """Tests that calling is_post returns False if the request method is not 'POST'""" 239 | self.request_mock.method = 'FOOBAR' 240 | self.assertFalse(is_post(self.get_response_mock, self.request_mock)) 241 | 242 | 243 | class IsPutTests(TestCase): 244 | def setUp(self): 245 | self.get_response_mock = MagicMock() 246 | self.request_mock = MagicMock() 247 | 248 | def test_returns_true_if_request_method_is_post(self): 249 | """Tests that calling is_put returns True if the request method is 'PUT'""" 250 | self.request_mock.method = 'PUT' 251 | self.assertTrue(is_put(self.get_response_mock, self.request_mock)) 252 | 253 | def test_returns_false_if_request_method_is_not_post(self): 254 | """Tests that calling is_put returns False if the request method is not 'PUT'""" 255 | self.request_mock.method = 'FOOBAR' 256 | self.assertFalse(is_put(self.get_response_mock, self.request_mock)) 257 | 258 | 259 | class HasRequestParameterPredicateTests(TestCase): 260 | def setUp(self): 261 | self.get_response_mock = MagicMock() 262 | self.request_mock = MagicMock() 263 | self.parameter_name = 'name' 264 | self.has_parameter = has_parameter(self.parameter_name) 265 | 266 | def test_returns_true_if_request_has_get_parameter(self): 267 | """Tests that calling has_parameter returns True if the request has a specific GET 268 | parameter""" 269 | self.request_mock.GET = {self.parameter_name: 'foobar'} 270 | self.assertTrue(self.has_parameter(self.get_response_mock, self.request_mock)) 271 | 272 | def test_returns_true_if_request_has_post_parameter(self): 273 | """Tests that calling has_parameter returns True if the request has a specific POST 274 | parameter""" 275 | self.request_mock.POST = {self.parameter_name: 'foobar'} 276 | self.assertTrue(self.has_parameter(self.get_response_mock, self.request_mock)) 277 | 278 | def test_returns_false_if_request_hasnt_get_parameter(self): 279 | """Tests that calling has_parameter returns False if the request doesn't have a specific GET 280 | parameter""" 281 | self.request_mock.GET = dict() 282 | self.assertFalse(self.has_parameter(self.get_response_mock, self.request_mock)) 283 | 284 | def test_returns_false_if_request_hasnt_post_parameter(self): 285 | """Tests that calling has_parameter returns False if the request doesn't have a specific 286 | POST parameter""" 287 | self.request_mock.POST = dict() 288 | self.assertFalse(self.has_parameter(self.get_response_mock, self.request_mock)) 289 | 290 | 291 | class PathMatchesRegexpPredicateTests(TestCase): 292 | def setUp(self): 293 | self.get_response_mock = MagicMock() 294 | self.request_mock = MagicMock() 295 | self.path_matches = path_matches('^/some_path.*') 296 | 297 | def test_returns_true_if_request_path_matches_regexp(self): 298 | """Tests that calling path_matches returns True if the request path matches the regexp""" 299 | self.request_mock.path = '/some_path/foobar' 300 | self.assertTrue(self.path_matches(self.get_response_mock, self.request_mock)) 301 | 302 | def test_returns_false_if_request_path_doesnt_match_regexp(self): 303 | """Tests that calling path_matches returns False if the request path doesn't match the 304 | regexp""" 305 | self.request_mock.path = '/some_other_path/foobar' 306 | self.assertFalse(self.path_matches(self.get_response_mock, self.request_mock)) 307 | 308 | 309 | class IsAuthenticatedPredicateTests(TestCase): 310 | def setUp(self): 311 | self.get_response_mock = MagicMock() 312 | self.request_mock = MagicMock() 313 | self.is_authenticated = is_authenticated() 314 | 315 | def test_returns_true_if_request_user_is_authenticated(self): 316 | """Tests that calling is_authenticated returns True if the request user is authenticated""" 317 | self.request_mock.user.is_authenticated = True 318 | self.assertTrue(self.is_authenticated(self.get_response_mock, self.request_mock)) 319 | 320 | def test_returns_false_if_request_user_isnt_authenticated(self): 321 | """Tests that calling is_authenticated returns False if the request user is not 322 | authenticated""" 323 | self.request_mock.user.is_authenticated = False 324 | self.assertFalse(self.is_authenticated(self.get_response_mock, self.request_mock)) 325 | request_mock = MagicMock(spec=[]) 326 | self.assertFalse(self.is_authenticated(self.get_response_mock, request_mock)) 327 | 328 | 329 | class IsUserPredicateTests(TestCase): 330 | def setUp(self): 331 | self.get_response_mock = MagicMock() 332 | self.request_mock = MagicMock() 333 | self.username = 'username' 334 | self.user_is = user_is(self.username) 335 | 336 | def test_returns_true_if_request_user_matches_predicates(self): 337 | """Tests that calling user_is returns True if request user matches the predicate's""" 338 | self.request_mock.user.username = self.username 339 | self.assertTrue(self.user_is(self.get_response_mock, self.request_mock)) 340 | 341 | def test_returns_false_if_request_user_doesnt_match_predicates(self): 342 | """Tests that calling user_is returns False if request user matches doesn't match the 343 | predicate's""" 344 | self.request_mock.user.username = 'foobar' 345 | self.assertFalse(self.user_is(self.get_response_mock, self.request_mock)) 346 | request_mock = MagicMock(spec=[]) 347 | self.assertFalse(self.user_is(self.get_response_mock, request_mock)) 348 | -------------------------------------------------------------------------------- /uncertainty/behaviours.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | from time import sleep 3 | 4 | from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, 5 | HttpResponseNotAllowed, HttpResponseServerError, JsonResponse) 6 | 7 | 8 | class Behaviour: 9 | """Base of all behaviours. It is also the default implementation which just just returns the 10 | result of calling get_response.""" 11 | def __call__(self, get_response, request): 12 | """Returns the result of calling get_response (as given by the UncertaintyMiddleware 13 | middleware with request as argument. It returns the same response that would have been 14 | created by the Django stack without the introduction of UncertaintyMiddleware. 15 | :param get_response: The get_response method provided by the Django stack 16 | :param request: The request that triggered the middleware 17 | :return: The result of calling get_response with the request parameter 18 | """ 19 | response = get_response(request) 20 | return response 21 | default = Behaviour 22 | _default = default() 23 | 24 | 25 | class HttpResponseBehaviour(Behaviour): 26 | def __init__(self, response_class, *args, **kwargs): 27 | """A Behaviour that overrides the default response with the result of calling an 28 | HttpResponse constructor. 29 | :param response_class: An HttpResponse class 30 | :param args: The positional arguments for the HttpResponse constructor 31 | :param kwargs: The named arguments for the HttpResponse constructor 32 | """ 33 | self._response_class = response_class 34 | self._args = args 35 | self._kwargs = kwargs 36 | 37 | def __call__(self, get_response, request): 38 | """Returns the result of calling the HttpResponse constructor with the positional and named 39 | arguments supplied. The get_response method provided by the Django stack is never called. 40 | :param get_response: The get_response method provided by the Django stack (ignored) 41 | :param request: The request that triggered the middleware (ignored) 42 | :return: The result of calling the HttpResponse constructor with the positional and named 43 | arguments supplied. 44 | """ 45 | response = self._response_class(*self._args, **self._kwargs) 46 | return response 47 | 48 | def __str__(self): 49 | return ('HttpResponseBehaviour(' 50 | 'response_class={response_class}, ' 51 | 'args={args}, ' 52 | 'kwargs={kwargs})').format(response_class=self._response_class, 53 | args=self._args, 54 | kwargs=self._kwargs) 55 | 56 | 57 | def html(*args, **kwargs): 58 | """A Behaviour that returns an HttpResponse object overriding the actual response of the Django 59 | stack. The function takes the same arguments as HttpResponse, which allows changing the content, 60 | status, or any other feature exposed through the constructor 61 | :param args: Positional arguments for the HttpResponse constructor 62 | :param kwargs: Named arguments for the HttpResponse constructor 63 | :return: An HttpResponse overriding the Django stack response 64 | """ 65 | return HttpResponseBehaviour(HttpResponse, *args, **kwargs) 66 | ok = html 67 | 68 | 69 | def bad_request(*args, **kwargs): 70 | """A Behaviour that returns an HttpResponseBadRequest object overriding the actual response of 71 | the Django stack. The function takes the same arguments as HttpResponseBadRequest, which allows 72 | changing the content, status, or any other feature exposed through the constructor 73 | :param args: Positional arguments for the HttpResponseBadRequest constructor 74 | :param kwargs: Named arguments for the HttpResponseBadRequest constructor 75 | :return: An HttpResponseBadRequest overriding the Django stack response 76 | """ 77 | return HttpResponseBehaviour(HttpResponseBadRequest, *args, **kwargs) 78 | 79 | 80 | def forbidden(*args, **kwargs): 81 | """A Behaviour that returns an HttpResponseForbidden object overriding the actual response of 82 | the Django stack. The function takes the same arguments as HttpResponseForbidden, which allows 83 | changing the content, status, or any other feature exposed through the constructor 84 | :param args: Positional arguments for the HttpResponseForbidden constructor 85 | :param kwargs: Named arguments for the HttpResponseForbidden constructor 86 | :return: An HttpResponseForbidden overriding the Django stack response 87 | """ 88 | return HttpResponseBehaviour(HttpResponseForbidden, *args, **kwargs) 89 | 90 | 91 | def not_allowed(*args, **kwargs): 92 | """A Behaviour that returns an HttpResponseNotAllowed object overriding the actual response of 93 | the Django stack. The function takes the same arguments as HttpResponseNotAllowed, which allows 94 | changing the content, status, or any other feature exposed through the constructor 95 | :param args: Positional arguments for the HttpResponseNotAllowed constructor 96 | :param kwargs: Named arguments for the HttpResponseNotAllowed constructor 97 | :return: An HttpResponseNotAllowed overriding the Django stack response 98 | """ 99 | return HttpResponseBehaviour(HttpResponseNotAllowed, *args, **kwargs) 100 | 101 | 102 | def server_error(*args, **kwargs): 103 | """A Behaviour that returns an HttpResponseServerError object overriding the actual response of 104 | the Django stack. The function takes the same arguments as HttpResponseServerError, which allows 105 | changing the content, status, or any other feature exposed through the constructor 106 | :param args: Positional arguments for the HttpResponseServerError constructor 107 | :param kwargs: Named arguments for the HttpResponseServerError constructor 108 | :return: An HttpResponseServerError overriding the Django stack response 109 | """ 110 | return HttpResponseBehaviour(HttpResponseServerError, *args, **kwargs) 111 | 112 | 113 | def not_found(*args, **kwargs): 114 | """A Behaviour that returns an HttpResponse object overriding the actual response of the Django 115 | stack with a 404 (Not found) status code. The function takes the same arguments as 116 | HttpResponse, which allows changing the content, status, or any other feature exposed through 117 | the constructor 118 | :param args: Positional arguments for the HttpResponse constructor 119 | :param kwargs: Named arguments for the HttpResponse constructor 120 | :return: An HttpResponse overriding the Django stack response 121 | """ 122 | return HttpResponseBehaviour(HttpResponse, status=404, *args, **kwargs) 123 | 124 | 125 | def status(status_code, *args, **kwargs): 126 | """A Behaviour that returns an HttpResponse object overriding the actual response of the Django 127 | stack with a specific status code. The function takes the same arguments as HttpResponse, which 128 | allows changing the content, status, or any other feature exposed through the constructor 129 | :param status_code: The status code of the response. 130 | :param args: Positional arguments for the HttpResponse constructor 131 | :param kwargs: Named arguments for the HttpResponse constructor 132 | :return: An HttpResponse overriding the Django stack response 133 | """ 134 | return HttpResponseBehaviour(HttpResponse, status=status_code, *args, **kwargs) 135 | 136 | 137 | def json(data, *args, **kwargs): 138 | """A Behaviour that returns an JsonResponse object overriding the actual response of the Django 139 | stack. The function takes the same arguments as JsonResponse, which allows changing the content, 140 | status, or any other feature exposed through the constructor 141 | :param data: A dictionary that is going to be serialized as JSON on the response 142 | :param args: Positional arguments for the JsonResponse constructor 143 | :param kwargs: Named arguments for the JsonResponse constructor 144 | :return: An JsonResponse overriding the Django stack response 145 | """ 146 | return HttpResponseBehaviour(JsonResponse, data, *args, **kwargs) 147 | 148 | 149 | class DelayResponseBehaviour(Behaviour): 150 | def __init__(self, behaviour, seconds): 151 | """A Behaviour that delays the response to the client a given amount of seconds. 152 | :param behaviour: The behaviour to invoke before delaying its response 153 | :param seconds: The amount of seconds to wait after requesting a response from the behaviour 154 | """ 155 | self._behaviour = behaviour 156 | self._seconds = seconds 157 | 158 | def __call__(self, get_response, request): 159 | """Returns the result of invoking the encapsulated behaviour using the parameters given by 160 | the Django middleware after waiting for a given amount of seconds. The get_response method 161 | provided by the Django stack is never called. 162 | :param get_response: The get_response method provided by the Django stack (ignored) 163 | :param request: The request that triggered the middleware (ignored) 164 | :return: The result of calling the encapsulated behaviour 165 | """ 166 | response = self._behaviour(get_response, request) 167 | sleep(self._seconds) 168 | return response 169 | 170 | def __str__(self): 171 | return ('DelayResponse(' 172 | 'behaviour={behaviour}, ' 173 | 'seconds={seconds})').format(behaviour=self._behaviour, 174 | seconds=self._seconds) 175 | delay = DelayResponseBehaviour 176 | 177 | 178 | class DelayRequestBehaviour(Behaviour): 179 | def __init__(self, behaviour, seconds): 180 | """A Behaviour that delays the response to the client a given amount of seconds. It 181 | introduces the delay BEFORE invoking the encapsulated behaviour. 182 | :param behaviour: The behaviour to invoke 183 | :param seconds: The amount of seconds to wait before requesting a response from the 184 | behaviour 185 | """ 186 | self._behaviour = behaviour 187 | self._seconds = seconds 188 | 189 | def __call__(self, get_response, request): 190 | """It waits a given amount of seconds and returns the result of invoking the encapsulated 191 | behaviour using the parameters given by the Django middleware. The get_response method 192 | provided by the Django stack is never called. 193 | :param get_response: The get_response method provided by the Django stack (ignored) 194 | :param request: The request that triggered the middleware (ignored) 195 | :return: The result of calling the encapsulated behaviour 196 | """ 197 | sleep(self._seconds) 198 | response = self._behaviour(get_response, request) 199 | return response 200 | 201 | def __str__(self): 202 | return ('DelayRequest(' 203 | 'behaviour={behaviour}, ' 204 | 'seconds={seconds})').format(behaviour=self._behaviour, 205 | seconds=self._seconds) 206 | delay_request = DelayRequestBehaviour 207 | 208 | 209 | class RandomChoiceBehaviour(Behaviour): 210 | def __init__(self, behaviours): 211 | """A behaviour that chooses randomly amongst the encapsulated behaviours. It is possible to 212 | specify different proportions between the behaviours. For instance, to specify a 50 percent 213 | of "Not Found" responses, 30 percent of "Server Error" responses and 20 percent of actual 214 | (that go through the Django stack) responses, you should use the following: 215 | 216 | RandomChoiceBehaviour([(not_found(), 0.5), (server_error(), 0.3), default())]) 217 | 218 | If you just want a random choice between several behaviours, you can omit the proportion 219 | specification: 220 | 221 | RandomChoiceBehaviour([not_found, server_error, default]) 222 | 223 | :param behaviours: A sequence of Behaviour objects or tuples of a Behaviour object and a 224 | number less than 1 representing the proportion of requests that are going to exhibit that 225 | behaviour. 226 | """ 227 | self._behaviours = self._init_cdf(behaviours) 228 | 229 | def _init_cdf(self, behaviours): 230 | with_proportions = list(filter(lambda s: isinstance(s, (list, tuple)), behaviours)) 231 | without_proportions = list(filter(lambda s: not isinstance(s, (list, tuple)), behaviours)) 232 | 233 | sum_with_proportions = sum(s[1] for s in with_proportions) 234 | if sum_with_proportions > 1 or (sum_with_proportions == 1 and len(without_proportions) > 0): 235 | raise ValueError('The sum of the behaviours proportions is greater than 1') 236 | 237 | if len(without_proportions) > 0: 238 | proportion = (1 - sum_with_proportions) / len(without_proportions) 239 | for behaviour in without_proportions: 240 | with_proportions.append((behaviour, proportion)) 241 | 242 | cum_sum = 0 243 | cdf = [] 244 | for behaviour, proportion in with_proportions: 245 | cum_sum += proportion 246 | cdf.append((behaviour, cum_sum)) 247 | 248 | return cdf 249 | 250 | def __call__(self, get_response, request): 251 | """Returns the result of invoking the a randomly chosen behaviour using the parameters 252 | given by the Django middleware. The get_response method provided by the Django stack is 253 | never called. 254 | :param get_response: The get_response method provided by the Django stack (ignored) 255 | :param request: The request that triggered the middleware (ignored) 256 | :return: The result of calling one of the encapsulated behaviours chosing randomly amognst 257 | them. 258 | """ 259 | x = random() 260 | for behaviour, f_x in self._behaviours: 261 | if x < f_x: 262 | return behaviour(get_response, request) 263 | return _default(get_response, request) 264 | 265 | def __str__(self): 266 | return ('RandomChoiceBehaviour(' 267 | 'behaviours=[{behaviours}])').format( 268 | behaviours=', '.join(b for b in self._behaviours)) 269 | random_choice = RandomChoiceBehaviour 270 | 271 | 272 | class ConditionalBehaviour(Behaviour): 273 | def __init__(self, predicate, behaviour, alternative_behaviour=None): 274 | """A Behaviour that invokes the encapsulated behaviour if a condition is met, otherwise it 275 | invokes the alternative behaviour. The default alternative behaviour is just going through 276 | the usual middleware path. 277 | :param predicate: The Predicate to invoke to determine if the condition is met 278 | :param behaviour: The behaviour to invoke if the condition is met 279 | :param alternative_behaviour: The alternative behaviour to invoke if the condition is not 280 | met 281 | """ 282 | self._predicate = predicate 283 | self._behaviour = behaviour 284 | self._alternative_behaviour = alternative_behaviour or _default # makes testing easier 285 | 286 | def __call__(self, get_response, request): 287 | """If the predicate condition is met it returns the result of invoking the encapsulated 288 | behaviour using the parameters given by the Django middleware. If the condition is not met, 289 | the alternative behaviour is invoked (with the usual parameters). 290 | :param get_response: The get_response method provided by the Django stack 291 | :param request: The request that triggered the middleware 292 | :return: The result of calling the encapsulated behaviour if the predicate condition is met 293 | or the result of invoking the alternative behaviour otherwise. 294 | """ 295 | if self._predicate(get_response, request): 296 | return self._behaviour(get_response, request) 297 | 298 | return self._alternative_behaviour(get_response, request) 299 | 300 | def __str__(self): 301 | return ('ConditionalBehaviour(' 302 | 'predicate={predicate}, ' 303 | 'behaviour={behaviour}, ' 304 | 'alternative_behaviour={alternative_behaviour})').format( 305 | predicate=self._predicate, behaviour=self._behaviour, 306 | alternative_behaviour=self._alternative_behaviour) 307 | conditional = ConditionalBehaviour 308 | cond = ConditionalBehaviour 309 | 310 | 311 | class MultiConditionalBehaviour(Behaviour): 312 | def __init__(self, predicates_behaviours, default_behaviour=None): 313 | """A Behaviour that takes several conditions (predicates) and behaviours and executes the 314 | behaviour associated with the fist condition that is met. If no conditions are met, 315 | the supplied default behaviour is invoked. 316 | :param predicates_behaviours: A list of (predicate, behaviour) tuples 317 | :param default_behaviour: The default behaviour to invoke if no conditions are met 318 | """ 319 | self._predicates_behaviours = predicates_behaviours 320 | self._default_behaviour = default_behaviour or _default # makes testing easier 321 | 322 | def __call__(self, get_response, request): 323 | """It iterates through the conditions until one is met, and its associated behaviour is 324 | invoked and its result is returned. If no conditions are met the method returns the result 325 | of invoking the default behaviour. 326 | :param get_response: The get_response method provided by the Django stack 327 | :param request: The request that triggered the middleware 328 | :return: The result of calling the behaviour that matches one of the conditions or the 329 | result of calling the default behaviour if no conditions are met. 330 | """ 331 | 332 | for predicate, behaviour in self._predicates_behaviours: 333 | if predicate(get_response, request): 334 | return behaviour(get_response, request) 335 | 336 | return self._default_behaviour(get_response, request) 337 | 338 | def __str__(self, *args, **kwargs): 339 | return ('MultiConditionalBehaviour(' 340 | 'predicates_behaviours=[{predicates_behaviours}])'.format( 341 | predicates_behaviours=', '.join( 342 | '{p} -> {b}'.format(p=p, b=b) for p, b in self._predicates_behaviours))) 343 | multi_conditional = MultiConditionalBehaviour 344 | multi_cond = MultiConditionalBehaviour 345 | case = MultiConditionalBehaviour 346 | 347 | 348 | class StreamBehaviour(Behaviour): 349 | def wrap_streaming_content(self, streaming_content): 350 | """ 351 | A generator that wraps the streaming content of the response returned get_response. Each 352 | chunk of the content is yielded. It is based on technique mentioned in 353 | https://docs.djangoproject.com/en/1.10/topics/http/middleware/#dealing-with-streaming-responses 354 | :param streaming_content: The streaming content of the response 355 | """ 356 | for chunk in streaming_content: 357 | yield chunk 358 | 359 | def __call__(self, get_response, request): 360 | """If the response returned by get_response (as given by the UncertaintyMiddleware 361 | middleware is a streaming response, the streaming content is wrapped by the 362 | wrap_streaming_content generator. If the response is not a streaming one. 363 | :param get_response: The get_response method provided by the Django stack 364 | :param request: The request that triggered the middleware 365 | :return: The result of calling get_response with the request parameter 366 | """ 367 | response = get_response(request) 368 | 369 | if response.streaming: 370 | response.streaming_content = self.wrap_streaming_content(response.streaming_content) 371 | 372 | return response 373 | 374 | 375 | class SlowdownStreamBehaviour(StreamBehaviour): 376 | def __init__(self, seconds): 377 | """A Behaviour that introduces a delay between each chunk of the streaming content 378 | returned by get_response. 379 | :param seconds: The amount of seconds to wait between each chunk of the streaming content 380 | """ 381 | self._seconds = seconds 382 | 383 | def wrap_streaming_content(self, streaming_content): 384 | """Introduces a delay before yielding each chunk of streaming_content. 385 | :param streaming_content: The streaming_content field of the response. 386 | """ 387 | for chunk in streaming_content: 388 | sleep(self._seconds) 389 | yield chunk 390 | 391 | def __str__(self): 392 | return ('SlowdownStreamBehaviour(' 393 | 'seconds={seconds})').format(seconds=self._seconds) 394 | slowdown = SlowdownStreamBehaviour 395 | 396 | 397 | class RandomStopStreamBehaviour(StreamBehaviour): 398 | def __init__(self, probability, stop_gracefully=True): 399 | """A Behaviour that stops the streaming with a certain probability. 400 | :param probability: The probability of stopping the stream 401 | """ 402 | self._probability = probability 403 | 404 | def wrap_streaming_content(self, streaming_content): 405 | """Stops the iterator with with a certain probability. 406 | :param streaming_content: The streaming_content field of the response. 407 | """ 408 | for chunk in streaming_content: 409 | if random() < self._probability: 410 | raise StopIteration() 411 | 412 | yield chunk 413 | 414 | def __str__(self): 415 | return ('RandomStopStreamBehaviour(' 416 | 'probability={probability},').format(probabilty=self._probability) 417 | random_stop = RandomStopStreamBehaviour 418 | -------------------------------------------------------------------------------- /uncertainty/tests/test_behaviours.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from uncertainty.behaviours import (Behaviour, default, HttpResponseBehaviour, html, ok, 5 | bad_request, forbidden, not_allowed, server_error, not_found, 6 | status, json, DelayResponseBehaviour, delay, 7 | DelayRequestBehaviour, delay_request, RandomChoiceBehaviour, 8 | cond, case, StreamBehaviour, slowdown, random_stop) 9 | 10 | 11 | class BehaviourTests(TestCase): 12 | def setUp(self): 13 | self.get_response_mock = MagicMock() 14 | self.request_mock = MagicMock() 15 | self.behaviour = Behaviour() 16 | 17 | def test_calls_get_response(self): 18 | """Tests that invoking the behaviour calls get_response""" 19 | self.behaviour(self.get_response_mock, self.request_mock) 20 | self.get_response_mock.assert_called_once_with(self.request_mock) 21 | 22 | def test_returns_get_response_result(self): 23 | """Tests that the behaviour returns the result of calling get_response""" 24 | self.assertEqual(self.get_response_mock.return_value, 25 | self.behaviour(self.get_response_mock, self.request_mock)) 26 | 27 | 28 | class DefaultTests(TestCase): 29 | def test_default_is_behaviour_base(self): 30 | """Test that default is the Behaviour base class""" 31 | self.assertEqual(default, Behaviour) 32 | 33 | 34 | class HttpResponseBehaviourTests(TestCase): 35 | def setUp(self): 36 | self.get_response_mock = MagicMock() 37 | self.request_mock = MagicMock() 38 | self.response_class_mock = MagicMock() 39 | self.args_mock = [MagicMock(), MagicMock()] 40 | self.kwargs_mock = {'kwarg0': MagicMock(), 'kwarg1': MagicMock()} 41 | self.behaviour = HttpResponseBehaviour(self.response_class_mock, *self.args_mock, 42 | **self.kwargs_mock) 43 | 44 | def test_calls_response_class_constructor(self): 45 | """Tests that invoking the behaviour calls response class constructor""" 46 | self.behaviour(self.get_response_mock, self.request_mock) 47 | self.response_class_mock.assert_called_once_with(*self.args_mock, **self.kwargs_mock) 48 | 49 | def test_returns_response_class_constructor_result(self): 50 | """Tests that the behaviour returns the result of calling the response class constructor""" 51 | self.assertEqual(self.response_class_mock.return_value, 52 | self.behaviour(self.get_response_mock, self.request_mock)) 53 | 54 | 55 | class HttpResponseBehaviourTestsBase(TestCase): 56 | def setUp(self): 57 | http_response_behaviour_patcher = patch('uncertainty.behaviours.HttpResponseBehaviour') 58 | self.http_response_behaviour_mock = http_response_behaviour_patcher.start() 59 | self.addCleanup(http_response_behaviour_patcher.stop) 60 | self.args_mock = [MagicMock(), MagicMock()] 61 | self.kwargs_mock = {'kwarg0': MagicMock(), 'kwarg1': MagicMock()} 62 | 63 | 64 | class HtmlTests(HttpResponseBehaviourTestsBase): 65 | def setUp(self): 66 | super().setUp() 67 | http_response_patcher = patch('uncertainty.behaviours.HttpResponse') 68 | self.http_response_mock = http_response_patcher.start() 69 | self.addCleanup(http_response_patcher.stop) 70 | 71 | def test_calls_http_response_behaviour(self): 72 | """Tests that html calls HttpResponseBehaviour with HttpResponse""" 73 | html(*self.args_mock, **self.kwargs_mock) 74 | self.http_response_behaviour_mock.assert_called_once_with(self.http_response_mock, 75 | *self.args_mock, 76 | **self.kwargs_mock) 77 | 78 | def test_returns_http_response_behaviour_result(self): 79 | """Tests that html returns the result of calling HttpResponseBehaviour constructor""" 80 | self.assertEqual(self.http_response_behaviour_mock.return_value, 81 | html(*self.args_mock, **self.kwargs_mock)) 82 | 83 | def test_ok_is_html(self): 84 | """Test that ok is an alias for html""" 85 | self.assertEqual(html, ok) 86 | 87 | 88 | class BadRequestTests(HttpResponseBehaviourTestsBase): 89 | def setUp(self): 90 | super().setUp() 91 | http_response_bad_request_patcher = patch( 92 | 'uncertainty.behaviours.HttpResponseBadRequest') 93 | self.http_response_bad_request_mock = http_response_bad_request_patcher.start() 94 | self.addCleanup(http_response_bad_request_patcher.stop) 95 | 96 | def test_calls_http_response_behaviour(self): 97 | """Tests that bad_request calls HttpResponseBehaviour with HttpResponseBadRequest""" 98 | bad_request(*self.args_mock, **self.kwargs_mock) 99 | self.http_response_behaviour_mock.assert_called_once_with( 100 | self.http_response_bad_request_mock, *self.args_mock, **self.kwargs_mock) 101 | 102 | def test_returns_http_response_behaviour_result(self): 103 | """Tests that bad_request returns the result of calling HttpResponseBehaviour 104 | constructor""" 105 | self.assertEqual(self.http_response_behaviour_mock.return_value, 106 | bad_request(*self.args_mock, **self.kwargs_mock)) 107 | 108 | 109 | class ForbiddenTests(HttpResponseBehaviourTestsBase): 110 | def setUp(self): 111 | super().setUp() 112 | http_response_forbidden_patcher = patch( 113 | 'uncertainty.behaviours.HttpResponseForbidden') 114 | self.http_response_forbidden_mock = http_response_forbidden_patcher.start() 115 | self.addCleanup(http_response_forbidden_patcher.stop) 116 | 117 | def test_calls_http_response_behaviour(self): 118 | """Tests that forbidden calls HttpResponseBehaviour with HttpResponseForbidden""" 119 | forbidden(*self.args_mock, **self.kwargs_mock) 120 | self.http_response_behaviour_mock.assert_called_once_with( 121 | self.http_response_forbidden_mock, *self.args_mock, **self.kwargs_mock) 122 | 123 | def test_returns_http_response_behaviour_result(self): 124 | """Tests that forbidden returns the result of calling HttpResponseBehaviour constructor""" 125 | self.assertEqual(self.http_response_behaviour_mock.return_value, 126 | forbidden(*self.args_mock, **self.kwargs_mock)) 127 | 128 | 129 | class NotAllowedTests(HttpResponseBehaviourTestsBase): 130 | def setUp(self): 131 | super().setUp() 132 | http_response_not_allowed_patcher = patch( 133 | 'uncertainty.behaviours.HttpResponseNotAllowed') 134 | self.http_response_not_allowed_mock = http_response_not_allowed_patcher.start() 135 | self.addCleanup(http_response_not_allowed_patcher.stop) 136 | 137 | def test_calls_http_response_behaviour(self): 138 | """Tests that not_allowed calls HttpResponseBehaviour with HttpResponseNotAllowed""" 139 | not_allowed(*self.args_mock, **self.kwargs_mock) 140 | self.http_response_behaviour_mock.assert_called_once_with( 141 | self.http_response_not_allowed_mock, *self.args_mock, **self.kwargs_mock) 142 | 143 | def test_returns_http_response_behaviour_result(self): 144 | """Tests that not_allowed returns the result of calling HttpResponseBehaviour constructor""" 145 | self.assertEqual(self.http_response_behaviour_mock.return_value, 146 | not_allowed(*self.args_mock, **self.kwargs_mock)) 147 | 148 | 149 | class ServerErrorTests(HttpResponseBehaviourTestsBase): 150 | def setUp(self): 151 | super().setUp() 152 | http_response_server_error_patcher = patch( 153 | 'uncertainty.behaviours.HttpResponseServerError') 154 | self.http_response_server_error_mock = http_response_server_error_patcher.start() 155 | self.addCleanup(http_response_server_error_patcher.stop) 156 | 157 | def test_calls_http_response_behaviour(self): 158 | """Tests that server_error calls HttpResponseBehaviour with HttpResponseNotAllowed""" 159 | server_error(*self.args_mock, **self.kwargs_mock) 160 | self.http_response_behaviour_mock.assert_called_once_with( 161 | self.http_response_server_error_mock, *self.args_mock, **self.kwargs_mock) 162 | 163 | def test_returns_http_response_behaviour_result(self): 164 | """Tests that server_error returns the result of calling HttpResponseServerError 165 | constructor""" 166 | self.assertEqual(self.http_response_behaviour_mock.return_value, 167 | server_error(*self.args_mock, **self.kwargs_mock)) 168 | 169 | 170 | class NotFoundTests(HttpResponseBehaviourTestsBase): 171 | def setUp(self): 172 | super().setUp() 173 | http_response_patcher = patch('uncertainty.behaviours.HttpResponse') 174 | self.http_response_mock = http_response_patcher.start() 175 | self.addCleanup(http_response_patcher.stop) 176 | 177 | def test_calls_http_response_behaviour(self): 178 | """Tests that not_found calls HttpResponseBehaviour with HttpResponse""" 179 | not_found(*self.args_mock, **self.kwargs_mock) 180 | self.http_response_behaviour_mock.assert_called_once_with(self.http_response_mock, 181 | status=404, 182 | *self.args_mock, 183 | **self.kwargs_mock) 184 | 185 | def test_returns_http_response_behaviour_result(self): 186 | """Tests that not_found returns the result of calling HttpResponseBehaviour constructor""" 187 | self.assertEqual(self.http_response_behaviour_mock.return_value, 188 | not_found(*self.args_mock, **self.kwargs_mock)) 189 | 190 | 191 | class StatusTests(HttpResponseBehaviourTestsBase): 192 | def setUp(self): 193 | super().setUp() 194 | http_response_patcher = patch('uncertainty.behaviours.HttpResponse') 195 | self.http_response_mock = http_response_patcher.start() 196 | self.addCleanup(http_response_patcher.stop) 197 | self.some_status = MagicMock() 198 | 199 | def test_calls_http_response_behaviour(self): 200 | """Tests that status calls HttpResponseBehaviour with HttpResponse""" 201 | status(self.some_status, *self.args_mock, **self.kwargs_mock) 202 | self.http_response_behaviour_mock.assert_called_once_with(self.http_response_mock, 203 | status=self.some_status, 204 | *self.args_mock, 205 | **self.kwargs_mock) 206 | 207 | def test_returns_http_response_behaviour_result(self): 208 | """Tests that status returns the result of calling HttpResponseBehaviour constructor""" 209 | self.assertEqual(self.http_response_behaviour_mock.return_value, 210 | status(self.some_status, *self.args_mock, **self.kwargs_mock)) 211 | 212 | 213 | class JsonTests(HttpResponseBehaviourTestsBase): 214 | def setUp(self): 215 | super().setUp() 216 | json_response_patcher = patch('uncertainty.behaviours.JsonResponse') 217 | self.json_response_mock = json_response_patcher.start() 218 | self.addCleanup(json_response_patcher.stop) 219 | self.some_data = MagicMock() 220 | 221 | def test_calls_http_response_behaviour(self): 222 | """Tests that json calls HttpResponseBehaviour with JsonResponse""" 223 | json(self.some_data, *self.args_mock, **self.kwargs_mock) 224 | self.http_response_behaviour_mock.assert_called_once_with( 225 | self.json_response_mock, self.some_data, *self.args_mock, **self.kwargs_mock) 226 | 227 | def test_returns_http_response_behaviour_result(self): 228 | """Tests that json returns the result of calling JsonResponse constructor""" 229 | self.assertEqual(self.http_response_behaviour_mock.return_value, 230 | json(self.some_data, *self.args_mock, **self.kwargs_mock)) 231 | 232 | 233 | class DelayResponseBehaviourTests(TestCase): 234 | def setUp(self): 235 | sleep_patcher = patch('uncertainty.behaviours.sleep') 236 | self.sleep_mock = sleep_patcher.start() 237 | self.addCleanup(self.sleep_mock.stop) 238 | self.get_response_mock = MagicMock() 239 | self.request_mock = MagicMock() 240 | self.some_behaviour = MagicMock() 241 | self.some_seconds = MagicMock() 242 | self.delay_response_behaviour = DelayResponseBehaviour(self.some_behaviour, 243 | self.some_seconds) 244 | 245 | def test_calls_encapsulated_behaviour(self): 246 | """Tests that DelayResponseBehaviour calls the encapsulated behaviour""" 247 | self.delay_response_behaviour(self.get_response_mock, self.request_mock) 248 | self.some_behaviour.assert_called_once_with(self.get_response_mock, self.request_mock) 249 | 250 | def test_returns_result_of_encapsulated_behaviour(self): 251 | """Tests that DelayResponseBehaviour returns the result of calling the encapsulated 252 | behaviour""" 253 | self.assertEqual(self.some_behaviour.return_value, 254 | self.delay_response_behaviour(self.get_response_mock, self.request_mock)) 255 | 256 | def test_calls_sleep(self): 257 | """Tests that DelayResponseBehaviour calls sleep for the given seconds""" 258 | self.delay_response_behaviour(self.get_response_mock, self.request_mock) 259 | self.sleep_mock.assert_called_once_with(self.some_seconds) 260 | 261 | def test_delay_is_delay_response_behaviour(self): 262 | """Tests that delay is an alias for DelayResponseBehaviour""" 263 | self.assertEqual(delay, DelayResponseBehaviour) 264 | 265 | 266 | class DelayRequestBehaviourTests(TestCase): 267 | def setUp(self): 268 | sleep_patcher = patch('uncertainty.behaviours.sleep') 269 | self.sleep_mock = sleep_patcher.start() 270 | self.addCleanup(self.sleep_mock.stop) 271 | self.get_response_mock = MagicMock() 272 | self.request_mock = MagicMock() 273 | self.some_behaviour = MagicMock() 274 | self.some_seconds = MagicMock() 275 | self.delay_request_behaviour = DelayRequestBehaviour(self.some_behaviour, 276 | self.some_seconds) 277 | 278 | def test_calls_encapsulated_behaviour(self): 279 | """Tests that DelayResponseBehaviour calls the encapsulated behaviour""" 280 | self.delay_request_behaviour(self.get_response_mock, self.request_mock) 281 | self.some_behaviour.assert_called_once_with(self.get_response_mock, self.request_mock) 282 | 283 | def test_returns_result_of_encapsulated_behaviour(self): 284 | """Tests that DelayResponseBehaviour returns the result of calling the encapsulated 285 | behaviour""" 286 | self.assertEqual(self.some_behaviour.return_value, 287 | self.delay_request_behaviour(self.get_response_mock, 288 | self.request_mock)) 289 | 290 | def test_calls_sleep(self): 291 | """Tests that DelayResponseBehaviour calls sleep for the given seconds""" 292 | self.delay_request_behaviour(self.get_response_mock, self.request_mock) 293 | self.sleep_mock.assert_called_once_with(self.some_seconds) 294 | 295 | def test_delay_is_delay_request_response_behaviour(self): 296 | """Tests that delay is an alias for DelayResponseBehaviour""" 297 | self.assertEqual(delay_request, DelayRequestBehaviour) 298 | 299 | 300 | class RandomChoiceBehaviourInitTests(TestCase): 301 | def setUp(self): 302 | self.behaviour_0 = MagicMock() 303 | self.behaviour_1 = MagicMock() 304 | self.behaviour_2 = MagicMock() 305 | 306 | def test_value_error_raised_on_proportions_sum_over_1(self): 307 | """Tests that if the sum of the specified proportions is greater than 1, a ValueError 308 | exception is raised""" 309 | self.assertRaises(ValueError, RandomChoiceBehaviour, 310 | ((self.behaviour_0, 0.5), (self.behaviour_0, 0.6))) 311 | self.assertRaises(ValueError, RandomChoiceBehaviour, ((self.behaviour_0, 1.2),)) 312 | 313 | def test_behaviours_with_no_proportions_evenly_distributed(self): 314 | """Tests that behaviours with no proportions are evenly distributed""" 315 | random_choice = RandomChoiceBehaviour((self.behaviour_0, self.behaviour_1)) 316 | self.assertEqual(0.5, random_choice._behaviours[0][1]) 317 | self.assertEqual(1.0, random_choice._behaviours[1][1]) 318 | 319 | def test_behaviours_with_no_proportions_evenly_distributes_rest(self): 320 | """Tests that if there are behaviours with proportions and behaviours with no proportions, 321 | the latter are evenly distributed""" 322 | random_choice = RandomChoiceBehaviour(((self.behaviour_0, 0.5), self.behaviour_1, 323 | self.behaviour_2)) 324 | self.assertEqual(0.5, random_choice._behaviours[0][1]) 325 | self.assertEqual(0.75, random_choice._behaviours[1][1]) 326 | self.assertEqual(1.0, random_choice._behaviours[2][1]) 327 | 328 | def test_proportions_are_accumulated(self): 329 | """Tests that proportions accumulate in a CDF fashion""" 330 | random_choice = RandomChoiceBehaviour(((self.behaviour_0, 0.3), (self.behaviour_1, 0.2), 331 | (self.behaviour_2, 0.1))) 332 | self.assertTupleEqual((self.behaviour_0, 0.3), random_choice._behaviours[0]) 333 | self.assertTupleEqual((self.behaviour_1, 0.5), random_choice._behaviours[1]) 334 | self.assertTupleEqual((self.behaviour_2, 0.6), random_choice._behaviours[2]) 335 | 336 | 337 | class RandomChoiceBehaviourTests(TestCase): 338 | def setUp(self): 339 | random_patcher = patch('uncertainty.behaviours.random') 340 | self.random_mock = random_patcher.start() 341 | self.addCleanup(self.random_mock.stop) 342 | default_patcher = patch('uncertainty.behaviours._default') 343 | self.default_mock = default_patcher.start() 344 | self.addCleanup(self.default_mock.stop) 345 | 346 | self.get_response_mock = MagicMock() 347 | self.request_mock = MagicMock() 348 | 349 | self.behaviour_0 = MagicMock() 350 | self.behaviour_1 = MagicMock() 351 | self.behaviour_2 = MagicMock() 352 | self.behaviour_0_prop = (self.behaviour_0, 0.2) 353 | self.behaviour_1_prop = (self.behaviour_1, 0.3) 354 | self.behaviour_2_prop = (self.behaviour_2, 0.1) 355 | 356 | self.random_choice = RandomChoiceBehaviour( 357 | (self.behaviour_0_prop, self.behaviour_1_prop, self.behaviour_2_prop)) 358 | 359 | def test_behaviour_0_invoked_if_random_is_zero(self): 360 | """Tests that if the random number is 0, behaviour_0 is invoked""" 361 | self.random_mock.return_value = 0 362 | self.random_choice(self.get_response_mock, self.request_mock) 363 | self.behaviour_0.assert_called_once_with(self.get_response_mock, self.request_mock) 364 | 365 | def test_behaviour_0_invoked_if_random_less_than_0_2(self): 366 | """Tests that if the random number is less than 0.2, behaviour_0 is invoked""" 367 | self.random_mock.return_value = 0.1 368 | self.random_choice(self.get_response_mock, self.request_mock) 369 | self.behaviour_0.assert_called_once_with(self.get_response_mock, self.request_mock) 370 | 371 | def test_behaviour_1_invoked_if_random_is_0_2(self): 372 | """Tests that if the random number is exactly 0.2, behaviour_1 is invoked""" 373 | self.random_mock.return_value = 0.2 374 | self.random_choice(self.get_response_mock, self.request_mock) 375 | self.behaviour_1.assert_called_once_with(self.get_response_mock, self.request_mock) 376 | 377 | def test_behaviour_1_invoked_if_random_less_than_0_5(self): 378 | """Tests that if the random number is less than 0.5, behaviour_1 is invoked""" 379 | self.random_mock.return_value = 0.45 380 | self.random_choice(self.get_response_mock, self.request_mock) 381 | self.behaviour_1.assert_called_once_with(self.get_response_mock, self.request_mock) 382 | 383 | def test_behaviour_2_invoked_if_random_is_0_5(self): 384 | """Tests that if the random number is exactly 0.5, behaviour_2 is invoked""" 385 | self.random_mock.return_value = 0.5 386 | self.random_choice(self.get_response_mock, self.request_mock) 387 | self.behaviour_2.assert_called_once_with(self.get_response_mock, self.request_mock) 388 | 389 | def test_behaviour_2_invoked_if_random_less_than_0_6(self): 390 | """Tests that if the random number is less than 0.6, behaviour_2 is invoked""" 391 | self.random_mock.return_value = 0.57 392 | self.random_choice(self.get_response_mock, self.request_mock) 393 | self.behaviour_2.assert_called_once_with(self.get_response_mock, self.request_mock) 394 | 395 | def test_default_invoked_if_random_is_0_6(self): 396 | """Tests that if the random number is exactly 0.6, the default behaviour is invoked""" 397 | self.random_mock.return_value = 0.6 398 | self.random_choice(self.get_response_mock, self.request_mock) 399 | self.default_mock.assert_called_once_with(self.get_response_mock, self.request_mock) 400 | 401 | def test_default_invoked_if_random_greater_than_0_6(self): 402 | """Tests that if the random number is greater than 0.6, the default behaviour is invoked""" 403 | self.random_mock.return_value = 0.7 404 | self.random_choice(self.get_response_mock, self.request_mock) 405 | self.default_mock.assert_called_once_with(self.get_response_mock, self.request_mock) 406 | 407 | 408 | class ConditionalBehaviourTests(TestCase): 409 | def setUp(self): 410 | default_patcher = patch('uncertainty.behaviours._default') 411 | self.default_mock = default_patcher.start() 412 | self.addCleanup(self.default_mock.stop) 413 | 414 | self.get_response_mock = MagicMock() 415 | self.request_mock = MagicMock() 416 | 417 | self.predicate = MagicMock() 418 | self.behaviour = MagicMock() 419 | self.alternative_behaviour = MagicMock() 420 | 421 | self.cond = cond(self.predicate, self.behaviour, self.alternative_behaviour) 422 | 423 | def test_behaviour_invoked_predicate_true(self): 424 | """Tests that if the predicate is True, behaviour is invoked""" 425 | self.predicate.return_value = True 426 | self.cond(self.get_response_mock, self.request_mock) 427 | self.behaviour.assert_called_once_with(self.get_response_mock, self.request_mock) 428 | 429 | def test_returns_behaviour_result_predicate_true(self): 430 | """Test that if the predicate is True, the result of invoking behaviour is returned""" 431 | self.predicate.return_value = True 432 | response = self.cond(self.get_response_mock, self.request_mock) 433 | self.assertEqual(self.behaviour.return_value, response) 434 | 435 | def test_behaviour_not_invoked_predicate_false(self): 436 | """Tests that if the predicate is False, behaviour is not invoked""" 437 | self.predicate.return_value = False 438 | self.cond(self.get_response_mock, self.request_mock) 439 | self.assertFalse(self.behaviour.called) 440 | 441 | def test_returns_alternative_behaviour_result_predicate_true(self): 442 | """Test that if the predicate is False, the result of invoking alternative_behaviour is 443 | returned""" 444 | self.predicate.return_value = False 445 | response = self.cond(self.get_response_mock, self.request_mock) 446 | self.assertEqual(self.alternative_behaviour.return_value, response) 447 | 448 | def test_alternate_behaviour_invoked_predicate_false(self): 449 | """Tests that if the predicate is False, alternate_behaviour is invoked""" 450 | self.predicate.return_value = False 451 | self.cond(self.get_response_mock, self.request_mock) 452 | self.alternative_behaviour.assert_called_once_with(self.get_response_mock, 453 | self.request_mock) 454 | 455 | def test_default_invoked_predicate_false(self): 456 | """Tests that if the predicate is False, and no alternate_behaviour is provided, default is 457 | invoked""" 458 | self.predicate.return_value = False 459 | cond_ = cond(self.predicate, self.behaviour) 460 | cond_(self.get_response_mock, self.request_mock) 461 | self.default_mock.assert_called_once_with(self.get_response_mock, self.request_mock) 462 | 463 | def test_returns_default_result_predicate_false(self): 464 | """Tests that if the predicate is False, and no alternate_behaviour is provided, the result 465 | of invoking default is returned""" 466 | self.predicate.return_value = False 467 | cond_ = cond(self.predicate, self.behaviour) 468 | response = cond_(self.get_response_mock, self.request_mock) 469 | self.assertEqual(self.default_mock.return_value, response) 470 | 471 | 472 | class MultiConditionalBehaviourTests(TestCase): 473 | def setUp(self): 474 | default_patcher = patch('uncertainty.behaviours._default') 475 | self.default_mock = default_patcher.start() 476 | self.addCleanup(self.default_mock.stop) 477 | 478 | self.get_response_mock = MagicMock() 479 | self.request_mock = MagicMock() 480 | 481 | self.predicate_0 = MagicMock() 482 | self.predicate_1 = MagicMock() 483 | self.behaviour_0 = MagicMock() 484 | self.behaviour_1 = MagicMock() 485 | self.default_behaviour = MagicMock() 486 | 487 | self.case = case([(self.predicate_0, self.behaviour_0), 488 | (self.predicate_1, self.behaviour_1) 489 | ], default_behaviour=self.default_behaviour) 490 | 491 | def test_behaviour_0_invoked_predicate_0_true(self): 492 | """Tests that if the predicate_0 is True, and predicate_1 is False, behaviour_0 is 493 | invoked""" 494 | self.predicate_0.return_value = True 495 | self.predicate_1.return_value = False 496 | self.case(self.get_response_mock, self.request_mock) 497 | self.behaviour_0.assert_called_once_with(self.get_response_mock, self.request_mock) 498 | 499 | def test_behaviour_0_result_returned_predicate_0_true(self): 500 | """Tests that if the predicate_0 is True, and predicate_1 is False, the result of invoking 501 | behaviour_0 is returned""" 502 | self.predicate_0.return_value = True 503 | self.predicate_1.return_value = False 504 | response = self.case(self.get_response_mock, self.request_mock) 505 | self.assertEqual(self.behaviour_0.return_value, response) 506 | 507 | def test_behaviour_1_not_invoked_predicate_0_true(self): 508 | """Tests that if the predicate_0 is True, and predicate_1 is False, behaviour_1 is not 509 | invoked""" 510 | self.predicate_0.return_value = True 511 | self.predicate_1.return_value = False 512 | self.case(self.get_response_mock, self.request_mock) 513 | self.assertFalse(self.behaviour_1.called) 514 | 515 | def test_default_behaviour_not_invoked_predicate_0_true(self): 516 | """Tests that if the predicate_0 is True, and predicate_1 is False, default_behaviour is 517 | not invoked""" 518 | self.predicate_0.return_value = True 519 | self.predicate_1.return_value = False 520 | self.case(self.get_response_mock, self.request_mock) 521 | self.assertFalse(self.default_behaviour.called) 522 | 523 | def test_behaviour_1_invoked_predicate_1_true(self): 524 | """Tests that if the predicate_0 is False, and predicate_1 is True, behaviour_1 is 525 | invoked""" 526 | self.predicate_0.return_value = False 527 | self.predicate_1.return_value = True 528 | self.case(self.get_response_mock, self.request_mock) 529 | self.behaviour_1.assert_called_once_with(self.get_response_mock, self.request_mock) 530 | 531 | def test_behaviour_1_result_returned_predicate_1_true(self): 532 | """Tests that if the predicate_0 is False, and predicate_0 is False, the result of invoking 533 | behaviour_1 is returned""" 534 | self.predicate_0.return_value = False 535 | self.predicate_1.return_value = True 536 | response = self.case(self.get_response_mock, self.request_mock) 537 | self.assertEqual(self.behaviour_1.return_value, response) 538 | 539 | def test_behaviour_0_not_invoked_predicate_1_true(self): 540 | """Tests that if the predicate_0 is False, and predicate_1 is True, behaviour_0 is not 541 | invoked""" 542 | self.predicate_0.return_value = False 543 | self.predicate_1.return_value = True 544 | self.case(self.get_response_mock, self.request_mock) 545 | self.assertFalse(self.behaviour_0.called) 546 | 547 | def test_default_behaviour_not_invoked_predicate_1_true(self): 548 | """Tests that if the predicate_0 is False, and predicate_1 is True, default_behaviour is 549 | not invoked""" 550 | self.predicate_0.return_value = False 551 | self.predicate_1.return_value = True 552 | self.case(self.get_response_mock, self.request_mock) 553 | self.assertFalse(self.default_behaviour.called) 554 | 555 | def test_behaviour_0_invoked_predicate_0_predicate_1_true(self): 556 | """Tests that if both predicates are True, behaviour_0 is invoked (short circuit)""" 557 | self.predicate_0.return_value = True 558 | self.predicate_1.return_value = True 559 | self.case(self.get_response_mock, self.request_mock) 560 | self.behaviour_0.assert_called_once_with(self.get_response_mock, self.request_mock) 561 | 562 | def test_behaviour_1_not_invoked_predicate_0_predicate_1_true(self): 563 | """Tests that if both predicates are True, behaviour_1 is not invoked (short circuit)""" 564 | self.predicate_0.return_value = True 565 | self.predicate_1.return_value = True 566 | self.case(self.get_response_mock, self.request_mock) 567 | self.assertFalse(self.behaviour_1.called) 568 | 569 | def test_default_behaviour_invoked_predicate_0_predicate_1_false(self): 570 | """Tests that if both predicates are False, default_behaviour is invoked""" 571 | self.predicate_0.return_value = False 572 | self.predicate_1.return_value = False 573 | self.case(self.get_response_mock, self.request_mock) 574 | self.default_behaviour.assert_called_once_with(self.get_response_mock, self.request_mock) 575 | 576 | def test_default_behaviour_resutl_returned_predicate_0_predicate_1_false(self): 577 | """Tests that if both predicates are False, the result of invoking default_behaviour is 578 | returned""" 579 | self.predicate_0.return_value = False 580 | self.predicate_1.return_value = False 581 | response = self.case(self.get_response_mock, self.request_mock) 582 | self.assertEqual(self.default_behaviour.return_value, response) 583 | 584 | def test_default_invoked_both_predicates_false(self): 585 | """Tests that if the both predicates are False, and no default_behaviour is provided, 586 | default is invoked""" 587 | self.predicate_0.return_value = False 588 | self.predicate_1.return_value = False 589 | case_ = case([(self.predicate_0, self.behaviour_0),(self.predicate_1, self.behaviour_1)]) 590 | case_(self.get_response_mock, self.request_mock) 591 | self.default_mock.assert_called_once_with(self.get_response_mock, self.request_mock) 592 | 593 | def test_returns_default_result_both_predicates_false(self): 594 | """Tests that if the predicate is False, and no default_behaviour is provided, the result 595 | of invoking default is returned""" 596 | self.predicate_0.return_value = False 597 | self.predicate_1.return_value = False 598 | case_ = case([(self.predicate_0, self.behaviour_0),(self.predicate_1, self.behaviour_1)]) 599 | response = case_(self.get_response_mock, self.request_mock) 600 | self.assertEqual(self.default_mock.return_value, response) 601 | 602 | 603 | # TODO Add StreamBehaviour tests 604 | # TODO Add SlowdownStreamBehaviour tests 605 | # TODO Add RandomStopStreamBehaviour tests 606 | --------------------------------------------------------------------------------