From f642b218d0e24f57fbdb69e1aa9bbb6fe57c2242 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:44:02 +0200 Subject: [PATCH 01/11] Consistance dans les noms de fichiers --- petitscours/tests/{test_petitscours_views.py => test_views.py} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename petitscours/tests/{test_petitscours_views.py => test_views.py} (99%) diff --git a/petitscours/tests/test_petitscours_views.py b/petitscours/tests/test_views.py similarity index 99% rename from petitscours/tests/test_petitscours_views.py rename to petitscours/tests/test_views.py index 9a3cc3dc..fed8f0a0 100644 --- a/petitscours/tests/test_petitscours_views.py +++ b/petitscours/tests/test_views.py @@ -1,9 +1,8 @@ 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 bbe831a2269a813eb230ba9b581f82ce649bc503 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:54:21 +0200 Subject: [PATCH 02/11] =?UTF-8?q?S=C3=A9pare=20un=20gros=20fourre-tout=20e?= =?UTF-8?q?n=20plus=20petits=20mixins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/tests/test_views.py | 5 +- gestioncof/tests/testcases.py | 5 +- shared/tests/testcases.py | 169 ++++++++++++++++++++------------- 3 files changed, 112 insertions(+), 67 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index f757b4c2..7a21fafe 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -17,6 +17,7 @@ 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 shared.tests.testcases import ICalMixin, MockLDAPMixin from shared.views.autocomplete import Clipper from .utils import create_member, create_root, create_user @@ -267,7 +268,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" @@ -815,7 +816,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 diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/testcases.py index 43f69bbc..6da8a28f 100644 --- a/gestioncof/tests/testcases.py +++ b/gestioncof/tests/testcases.py @@ -1,4 +1,7 @@ -from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin +from shared.tests.testcases import ( + CSVResponseMixin, + ViewTestCaseMixin as BaseViewTestCaseMixin, +) from .utils import create_member, create_staff, create_user diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 507e1361..65725af2 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -13,6 +13,111 @@ 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: + reader_class = csv.DictReader + else: + reader_class = csv.reader + + return list(reader_class(content, **reader_kwargs)) + + +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 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) + + class TestCaseMixin: def assertForbidden(self, response): """ @@ -91,70 +196,6 @@ 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): From 88c9187e2eb2dba79e48d67b997a2bbde0deca8e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:56:45 +0200 Subject: [PATCH 03/11] MegaHelpers devient un mixin --- gestioncof/tests/test_views.py | 51 ++++------------------------------ gestioncof/tests/testcases.py | 47 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 7a21fafe..37f105fd 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -16,7 +16,7 @@ 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.testcases import MegaHelperMixin, ViewTestCaseMixin from shared.tests.testcases import ICalMixin, MockLDAPMixin from shared.views.autocomplete import Clipper @@ -505,48 +505,7 @@ class ExportMembersViewTests(ViewTestCaseMixin, TestCase): 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" @@ -575,7 +534,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" @@ -604,7 +563,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" @@ -621,7 +580,7 @@ class ExportMegaParticipantsViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): ) -class ExportMegaRemarksViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): +class ExportMegaRemarksViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): url_name = "cof.mega_export_remarks" url_expected = "/export/mega/avecremarques" diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/testcases.py index 6da8a28f..2c6cbb9d 100644 --- a/gestioncof/tests/testcases.py +++ b/gestioncof/tests/testcases.py @@ -1,3 +1,4 @@ +from gestioncof.models import Event from shared.tests.testcases import ( CSVResponseMixin, ViewTestCaseMixin as BaseViewTestCaseMixin, @@ -6,6 +7,52 @@ from shared.tests.testcases import ( 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. From b1c69eddb56974f4cc0573aa024107a8403af2b9 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:58:13 +0200 Subject: [PATCH 04/11] =?UTF-8?q?Meilleure=20doc=20(j'esp=C3=A8re=20!)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/tests/testcases.py | 113 +++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 65725af2..ae0eeb02 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -197,75 +197,68 @@ class TestCaseMixin: self.assertEqual(actual, expected) - 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 . - 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 '_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 + _data. + TODO (?): faire pareil pour vérifier les GET/POST classiques (code 200) """ url_name = None @@ -280,19 +273,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(): From bb72a16b6427c7c205123e1e1fb36491591368f2 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:58:46 +0200 Subject: [PATCH 05/11] =?UTF-8?q?Lisibilit=C3=A9:=20t=5Furls=20->=20revers?= =?UTF-8?q?ed=5Furls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/tests/test_views.py | 6 +++--- shared/tests/testcases.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 37f105fd..c5cb49b7 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -228,7 +228,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) @@ -241,7 +241,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) @@ -253,7 +253,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) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index ae0eeb02..2a1960fb 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -350,7 +350,7 @@ class ViewTestCaseMixin(TestCaseMixin): ] @property - def t_urls(self): + def reversed_urls(self): return [ reverse( url_conf["name"], @@ -363,16 +363,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): From 3b43ad84b572dc51dd1d7e1826e00c3e5698836d Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 11 May 2020 00:19:43 +0200 Subject: [PATCH 06/11] Renomme testcases.py -> mixins.py --- bda/tests/{testcases.py => mixins.py} | 2 +- bda/tests/test_views.py | 2 +- gestioncof/tests/{testcases.py => mixins.py} | 2 +- gestioncof/tests/test_views.py | 4 ++-- petitscours/tests/test_views.py | 2 +- shared/tests/{testcases.py => mixins.py} | 0 6 files changed, 6 insertions(+), 6 deletions(-) rename bda/tests/{testcases.py => mixins.py} (97%) rename gestioncof/tests/{testcases.py => mixins.py} (98%) rename shared/tests/{testcases.py => mixins.py} (100%) diff --git a/bda/tests/testcases.py b/bda/tests/mixins.py similarity index 97% rename from bda/tests/testcases.py rename to bda/tests/mixins.py index f5ac7f83..a4ba057b 100644 --- a/bda/tests/testcases.py +++ b/bda/tests/mixins.py @@ -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 diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index d13fcf6c..7082725c 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -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() diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/mixins.py similarity index 98% rename from gestioncof/tests/testcases.py rename to gestioncof/tests/mixins.py index 2c6cbb9d..5c8d767a 100644 --- a/gestioncof/tests/testcases.py +++ b/gestioncof/tests/mixins.py @@ -1,5 +1,5 @@ from gestioncof.models import Event -from shared.tests.testcases import ( +from shared.tests.mixins import ( CSVResponseMixin, ViewTestCaseMixin as BaseViewTestCaseMixin, ) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index c5cb49b7..e33fce03 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -16,8 +16,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 MegaHelperMixin, ViewTestCaseMixin -from shared.tests.testcases import ICalMixin, MockLDAPMixin +from gestioncof.tests.mixins import MegaHelperMixin, ViewTestCaseMixin +from shared.tests.mixins import ICalMixin, MockLDAPMixin from shared.views.autocomplete import Clipper from .utils import create_member, create_root, create_user diff --git a/petitscours/tests/test_views.py b/petitscours/tests/test_views.py index fed8f0a0..3ef68a5a 100644 --- a/petitscours/tests/test_views.py +++ b/petitscours/tests/test_views.py @@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model 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, diff --git a/shared/tests/testcases.py b/shared/tests/mixins.py similarity index 100% rename from shared/tests/testcases.py rename to shared/tests/mixins.py From 65171d1276484bfb6c707be1b9f73e9491697203 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 11 May 2020 01:16:58 +0200 Subject: [PATCH 07/11] Fix event tests --- events/tests/test_views.py | 54 ++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index ee17128b..1ccd3530 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -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,26 @@ 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) + + content = self.load_from_csv_response(response, as_dict=True) + self.assertListEqual( + content, + [ + { + "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 +140,22 @@ 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] + response = self.client.get(self.url) + content = self.load_from_csv_response(response, as_dict=True) + toto_dict = content[0] # This is not super nice, but it makes the test deterministic. - if toto_registration[5] == "f & d": - toto_registration[5] = "d & f" + toto_dict["def"] = [x.strip() for x in toto_dict["def"].split("&")] - self.assertEqual( - ["toto_foo", "toto@a.b", "toto", "foo", "a", "d & f", "hello"], - toto_registration, + self.assertDictEqual( + toto_dict, + { + "username": "toto_foo", + "prénom": "toto", + "nom de famille": "foo", + "email": "toto@a.b", + "abc": "a", + "def": ["d", "f"], + "remarks": "hello", + }, ) From 50266f2466d72cd175e771c8a3ddd1754c3e15cd Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 11 May 2020 12:44:14 +0200 Subject: [PATCH 08/11] Fix tests for python3.7 (?) --- events/tests/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 1ccd3530..7e9b0c77 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -142,10 +142,10 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): response = self.client.get(self.url) content = self.load_from_csv_response(response, as_dict=True) - toto_dict = content[0] + toto_dict = dict(content[0]) # This is not super nice, but it makes the test deterministic. - toto_dict["def"] = [x.strip() for x in toto_dict["def"].split("&")] + toto_dict["def"] = set(x.strip() for x in toto_dict["def"].split("&")) self.assertDictEqual( toto_dict, @@ -155,7 +155,7 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): "nom de famille": "foo", "email": "toto@a.b", "abc": "a", - "def": ["d", "f"], + "def": {"d", "f"}, "remarks": "hello", }, ) From 9b0440429c435084e5b61dbb7795d092a622b720 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 12 May 2020 00:47:48 +0200 Subject: [PATCH 09/11] Fix ical tests --- shared/tests/mixins.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/shared/tests/mixins.py b/shared/tests/mixins.py index 2a1960fb..235940df 100644 --- a/shared/tests/mixins.py +++ b/shared/tests/mixins.py @@ -101,8 +101,8 @@ class ICalMixin: 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 + return i + return None def assertCalEqual(self, ical_content, expected): remaining = expected.copy() @@ -111,12 +111,15 @@ class ICalMixin: cal = icalendar.Calendar.from_ical(ical_content) for ev in cal.walk("vevent"): - found, i_found = self._find_event(ev, remaining) - if found: + 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): From 6fff995ccdcc66210a63850cedba585e0d75532b Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 12 May 2020 01:11:59 +0200 Subject: [PATCH 10/11] Expand CSVResponseMixin functionality --- events/tests/test_views.py | 7 ++--- gestioncof/tests/test_views.py | 56 ++++++++++++++++------------------ shared/tests/mixins.py | 24 ++++++++++++--- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 7e9b0c77..a8b4ba4a 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -92,9 +92,8 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): response = self.client.get(self.url) - content = self.load_from_csv_response(response, as_dict=True) - self.assertListEqual( - content, + self.assertCSVEqual( + response, [ { "username": "toto_foo", @@ -141,7 +140,7 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): ) response = self.client.get(self.url) - content = self.load_from_csv_response(response, as_dict=True) + content = self._load_from_csv_response(response, as_dict=True) toto_dict = dict(content[0]) # This is not super nice, but it makes the test deterministic. diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index e33fce03..d522a648 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -1,4 +1,3 @@ -import csv import os import uuid from datetime import timedelta @@ -17,7 +16,7 @@ from django.urls import reverse from bda.models import Salle, Tirage from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer from gestioncof.tests.mixins import MegaHelperMixin, ViewTestCaseMixin -from shared.tests.mixins import ICalMixin, MockLDAPMixin +from shared.tests.mixins import CSVResponseMixin, ICalMixin, MockLDAPMixin from shared.views.autocomplete import Clipper from .utils import create_member, create_root, create_user @@ -463,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" @@ -483,26 +482,24 @@ 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 ExportMegaViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): @@ -516,8 +513,8 @@ class ExportMegaViewTests(MegaHelperMixin, 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", @@ -546,8 +543,8 @@ class ExportMegaOrgasViewTests(MegaHelperMixin, 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,9 +571,8 @@ class ExportMegaParticipantsViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCa 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), "", ""]], ) @@ -591,8 +587,8 @@ class ExportMegaRemarksViewTests(MegaHelperMixin, 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", diff --git a/shared/tests/mixins.py b/shared/tests/mixins.py index 235940df..8a00480e 100644 --- a/shared/tests/mixins.py +++ b/shared/tests/mixins.py @@ -58,17 +58,33 @@ class CSVResponseMixin: par la première ligne du CSV. """ - def load_from_csv_response(self, r, as_dict=False, **reader_kwargs): + 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: - reader_class = csv.DictReader + content = csv.DictReader(content, **reader_kwargs) + # en python3.7, content est une liste d'OrderedDicts + return list(map(dict, content)) else: - reader_class = csv.reader + content = csv.reader(content, **reader_kwargs) + return list(content) - return list(reader_class(content, **reader_kwargs)) + 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: From 707b7b76dbecaeca689313d39ca86b7f1ee53e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 14 May 2020 21:23:25 +0200 Subject: [PATCH 11/11] Make events tests deterministic --- events/tests/test_views.py | 30 +++++++++++++----------------- events/views.py | 2 +- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index a8b4ba4a..3e13d8cd 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -140,21 +140,17 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): ) response = self.client.get(self.url) - content = self._load_from_csv_response(response, as_dict=True) - toto_dict = dict(content[0]) - - # This is not super nice, but it makes the test deterministic. - toto_dict["def"] = set(x.strip() for x in toto_dict["def"].split("&")) - - self.assertDictEqual( - toto_dict, - { - "username": "toto_foo", - "prénom": "toto", - "nom de famille": "foo", - "email": "toto@a.b", - "abc": "a", - "def": {"d", "f"}, - "remarks": "hello", - }, + self.assertCSVEqual( + response, + [ + { + "username": "toto_foo", + "prénom": "toto", + "nom de famille": "foo", + "email": "toto@a.b", + "abc": "a", + "def": "d & f", + "remarks": "hello", + } + ], ) diff --git a/events/views.py b/events/views.py index 248c4284..b47ae76f 100644 --- a/events/views.py +++ b/events/views.py @@ -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