Skip to content

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

Merged
vigneshhari merged 12 commits into
developfrom
ENG-445-move-care-users-api-viewsets-change-password-py-to-pydantic
Jul 3, 2026
Merged

[ENG-445] refact: user change password py to pydantic#3667
vigneshhari merged 12 commits into
developfrom
ENG-445-move-care-users-api-viewsets-change-password-py-to-pydantic

Conversation

@nandkishorr

@nandkishorr nandkishorr commented May 29, 2026

Copy link
Copy Markdown
Contributor

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, properly verifies the old password, enforces password-strength validators, and returns consistent error responses for failures.
  • Tests

    • Reorganized and expanded password-change tests to cover success, wrong-old, weak/invalid new passwords, and whitespace-handling; removed duplicate whitespace tests from another test module.

@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

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces DRF serializer flow with a Pydantic ChangePasswordSpec that strips whitespace and validates old/new passwords; updates ChangePasswordView to parse via the spec and return via the configured exception handler; 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 and validators that strip whitespace and perform password checks.
OpenAPI schema & exception handler
care/users/api/viewsets/change_password.py
Updates put/patch request body schema to reference ChangePasswordSpec and overrides get_exception_handler to return emr_exception_handler.
Endpoint implementation and validation
care/users/api/viewsets/change_password.py
ChangePasswordView.update() now uses ChangePasswordSpec.model_validate(..., context={"user": request.user}), relies on the exception handler for validation errors, sets the new password, and saves the user.
Test suite updates and new whitespace tests
care/users/tests/test_change_password.py
Rewrites tests to inherit CareAPITestBase, centralizes payload in setUp, updates success/weak/wrong-old-password tests, adds invalid-password rejection test, and adds three tests ensuring whitespace in old_password is stripped and accepted.
Removed duplicate tests and minor formatting
care/emr/tests/test_reset_password_api.py
Removes three whitespace-handling tests from this module and adjusts client.post(...) formatting in one unrelated test.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

  • ohcnetwork/care#3669: Overlaps on error-response structure and handling for wrong old_password in the change-password endpoint.
  • ohcnetwork/care#3434: Both PRs update the password change flow to enforce Django password validation and adjust tests for weak/invalid passwords and wrong-old-password scenarios.

Suggested labels

waiting-for-review

Suggested Reviewers

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

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% 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 is incomplete. While it mentions the main change and links the associated issue, it lacks details about what was actually changed, why the refactoring was necessary, and the architectural implications of moving from DRF serializers to pydantic. Expand the description to include specific changes made, benefits of the pydantic migration, and note the test updates and moved whitespace-handling tests for clarity.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main refactoring work: moving the change password serializer implementation from DRF to pydantic, which aligns with the core changes in the changeset.
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 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.

@greptile-apps

greptile-apps Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors the change-password endpoint from a DRF serializer to a Pydantic BaseModel, and fixes several issues identified in the previous review cycle. The Pydantic ValidationError is now correctly surfaced as a 400 via get_exception_handleremr_exception_handler, extend_schema decorators are correctly applied per-method, and validation messages are joined as a clean string rather than a Python list.

  • ChangePasswordSpec uses a field_validator to strip whitespace from both passwords before validation, and a model_validator(mode="after") to check the old password and run Django's strength validators — with the ValidationInfo context used to thread request.user into Pydantic without coupling the model to the request object.
  • Tests are migrated to CareAPITestBase and now cover success, wrong-old-password, weak password, and whitespace-trimming cases; the three duplicate whitespace tests are removed from test_reset_password_api.py.

Confidence Score: 4/5

Safe to merge after confirming CommonPasswordValidator is in AUTH_PASSWORD_VALIDATORS and that API consumers can tolerate the new model-level error shape.

The core logic is correct and the previously identified 500-error and schema annotation bugs are fixed. The one lingering concern is that test_change_password_invalid_password implicitly relies on a specific validator being configured — if that assumption doesn't hold the test will fail rather than erroneously pass, so it is a test reliability issue rather than a silent production defect.

care/users/tests/test_change_password.py — the test_change_password_invalid_password test's correctness depends on validator configuration that is not stated in the test itself.

Important Files Changed

Filename Overview
care/users/api/viewsets/change_password.py Refactors password change to Pydantic; correctly wires emr_exception_handler, strips whitespace, and joins validation messages — redundant explicit get_password_validators call is a minor style concern.
care/users/tests/test_change_password.py Expanded test suite covering success, wrong-old-password, weak password, and whitespace trimming; migrated to CareAPITestBase; one test relies on implicit validator configuration.
care/emr/tests/test_reset_password_api.py Removes three whitespace-stripping tests now covered in the dedicated change-password test module.

Reviews (3): Last reviewed commit: "Fix review comment" | Re-trigger Greptile

Comment thread care/users/api/viewsets/change_password.py Outdated
Comment thread care/users/api/viewsets/change_password.py Outdated
Comment thread care/users/api/viewsets/change_password.py
Comment thread care/users/tests/test_change_password.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 Outdated
Comment thread care/users/tests/test_change_password.py
Comment thread care/users/tests/test_change_password.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.66%. Comparing base (c8baced) to head (060731f).
⚠️ Report is 7 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3667      +/-   ##
===========================================
+ Coverage    79.55%   79.66%   +0.10%     
===========================================
  Files          479      479              
  Lines        22996    23010      +14     
  Branches      2378     2379       +1     
===========================================
+ Hits         18295    18330      +35     
+ Misses        4096     4080      -16     
+ Partials       605      600       -5     

☔ View full report in Codecov by Harness.
📢 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.

Comment thread care/users/api/viewsets/change_password.py
@vigneshhari vigneshhari changed the title [Eng-445 ] refact: user change password py to pydantic [Eng-445] refact: user change password py to pydantic May 30, 2026
@nandkishorr nandkishorr requested a review from vigneshhari May 31, 2026 10:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 27-44: The validator validate_passwords is running after
new_password has been normalized/stripped so the server may silently change the
credential; fix by validating the original, unmodified new password instead of
mutating self.new_password: retrieve the raw submitted value from the
request/validation context (e.g., info.context["raw_new_password"] or similar)
and pass that raw value into validate_password (leave self.new_password
untouched), or alternatively move normalization to happen after
validate_passwords so validate_passwords always sees the exact user-submitted
new_password; ensure no assignment to self.new_password in validate_passwords
and keep references to old_password, new_password, validate_passwords, and
set_password in mind when making the change.
- Around line 65-66: ChangePasswordSpec is too permissive: add Pydantic model
config to forbid unknown fields so
ChangePasswordSpec.model_validate(request.data, context={"user": request.user})
will raise on extra keys. Modify the ChangePasswordSpec class to include
model_config = ConfigDict(extra="forbid") (import ConfigDict from pydantic) so
unexpected request keys are rejected; keep using
ChangePasswordSpec.model_validate(...) where currently called.
🪄 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: 3fa8b4ef-a714-41bc-a9e4-4f43655f5eee

📥 Commits

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

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

Comment thread care/users/api/viewsets/change_password.py Outdated
Comment thread care/users/api/viewsets/change_password.py
@vigneshhari vigneshhari changed the title [Eng-445] refact: user change password py to pydantic [ENG-445] refact: user change password py to pydantic Jun 8, 2026
@nandkishorr nandkishorr force-pushed the ENG-445-move-care-users-api-viewsets-change-password-py-to-pydantic branch from c382f93 to 9412d6d Compare June 8, 2026 10:05
Comment thread care/users/api/viewsets/change_password.py
@vigneshhari vigneshhari merged commit c6ca8ef into develop Jul 3, 2026
13 checks passed
@vigneshhari vigneshhari deleted the ENG-445-move-care-users-api-viewsets-change-password-py-to-pydantic branch July 3, 2026 10:58
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.

2 participants