Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
68 changes: 28 additions & 40 deletions care/users/api/viewsets/change_password.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,52 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as DjangoValidationError
from django.conf import settings
from django.contrib.auth.password_validation import (
get_password_validators,
validate_password,
)
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import serializers, status
from pydantic import BaseModel
from rest_framework import status
from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response

User = get_user_model()


class ChangePasswordSerializer(serializers.Serializer):
"""
Serializer for the change password endpoint.

Validates the new password using Django's built-in password validators.
"""

old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)

def validate_new_password(self, value):
"""
Validate the new password against Django's password policies.
"""
user = self.context["request"].user
try:
validate_password(value, user=user)
Comment thread
nandkishorr marked this conversation as resolved.
except DjangoValidationError as e:
raise serializers.ValidationError(e.messages) from e
return value
class ChangePasswordSpec(BaseModel):
old_password: str
new_password: str


@extend_schema_view(
put=extend_schema(tags=["users"]),
patch=extend_schema(tags=["users"]),
request=ChangePasswordSpec,
)
Comment thread
nandkishorr marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
class ChangePasswordView(UpdateAPIView):
"""
API endpoint for allowing authenticated users to change their password.
"""

serializer_class = ChangePasswordSerializer
model = User

def update(self, request, *args, **kwargs):
"""
Handle password update request for the authenticated user.
"""
self.object = self.request.user
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)

if not self.object.check_password(
serializer.validated_data.get("old_password")
):
data = ChangePasswordSpec(**request.data)
Comment thread
nandkishorr marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if not request.user.check_password(data.old_password):
return Response(
{"old_password": ["Wrong password entered. Please check your password."]},
{
"old_password": [
"Wrong password entered. Please check your password."
]
},
status=status.HTTP_400_BAD_REQUEST,
)

self.object.set_password(serializer.validated_data.get("new_password"))
self.object.save()
validate_password(
data.new_password,
user=request.user,
password_validators=get_password_validators(
settings.AUTH_PASSWORD_VALIDATORS
),
)
Comment thread
nandkishorr marked this conversation as resolved.
Outdated

request.user.set_password(data.new_password)
request.user.save()
return Response({"message": "Password updated successfully"})
Empty file added care/users/tests/__init__.py
Empty file.
68 changes: 33 additions & 35 deletions care/users/tests/test_change_password.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,49 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model

User = get_user_model()
from care.utils.tests.base import CareAPITestBase


class TestChangePassword(APITestCase):
class UserChangePasswordTestCase(CareAPITestBase):
def setUp(self):
self.user = User.objects.create_user(
username="vipul",
password="StrongPass@123",
super().setUp()
self.user = self.create_user_with_password(
username="testuser", password="password123"
)
self.client.force_authenticate(user=self.user)
self.url = reverse("change_password_view")
self.payload = {"old_password": "password123", "new_password": "newpassword456"}
Comment thread
nandkishorr marked this conversation as resolved.

def test_weak_password_is_rejected(self):
"""Test that passwords failing Django's built-in validation are rejected."""
payload = {
"old_password": "StrongPass@123",
"new_password": "123", # Too short for Django's default (8 chars)
}
response = self.client.put(self.url, payload)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# Check that 'new_password' is in the error response keys
self.assertIn("new_password", response.data)
def test_change_password_success(self):
response = self.client.put(self.url, self.payload, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["message"], "Password updated successfully")
self.user.refresh_from_db()
self.assertTrue(self.user.check_password("newpassword456"))

def test_wrong_old_password_fails(self):
"""Ensure the user must provide the correct current password."""
payload = {
"old_password": "WrongCurrentPassword",
"new_password": "NewStrongPass@456",
}
response = self.client.put(self.url, payload)
def test_change_password_wrong_old_password(self):
self.payload["old_password"] = "wrongpassword"
response = self.client.put(self.url, self.payload, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("old_password", response.data)
self.assertEqual(
response.data["old_password"][0],
"Wrong password entered. Please check your password.",
)

def test_password_change_success(self):
"""Test that a valid password change works correctly."""
payload = {
"old_password": "StrongPass@123",
"new_password": "NewStrongPass@456",
}
response = self.client.put(self.url, payload)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_change_password_weak_new_password(self):
self.payload["new_password"] = "123"
response = self.client.put(self.url, self.payload, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertContains(
response,
"This password is too short",
status_code=status.HTTP_400_BAD_REQUEST,
)

# Verify the password actually changed in the DB
def test_change_password_invalid_password(self):
self.payload["new_password"] = "password123"
response = self.client.put(self.url, self.payload, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.user.refresh_from_db()
self.assertTrue(self.user.check_password("NewStrongPass@456"))
self.assertTrue(self.user.check_password("password123"))
Comment thread
nandkishorr marked this conversation as resolved.
Comment thread
nandkishorr marked this conversation as resolved.
Loading