361 lines
12 KiB
Python
361 lines
12 KiB
Python
from unittest import mock
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
from django.http import QueryDict
|
|
from django.test import Client
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.functional import cached_property
|
|
|
|
from .utils import create_root, create_team, create_user
|
|
|
|
|
|
class TestCaseMixin:
|
|
"""Extends TestCase for kfet application tests."""
|
|
|
|
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 = "/gestion/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 assertForbiddenKfet(self, response, form_ctx="form"):
|
|
"""
|
|
Test that a response (retrieved with a Client) contains error due to
|
|
lack of kfet permissions.
|
|
|
|
It checks that 'Permission refusée' is present in the non-field errors
|
|
of the form of response context at key 'form_ctx', or present in
|
|
messages.
|
|
|
|
This should be used for pages which can be accessed by the kfet team
|
|
members, but require additionnal permission(s) to make an operation.
|
|
|
|
"""
|
|
try:
|
|
self.assertEqual(response.status_code, 200)
|
|
try:
|
|
form = response.context[form_ctx]
|
|
errors = [y for x in form.errors.as_data().values() for y in x]
|
|
self.assertTrue(any(e.code == "permission-denied" for e in errors))
|
|
except (AssertionError, AttributeError, KeyError):
|
|
self.assertTrue(
|
|
any(
|
|
"permission-denied" in msg.tags
|
|
for msg in response.context["messages"]
|
|
)
|
|
)
|
|
except AssertionError:
|
|
request = response.wsgi_request
|
|
raise AssertionError(
|
|
"%(http_method)s request at %(path)s should raise an error "
|
|
"for %(username)s user.\n"
|
|
"Cannot find any errors in non-field errors of form "
|
|
"'%(form_ctx)s', nor in messages."
|
|
% {
|
|
"http_method": request.method,
|
|
"path": request.get_full_path(),
|
|
"username": (
|
|
"'%s'" % request.user
|
|
if request.user.is_authenticated
|
|
else "anonymous"
|
|
),
|
|
"form_ctx": form_ctx,
|
|
}
|
|
)
|
|
|
|
def assertInstanceExpected(self, instance, expected):
|
|
"""
|
|
Test that the values of the attributes and without-argument methods of
|
|
'instance' are equal to 'expected' pairs.
|
|
"""
|
|
for attr, expected_value in expected.items():
|
|
value = getattr(instance, attr)
|
|
if callable(value):
|
|
value = value()
|
|
self.assertEqual(value, expected_value)
|
|
|
|
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)
|
|
|
|
|
|
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, three users are created with their kfet account:
|
|
- 'user': a basic user without any permission, account trigramme: 000,
|
|
- 'team': a user with kfet.is_team permission, account trigramme: 100,
|
|
- 'root': a superuser, account trigramme: 200,
|
|
- 'liq': if class attribute 'with_liq' is 'True', account
|
|
trigramme: LIQ.
|
|
Their password is their username.
|
|
|
|
One can create additionnal users with 'get_users_extra' method, or prevent
|
|
these 3 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. Similarly, their kfet account is
|
|
registered on 'accounts' attribute.
|
|
|
|
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 = []
|
|
|
|
with_liq = False
|
|
|
|
def setUp(self):
|
|
"""
|
|
Warning: Do not forget to call super().setUp() in subclasses.
|
|
"""
|
|
# Signals handlers on login/logout send messages.
|
|
# Due to the way the Django' test Client performs login, this raise an
|
|
# error. As workaround, we mock the Django' messages module.
|
|
patcher_messages = mock.patch("gestioncof.signals.messages")
|
|
patcher_messages.start()
|
|
self.addCleanup(patcher_messages.stop)
|
|
|
|
# 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()
|
|
|
|
# These attributes register users and accounts instances.
|
|
self.users = {}
|
|
self.accounts = {}
|
|
|
|
for label, user in dict(self.users_base, **self.users_extra).items():
|
|
self.register_user(label, user)
|
|
|
|
if self.auth_user:
|
|
self.client.force_login(self.users[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.
|
|
|
|
"""
|
|
# Format desc: username, password, trigramme
|
|
users_base = {
|
|
# user, user, 000
|
|
"user": create_user(),
|
|
# team, team, 100
|
|
"team": create_team(),
|
|
# root, root, 200
|
|
"root": create_root(),
|
|
}
|
|
if self.with_liq:
|
|
users_base["liq"] = create_user("liq", "LIQ")
|
|
return users_base
|
|
|
|
@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
|
|
if hasattr(user.profile, "account_kfet"):
|
|
self.accounts[label] = user.profile.account_kfet
|
|
|
|
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)
|