Merge branch 'aureplop/cof-tests_survey' into 'master'

cof -- Add tests for survey views

See merge request cof-geek/gestioCOF!285
This commit is contained in:
Martin Pepin 2018-02-06 13:48:46 +01:00
commit 8f0eec0e88
9 changed files with 551 additions and 3 deletions

View file

@ -8,7 +8,7 @@
{% if survey.details %}
<p>{{ survey.details }}</p>
{% endif %}
<form class="form-horizontal" method="post" action="{% url 'gestioncof.views.survey' survey.id %}">
<form class="form-horizontal" method="post" action="{% url 'survey.details' survey.id %}">
{% csrf_token %}
{{ form | bootstrap}}

View file

@ -11,7 +11,7 @@
{% endif %}
<h3>Filtres</h3>
{% include "tristate_js.html" %}
<form method="post" action="{% url 'gestioncof.views.survey_status' survey.id %}">
<form method="post" action="{% url 'survey.details.status' survey.id %}">
{% csrf_token %}
{{ form.as_p }}
<input style="margin-top:10px;" type="submit" class="btn btn-primary" value="Filtrer" />

View file

View file

@ -0,0 +1,178 @@
from django.contrib import messages
from django.contrib.messages import get_messages
from django.contrib.messages.storage.base import Message
from django.test import TestCase
from gestioncof.models import Survey, SurveyAnswer
from gestioncof.tests.testcases import ViewTestCaseMixin
class SurveyViewTests(ViewTestCaseMixin, TestCase):
url_name = 'survey.details'
http_methods = ['GET', 'POST']
auth_user = 'user'
auth_forbidden = [None]
post_expected_message = Message(messages.SUCCESS, (
"Votre réponse a bien été enregistrée ! Vous pouvez cependant la "
"modifier jusqu'à la fin du sondage."
))
@property
def url_kwargs(self):
return {'survey_id': self.s.pk}
@property
def url_expected(self):
return '/survey/{}'.format(self.s.pk)
def setUp(self):
super().setUp()
self.s = Survey.objects.create(title='Title')
self.q1 = self.s.questions.create(question='Question 1 ?')
self.q2 = self.s.questions.create(
question='Question 2 ?',
multi_answers=True,
)
self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1')
self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2')
self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1')
self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2')
def test_get(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_post_new(self):
r = self.client.post(self.url, {
'question_{}'.format(self.q1.pk): [str(self.qa1.pk)],
'question_{}'.format(self.q2.pk): [
str(self.qa3.pk), str(self.qa4.pk),
],
})
self.assertEqual(r.status_code, 200)
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
a = self.s.surveyanswer_set.get(user=self.users['user'])
self.assertQuerysetEqual(
a.answers.all(), map(repr, [self.qa1, self.qa3, self.qa4]),
ordered=False,
)
def test_post_edit(self):
a = self.s.surveyanswer_set.create(user=self.users['user'])
a.answers.add(self.qa1, self.qa1, self.qa4)
r = self.client.post(self.url, {
'question_{}'.format(self.q1.pk): [],
'question_{}'.format(self.q2.pk): [str(self.qa3.pk)],
})
self.assertEqual(r.status_code, 200)
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
a.refresh_from_db()
self.assertQuerysetEqual(
a.answers.all(), map(repr, [self.qa3]),
ordered=False,
)
def test_post_delete(self):
a = self.s.surveyanswer_set.create(user=self.users['user'])
a.answers.add(self.qa1, self.qa4)
r = self.client.post(self.url, {'delete': '1'})
self.assertEqual(r.status_code, 200)
expected_message = Message(
messages.SUCCESS, "Votre réponse a bien été supprimée")
self.assertIn(expected_message, get_messages(r.wsgi_request))
with self.assertRaises(SurveyAnswer.DoesNotExist):
a.refresh_from_db()
def test_forbidden_closed(self):
self.s.survey_open = False
self.s.save()
r = self.client.get(self.url)
self.assertNotEqual(r.status_code, 200)
def test_forbidden_old(self):
self.s.old = True
self.s.save()
r = self.client.get(self.url)
self.assertNotEqual(r.status_code, 200)
class SurveyStatusViewTests(ViewTestCaseMixin, TestCase):
url_name = 'survey.details.status'
http_methods = ['GET', 'POST']
auth_user = 'staff'
auth_forbidden = [None, 'user', 'member']
@property
def url_kwargs(self):
return {'survey_id': self.s.pk}
@property
def url_expected(self):
return '/survey/{}/status'.format(self.s.pk)
def setUp(self):
super().setUp()
self.s = Survey.objects.create(title='Title')
self.q1 = self.s.questions.create(question='Question 1 ?')
self.q2 = self.s.questions.create(
question='Question 2 ?',
multi_answers=True,
)
self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1')
self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2')
self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1')
self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2')
self.a1 = self.s.surveyanswer_set.create(user=self.users['user'])
self.a1.answers.add(self.qa1)
self.a2 = self.s.surveyanswer_set.create(user=self.users['member'])
def test_get(self):
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def _get_qa_filter_name(self, qa):
return 'question_{}_answer_{}'.format(qa.survey_question.pk, qa.pk)
def _test_filters(self, filters, expected):
r = self.client.post(self.url, {
self._get_qa_filter_name(qa): v for qa, v in filters
})
self.assertEqual(r.status_code, 200)
self.assertQuerysetEqual(
r.context['user_answers'], map(repr, expected),
ordered=False,
)
def test_filter_none(self):
self._test_filters([(self.qa1, 'none')], [self.a1, self.a2])
def test_filter_yes(self):
self._test_filters([(self.qa1, 'yes')], [self.a1])
def test_filter_no(self):
self._test_filters([(self.qa1, 'no')], [self.a2])

View file

@ -0,0 +1,24 @@
from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin
from .utils import create_user, create_member, create_staff
class ViewTestCaseMixin(BaseViewTestCaseMixin):
"""
TestCase extension to ease testing of cof views.
Most information can be found in the base parent class doc.
This class performs some changes to users management, detailed below.
During setup, three users are created:
- 'user': a basic user without any permission,
- 'member': (profile.is_cof is True),
- 'staff': (profile.is_cof is True) && (profile.is_buro is True).
"""
def get_users_base(self):
return {
'user': create_user('user'),
'member': create_member('member'),
'staff': create_staff('staff'),
}

51
gestioncof/tests/utils.py Normal file
View file

@ -0,0 +1,51 @@
from django.contrib.auth import get_user_model
User = get_user_model()
def _create_user(username, is_cof=False, is_staff=False, attrs=None):
if attrs is None:
attrs = {}
password = attrs.pop('password', username)
user_keys = ['first_name', 'last_name', 'email', 'is_staff']
user_attrs = {k: v for k, v in attrs.items() if k in user_keys}
profile_keys = [
'is_cof', 'login_clipper', 'phone', 'occupation', 'departement',
'type_cotiz', 'mailing_cof', 'mailing_bda', 'mailing_bda_revente',
'comments', 'is_buro', 'petit_cours_accept',
'petit_cours_remarques',
]
profile_attrs = {k: v for k, v in attrs.items() if k in profile_keys}
if is_cof:
profile_attrs['is_cof'] = True
if is_staff:
# At the moment, admin is accessible by COF staff.
user_attrs['is_staff'] = True
profile_attrs['is_buro'] = True
user = User(username=username, **user_attrs)
user.set_password(password)
user.save()
for k, v in profile_attrs.items():
setattr(user.profile, k, v)
user.profile.save()
return user
def create_user(username, attrs=None):
return _create_user(username, attrs=attrs)
def create_member(username, attrs=None):
return _create_user(username, is_cof=True, attrs=attrs)
def create_staff(username, attrs=None):
return _create_user(username, is_cof=True, is_staff=True, attrs=attrs)

View file

@ -20,6 +20,7 @@ class TriStateCheckbox(Widget):
def render(self, name, value, attrs=None, choices=()):
if value is None:
value = 'none'
final_attrs = self.build_attrs(attrs, value=value)
attrs['value'] = value
final_attrs = self.build_attrs(self.attrs, attrs)
output = ["<span class=\"tristate\"%s></span>" % flatatt(final_attrs)]
return mark_safe('\n'.join(output))

294
shared/tests/testcases.py Normal file
View file

@ -0,0 +1,294 @@
from unittest import mock
from urllib.parse import parse_qs, urlparse
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('cof-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)
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.
"""
# 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()
# 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)