From f02efd3b39f5478d09a77bb176873c3835c5c92c Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 3 Sep 2020 20:03:40 +0200 Subject: [PATCH] 100% coverage on registration app --- apps/member/models.py | 10 +- apps/member/views.py | 4 +- apps/registration/tests/__init__.py | 0 apps/registration/tests/test_registration.py | 386 +++++++++++++++++++ apps/registration/views.py | 21 +- 5 files changed, 401 insertions(+), 20 deletions(-) create mode 100644 apps/registration/tests/__init__.py create mode 100644 apps/registration/tests/test_registration.py diff --git a/apps/member/models.py b/apps/member/models.py index a1628fae..d1218e94 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -172,19 +172,21 @@ class Profile(models.Model): def send_email_validation_link(self): subject = "[Note Kfet] " + str(_("Activate your Note Kfet account")) + token = email_validation_token.make_token(self.user) + uid = urlsafe_base64_encode(force_bytes(self.user_id)) message = loader.render_to_string('registration/mails/email_validation_email.txt', { 'user': self.user, 'domain': os.getenv("NOTE_URL", "note.example.com"), - 'token': email_validation_token.make_token(self.user), - 'uid': urlsafe_base64_encode(force_bytes(self.user.pk)), + 'token': token, + 'uid': uid, }) html = loader.render_to_string('registration/mails/email_validation_email.html', { 'user': self.user, 'domain': os.getenv("NOTE_URL", "note.example.com"), - 'token': email_validation_token.make_token(self.user), - 'uid': urlsafe_base64_encode(force_bytes(self.user.pk)), + 'token': token, + 'uid': uid, }) self.user.email_user(subject, message, html_message=html) diff --git a/apps/member/views.py b/apps/member/views.py index c2f9f136..4534c9e8 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -140,9 +140,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ We can't display information of a not registered user. """ - qs = super().get_queryset() - return qs if self.request.user.is_superuser and self.request.session.get("permission_mask", -1) >= 42\ - else qs.filter(profile__registration_valid=True) + return super().get_queryset().filter(profile__registration_valid=True) def get_context_data(self, **kwargs): """ diff --git a/apps/registration/tests/__init__.py b/apps/registration/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/registration/tests/test_registration.py b/apps/registration/tests/test_registration.py new file mode 100644 index 00000000..e2191445 --- /dev/null +++ b/apps/registration/tests/test_registration.py @@ -0,0 +1,386 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib.auth.models import User +from django.db.models import Q +from django.test import TestCase +from django.urls import reverse +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode +from member.models import Club, Membership +from note.models import NoteUser, NoteSpecial, Transaction +from registration.tokens import email_validation_token +from treasury.models import SogeCredit + +""" +Check that pre-registrations and validations are working as well. +""" + + +class TestSignup(TestCase): + """ + Assume we are a new user. + Check that it can pre-register without any problem. + """ + + fixtures = ("initial", ) + + def test_signup(self): + """ + A first year member signs up and validates its email address. + """ + response = self.client.get(reverse("registration:signup")) + self.assertEqual(response.status_code, 200) + + # Signup + response = self.client.post(reverse("registration:signup"), dict( + first_name="Toto", + last_name="TOTO", + username="toto", + email="toto@example.com", + password1="toto1234", + password2="toto1234", + phone_number="+33123456789", + department="EXT", + promotion=Club.objects.get(name="BDE").membership_start.year, + address="Earth", + paid=False, + ml_events_registration="en", + ml_sport_registration=True, + ml_art_registration=True, + )) + self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200) + self.assertTrue(User.objects.filter(username="toto").exists()) + user = User.objects.get(username="toto") + # A preregistred user has no note + self.assertFalse(NoteUser.objects.filter(user=user).exists()) + self.assertFalse(user.profile.registration_valid) + self.assertFalse(user.profile.email_confirmed) + self.assertFalse(user.is_active) + + response = self.client.get(reverse("registration:email_validation_sent")) + self.assertEqual(response.status_code, 200) + + # Check that the email validation link is valid + token = email_validation_token.make_token(user) + uid = urlsafe_base64_encode(force_bytes(user.pk)) + response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=uid, token=token))) + self.assertEqual(response.status_code, 200) + user.profile.refresh_from_db() + self.assertTrue(user.profile.email_confirmed) + + # Token has expired + response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=uid, token=token))) + self.assertEqual(response.status_code, 400) + + # Uid does not exist + response = self.client.get(reverse("registration:email_validation", kwargs=dict(uidb64=0, token="toto"))) + self.assertEqual(response.status_code, 400) + + def test_invalid_signup(self): + """ + Send wrong data and check that it is not valid + """ + User.objects.create_superuser( + first_name="Toto", + last_name="TOTO", + username="toto", + email="toto@example.com", + password="toto1234", + ) + + # The email is already used + response = self.client.post(reverse("registration:signup"), dict( + first_name="Toto", + last_name="TOTO", + username="tôtö", + email="toto@example.com", + password1="toto1234", + password2="toto1234", + phone_number="+33123456789", + department="EXT", + promotion=Club.objects.get(name="BDE").membership_start.year, + address="Earth", + paid=False, + ml_events_registration="en", + ml_sport_registration=True, + ml_art_registration=True, + )) + self.assertTrue(response.status_code, 200) + + # The username is similar to a known alias + response = self.client.post(reverse("registration:signup"), dict( + first_name="Toto", + last_name="TOTO", + username="tôtö", + email="othertoto@example.com", + password1="toto1234", + password2="toto1234", + phone_number="+33123456789", + department="EXT", + promotion=Club.objects.get(name="BDE").membership_start.year, + address="Earth", + paid=False, + ml_events_registration="en", + ml_sport_registration=True, + ml_art_registration=True, + )) + self.assertTrue(response.status_code, 200) + + # The phone number is invalid + response = self.client.post(reverse("registration:signup"), dict( + first_name="Toto", + last_name="TOTO", + username="Ihaveanotherusername", + email="othertoto@example.com", + password1="toto1234", + password2="toto1234", + phone_number="invalid phone number", + department="EXT", + promotion=Club.objects.get(name="BDE").membership_start.year, + address="Earth", + paid=False, + ml_events_registration="en", + ml_sport_registration=True, + ml_art_registration=True, + )) + self.assertTrue(response.status_code, 200) + + +class TestValidateRegistration(TestCase): + """ + Test the admin interface to validate users + """ + + fixtures = ('initial',) + + def setUp(self) -> None: + self.superuser = User.objects.create_superuser( + username="admintoto", + password="toto1234", + email="admin.toto@example.com", + ) + self.client.force_login(self.superuser) + + self.user = User.objects.create( + username="toto", + first_name="Toto", + last_name="TOTO", + email="toto@example.com", + ) + + sess = self.client.session + sess["permission_mask"] = 42 + sess.save() + + def test_future_user_list(self): + """ + Display the list of pre-registered users + """ + response = self.client.get(reverse("registration:future_user_list")) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse("registration:future_user_list") + "?search=toto") + self.assertEqual(response.status_code, 200) + + def test_invalid_registrations(self): + """ + Send wrong data and check that errors are detected + """ + + # BDE Membership is mandatory + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=4200, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=False, + join_Kfet=False, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + # Same + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type="", + credit_amount=0, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=False, + join_Kfet=True, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + # The BDE membership is not free + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Espèces").id, + credit_amount=0, + last_name="TOTO", + first_name="Toto", + bank="J'ai pas d'argent", + join_BDE=True, + join_Kfet=True, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + # Last and first names are required for a credit + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=4000, + last_name="", + first_name="", + bank="", + join_BDE=True, + join_Kfet=True, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + # The username admïntoto is too similar with the alias admintoto. + # Since the form is valid, the user must update its username. + self.user.username = "admïntoto" + self.user.save() + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=500, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=True, + join_Kfet=False, + )) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["form"].errors) + + def test_validate_bde_registration(self): + """ + The user wants only to join the BDE. We validate the registration. + """ + response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 404) + + self.user.profile.email_confirmed = True + self.user.profile.save() + + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Chèque").id, + credit_amount=500, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=True, + join_Kfet=False, + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + self.user.profile.refresh_from_db() + self.assertTrue(self.user.profile.registration_valid) + self.assertTrue(NoteUser.objects.filter(user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists()) + self.assertFalse(Membership.objects.filter(club__name="Kfet", user=self.user).exists()) + self.assertFalse(SogeCredit.objects.filter(user=self.user).exists()) + self.assertEqual(Transaction.objects.filter( + Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_validate_kfet_registration(self): + """ + The user joins the BDE and the Kfet. + """ + response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 404) + + self.user.profile.email_confirmed = True + self.user.profile.save() + + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=False, + credit_type=NoteSpecial.objects.get(special_type="Espèces").id, + credit_amount=4000, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=True, + join_Kfet=True, + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + self.user.profile.refresh_from_db() + self.assertTrue(self.user.profile.registration_valid) + self.assertTrue(NoteUser.objects.filter(user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists()) + self.assertFalse(SogeCredit.objects.filter(user=self.user).exists()) + self.assertEqual(Transaction.objects.filter( + Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_validate_kfet_registration_with_soge(self): + """ + The user joins the BDE and the Kfet, but the membership is paid by the Société générale. + """ + response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 404) + + self.user.profile.email_confirmed = True + self.user.profile.save() + + response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict( + soge=True, + credit_type=NoteSpecial.objects.get(special_type="Espèces").id, + credit_amount=4000, + last_name="TOTO", + first_name="Toto", + bank="Société générale", + join_BDE=True, + join_Kfet=True, + )) + self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) + self.user.profile.refresh_from_db() + self.assertTrue(self.user.profile.registration_valid) + self.assertTrue(NoteUser.objects.filter(user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists()) + self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists()) + self.assertTrue(SogeCredit.objects.filter(user=self.user).exists()) + self.assertEqual(Transaction.objects.filter( + Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2) + self.assertFalse(Transaction.objects.filter(valid=True).exists()) + + response = self.client.get(self.user.profile.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_invalidate_registration(self): + """ + Try to invalidate (= delete) pre-registration. + """ + response = self.client.get(reverse("registration:future_user_invalidate", args=(self.user.pk,))) + self.assertRedirects(response, reverse("registration:future_user_list"), 302, 200) + self.assertFalse(User.objects.filter(pk=self.user.pk).exists()) + + def test_resend_email_validation_link(self): + """ + Resend email validation linK. + """ + response = self.client.get(reverse("registration:email_validation_resend", args=(self.user.pk,))) + self.assertRedirects(response, reverse("registration:future_user_detail", args=(self.user.pk,)), 302, 200) diff --git a/apps/registration/views.py b/apps/registration/views.py index bf68a8ed..7a924591 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -16,7 +16,7 @@ from django.views.generic.edit import FormMixin from django_tables2 import SingleTableView from member.forms import ProfileForm from member.models import Membership, Club -from note.models import SpecialTransaction +from note.models import SpecialTransaction, Alias from note.templatetags.pretty_money import pretty_money from permission.backends import PermissionBackend from permission.models import Role @@ -101,7 +101,7 @@ class UserValidateView(TemplateView): user.profile.email_confirmed = True user.save() user.profile.save() - return self.render_to_response(self.get_context_data()) + return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400) def get_user(self, uidb64): """ @@ -169,12 +169,9 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi :return: """ qs = super().get_queryset().distinct().filter(profile__registration_valid=False) - if "search" in self.request.GET: + if "search" in self.request.GET and self.request.GET["search"]: pattern = self.request.GET["search"] - if not pattern: - return qs.none() - qs = qs.filter( Q(first_name__iregex=pattern) | Q(last_name__iregex=pattern) @@ -205,10 +202,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, def post(self, request, *args, **kwargs): form = self.get_form() self.object = self.get_object() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) + return self.form_valid(form) if form.is_valid() else self.form_invalid(form) def get_queryset(self, **kwargs): """ @@ -239,6 +233,10 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, def form_valid(self, form): user = self.get_object() + if Alias.objects.filter(normalized_name=Alias.normalize(user.username)).exists(): + form.add_error(None, _("An alias with a similar name already exists.")) + return self.form_invalid(form) + # Get form data soge = form.cleaned_data["soge"] credit_type = form.cleaned_data["credit_type"] @@ -276,9 +274,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, if credit_type is None: credit_amount = 0 - if join_Kfet and not join_BDE: - form.add_error('join_Kfet', _("You must join BDE club before joining Kfet club.")) - if fee > credit_amount and not soge: # Check if the user credits enough money form.add_error('credit_type',