Repository: pypi/warehouse

Issue:
Send 'password change' email when a user changes their password via the reset workflow
## Current behaviour

An email (`warehouse/templates/email/password-change`) is sent to a user when they reset their password via their account settings page (assuming their email is verified)

##  Requested behaviour

The email should be sent to the user:

- When they reset their password via their account settings page **and**
- When they reset their password via the password reset workflow

---

**Good First Issue**: This issue is good for first time contributors. If you've already contributed to Warehouse, work on [another issue without this label](https://github.com/pypa/warehouse/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+-label%3A%22good+first+issue%22) instead. If there is not a corresponding pull request for this issue, it is up for grabs. For directions for getting set up, see our [Getting Started Guide](https://warehouse.pypa.io/development/getting-started/). If you are working on this issue and have questions, feel free to ask them here, [`#pypa-dev` on Freenode](https://webchat.freenode.net/?channels=%23pypa-dev), or the [pypa-dev mailing list](https://groups.google.com/forum/#!forum/pypa-dev).




Discussion:
I've added the "good first issue" label to this issue. Looks like we need to add something like:

https://github.com/pypa/warehouse/blob/30f2389b27f0d57462e03c23c3126ff707daf8dd/warehouse/manage/views.py#L303

here: https://github.com/pypa/warehouse/blob/30f2389b27f0d57462e03c23c3126ff707daf8dd/warehouse/accounts/views.py#L518-L529

Plus updated tests.

Resolving patch:
diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py
--- a/warehouse/accounts/views.py
+++ b/warehouse/accounts/views.py
@@ -46,7 +46,11 @@
 from warehouse.accounts.models import Email, User
 from warehouse.admin.flags import AdminFlagValue
 from warehouse.cache.origin import origin_cache
-from warehouse.email import send_email_verification_email, send_password_reset_email
+from warehouse.email import (
+    send_email_verification_email,
+    send_password_change_email,
+    send_password_reset_email,
+)
 from warehouse.i18n import localize as _
 from warehouse.packaging.models import Project, Release
 from warehouse.utils.http import is_safe_url
@@ -528,6 +532,9 @@ def _error(message):
             user.id, tag="account:password:reset", ip_address=request.remote_addr
         )
 
+        # Send password change email
+        send_password_change_email(request, user)
+
         # Flash a success message
         request.session.flash(_("You have reset your password"), queue="success")
 


Test patch:
diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py
--- a/tests/unit/accounts/test_views.py
+++ b/tests/unit/accounts/test_views.py
@@ -1186,7 +1186,7 @@ def test_get(self, db_request, user_service, token_service):
             pretend.call(ITokenService, name="password"),
         ]
 
-    def test_reset_password(self, db_request, user_service, token_service):
+    def test_reset_password(self, monkeypatch, db_request, user_service, token_service):
         user = UserFactory.create()
         db_request.method = "POST"
         db_request.POST.update({"token": "RANDOM_KEY"})
@@ -1199,6 +1199,9 @@ def test_reset_password(self, db_request, user_service, token_service):
 
         breach_service = pretend.stub(check_password=lambda pw: False)
 
+        send_email = pretend.call_recorder(lambda *a: None)
+        monkeypatch.setattr(views, "send_password_change_email", send_email)
+
         db_request.route_path = pretend.call_recorder(lambda name: "/account/login")
         db_request.remote_addr = "0.0.0.0"
         token_service.loads = pretend.call_recorder(
@@ -1242,6 +1245,7 @@ def test_reset_password(self, db_request, user_service, token_service):
         assert user_service.update_user.calls == [
             pretend.call(user.id, password=form_obj.new_password.data)
         ]
+        assert send_email.calls == [pretend.call(db_request, user)]
         assert db_request.session.flash.calls == [
             pretend.call("You have reset your password", queue="success")
         ]