├── 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 |
--------------------------------------------------------------------------------