gestioCOF/shared/tests/testcases.py

353 lines
11 KiB
Python
Raw Normal View History

import csv
2018-01-19 17:48:00 +01:00
from unittest import mock
from urllib.parse import parse_qs, urlparse
import icalendar
2018-01-19 17:48:00 +01:00
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.http import QueryDict
from django.test import Client
from django.utils import timezone
from django.utils.functional import cached_property
User = get_user_model()
class TestCaseMixin:
def assertForbidden(self, response):
"""
Test that the response (retrieved with a Client) is a denial of access.
The response should verify one of the following:
- its HTTP response code is 403,
- it redirects to the login page with a GET parameter named 'next'
whose value is the url of the requested page.
"""
request = response.wsgi_request
try:
try:
# Is this an HTTP Forbidden response ?
self.assertEqual(response.status_code, 403)
except AssertionError:
# A redirection to the login view is fine too.
# Let's build the login url with the 'next' param on current
# page.
full_path = request.get_full_path()
querystring = QueryDict(mutable=True)
querystring["next"] = full_path
2018-01-19 17:48:00 +01:00
login_url = "{}?{}".format(
core -- Install django-allauth-ens Refer to allauth doc for an accurate features list: http://django-allauth.readthedocs.io/en/latest/ Users can now change their password, ask for a password reset, or set one if they don't have one. In particular, it allows users whose account has been created via a clipper authentication to configure a password before losing their clipper. Even if they have already lost it, they are able to get one using the "Reset password" functionality. Allauth multiple emails management is deactivated. Requests to the related url redirect to the home page. All the login and logout views are replaced by the allauth' ones. It also concerns the Django and Wagtail admin sites. Note that users are no longer logged out of the clipper CAS server when they authenticated via this server. Instead a message suggests the user to disconnect. Clipper connections and `login_clipper` --------------------------------------- - Non-empty `login_clipper` are now unique among `CofProfile` instances. - They are created once for users with a non-empty 'login_clipper' (with the data migration 0014_create_clipper_connections). - The `login_clipper` of CofProfile instances are sync with their clipper connections: * `CofProfile.sync_clipper_connections` method updates the connections based on `login_clipper`. * Signals receivers `sync_clipper…` update `login_clipper` based on connections creations/updates/deletions. Misc ---- - Add NullCharField (model field) which allows to use `unique=True` on CharField (even with empty strings). - Parts of kfet mixins for TestCase are now in shared.tests.testcase, as they are used elsewhere than in the kfet app.
2017-10-19 01:12:52 +02:00
reverse("account_login"), querystring.urlencode(safe="/")
)
2018-01-19 17:48:00 +01:00
# We don't focus on what the login view does.
# So don't fetch the redirect.
self.assertRedirects(response, login_url, fetch_redirect_response=False)
2018-01-19 17:48:00 +01:00
except AssertionError:
raise AssertionError(
"%(http_method)s request at %(path)s should be forbidden for "
"%(username)s user.\n"
"Response isn't 403, nor a redirect to login view. Instead, "
"response code is %(code)d."
% {
"http_method": request.method,
"path": request.get_full_path(),
"username": (
2018-01-19 17:48:00 +01:00
"'{}'".format(request.user)
if request.user.is_authenticated()
else "anonymous"
2018-01-19 17:48:00 +01:00
),
"code": response.status_code,
2018-01-19 17:48:00 +01:00
}
)
def assertUrlsEqual(self, actual, expected):
"""
Test that the url 'actual' is as 'expected'.
Arguments:
actual (str): Url to verify.
expected: Two forms are accepted.
* (str): Expected url. Strings equality is checked.
* (dict): Its keys must be attributes of 'urlparse(actual)'.
Equality is checked for each present key, except for
'query' which must be a dict of the expected query string
parameters.
"""
if type(expected) == dict:
parsed = urlparse(actual)
for part, expected_part in expected.items():
if part == "query":
2018-01-19 17:48:00 +01:00
self.assertDictEqual(
parse_qs(parsed.query), expected.get("query", {})
2018-01-19 17:48:00 +01:00
)
else:
self.assertEqual(getattr(parsed, part), expected_part)
else:
self.assertEqual(actual, expected)
def mockLDAP(self, results):
class Elt:
def __init__(self, value):
self.value = value
class Entry:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, Elt(v))
results_as_ldap = [Entry(uid=uid, cn=name) for uid, name in results]
mock_connection = mock.MagicMock()
mock_connection.entries = results_as_ldap
# Connection is used as a context manager.
mock_context_manager = mock.MagicMock()
mock_context_manager.return_value.__enter__.return_value = mock_connection
patcher = mock.patch(
"gestioncof.autocomplete.Connection", new=mock_context_manager
)
patcher.start()
self.addCleanup(patcher.stop)
return mock_connection
def load_from_csv_response(self, r):
decoded = r.content.decode("utf-8")
return list(csv.reader(decoded.split("\n")[:-1]))
def _test_event_equal(self, event, exp):
for k, v_desc in exp.items():
if isinstance(v_desc, tuple):
v_getter = v_desc[0]
v = v_desc[1]
else:
v_getter = lambda v: v
v = v_desc
if v_getter(event[k.upper()]) != v:
return False
return True
def _find_event(self, ev, l):
for i, elt in enumerate(l):
if self._test_event_equal(ev, elt):
return elt, i
return False, -1
def assertCalEqual(self, ical_content, expected):
remaining = expected.copy()
unexpected = []
cal = icalendar.Calendar.from_ical(ical_content)
for ev in cal.walk("vevent"):
found, i_found = self._find_event(ev, remaining)
if found:
remaining.pop(i_found)
else:
unexpected.append(ev)
self.assertListEqual(unexpected, [])
self.assertListEqual(remaining, [])
2018-01-19 17:48:00 +01:00
class ViewTestCaseMixin(TestCaseMixin):
"""
TestCase extension to ease tests of kfet views.
Urls concerns
-------------
# Basic usage
Attributes:
url_name (str): Name of view under test, as given to 'reverse'
function.
url_args (list, optional): Will be given to 'reverse' call.
url_kwargs (dict, optional): Same.
url_expcted (str): What 'reverse' should return given previous
attributes.
View url can then be accessed at the 'url' attribute.
# Advanced usage
If multiple combinations of url name, args, kwargs can be used for a view,
it is possible to define 'urls_conf' attribute. It must be a list whose
each item is a dict defining arguments for 'reverse' call ('name', 'args',
'kwargs' keys) and its expected result ('expected' key).
The reversed urls can be accessed at the 't_urls' attribute.
Users concerns
--------------
During setup, the following users are created:
- 'user': a basic user without any permission,
- 'root': a superuser, account trigramme: 200.
Their password is their username.
One can create additionnal users with 'get_users_extra' method, or prevent
these users to be created with 'get_users_base' method. See these two
methods for further informations.
By using 'register_user' method, these users can then be accessed at
'users' attribute by their label.
A user label can be given to 'auth_user' attribute. The related user is
then authenticated on self.client during test setup. Its value defaults to
'None', meaning no user is authenticated.
Automated tests
---------------
# Url reverse
Based on url-related attributes/properties, the test 'test_urls' checks
that expected url is returned by 'reverse' (once with basic url usage and
each for advanced usage).
# Forbidden responses
The 'test_forbidden' test verifies that each user, from labels of
'auth_forbidden' attribute, can't access the url(s), i.e. response should
be a 403, or a redirect to login view.
Tested HTTP requests are given by 'http_methods' attribute. Additional data
can be given by defining an attribute '<method(lowercase)>_data'.
"""
2018-01-19 17:48:00 +01:00
url_name = None
url_expected = None
http_methods = ["GET"]
2018-01-19 17:48:00 +01:00
auth_user = None
auth_forbidden = []
def setUp(self):
"""
Warning: Do not forget to call super().setUp() in subclasses.
"""
# A test can mock 'django.utils.timezone.now' and give this as return
# value. E.g. it is useful if the test checks values of 'auto_now' or
# 'auto_now_add' fields.
self.now = timezone.now()
# Register of User instances.
self.users = {}
for label, user in dict(self.users_base, **self.users_extra).items():
self.register_user(label, user)
if self.auth_user:
# The wrapper is a sanity check.
self.assertTrue(
self.client.login(username=self.auth_user, password=self.auth_user)
2018-01-19 17:48:00 +01:00
)
def tearDown(self):
del self.users_base
del self.users_extra
def get_users_base(self):
"""
Dict of <label: user instance>.
Note: Don't access yourself this property. Use 'users_base' attribute
which cache the returned value from here.
It allows to give functions calls, which creates users instances, as
values here.
"""
return {
"user": User.objects.create_user("user", "", "user"),
"root": User.objects.create_superuser("root", "", "root"),
2018-01-19 17:48:00 +01:00
}
@cached_property
def users_base(self):
return self.get_users_base()
def get_users_extra(self):
"""
Dict of <label: user instance>.
Note: Don't access yourself this property. Use 'users_base' attribute
which cache the returned value from here.
It allows to give functions calls, which create users instances, as
values here.
"""
return {}
@cached_property
def users_extra(self):
return self.get_users_extra()
def register_user(self, label, user):
self.users[label] = user
def get_user(self, label):
if self.auth_user is not None:
return self.auth_user
return self.auth_user_mapping.get(label)
@property
def urls_conf(self):
return [
{
"name": self.url_name,
"args": getattr(self, "url_args", []),
"kwargs": getattr(self, "url_kwargs", {}),
"expected": self.url_expected,
}
]
2018-01-19 17:48:00 +01:00
@property
def t_urls(self):
return [
reverse(
url_conf["name"],
args=url_conf.get("args", []),
kwargs=url_conf.get("kwargs", {}),
2018-01-19 17:48:00 +01:00
)
for url_conf in self.urls_conf
]
2018-01-19 17:48:00 +01:00
@property
def url(self):
return self.t_urls[0]
def test_urls(self):
for url, conf in zip(self.t_urls, self.urls_conf):
self.assertEqual(url, conf["expected"])
2018-01-19 17:48:00 +01:00
def test_forbidden(self):
for method in self.http_methods:
for user in self.auth_forbidden:
for url in self.t_urls:
self.check_forbidden(method, url, user)
def check_forbidden(self, method, url, user=None):
method = method.lower()
client = Client()
if user is not None:
client.login(username=user, password=user)
send_request = getattr(client, method)
data = getattr(self, "{}_data".format(method), {})
2018-01-19 17:48:00 +01:00
r = send_request(url, data)
self.assertForbidden(r)