Skip to content

Commit

Permalink
feat(User): PRWLR-4988 Make users' email case insensitive (#56)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
vicferpoy authored Oct 17, 2024
1 parent 6d69a19 commit a8825c3
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 19 deletions.
3 changes: 3 additions & 0 deletions src/backend/api/db_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
7 changes: 6 additions & 1 deletion src/backend/api/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
7 changes: 6 additions & 1 deletion src/backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"

Expand Down
29 changes: 12 additions & 17 deletions src/backend/api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/backend/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit a8825c3

Please # to comment.