diff --git a/apps/member/models.py b/apps/member/models.py
index a1628fae63482abc1c83403fd8b75331ffe4c888..d1218e944e407de6048c78970fba1b82c769c6ef 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 c2f9f1363f1130ac8107374ff46caed121513587..4534c9e8be96b7eeb4c4dd850f4676f522aa99ac 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/apps/registration/tests/test_registration.py b/apps/registration/tests/test_registration.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2191445f3bed1132493d052c88a3c19c469b059
--- /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 bf68a8ed2560b6bd113bf960c512888d4ca7e63b..7a92459162e8ea15cf18299340e0cf7f290a99e3 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',