Merge branch 'Aufinal/simplify_tests' into 'master'
Utilitaire de tests simplifié See merge request klub-dev-ens/gestioCOF!421
This commit is contained in:
commit
90fc6aa3e7
9 changed files with 328 additions and 261 deletions
|
@ -4,7 +4,7 @@ from django.conf import settings
|
|||
from django.core.management import call_command
|
||||
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 .utils import create_user
|
|
@ -8,7 +8,7 @@ from django.urls import reverse
|
|||
from django.utils import formats, timezone
|
||||
|
||||
from ..models import Participant, Tirage
|
||||
from .testcases import BdATestHelpers, BdAViewTestCaseMixin
|
||||
from .mixins import BdATestHelpers, BdAViewTestCaseMixin
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import csv
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
@ -14,6 +13,7 @@ from events.models import (
|
|||
OptionChoice,
|
||||
Registration,
|
||||
)
|
||||
from shared.tests.mixins import CSVResponseMixin
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -70,7 +70,7 @@ class CSVExportAccessTest(MessagePatch, TestCase):
|
|||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
|
||||
class CSVExportContentTest(MessagePatch, TestCase):
|
||||
class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
|
@ -90,13 +90,25 @@ class CSVExportContentTest(MessagePatch, TestCase):
|
|||
def test_simple_event(self):
|
||||
self.event.subscribers.set([self.u1, self.u2])
|
||||
|
||||
participants = self.client.get(self.url).content.decode("utf-8")
|
||||
participants = [
|
||||
line for line in csv.reader(participants.split("\n")) if line != []
|
||||
]
|
||||
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"])
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertCSVEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
"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):
|
||||
registration = Registration.objects.create(event=self.event, user=self.u1)
|
||||
|
@ -127,15 +139,18 @@ class CSVExportContentTest(MessagePatch, TestCase):
|
|||
field=field, registration=registration, content="hello"
|
||||
)
|
||||
|
||||
participants = self.client.get(self.url).content.decode("utf-8")
|
||||
participants = list(csv.reader(participants.split("\n")))
|
||||
toto_registration = participants[1]
|
||||
|
||||
# This is not super nice, but it makes the test deterministic.
|
||||
if toto_registration[5] == "f & d":
|
||||
toto_registration[5] = "d & f"
|
||||
|
||||
self.assertEqual(
|
||||
["toto_foo", "toto@a.b", "toto", "foo", "a", "d & f", "hello"],
|
||||
toto_registration,
|
||||
response = self.client.get(self.url)
|
||||
self.assertCSVEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
"username": "toto_foo",
|
||||
"prénom": "toto",
|
||||
"nom de famille": "foo",
|
||||
"email": "toto@a.b",
|
||||
"abc": "a",
|
||||
"def": "d & f",
|
||||
"remarks": "hello",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
|
|
@ -38,7 +38,7 @@ def participants_csv(request, event_id):
|
|||
# Options
|
||||
all_choices = registration.options_choices.values_list("choice", flat=True)
|
||||
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")
|
||||
]
|
||||
row += options_choices
|
||||
|
|
74
gestioncof/tests/mixins.py
Normal file
74
gestioncof/tests/mixins.py
Normal 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"),
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import csv
|
||||
import os
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
@ -16,7 +15,8 @@ from django.urls import reverse
|
|||
|
||||
from bda.models import Salle, Tirage
|
||||
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 .utils import create_member, create_root, create_user
|
||||
|
@ -227,7 +227,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase):
|
|||
auth_forbidden = [None, "user", "member"]
|
||||
|
||||
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("profile_form", r.context)
|
||||
|
@ -240,7 +240,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase):
|
|||
u.last_name = "last"
|
||||
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("profile_form", r.context)
|
||||
|
@ -252,7 +252,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase):
|
|||
self.assertEqual(user_form["last_name"].initial, "last")
|
||||
|
||||
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("profile_form", r.context)
|
||||
|
@ -267,7 +267,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase):
|
|||
|
||||
|
||||
@override_settings(LDAP_SERVER_URL="ldap_url")
|
||||
class RegistrationAutocompleteViewTests(ViewTestCaseMixin, TestCase):
|
||||
class RegistrationAutocompleteViewTests(MockLDAPMixin, ViewTestCaseMixin, TestCase):
|
||||
url_name = "cof.registration.autocomplete"
|
||||
url_expected = "/autocomplete/registration"
|
||||
|
||||
|
@ -462,7 +462,7 @@ class UserAutocompleteViewTests(ViewTestCaseMixin, TestCase):
|
|||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class ExportMembersViewTests(ViewTestCaseMixin, TestCase):
|
||||
class ExportMembersViewTests(CSVResponseMixin, ViewTestCaseMixin, TestCase):
|
||||
url_name = "cof.membres_export"
|
||||
url_expected = "/export/members"
|
||||
|
||||
|
@ -482,70 +482,27 @@ class ExportMembersViewTests(ViewTestCaseMixin, TestCase):
|
|||
r = self.client.get(self.url)
|
||||
|
||||
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),
|
||||
"member",
|
||||
"first",
|
||||
"last",
|
||||
"user@mail.net",
|
||||
"0123456789",
|
||||
"1A",
|
||||
"Dept",
|
||||
"normalien",
|
||||
[
|
||||
str(u1.pk),
|
||||
"member",
|
||||
"first",
|
||||
"last",
|
||||
"user@mail.net",
|
||||
"0123456789",
|
||||
"1A",
|
||||
"Dept",
|
||||
"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:
|
||||
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):
|
||||
class ExportMegaViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase):
|
||||
url_name = "cof.mega_export"
|
||||
url_expected = "/export/mega"
|
||||
|
||||
|
@ -556,8 +513,8 @@ class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
|||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertListEqual(
|
||||
self.load_from_csv_response(r),
|
||||
self.assertCSVEqual(
|
||||
r,
|
||||
[
|
||||
[
|
||||
"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_expected = "/export/mega/orgas"
|
||||
|
||||
|
@ -586,8 +543,8 @@ class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
|||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertListEqual(
|
||||
self.load_from_csv_response(r),
|
||||
self.assertCSVEqual(
|
||||
r,
|
||||
[
|
||||
[
|
||||
"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_expected = "/export/mega/participants"
|
||||
|
||||
|
@ -614,13 +571,12 @@ class ExportMegaParticipantsViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
|||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertListEqual(
|
||||
self.load_from_csv_response(r),
|
||||
[["u2", "", "", "", "", str(self.u2.pk), "", ""]],
|
||||
self.assertCSVEqual(
|
||||
r, [["u2", "", "", "", "", str(self.u2.pk), "", ""]],
|
||||
)
|
||||
|
||||
|
||||
class ExportMegaRemarksViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||
class ExportMegaRemarksViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase):
|
||||
url_name = "cof.mega_export_remarks"
|
||||
url_expected = "/export/mega/avecremarques"
|
||||
|
||||
|
@ -631,8 +587,8 @@ class ExportMegaRemarksViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
|||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertListEqual(
|
||||
self.load_from_csv_response(r),
|
||||
self.assertCSVEqual(
|
||||
r,
|
||||
[
|
||||
[
|
||||
"u1",
|
||||
|
@ -815,7 +771,7 @@ class CalendarViewTests(ViewTestCaseMixin, TestCase):
|
|||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class CalendarICSViewTests(ViewTestCaseMixin, TestCase):
|
||||
class CalendarICSViewTests(ICalMixin, ViewTestCaseMixin, TestCase):
|
||||
url_name = "calendar.ics"
|
||||
|
||||
auth_user = None
|
||||
|
|
|
@ -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"),
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
from django.contrib import messages
|
||||
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 gestioncof.tests.testcases import ViewTestCaseMixin
|
||||
from gestioncof.tests.mixins import ViewTestCaseMixin
|
||||
|
||||
from .utils import (
|
||||
PetitCoursTestHelpers,
|
|
@ -13,6 +13,130 @@ from django.utils.functional import cached_property
|
|||
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:
|
||||
def assertForbidden(self, response):
|
||||
"""
|
||||
|
@ -91,140 +215,69 @@ class TestCaseMixin:
|
|||
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(
|
||||
"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):
|
||||
"""
|
||||
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 là aussi un dict <label: User instance>.
|
||||
|
||||
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.
|
||||
Misc QoL
|
||||
------------------------
|
||||
Pour éviter une erreur de login (puisque les messages de Django ne sont pas
|
||||
disponibles), les messages de bienvenue de GestioCOF sont patchés.
|
||||
Un attribut `self.now` est fixé au démarrage, pour être donné comme valeur
|
||||
de retour à un patch local de `django.utils.timezone.now`. Cela permet de
|
||||
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,
|
||||
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).
|
||||
# Usage avancé
|
||||
On peut tester plusieurs URLs pour une même vue, en redéfinissant la fonction
|
||||
`urls_conf()`. Cette fonction doit retourner une liste de dicts, avec les clés
|
||||
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
|
||||
--------------
|
||||
|
||||
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'.
|
||||
Test de restrictions d'accès
|
||||
------------------------
|
||||
L'utilitaire vérifie automatiquement que certains utilisateurs n'ont pas accès à la
|
||||
vue. Plus spécifiquement, sont testés toutes les méthodes dans `self.http_methods`
|
||||
et tous les utilisateurs dans `self.auth_forbidden`. Pour rappel, l'utilisateur
|
||||
`None` sert à tester la vue sans authentification.
|
||||
On peut donner des paramètres GET/POST/etc. aux tests en définissant un attribut
|
||||
<methode>_data.
|
||||
|
||||
TODO (?): faire pareil pour vérifier les GET/POST classiques (code 200)
|
||||
"""
|
||||
|
||||
url_name = None
|
||||
|
@ -239,19 +292,13 @@ class ViewTestCaseMixin(TestCaseMixin):
|
|||
"""
|
||||
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():
|
||||
|
@ -322,7 +369,7 @@ class ViewTestCaseMixin(TestCaseMixin):
|
|||
]
|
||||
|
||||
@property
|
||||
def t_urls(self):
|
||||
def reversed_urls(self):
|
||||
return [
|
||||
reverse(
|
||||
url_conf["name"],
|
||||
|
@ -335,16 +382,16 @@ class ViewTestCaseMixin(TestCaseMixin):
|
|||
|
||||
@property
|
||||
def url(self):
|
||||
return self.t_urls[0]
|
||||
return self.reversed_urls[0]
|
||||
|
||||
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"])
|
||||
|
||||
def test_forbidden(self):
|
||||
for method in self.http_methods:
|
||||
for user in self.auth_forbidden:
|
||||
for url in self.t_urls:
|
||||
for url in self.reversed_urls:
|
||||
self.check_forbidden(method, url, user)
|
||||
|
||||
def check_forbidden(self, method, url, user=None):
|
Loading…
Reference in a new issue