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.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
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
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 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
|
||||||
|
|
|
@ -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 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,
|
|
@ -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 là 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):
|
Loading…
Reference in a new issue