Skip to content

[Eng-445 ] refact: user change password py to pydantic#3667

Open
nandkishorr wants to merge 3 commits into
developfrom
ENG-445-move-care-users-api-viewsets-change-password-py-to-pydantic
Open

[Eng-445 ] refact: user change password py to pydantic#3667
nandkishorr wants to merge 3 commits into
developfrom
ENG-445-move-care-users-api-viewsets-change-password-py-to-pydantic

Conversation

@nandkishorr
Copy link
Copy Markdown
Contributor

@nandkishorr nandkishorr commented May 29, 2026

Proposed Changes

  • Moved the serializer to pydantic.

Associated Issue

Merge Checklist

  • Tests added/fixed
  • Update docs in /docs
  • Linting Complete
  • Any other necessary step

Only PR's with test cases included and passing lint and test pipelines will be reviewed

@ohcnetwork/care-backend-maintainers @ohcnetwork/care-backend-admins

Summary by CodeRabbit

  • Bug Fixes

    • Password change now trims leading/trailing whitespace from the submitted old password, reducing false rejection.
    • Improved error responses for incorrect old passwords and clearer rejection for weak new passwords.
  • Tests

    • Expanded change-password tests to cover weak/invalid passwords, incorrect old passwords, successful changes, and whitespace-handling scenarios.

Review Change Stack

@nandkishorr nandkishorr self-assigned this May 29, 2026
@nandkishorr nandkishorr requested a review from a team as a code owner May 29, 2026 11:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR replaces the DRF Serializer-based password-change flow with a Pydantic ChangePasswordSpec for request parsing (stripping whitespace), updates ChangePasswordView to parse and validate passwords directly using user.check_password() and Django's validate_password(), and rewrites tests to centralize payloads and add whitespace-handling and invalid-password cases.

Changes

Password Change Validation Refactor

Layer / File(s) Summary
Request model and whitespace stripping
care/users/api/viewsets/change_password.py
Adds ChangePasswordSpec Pydantic model with old_password and new_password fields and a field validator that strips leading/trailing whitespace.
Endpoint implementation and validation
care/users/api/viewsets/change_password.py
Updates ChangePasswordView OpenAPI schema to use ChangePasswordSpec, overrides get_exception_handler, parses requests via ChangePasswordSpec(**request.data), checks request.user.check_password() returning HTTP 400 with old_password field error on mismatch, validates new_password via Django validate_password() with configured validators, sets the new password, and saves the user.
Test suite updates and new whitespace tests
care/users/tests/test_change_password.py
Rewrites test class to inherit from CareAPITestBase, centralizes payload in setUp, updates success/weak/wrong-old-password tests, adds an invalid-password rejection test, and adds three tests ensuring leading/trailing/combined whitespace in the submitted old password are stripped and still allow a successful change.
Removed duplicate tests and minor formatting
care/emr/tests/test_reset_password_api.py
Removes three whitespace-handling change-password tests from this module and adjusts client.post(...) call formatting in test_reset_password_request_email_failure.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

  • ohcnetwork/care#3434: Both PRs update the same password change flow to enforce Django password validation and adjust test coverage for weak/invalid passwords and wrong-old-password scenarios.

Suggested Reviewers

  • vigneshhari
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description lacks critical detail about what was actually changed and why. It only states 'Moved the serializer to pydantic' without explaining the scope of changes or implementation details. Expand the 'Proposed Changes' section to describe: (1) what files were modified, (2) the key implementation changes (e.g., ChangePasswordSpec model, password validation approach), and (3) why this refactor improves the codebase.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: refactoring the change password module to use Pydantic instead of DRF serializers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ENG-445-move-care-users-api-viewsets-change-password-py-to-pydantic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR refactors the change-password endpoint from a DRF Serializer to a Pydantic BaseModel for input parsing, aligning with the pattern used elsewhere in the EMR module, and migrates the test class to CareAPITestBase.

  • The global exception handler (config/exception_handler.py) does not handle pydantic.ValidationError, so any request missing old_password or new_password will now return a 500 instead of 400. The EMR viewsets handle this via their own emr_exception_handler; this standalone UpdateAPIView has no equivalent.
  • validate_password now propagates through the global handler as {\"detail\": \"...\"} instead of the previous field-keyed {\"new_password\": [...]} format, which is a breaking change in the error response contract.
  • The extend_schema_view schema annotation for the request body is placed at the wrong level and will have no effect on the generated OpenAPI schema.

Confidence Score: 3/5

The change-password endpoint now returns 500 for requests with missing fields and silently changes its error response format for password policy violations — both are regressions from the old serializer behaviour.

Two concrete regressions exist in the changed view: missing-field payloads now crash with an unhandled Pydantic exception, and password-validation failures return a different JSON shape than before. The test suite does not catch either because it never sends a malformed request and uses assertContains (text-anywhere check) rather than asserting the specific field key in the error body.

care/users/api/viewsets/change_password.py needs the most attention — both regressions live there.

Important Files Changed

Filename Overview
care/users/api/viewsets/change_password.py Migrates password change from DRF serializer to Pydantic, but introduces two regressions: unhandled Pydantic ValidationError causes 500 on missing/invalid fields, and the password-validation error response format changed from field-keyed {"new_password": [...]} to {"detail": "..."}. The extend_schema_view schema annotation is also misplaced.
care/users/tests/test_change_password.py Tests updated to use the project's CareAPITestBase. Coverage improved with a success path and wrong-password check, but test_change_password_invalid_password contains a tautological assertion that will always pass regardless of whether the password was changed.
care/users/tests/init.py Empty init.py added to make the tests directory a Python package.

Reviews (1): Last reviewed commit: "refact:updted the tests" | Re-trigger Greptile

Comment thread care/users/api/viewsets/change_password.py
Comment thread care/users/api/viewsets/change_password.py
Comment thread care/users/api/viewsets/change_password.py
Comment thread care/users/tests/test_change_password.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@care/users/api/viewsets/change_password.py`:
- Around line 18-22: The top-level request=ChangePasswordSpec on
extend_schema_view won't apply to per-operation docs; move the
request=ChangePasswordSpec into each per-method extend_schema call so the PUT
and PATCH operations get the ChangePasswordSpec request body. Update the
extend_schema_view usage so extend_schema(tags=["users"],
request=ChangePasswordSpec) is applied for the put and patch entries (i.e.,
put=extend_schema(tags=["users"], request=ChangePasswordSpec) and
patch=extend_schema(tags=["users"], request=ChangePasswordSpec)), referencing
extend_schema_view, extend_schema, put, patch, and ChangePasswordSpec.
- Line 32: Wrap the construction of ChangePasswordSpec(**request.data) in a
try/except that catches pydantic.ValidationError in the change password view
(change_password.py) and translate it to a DRF 400 response by raising
rest_framework.exceptions.ValidationError (or returning Response with status=400
and the validation details); reference the ChangePasswordSpec symbol and the
view's handler where request.data is parsed. Also add a new test
test_change_password_missing_fields in care/users/tests/test_change_password.py
that POSTs with missing/invalid old_password or new_password and asserts HTTP
400 plus the returned validation error details.

In `@care/users/tests/test_change_password.py`:
- Around line 44-49: The test test_change_password_invalid_password currently
sets self.payload["new_password"] to the same value as the current password, so
the final assertion self.user.check_password("password123") is meaningless;
change the test to record the current password (or use its known value) as
original_password, set self.payload["new_password"] to a distinct invalid value
(e.g. "invalid_new_pwd"), call self.client.put(self.url, self.payload,
format="json") as before, refresh the user with self.user.refresh_from_db(), and
assert that self.user.check_password(original_password) is still True to prove
the password was not changed.
- Around line 10-15: The test uses hardcoded passwords
(password123/newpassword456) in care/users/tests/test_change_password.py via
create_user_with_password and payload which will trigger Ruff bandit rules
S105/S106; either add "S105,S106" to
tool.ruff.lint.per-file-ignores["**/tests/**"] in ruff.toml so tests are exempt,
or change the test to generate non-literal passwords (e.g., derive them at
runtime via secrets or a test factory) and assign them to the same variables
used (self.create_user_with_password call and
self.payload["old_password"/"new_password"]) to avoid hardcoded string literals.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 89f9cbab-0607-4c27-bfe2-4ce029b0d1ba

📥 Commits

Reviewing files that changed from the base of the PR and between ece71a8 and c35a9e3.

📒 Files selected for processing (3)
  • care/users/api/viewsets/change_password.py
  • care/users/tests/__init__.py
  • care/users/tests/test_change_password.py

Comment thread care/users/api/viewsets/change_password.py
Comment thread care/users/api/viewsets/change_password.py
Comment thread care/users/tests/test_change_password.py
Comment thread care/users/tests/test_change_password.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
care/users/tests/test_change_password.py (1)

51-91: ⚡ Quick win

Whitespace coverage only exercises old_password; new_password stripping is left untested.

These three tests thoroughly cover stripping on old_password, which is nice. However, ChangePasswordSpec.strip_passwords also strips new_password — meaning a user submitting "newpassword456 " would silently get a different stored password than they typed. That behavior currently has no test guarding it. Consider adding a case that submits a new_password with surrounding whitespace and asserts the stripped value is what's persisted.

💚 Suggested additional test
def test_change_password_strips_new_password_whitespace(self):
    self.payload["new_password"] = "  newpassword456  "
    response = self.client.put(self.url, self.payload, format="json")
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.user.refresh_from_db()
    self.assertTrue(self.user.check_password("newpassword456"))
    self.assertFalse(self.user.check_password("  newpassword456  "))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@care/users/tests/test_change_password.py` around lines 51 - 91, The tests
exercise stripping of old_password but miss asserting that new_password is
stripped before being stored; add a test in
care/users/tests/test_change_password.py (e.g.,
test_change_password_strips_new_password_whitespace) that sets
self.payload["new_password"] to a value with surrounding whitespace, sends the
PUT to self.url, asserts a 200 response, refreshes self.user
(self.user.refresh_from_db()) and then asserts the stored password matches the
stripped value (self.user.check_password("newpassword456")) and not the
unstripped value; this verifies ChangePasswordSpec.strip_passwords correctly
strips new_password as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@care/users/tests/test_change_password.py`:
- Around line 51-91: The tests exercise stripping of old_password but miss
asserting that new_password is stripped before being stored; add a test in
care/users/tests/test_change_password.py (e.g.,
test_change_password_strips_new_password_whitespace) that sets
self.payload["new_password"] to a value with surrounding whitespace, sends the
PUT to self.url, asserts a 200 response, refreshes self.user
(self.user.refresh_from_db()) and then asserts the stored password matches the
stripped value (self.user.check_password("newpassword456")) and not the
unstripped value; this verifies ChangePasswordSpec.strip_passwords correctly
strips new_password as well.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d62a9170-ec83-4e85-b589-77ef178d2a0a

📥 Commits

Reviewing files that changed from the base of the PR and between c35a9e3 and 52c1abe.

📒 Files selected for processing (3)
  • care/emr/tests/test_reset_password_api.py
  • care/users/api/viewsets/change_password.py
  • care/users/tests/test_change_password.py
💤 Files with no reviewable changes (1)
  • care/emr/tests/test_reset_password_api.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • care/users/api/viewsets/change_password.py

@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.85%. Comparing base (ece71a8) to head (52c1abe).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #3667   +/-   ##
========================================
  Coverage    75.85%   75.85%           
========================================
  Files          479      479           
  Lines        23043    23043           
  Branches      2380     2380           
========================================
  Hits         17480    17480           
  Misses        4990     4990           
  Partials       573      573           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant