Merge branch 'Aufinal/simplify_tests' into 'master'

Utilitaire de tests simplifié

See merge request klub-dev-ens/gestioCOF!421
This commit is contained in:
Ludovic Stephan 2020-05-15 16:12:47 +02:00
commit 90fc6aa3e7
9 changed files with 328 additions and 261 deletions

View file

@ -4,7 +4,7 @@ from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
from django.utils import timezone from django.utils import timezone
from shared.tests.testcases import ViewTestCaseMixin from shared.tests.mixins import ViewTestCaseMixin
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
from .utils import create_user from .utils import create_user

View file

@ -8,7 +8,7 @@ from django.urls import reverse
from django.utils import formats, timezone from django.utils import formats, timezone
from ..models import Participant, Tirage from ..models import Participant, Tirage
from .testcases import BdATestHelpers, BdAViewTestCaseMixin from .mixins import BdATestHelpers, BdAViewTestCaseMixin
User = get_user_model() User = get_user_model()

View file

@ -1,4 +1,3 @@
import csv
from unittest import mock from unittest import mock
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -14,6 +13,7 @@ from events.models import (
OptionChoice, OptionChoice,
Registration, Registration,
) )
from shared.tests.mixins import CSVResponseMixin
User = get_user_model() User = get_user_model()
@ -70,7 +70,7 @@ class CSVExportAccessTest(MessagePatch, TestCase):
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
class CSVExportContentTest(MessagePatch, TestCase): class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -90,13 +90,25 @@ class CSVExportContentTest(MessagePatch, TestCase):
def test_simple_event(self): def test_simple_event(self):
self.event.subscribers.set([self.u1, self.u2]) self.event.subscribers.set([self.u1, self.u2])
participants = self.client.get(self.url).content.decode("utf-8") response = self.client.get(self.url)
participants = [
line for line in csv.reader(participants.split("\n")) if line != [] self.assertCSVEqual(
] response,
self.assertEqual(len(participants), 3) [
self.assertEqual(participants[1], ["toto_foo", "toto@a.b", "toto", "foo"]) {
self.assertEqual(participants[2], ["titi_bar", "titi@a.b", "titi", "bar"]) "username": "toto_foo",
"prénom": "toto",
"nom de famille": "foo",
"email": "toto@a.b",
},
{
"username": "titi_bar",
"prénom": "titi",
"nom de famille": "bar",
"email": "titi@a.b",
},
],
)
def test_complex_event(self): def test_complex_event(self):
registration = Registration.objects.create(event=self.event, user=self.u1) registration = Registration.objects.create(event=self.event, user=self.u1)
@ -127,15 +139,18 @@ class CSVExportContentTest(MessagePatch, TestCase):
field=field, registration=registration, content="hello" field=field, registration=registration, content="hello"
) )
participants = self.client.get(self.url).content.decode("utf-8") response = self.client.get(self.url)
participants = list(csv.reader(participants.split("\n"))) self.assertCSVEqual(
toto_registration = participants[1] response,
[
# This is not super nice, but it makes the test deterministic. {
if toto_registration[5] == "f & d": "username": "toto_foo",
toto_registration[5] = "d & f" "prénom": "toto",
"nom de famille": "foo",
self.assertEqual( "email": "toto@a.b",
["toto_foo", "toto@a.b", "toto", "foo", "a", "d & f", "hello"], "abc": "a",
toto_registration, "def": "d & f",
"remarks": "hello",
}
],
) )

View file

@ -38,7 +38,7 @@ def participants_csv(request, event_id):
# Options # Options
all_choices = registration.options_choices.values_list("choice", flat=True) all_choices = registration.options_choices.values_list("choice", flat=True)
options_choices = [ options_choices = [
" & ".join(all_choices.filter(option__id=id)) " & ".join(all_choices.filter(option__id=id).order_by("id"))
for id in event.options.values_list("id", flat=True).order_by("id") for id in event.options.values_list("id", flat=True).order_by("id")
] ]
row += options_choices row += options_choices

View file

@ -0,0 +1,74 @@
from gestioncof.models import Event
from shared.tests.mixins import (
CSVResponseMixin,
ViewTestCaseMixin as BaseViewTestCaseMixin,
)
from .utils import create_member, create_staff, create_user
class MegaHelperMixin(CSVResponseMixin):
"""
Mixin pour aider aux tests du MEGA: création de l'event et de plusieurs
inscriptions, avec options et commentaires.
"""
def setUp(self):
super().setUp()
u1 = create_user("u1")
u1.first_name = "first"
u1.last_name = "last"
u1.email = "user@mail.net"
u1.save()
u1.profile.phone = "0123456789"
u1.profile.departement = "Dept"
u1.profile.comments = "profile.comments"
u1.profile.save()
u2 = create_user("u2")
u2.profile.save()
m = Event.objects.create(title="MEGA 2018")
cf1 = m.commentfields.create(name="Commentaires")
cf2 = m.commentfields.create(name="Comment Field 2", fieldtype="char")
option_type = m.options.create(name="Orga ? Conscrit ?")
choice_orga = option_type.choices.create(value="Orga")
choice_conscrit = option_type.choices.create(value="Conscrit")
mr1 = m.eventregistration_set.create(user=u1)
mr1.options.add(choice_orga)
mr1.comments.create(commentfield=cf1, content="Comment 1")
mr1.comments.create(commentfield=cf2, content="Comment 2")
mr2 = m.eventregistration_set.create(user=u2)
mr2.options.add(choice_conscrit)
self.u1 = u1
self.u2 = u2
self.m = m
self.choice_orga = choice_orga
self.choice_conscrit = choice_conscrit
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"),
}

View file

@ -1,4 +1,3 @@
import csv
import os import os
import uuid import uuid
from datetime import timedelta from datetime import timedelta
@ -16,7 +15,8 @@ from django.urls import reverse
from bda.models import Salle, Tirage from bda.models import Salle, Tirage
from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer
from gestioncof.tests.testcases import ViewTestCaseMixin from gestioncof.tests.mixins import MegaHelperMixin, ViewTestCaseMixin
from shared.tests.mixins import CSVResponseMixin, ICalMixin, MockLDAPMixin
from shared.views.autocomplete import Clipper from shared.views.autocomplete import Clipper
from .utils import create_member, create_root, create_user from .utils import create_member, create_root, create_user
@ -227,7 +227,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase):
auth_forbidden = [None, "user", "member"] auth_forbidden = [None, "user", "member"]
def test_empty(self): def test_empty(self):
r = self.client.get(self.t_urls[0]) r = self.client.get(self.reversed_urls[0])
self.assertIn("user_form", r.context) self.assertIn("user_form", r.context)
self.assertIn("profile_form", r.context) self.assertIn("profile_form", r.context)
@ -240,7 +240,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase):
u.last_name = "last" u.last_name = "last"
u.save() u.save()
r = self.client.get(self.t_urls[1]) r = self.client.get(self.reversed_urls[1])
self.assertIn("user_form", r.context) self.assertIn("user_form", r.context)
self.assertIn("profile_form", r.context) self.assertIn("profile_form", r.context)
@ -252,7 +252,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(user_form["last_name"].initial, "last") self.assertEqual(user_form["last_name"].initial, "last")
def test_clipper(self): def test_clipper(self):
r = self.client.get(self.t_urls[2]) r = self.client.get(self.reversed_urls[2])
self.assertIn("user_form", r.context) self.assertIn("user_form", r.context)
self.assertIn("profile_form", r.context) self.assertIn("profile_form", r.context)
@ -267,7 +267,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase):
@override_settings(LDAP_SERVER_URL="ldap_url") @override_settings(LDAP_SERVER_URL="ldap_url")
class RegistrationAutocompleteViewTests(ViewTestCaseMixin, TestCase): class RegistrationAutocompleteViewTests(MockLDAPMixin, ViewTestCaseMixin, TestCase):
url_name = "cof.registration.autocomplete" url_name = "cof.registration.autocomplete"
url_expected = "/autocomplete/registration" url_expected = "/autocomplete/registration"
@ -462,7 +462,7 @@ class UserAutocompleteViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
class ExportMembersViewTests(ViewTestCaseMixin, TestCase): class ExportMembersViewTests(CSVResponseMixin, ViewTestCaseMixin, TestCase):
url_name = "cof.membres_export" url_name = "cof.membres_export"
url_expected = "/export/members" url_expected = "/export/members"
@ -482,8 +482,10 @@ class ExportMembersViewTests(ViewTestCaseMixin, TestCase):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
data = list(csv.reader(r.content.decode("utf-8").split("\n")[:-1]))
expected = [ self.assertCSVEqual(
r,
[
[ [
str(u1.pk), str(u1.pk),
"member", "member",
@ -496,56 +498,11 @@ class ExportMembersViewTests(ViewTestCaseMixin, TestCase):
"normalien", "normalien",
], ],
[str(u2.pk), "staff", "", "", "", "", "1A", "", "normalien"], [str(u2.pk), "staff", "", "", "", "", "1A", "", "normalien"],
] ],
# Sort before checking equality, the order of the output of csv.reader )
# does not seem deterministic
expected.sort(key=lambda row: int(row[0]))
data.sort(key=lambda row: int(row[0]))
self.assertListEqual(data, expected)
class MegaHelpers: class ExportMegaViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase):
def setUp(self):
super().setUp()
u1 = create_user("u1")
u1.first_name = "first"
u1.last_name = "last"
u1.email = "user@mail.net"
u1.save()
u1.profile.phone = "0123456789"
u1.profile.departement = "Dept"
u1.profile.comments = "profile.comments"
u1.profile.save()
u2 = create_user("u2")
u2.profile.save()
m = Event.objects.create(title="MEGA 2018")
cf1 = m.commentfields.create(name="Commentaires")
cf2 = m.commentfields.create(name="Comment Field 2", fieldtype="char")
option_type = m.options.create(name="Orga ? Conscrit ?")
choice_orga = option_type.choices.create(value="Orga")
choice_conscrit = option_type.choices.create(value="Conscrit")
mr1 = m.eventregistration_set.create(user=u1)
mr1.options.add(choice_orga)
mr1.comments.create(commentfield=cf1, content="Comment 1")
mr1.comments.create(commentfield=cf2, content="Comment 2")
mr2 = m.eventregistration_set.create(user=u2)
mr2.options.add(choice_conscrit)
self.u1 = u1
self.u2 = u2
self.m = m
self.choice_orga = choice_orga
self.choice_conscrit = choice_conscrit
class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
url_name = "cof.mega_export" url_name = "cof.mega_export"
url_expected = "/export/mega" url_expected = "/export/mega"
@ -556,8 +513,8 @@ class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertListEqual( self.assertCSVEqual(
self.load_from_csv_response(r), r,
[ [
[ [
"u1", "u1",
@ -574,7 +531,7 @@ class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
) )
class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): class ExportMegaOrgasViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase):
url_name = "cof.mega_export_orgas" url_name = "cof.mega_export_orgas"
url_expected = "/export/mega/orgas" url_expected = "/export/mega/orgas"
@ -586,8 +543,8 @@ class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertListEqual( self.assertCSVEqual(
self.load_from_csv_response(r), r,
[ [
[ [
"u1", "u1",
@ -603,7 +560,7 @@ class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
) )
class ExportMegaParticipantsViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): class ExportMegaParticipantsViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase):
url_name = "cof.mega_export_participants" url_name = "cof.mega_export_participants"
url_expected = "/export/mega/participants" url_expected = "/export/mega/participants"
@ -614,13 +571,12 @@ class ExportMegaParticipantsViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertListEqual( self.assertCSVEqual(
self.load_from_csv_response(r), r, [["u2", "", "", "", "", str(self.u2.pk), "", ""]],
[["u2", "", "", "", "", str(self.u2.pk), "", ""]],
) )
class ExportMegaRemarksViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): class ExportMegaRemarksViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase):
url_name = "cof.mega_export_remarks" url_name = "cof.mega_export_remarks"
url_expected = "/export/mega/avecremarques" url_expected = "/export/mega/avecremarques"
@ -631,8 +587,8 @@ class ExportMegaRemarksViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertListEqual( self.assertCSVEqual(
self.load_from_csv_response(r), r,
[ [
[ [
"u1", "u1",
@ -815,7 +771,7 @@ class CalendarViewTests(ViewTestCaseMixin, TestCase):
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
class CalendarICSViewTests(ViewTestCaseMixin, TestCase): class CalendarICSViewTests(ICalMixin, ViewTestCaseMixin, TestCase):
url_name = "calendar.ics" url_name = "calendar.ics"
auth_user = None auth_user = None

View file

@ -1,24 +0,0 @@
from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin
from .utils import create_member, create_staff, create_user
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"),
}

View file

@ -1,12 +1,11 @@
import json import json
import os import os
from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import Client, TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from gestioncof.tests.testcases import ViewTestCaseMixin from gestioncof.tests.mixins import ViewTestCaseMixin
from .utils import ( from .utils import (
PetitCoursTestHelpers, PetitCoursTestHelpers,

View file

@ -13,6 +13,130 @@ from django.utils.functional import cached_property
User = get_user_model() User = get_user_model()
class MockLDAPMixin:
"""
Mixin pour simuler un appel à un serveur LDAP (e.g., celui de l'ENS) dans des
tests unitaires. La réponse est une instance d'une classe Entry, qui simule
grossièrement l'interface de ldap3.
Cette classe patche la méthode magique `__enter__`, le code correspondant doit donc
appeler `with Connection(*args, **kwargs) as foo` pour que le test fonctionne.
"""
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(
"shared.views.autocomplete.Connection", new=mock_context_manager
)
patcher.start()
self.addCleanup(patcher.stop)
return mock_connection
class CSVResponseMixin:
"""
Mixin pour manipuler des réponses données via CSV. Deux choix sont possibles:
- si `as_dict=False`, convertit le CSV en une liste de listes (une liste par ligne)
- si `as_dict=True`, convertit le CSV en une liste de dicts, avec les champs donnés
par la première ligne du CSV.
"""
def _load_from_csv_response(self, r, as_dict=False, **reader_kwargs):
content = r.content.decode("utf-8")
# la dernière ligne du fichier CSV est toujours vide
content = content.split("\n")[:-1]
if as_dict:
content = csv.DictReader(content, **reader_kwargs)
# en python3.7, content est une liste d'OrderedDicts
return list(map(dict, content))
else:
content = csv.reader(content, **reader_kwargs)
return list(content)
def assertCSVEqual(self, response, expected):
if type(expected[0]) == list:
as_dict = False
elif type(expected[0]) == dict:
as_dict = True
else:
raise AssertionError(
"Unsupported type in `assertCSVEqual`: "
"%(expected)s is not of type `list` nor `dict` !"
% {"expected": str(expected[0])}
)
content = self._load_from_csv_response(response, as_dict=as_dict)
self.assertCountEqual(content, expected)
class ICalMixin:
"""
Mixin pour manipuler des iCalendars. Permet de tester l'égalité entre
in iCal d'une part, et une liste d'évènements (représentés par des dicts)
d'autre part.
"""
def _test_event_equal(self, event, exp):
"""
Les éléments du dict peuvent être de deux types:
- un tuple `(getter, expected_value)`, auquel cas on teste l'égalité
`getter(event[key]) == value)`;
- une variable `value` de n'importe quel autre type, auquel cas on teste
`event[key] == value`.
"""
for key, value in exp.items():
if isinstance(value, tuple):
getter = value[0]
v = value[1]
else:
getter = lambda v: v
v = value
# dans un iCal, les fields sont en majuscules
if getter(event[key.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 i
return None
def assertCalEqual(self, ical_content, expected):
remaining = expected.copy()
unexpected = []
cal = icalendar.Calendar.from_ical(ical_content)
for ev in cal.walk("vevent"):
i_found = self._find_event(ev, remaining)
if i_found is not None:
remaining.pop(i_found)
else:
unexpected.append(ev)
self.assertListEqual(remaining, [])
self.assertListEqual(unexpected, [])
class TestCaseMixin: class TestCaseMixin:
def assertForbidden(self, response): def assertForbidden(self, response):
""" """
@ -91,140 +215,69 @@ class TestCaseMixin:
else: else:
self.assertEqual(actual, expected) 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(
"shared.views.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): class ViewTestCaseMixin(TestCaseMixin):
""" """
TestCase extension to ease tests of kfet views. Utilitaire pour automatiser certains tests sur les vues Django.
Création d'utilisateurs
------------------------
# Données de base
On crée dans tous les cas deux utilisateurs : un utilisateur normal "user",
et un superutilisateur "root", avec un mot de passe identique au username.
Urls concerns # Accès et utilisateurs supplémentaires
------------- Les utilisateurs créés sont accessibles dans le dict `self.users`, qui associe
un label à une instance de User.
# Basic usage Pour rajouter des utilisateurs supplémentaires (et s'assurer qu'ils sont
disponibles dans `self.users`), on peut redéfinir la fonction `get_users_extra()`,
qui doit renvoyer aussi un dict <label: User instance>.
Attributes: Misc QoL
url_name (str): Name of view under test, as given to 'reverse' ------------------------
function. Pour éviter une erreur de login (puisque les messages de Django ne sont pas
url_args (list, optional): Will be given to 'reverse' call. disponibles), les messages de bienvenue de GestioCOF sont patchés.
url_kwargs (dict, optional): Same. Un attribut `self.now` est fixé au démarrage, pour être donné comme valeur
url_expcted (str): What 'reverse' should return given previous de retour à un patch local de `django.utils.timezone.now`. Cela permet de
attributes. tester des dates/heures de manière robuste.
View url can then be accessed at the 'url' attribute. Test d'URLS
------------------------
# Advanced usage # Usage basique
Teste que l'URL générée par `reverse` correspond bien à l'URL théorique.
Attributs liés :
- `url_name` : nom de l'URL qui sera donné à `reverse`,
- `url_expected` : URL attendue en retour.
- (optionnels) `url_args` et `url_kwargs` : arguments de l'URL pour `reverse`.
If multiple combinations of url name, args, kwargs can be used for a view, # Usage avancé
it is possible to define 'urls_conf' attribute. It must be a list whose On peut tester plusieurs URLs pour une même vue, en redéfinissant la fonction
each item is a dict defining arguments for 'reverse' call ('name', 'args', `urls_conf()`. Cette fonction doit retourner une liste de dicts, avec les clés
'kwargs' keys) and its expected result ('expected' key). suivantes : `name`, `args`, `kwargs`, `expected`.
The reversed urls can be accessed at the 't_urls' attribute. # Accès aux URLs générées
Dans le cas d'usage basique, l'attribut `self.url` contient l'URL de la vue testée
(telle que renvoyée par `reverse()`). Si plusieurs URLs sont définies dans
`urls_conf()`, elles sont accessibles par la suite dans `self.reversed_urls`.
Authentification
------------------------
Si l'attribut `auth_user` est dans `self.users`, l'utilisateur correspondant
est authentifié avant chaque test (cela n'empêche bien sûr pas de login un autre
utilisateur à la main).
Users concerns Test de restrictions d'accès
-------------- ------------------------
L'utilitaire vérifie automatiquement que certains utilisateurs n'ont pas accès à la
During setup, the following users are created: vue. Plus spécifiquement, sont testés toutes les méthodes dans `self.http_methods`
- 'user': a basic user without any permission, et tous les utilisateurs dans `self.auth_forbidden`. Pour rappel, l'utilisateur
- 'root': a superuser, account trigramme: 200. `None` sert à tester la vue sans authentification.
Their password is their username. On peut donner des paramètres GET/POST/etc. aux tests en définissant un attribut
<methode>_data.
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'.
TODO (?): faire pareil pour vérifier les GET/POST classiques (code 200)
""" """
url_name = None url_name = None
@ -239,19 +292,13 @@ class ViewTestCaseMixin(TestCaseMixin):
""" """
Warning: Do not forget to call super().setUp() in subclasses. 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 = mock.patch("gestioncof.signals.messages")
patcher_messages.start() patcher_messages.start()
self.addCleanup(patcher_messages.stop) 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() self.now = timezone.now()
# Register of User instances.
self.users = {} self.users = {}
for label, user in dict(self.users_base, **self.users_extra).items(): for label, user in dict(self.users_base, **self.users_extra).items():
@ -322,7 +369,7 @@ class ViewTestCaseMixin(TestCaseMixin):
] ]
@property @property
def t_urls(self): def reversed_urls(self):
return [ return [
reverse( reverse(
url_conf["name"], url_conf["name"],
@ -335,16 +382,16 @@ class ViewTestCaseMixin(TestCaseMixin):
@property @property
def url(self): def url(self):
return self.t_urls[0] return self.reversed_urls[0]
def test_urls(self): def test_urls(self):
for url, conf in zip(self.t_urls, self.urls_conf): for url, conf in zip(self.reversed_urls, self.urls_conf):
self.assertEqual(url, conf["expected"]) self.assertEqual(url, conf["expected"])
def test_forbidden(self): def test_forbidden(self):
for method in self.http_methods: for method in self.http_methods:
for user in self.auth_forbidden: for user in self.auth_forbidden:
for url in self.t_urls: for url in self.reversed_urls:
self.check_forbidden(method, url, user) self.check_forbidden(method, url, user)
def check_forbidden(self, method, url, user=None): def check_forbidden(self, method, url, user=None):