Repository: pypi/warehouse

Issue:
Tell user when email is not found when resetting password
Right now if a user tries to recover their account by providing an email address, we don't give any indication whether the email address is actually attached to an account.

Since attempting to register with a already "used" email address will provide the same information (that the email is in use by _some_ PyPI user) there's no real sense in protecting this bit of information here.

Instead we should tell the user that the email was not found, so that they can try a different email they might own, rather than mistakenly thinking that it was the correct email and that the email was not delivered (or that resetting a password is somehow broken).


Discussion:
@di How about this message? "No user found with that username or email."
@waseem18 Sounds good!

Resolving patch:
diff --git a/warehouse/accounts/forms.py b/warehouse/accounts/forms.py
--- a/warehouse/accounts/forms.py
+++ b/warehouse/accounts/forms.py
@@ -194,6 +194,15 @@ def __init__(self, *args, user_service, **kwargs):
         super().__init__(*args, **kwargs)
         self.user_service = user_service
 
+    def validate_username_or_email(self, field):
+        username_or_email = self.user_service.get_user_by_username(field.data)
+        if username_or_email is None:
+            username_or_email = self.user_service.get_user_by_email(field.data)
+        if username_or_email is None:
+            raise wtforms.validators.ValidationError(
+                "No user found with that username or email"
+            )
+
 
 class ResetPasswordForm(NewPasswordMixin, forms.Form):
 
diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py
--- a/warehouse/accounts/views.py
+++ b/warehouse/accounts/views.py
@@ -282,13 +282,12 @@ def request_password_reset(request, _form_class=RequestPasswordResetForm):
 
     user_service = request.find_service(IUserService, context=None)
     form = _form_class(request.POST, user_service=user_service)
-
     if request.method == "POST" and form.validate():
         user = user_service.get_user_by_username(form.username_or_email.data)
         if user is None:
             user = user_service.get_user_by_email(form.username_or_email.data)
-        if user:
-            send_password_reset_email(request, user)
+
+        send_password_reset_email(request, user)
 
         token_service = request.find_service(ITokenService, name='password')
         n_hours = token_service.max_age // 60 // 60
diff --git a/warehouse/email.py b/warehouse/email.py
--- a/warehouse/email.py
+++ b/warehouse/email.py
@@ -36,7 +36,6 @@ def send_email(task, request, body, subject, *, recipients=None):
 
 def send_password_reset_email(request, user):
     token_service = request.find_service(ITokenService, name='password')
-
     token = token_service.dumps({
         'action': 'password-reset',
         'user.id': str(user.id),


Test patch:
diff --git a/tests/unit/accounts/test_forms.py b/tests/unit/accounts/test_forms.py
--- a/tests/unit/accounts/test_forms.py
+++ b/tests/unit/accounts/test_forms.py
@@ -365,6 +365,39 @@ def test_no_password_field(self):
         form = forms.RequestPasswordResetForm(user_service=user_service)
         assert 'password' not in form._fields
 
+    def test_validate_username_or_email(self):
+        user_service = pretend.stub(
+            get_user_by_username=pretend.call_recorder(lambda userid: "1"),
+            get_user_by_email=pretend.call_recorder(lambda userid: "1"),
+        )
+        form = forms.RequestPasswordResetForm(user_service=user_service)
+        field = pretend.stub(data="username_or_email")
+
+        form.validate_username_or_email(field)
+
+        assert user_service.get_user_by_username.calls == [
+            pretend.call("username_or_email")
+        ]
+
+    def test_validate_username_or_email_with_none(self):
+        user_service = pretend.stub(
+            get_user_by_username=pretend.call_recorder(lambda userid: None),
+            get_user_by_email=pretend.call_recorder(lambda userid: None),
+        )
+        form = forms.RequestPasswordResetForm(user_service=user_service)
+        field = pretend.stub(data="username_or_email")
+
+        with pytest.raises(wtforms.validators.ValidationError):
+            form.validate_username_or_email(field)
+
+        assert user_service.get_user_by_username.calls == [
+            pretend.call("username_or_email")
+        ]
+
+        assert user_service.get_user_by_email.calls == [
+            pretend.call("username_or_email")
+        ]
+
 
 class TestResetPasswordForm:
 
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
@@ -592,7 +592,9 @@ def test_request_password_reset_with_wrong_credentials(
         assert form_class.calls == [
             pretend.call(pyramid_request.POST, user_service=user_service),
         ]
-        assert send_password_reset_email.calls == []
+        assert send_password_reset_email.calls == [
+            pretend.call(pyramid_request, None)
+        ]
 
     def test_redirect_authenticated_user(self):
         pyramid_request = pretend.stub(authenticated_userid=1)