05eeb6a25c
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.
352 lines
11 KiB
Python
352 lines
11 KiB
Python
import csv
|
|
from unittest import mock
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
import icalendar
|
|
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
|
|
|
|
login_url = "{}?{}".format(
|
|
reverse("account_login"), querystring.urlencode(safe="/")
|
|
)
|
|
|
|
# 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)
|
|
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": (
|
|
"'{}'".format(request.user)
|
|
if request.user.is_authenticated()
|
|
else "anonymous"
|
|
),
|
|
"code": response.status_code,
|
|
}
|
|
)
|
|
|
|
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":
|
|
self.assertDictEqual(
|
|
parse_qs(parsed.query), expected.get("query", {})
|
|
)
|
|
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, [])
|
|
|
|
|
|
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'.
|
|
|
|
"""
|
|
|
|
url_name = None
|
|
url_expected = None
|
|
|
|
http_methods = ["GET"]
|
|
|
|
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)
|
|
)
|
|
|
|
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"),
|
|
}
|
|
|
|
@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,
|
|
}
|
|
]
|
|
|
|
@property
|
|
def t_urls(self):
|
|
return [
|
|
reverse(
|
|
url_conf["name"],
|
|
args=url_conf.get("args", []),
|
|
kwargs=url_conf.get("kwargs", {}),
|
|
)
|
|
for url_conf in self.urls_conf
|
|
]
|
|
|
|
@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"])
|
|
|
|
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), {})
|
|
|
|
r = send_request(url, data)
|
|
self.assertForbidden(r)
|