Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
45 changes: 0 additions & 45 deletions care/emr/tests/test_reset_password_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,48 +457,3 @@ def test_reset_password_request_email_failure(self):
format="json",
)
self.assertEqual(response.status_code, 400)

def test_change_password_with_leading_whitespace(self):
"""
Test that password with leading whitespace is handled consistently.
The password should be stripped before validation, matching login behavior.
"""
self.client.force_authenticate(user=self.user)
new_password = "newpassword@123"
response = self.client.put(
self.change_password_url,
{"old_password": f" {self.password}", "new_password": new_password},
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {"message": "Password updated successfully"})

def test_change_password_with_trailing_whitespace(self):
"""
Test that password with trailing whitespace is handled consistently.
The password should be stripped before validation, matching login behavior.
"""
self.client.force_authenticate(user=self.user)
new_password = "newpassword@123"
response = self.client.put(
self.change_password_url,
{"old_password": f"{self.password} ", "new_password": new_password},
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {"message": "Password updated successfully"})

def test_change_password_with_leading_and_trailing_whitespace(self):
"""
Test that password with both leading and trailing whitespace is handled consistently.
The password should be stripped before validation, matching login behavior.
"""
self.client.force_authenticate(user=self.user)
new_password = "newpassword@123"
response = self.client.put(
self.change_password_url,
{"old_password": f" {self.password} ", "new_password": new_password},
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {"message": "Password updated successfully"})
75 changes: 38 additions & 37 deletions care/users/api/viewsets/change_password.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,65 @@
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, field_validator
from rest_framework import status
from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response

User = get_user_model()

from care.emr.api.viewsets.base import emr_exception_handler

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

Validates the new password using Django's built-in password validators.
"""
class ChangePasswordSpec(BaseModel):
old_password: str
new_password: str

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
@field_validator("old_password", "new_password", mode="before")
@classmethod
def strip_passwords(cls, v):
"""Strip leading and trailing whitespace from passwords."""
if isinstance(v, str):
return v.strip()
return v


@extend_schema_view(
put=extend_schema(tags=["users"]),
patch=extend_schema(tags=["users"]),
put=extend_schema(tags=["users"], request=ChangePasswordSpec),
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 get_exception_handler(self):
return emr_exception_handler

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)
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 self.object.check_password(
serializer.validated_data.get("old_password")
):
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,
)
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

self.object.set_password(serializer.validated_data.get("new_password"))
self.object.save()
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.
110 changes: 75 additions & 35 deletions care/users/tests/test_change_password.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,91 @@
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)
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"))

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_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.

def test_change_password_with_leading_whitespace(self):
"""
Test that password with leading whitespace is handled consistently.
The password should be stripped before validation, matching login behavior.
"""
self.payload["old_password"] = f" {self.payload['old_password']}"
response = self.client.put(
self.url,
self.payload,
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {"message": "Password updated successfully"})

def test_change_password_with_trailing_whitespace(self):
"""
Test that password with trailing whitespace is handled consistently.
The password should be stripped before validation, matching login behavior.
"""
self.payload["old_password"] = f"{self.payload['old_password']} "
response = self.client.put(
self.url,
self.payload,
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {"message": "Password updated successfully"})

def test_change_password_with_leading_and_trailing_whitespace(self):
"""
Test that password with both leading and trailing whitespace is handled consistently.
The password should be stripped before validation, matching login behavior.
"""
self.payload["old_password"] = f" {self.payload['old_password']} "
response = self.client.put(
self.url,
self.payload,
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {"message": "Password updated successfully"})
Loading