From e9b901337e0d7f547ba25848d8957c8f34f660d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Jan 2018 12:13:15 +0100 Subject: [PATCH 1/5] A few tests for BdA views --- bda/tests.py | 105 ------------------- bda/tests/__init__.py | 0 bda/tests/test_views.py | 222 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+), 105 deletions(-) delete mode 100644 bda/tests.py create mode 100644 bda/tests/__init__.py create mode 100644 bda/tests/test_views.py diff --git a/bda/tests.py b/bda/tests.py deleted file mode 100644 index 97a220c9..00000000 --- a/bda/tests.py +++ /dev/null @@ -1,105 +0,0 @@ -import json - -from django.contrib.auth.models import User -from django.test import TestCase, Client -from django.utils import timezone - -from .models import Tirage, Spectacle, Salle, CategorieSpectacle - - -class TestBdAViews(TestCase): - def setUp(self): - self.tirage = Tirage.objects.create( - title="Test tirage", - appear_catalogue=True, - ouverture=timezone.now(), - fermeture=timezone.now(), - ) - self.category = CategorieSpectacle.objects.create(name="Category") - self.location = Salle.objects.create(name="here") - Spectacle.objects.bulk_create([ - Spectacle( - title="foo", date=timezone.now(), location=self.location, - price=0, slots=42, tirage=self.tirage, listing=False, - category=self.category - ), - Spectacle( - title="bar", date=timezone.now(), location=self.location, - price=1, slots=142, tirage=self.tirage, listing=False, - category=self.category - ), - Spectacle( - title="baz", date=timezone.now(), location=self.location, - price=2, slots=242, tirage=self.tirage, listing=False, - category=self.category - ), - ]) - - self.bda_user = User.objects.create_user( - username="bda_user", password="bda4ever" - ) - self.bda_user.profile.is_cof = True - self.bda_user.profile.is_buro = True - self.bda_user.profile.save() - - def bda_participants(self): - """The BdA participants views can be queried""" - client = Client() - show = self.tirage.spectacle_set.first() - - client.login(self.bda_user.username, "bda4ever") - tirage_resp = client.get("/bda/spectacles/{}".format(self.tirage.id)) - show_resp = client.get( - "/bda/spectacles/{}/{}".format(self.tirage.id, show.id) - ) - reminder_url = "/bda/mails-rappel/{}".format(show.id) - reminder_get_resp = client.get(reminder_url) - reminder_post_resp = client.post(reminder_url) - self.assertEqual(200, tirage_resp.status_code) - self.assertEqual(200, show_resp.status_code) - self.assertEqual(200, reminder_get_resp.status_code) - self.assertEqual(200, reminder_post_resp.status_code) - - def test_catalogue(self): - """Test the catalogue JSON API""" - client = Client() - - # The `list` hook - resp = client.get("/bda/catalogue/list") - self.assertJSONEqual( - resp.content.decode("utf-8"), - [{"id": self.tirage.id, "title": self.tirage.title}] - ) - - # The `details` hook - resp = client.get( - "/bda/catalogue/details?id={}".format(self.tirage.id) - ) - self.assertJSONEqual( - resp.content.decode("utf-8"), - { - "categories": [{ - "id": self.category.id, - "name": self.category.name - }], - "locations": [{ - "id": self.location.id, - "name": self.location.name - }], - } - ) - - # The `descriptions` hook - resp = client.get( - "/bda/catalogue/descriptions?id={}".format(self.tirage.id) - ) - raw = resp.content.decode("utf-8") - try: - results = json.loads(raw) - except ValueError: - self.fail("Not valid JSON: {}".format(raw)) - self.assertEqual(len(results), 3) - self.assertEqual( - {(s["title"], s["price"], s["slots"]) for s in results}, - {("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)} - ) diff --git a/bda/tests/__init__.py b/bda/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py new file mode 100644 index 00000000..125d5d57 --- /dev/null +++ b/bda/tests/test_views.py @@ -0,0 +1,222 @@ +import json + +from datetime import timedelta +from unittest import mock + +from django.contrib.auth.models import User +from django.template.defaultfilters import urlencode +from django.test import TestCase, Client +from django.utils import timezone + +from ..models import Tirage, Spectacle, Salle, CategorieSpectacle + + +def create_user(username, is_cof=False, is_buro=False): + user = User.objects.create_user(username=username, password=username) + user.profile.is_cof = is_cof + user.profile.is_buro = is_buro + user.profile.save() + return user + + +def user_is_cof(user): + return (user is not None) and user.profile.is_cof + + +def user_is_staff(user): + return (user is not None) and user.profile.is_buro + + +class BdATestHelpers: + def setUp(self): + # Some user with different access privileges + staff = create_user(username="bda_staff", is_cof=True, is_buro=True) + staff_c = Client() + staff_c.force_login(staff) + + member = create_user(username="bda_member", is_cof=True) + member_c = Client() + member_c.force_login(member) + + other = create_user(username="bda_other") + other_c = Client() + other_c.force_login(other) + + self.client_matrix = [ + (staff, staff_c), + (member, member_c), + (other, other_c), + (None, Client()) + ] + + def check_restricted_access(self, url, validate_user=user_is_cof, redirect_url=None): + def craft_redirect_url(user): + if redirect_url: + return redirect_url + elif user is None: + # client is not logged in + return "/login?next={}".format(urlencode(urlencode(url))) + else: + return "/" + + for (user, client) in self.client_matrix: + resp = client.get(url, follow=True) + if validate_user(user): + self.assertEqual(200, resp.status_code) + else: + self.assertRedirects(resp, craft_redirect_url(user)) + + +class TestBdAViews(BdATestHelpers, TestCase): + def setUp(self): + # 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) + # Set up the helpers + BdATestHelpers.setUp(self) + # Some BdA stuff + self.tirage = Tirage.objects.create( + title="Test tirage", + appear_catalogue=True, + ouverture=timezone.now(), + fermeture=timezone.now(), + ) + self.category = CategorieSpectacle.objects.create(name="Category") + self.location = Salle.objects.create(name="here") + Spectacle.objects.bulk_create([ + Spectacle( + title="foo", date=timezone.now(), location=self.location, + price=0, slots=42, tirage=self.tirage, listing=False, + category=self.category + ), + Spectacle( + title="bar", date=timezone.now(), location=self.location, + price=1, slots=142, tirage=self.tirage, listing=False, + category=self.category + ), + Spectacle( + title="baz", date=timezone.now(), location=self.location, + price=2, slots=242, tirage=self.tirage, listing=False, + category=self.category + ), + ]) + + def test_bda_inscriptions(self): + # TODO: test the form + url = "/bda/inscription/{}".format(self.tirage.id) + self.check_restricted_access(url) + + def test_bda_places(self): + url = "/bda/places/{}".format(self.tirage.id) + self.check_restricted_access(url) + + def test_etat_places(self): + url = "/bda/etat-places/{}".format(self.tirage.id) + self.check_restricted_access(url) + + def test_perform_tirage(self): + # Only staff member can perform a tirage + url = "/bda/tirage/{}".format(self.tirage.id) + self.check_restricted_access(url, validate_user=user_is_staff) + + _, staff_c = self.client_matrix[0] + # Cannot be performed if disabled + self.tirage.enable_do_tirage = False + self.tirage.save() + resp = staff_c.get(url) + self.assertTemplateUsed(resp, "tirage-failed.html") + # Cannot be performed if registrations are still open + self.tirage.enable_do_tirage = True + self.tirage.fermeture = timezone.now() + timedelta(seconds=3600) + self.tirage.save() + resp = staff_c.get(url) + self.assertTemplateUsed(resp, "tirage-failed.html") + # Otherwise, perform the tirage + self.tirage.fermeture = timezone.now() + self.tirage.save() + resp = staff_c.get(url) + self.assertTemplateNotUsed(resp, "tirage-failed.html") + + def test_spectacles_list(self): + url = "/bda/spectacles/{}".format(self.tirage.id) + self.check_restricted_access(url, validate_user=user_is_staff) + + def test_spectacle_detail(self): + show = self.tirage.spectacle_set.first() + url = "/bda/spectacles/{}/{}".format(self.tirage.id, show.id) + self.check_restricted_access(url, validate_user=user_is_staff) + + def test_tirage_unpaid(self): + url = "/bda/spectacles/unpaid/{}".format(self.tirage.id) + self.check_restricted_access(url, validate_user=user_is_staff) + + def test_send_reminders(self): + # Just get the page + url = "/bda/mails-rappel/{}".format(self.tirage.id) + self.check_restricted_access(url, validate_user=user_is_staff) + # Actually send the reminder emails + # TODO: first load the emails into the database + _, staff_c = self.client_matrix[0] + resp = staff_c.post(url) + self.assertEqual(200, resp.status_code) + # TODO: check that emails are sent + + def test_catalogue_api(self): + url_list = "/bda/catalogue/list" + url_details = "/bda/catalogue/details?id={}".format(self.tirage.id) + url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id) + + # Anyone can get + def anyone_can_get(url): + self.check_restricted_access(url, validate_user=lambda user: True) + + anyone_can_get(url_list) + anyone_can_get(url_details) + anyone_can_get(url_descriptions) + + # The resulting JSON contains the information + _, client = self.client_matrix[0] + + # List + resp = client.get(url_list) + self.assertJSONEqual( + resp.content.decode("utf-8"), + [{"id": self.tirage.id, "title": self.tirage.title}] + ) + + # Details + resp = client.get(url_details) + self.assertJSONEqual( + resp.content.decode("utf-8"), + { + "categories": [{ + "id": self.category.id, + "name": self.category.name + }], + "locations": [{ + "id": self.location.id, + "name": self.location.name + }], + } + ) + + # Descriptions + resp = client.get(url_descriptions) + raw = resp.content.decode("utf-8") + try: + results = json.loads(raw) + except ValueError: + self.fail("Not valid JSON: {}".format(raw)) + self.assertEqual(len(results), 3) + self.assertEqual( + {(s["title"], s["price"], s["slots"]) for s in results}, + {("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)} + ) + + +class TestBdaRevente: + pass + # TODO From c80e63415b0e550aff236d5a2e0861e46a6cc8bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Jan 2018 14:30:33 +0100 Subject: [PATCH 2/5] Load custommails before bda tests --- bda/tests/test_views.py | 6 +- gestioncof/management/commands/syncmails.py | 127 ++++++++++---------- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 125d5d57..038297d8 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -49,6 +49,10 @@ class BdATestHelpers: (None, Client()) ] + def require_custommails(self): + from gestioncof.management.commands import syncmails + syncmails.load_from_file() + def check_restricted_access(self, url, validate_user=user_is_cof, redirect_url=None): def craft_redirect_url(user): if redirect_url: @@ -154,11 +158,11 @@ class TestBdAViews(BdATestHelpers, TestCase): self.check_restricted_access(url, validate_user=user_is_staff) def test_send_reminders(self): + self.require_custommails() # Just get the page url = "/bda/mails-rappel/{}".format(self.tirage.id) self.check_restricted_access(url, validate_user=user_is_staff) # Actually send the reminder emails - # TODO: first load the emails into the database _, staff_c = self.client_matrix[0] resp = staff_c.post(url) self.assertEqual(200, resp.status_code) diff --git a/gestioncof/management/commands/syncmails.py b/gestioncof/management/commands/syncmails.py index 1d3dddb8..0308a949 100644 --- a/gestioncof/management/commands/syncmails.py +++ b/gestioncof/management/commands/syncmails.py @@ -11,6 +11,70 @@ from django.core.management.base import BaseCommand from django.contrib.contenttypes.models import ContentType +DATA_LOCATION = os.path.join(os.path.dirname(__file__), "..", "data", "custommail.json") + + +def dummy_log(__): + pass + + +# XXX. this should probably be in the custommail package +def load_from_file(log=dummy_log): + with open(DATA_LOCATION, 'r') as jsonfile: + mail_data = json.load(jsonfile) + + # On se souvient à quel objet correspond quel pk du json + assoc = {'types': {}, 'mails': {}} + status = {'synced': 0, 'unchanged': 0} + + for obj in mail_data: + fields = obj['fields'] + + # Pour les trois types d'objets : + # - On récupère les objets référencés par les clefs étrangères + # - On crée l'objet si nécessaire + # - On le stocke éventuellement dans les deux dictionnaires définis + # plus haut + + # Variable types + if obj['model'] == 'custommail.variabletype': + fields['inner1'] = assoc['types'].get(fields['inner1']) + fields['inner2'] = assoc['types'].get(fields['inner2']) + if fields['kind'] == 'model': + fields['content_type'] = ( + ContentType.objects + .get_by_natural_key(*fields['content_type']) + ) + var_type, _ = Type.objects.get_or_create(**fields) + assoc['types'][obj['pk']] = var_type + + # Custom mails + if obj['model'] == 'custommail.custommail': + mail = None + try: + mail = CustomMail.objects.get(shortname=fields['shortname']) + status['unchanged'] += 1 + except CustomMail.DoesNotExist: + mail = CustomMail.objects.create(**fields) + status['synced'] += 1 + log('SYNCED {:s}'.format(fields['shortname'])) + assoc['mails'][obj['pk']] = mail + + # Variables + if obj['model'] == 'custommail.custommailvariable': + fields['custommail'] = assoc['mails'].get(fields['custommail']) + fields['type'] = assoc['types'].get(fields['type']) + try: + Variable.objects.get( + custommail=fields['custommail'], + name=fields['name'] + ) + except Variable.DoesNotExist: + Variable.objects.create(**fields) + + log('{synced:d} mails synchronized {unchanged:d} unchanged'.format(**status)) + + class Command(BaseCommand): help = ("Va chercher les données mails de GestioCOF stocké au format json " "dans /gestioncof/management/data/custommails.json. Le format des " @@ -22,65 +86,4 @@ class Command(BaseCommand): "remplacer par le nouveau résultat de la commande précédente.") def handle(self, *args, **options): - path = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - 'data', 'custommail.json') - with open(path, 'r') as jsonfile: - mail_data = json.load(jsonfile) - - # On se souvient à quel objet correspond quel pk du json - assoc = {'types': {}, 'mails': {}} - status = {'synced': 0, 'unchanged': 0} - - for obj in mail_data: - fields = obj['fields'] - - # Pour les trois types d'objets : - # - On récupère les objets référencés par les clefs étrangères - # - On crée l'objet si nécessaire - # - On le stocke éventuellement dans les deux dictionnaires définis - # plus haut - - # Variable types - if obj['model'] == 'custommail.variabletype': - fields['inner1'] = assoc['types'].get(fields['inner1']) - fields['inner2'] = assoc['types'].get(fields['inner2']) - if fields['kind'] == 'model': - fields['content_type'] = ( - ContentType.objects - .get_by_natural_key(*fields['content_type']) - ) - var_type, _ = Type.objects.get_or_create(**fields) - assoc['types'][obj['pk']] = var_type - - # Custom mails - if obj['model'] == 'custommail.custommail': - mail = None - try: - mail = CustomMail.objects.get( - shortname=fields['shortname']) - status['unchanged'] += 1 - except CustomMail.DoesNotExist: - mail = CustomMail.objects.create(**fields) - status['synced'] += 1 - self.stdout.write( - 'SYNCED {:s}'.format(fields['shortname'])) - assoc['mails'][obj['pk']] = mail - - # Variables - if obj['model'] == 'custommail.custommailvariable': - fields['custommail'] = assoc['mails'].get(fields['custommail']) - fields['type'] = assoc['types'].get(fields['type']) - try: - Variable.objects.get( - custommail=fields['custommail'], - name=fields['name'] - ) - except Variable.DoesNotExist: - Variable.objects.create(**fields) - - # C'est agréable d'avoir le résultat affiché - self.stdout.write( - '{synced:d} mails synchronized {unchanged:d} unchanged' - .format(**status) - ) + load_from_file(log=self.stdout.write) From 5086128f16e801a0ff0e0cf186f8e511f4611ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Jan 2018 14:43:54 +0100 Subject: [PATCH 3/5] Fix ill-formed url in tests --- bda/tests/test_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 038297d8..3ee46792 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -160,7 +160,8 @@ class TestBdAViews(BdATestHelpers, TestCase): def test_send_reminders(self): self.require_custommails() # Just get the page - url = "/bda/mails-rappel/{}".format(self.tirage.id) + show = self.tirage.spectacle_set.first() + url = "/bda/mails-rappel/{}".format(show.id) self.check_restricted_access(url, validate_user=user_is_staff) # Actually send the reminder emails _, staff_c = self.client_matrix[0] From d57c75d2a0b8ac2830e58f9d3194ae1967c6d788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 5 Feb 2018 22:35:35 +0100 Subject: [PATCH 4/5] Minor simplificatons after code review --- bda/tests/test_views.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 3ee46792..b0846fd7 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -50,10 +50,12 @@ class BdATestHelpers: ] def require_custommails(self): - from gestioncof.management.commands import syncmails - syncmails.load_from_file() + from django.core.management import call_command + call_command("syncmails") - def check_restricted_access(self, url, validate_user=user_is_cof, redirect_url=None): + def check_restricted_access(self, url, + validate_user=user_is_cof, + redirect_url=None): def craft_redirect_url(user): if redirect_url: return redirect_url @@ -73,14 +75,14 @@ class BdATestHelpers: class TestBdAViews(BdATestHelpers, TestCase): def setUp(self): - # Signals handlers on login/logout send messages. + # 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) # Set up the helpers - BdATestHelpers.setUp(self) + super().setUp() # Some BdA stuff self.tirage = Tirage.objects.create( title="Test tirage", From 6858df02be7772b06d04199406f32eee60ad71c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 30 Sep 2018 13:22:22 +0200 Subject: [PATCH 5/5] bda.tests -- Use urllib urlencode --- bda/tests/test_views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index b0846fd7..39c11c79 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -2,9 +2,9 @@ import json from datetime import timedelta from unittest import mock +from urllib.parse import urlencode from django.contrib.auth.models import User -from django.template.defaultfilters import urlencode from django.test import TestCase, Client from django.utils import timezone @@ -61,7 +61,11 @@ class BdATestHelpers: return redirect_url elif user is None: # client is not logged in - return "/login?next={}".format(urlencode(urlencode(url))) + login_url = "/login" + if url: + login_url += "?{}".format(urlencode({"next": url}, + safe="/")) + return login_url else: return "/"