From a8825c385bcf695c06cae7a495aeb464719d61b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Thu, 17 Oct 2024 19:22:20 +0200 Subject: [PATCH] feat(User): PRWLR-4988 Make users' email case insensitive (#56) * feat(User): PRWLR-4988 make User.email case insensitive * test(User): PRWLR-4988 update unit tests * feat(User): PRWLR-4988 include email validation in serializer --- src/backend/api/db_utils.py | 3 +++ src/backend/api/migrations/0001_initial.py | 7 +++++- src/backend/api/models.py | 7 +++++- src/backend/api/tests/test_views.py | 29 +++++++++------------- src/backend/api/v1/serializers.py | 6 +++++ 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/backend/api/db_utils.py b/src/backend/api/db_utils.py index 186932c5a01..282277322db 100644 --- a/src/backend/api/db_utils.py +++ b/src/backend/api/db_utils.py @@ -58,6 +58,9 @@ def create_user(self, email, password=None, **extra_fields): user.save(using=self._db) return user + def get_by_natural_key(self, email): + return self.get(email__iexact=email) + def enum_to_choices(enum_class): """ diff --git a/src/backend/api/migrations/0001_initial.py b/src/backend/api/migrations/0001_initial.py index ca0caaf830c..13b5a8970ea 100644 --- a/src/backend/api/migrations/0001_initial.py +++ b/src/backend/api/migrations/0001_initial.py @@ -156,7 +156,12 @@ class Migration(migrations.Migration): blank=True, null=True, verbose_name="last login" ), ), - ("email", models.EmailField(max_length=254, unique=True)), + ( + "email", + models.EmailField( + max_length=254, unique=True, help_text="Case insensitive" + ), + ), ("company_name", models.CharField(max_length=150, blank=True)), ("is_active", models.BooleanField(default=True)), ("date_joined", models.DateTimeField(auto_now_add=True)), diff --git a/src/backend/api/models.py b/src/backend/api/models.py index 303cc96aa8f..00b6c499e31 100644 --- a/src/backend/api/models.py +++ b/src/backend/api/models.py @@ -69,7 +69,7 @@ class StateChoices(models.TextChoices): class User(AbstractBaseUser): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) name = models.CharField(max_length=150, validators=[MinLengthValidator(3)]) - email = models.EmailField(max_length=254, unique=True) + email = models.EmailField(max_length=254, unique=True, help_text="Case insensitive") company_name = models.CharField(max_length=150, blank=True) is_active = models.BooleanField(default=True) date_joined = models.DateTimeField(auto_now_add=True, editable=False) @@ -82,6 +82,11 @@ class User(AbstractBaseUser): def is_member_of_tenant(self, tenant_id): return self.memberships.filter(tenant_id=tenant_id).exists() + def save(self, *args, **kwargs): + if self.email: + self.email = self.email.strip().lower() + super().save(*args, **kwargs) + class Meta: db_table = "users" diff --git a/src/backend/api/tests/test_views.py b/src/backend/api/tests/test_views.py index 0312d6aaea6..d230f5c18df 100644 --- a/src/backend/api/tests/test_views.py +++ b/src/backend/api/tests/test_views.py @@ -38,32 +38,27 @@ def test_users_create(self, client): valid_user_payload = { "name": "test", "password": "newpassword123", - "email": "newuser@example.com", + "email": "NeWuSeR@example.com", } response = client.post( reverse("user-list"), data=valid_user_payload, format="json" ) assert response.status_code == status.HTTP_201_CREATED - assert User.objects.filter(email=valid_user_payload["email"]).exists() + assert User.objects.filter(email__iexact=valid_user_payload["email"]).exists() assert ( response.json()["data"]["attributes"]["email"] - == valid_user_payload["email"] + == valid_user_payload["email"].lower() ) - def test_users_invalid_create(self, client): - invalid_user_payload = { - "name": "test", - "password": "thepasswordisfine123", - "email": "invalidemail", - } - response = client.post( - reverse("user-list"), data=invalid_user_payload, format="json" - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert ( - response.json()["errors"][0]["source"]["pointer"] - == "/data/attributes/email" - ) + def test_users_create_duplicated_email(self, client): + # Create a user + self.test_users_create(client) + + # Try to create it again and expect a 400 + with pytest.raises(AssertionError) as assertion_error: + self.test_users_create(client) + + assert "Response status_code=400" in str(assertion_error) @pytest.mark.parametrize( "password", diff --git a/src/backend/api/v1/serializers.py b/src/backend/api/v1/serializers.py index 2423f9d5e1e..ec82401d0d8 100644 --- a/src/backend/api/v1/serializers.py +++ b/src/backend/api/v1/serializers.py @@ -177,6 +177,12 @@ def validate_password(self, value): validate_password(value, user=user) return value + def validate_email(self, value): + normalized_email = value.strip().lower() + if User.objects.filter(email__iexact=normalized_email).exists(): + raise ValidationError("User with this email already exists.") + return value + def create(self, validated_data): password = validated_data.pop("password") user = User(**validated_data)