Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,27 @@ class Meta:
"created_at",
"status",
]


class UpdateEmailSerializer(serializers.Serializer):
"""
Serializer for updating user email address.
"""

email = serializers.EmailField()

def validate_email(self, value):
"""
Validate that the email is properly formatted.
"""

return value.lower()

def save(self):
"""
Update the authenticated user's email address.
"""
user = self.context["user"]
user.email = self.validated_data["email"]
user.save(update_fields=["email"])
return user
102 changes: 102 additions & 0 deletions core/tests/test_update_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import pytest
from django.urls import reverse
from knox.models import AuthToken
from rest_framework import status
from rest_framework.test import APITestCase

from core.models import User
from core.tests.factories import UserFactory


class UpdateEmailAPIViewTest(APITestCase):
"""
Test cases for the UpdateEmailAPIView endpoint.
"""

def setUp(self):
"""Set up test data."""
self.user = UserFactory()
self.url = reverse("update-email")

self.token = AuthToken.objects.create(self.user)[1]
self.auth_header = f"Token {self.token}"

def test_successful_email_update(self):
"""Test successful email update for authenticated user."""
new_email = "newemail@example.com"
data = {"email": new_email}

self.client.credentials(HTTP_AUTHORIZATION=self.auth_header)
response = self.client.post(self.url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["message"], "Email updated successfully")
self.assertEqual(response.data["email"], new_email)

# Verify email was updated in database
self.user.refresh_from_db()
self.assertEqual(self.user.email, new_email)

def test_invalid_email_format(self):
"""Test validation failure for invalid email format."""
invalid_emails = [
"invalid-email",
"invalid@",
"@example.com",
"invalid..email@example.com",
"",
]

self.client.credentials(HTTP_AUTHORIZATION=self.auth_header)

for invalid_email in invalid_emails:
data = {"email": invalid_email}
response = self.client.post(self.url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("email", response.data)

def test_unauthorized_access(self):
"""Test that unauthenticated requests return 401."""
data = {"email": "test@example.com"}

# Make request without authentication
response = self.client.post(self.url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_missing_email_field(self):
"""Test validation failure when email field is missing."""
data = {} # No email field

self.client.credentials(HTTP_AUTHORIZATION=self.auth_header)
response = self.client.post(self.url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("email", response.data)

def test_email_normalization(self):
"""Test that email is normalized to lowercase."""
mixed_case_email = "Test.Email@EXAMPLE.COM"
expected_email = "test.email@example.com"
data = {"email": mixed_case_email}

self.client.credentials(HTTP_AUTHORIZATION=self.auth_header)
response = self.client.post(self.url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["email"], expected_email)

# Verify email was normalized in database
self.user.refresh_from_db()
self.assertEqual(self.user.email, expected_email)

def test_invalid_token(self):
"""Test that invalid tokens return 401."""
data = {"email": "test@example.com"}

# Use invalid token
self.client.credentials(HTTP_AUTHORIZATION="Token invalid-token")
response = self.client.post(self.url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
5 changes: 5 additions & 0 deletions core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@
),
path("offer/create/", views.OfferCreateAPIView.as_view(), name="create-offer"),
path("offer/cancel/", views.OfferCancelAPIView.as_view(), name="cancel-offer"),
path(
"account/update-email/",
views.UpdateEmailAPIView.as_view(),
name="update-email",
),
]
33 changes: 33 additions & 0 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,36 @@ def get_queryset(self):
queryset = queryset.filter(user__public_key__iexact=borrower_address)

return queryset


class UpdateEmailAPIView(GenericAPIView):
"""
API endpoint for authenticated users to update their email address.

Requires authentication via Knox token.
Validates email format and updates the user's email in the database.
"""

serializer_class = serializers.UpdateEmailSerializer
permission_classes = [IsAuthenticated]

def post(self, request):
"""
Update authenticated user's email address.

Returns:
200 OK: Email updated successfully
400 Bad Request: Invalid email format
401 Unauthorized: User not authenticated
"""
serializer = self.serializer_class(
data=request.data, context={"user": request.user}
)
serializer.is_valid(raise_exception=True)

updated_user = serializer.save()

return Response(
{"message": "Email updated successfully", "email": updated_user.email},
status=status.HTTP_200_OK,
)