Repository: saleor/saleor

Issue:
Add a graphQL mutation to reset password
### What I'm trying to achieve

I want to have a functionality which allows users to reset their password via email in the storefront.

### Describe a proposed solution

Add a graphQL mutation to reset password which can be performed without special permissions.




Discussion:

Resolving patch:
diff --git a/saleor/graphql/account/mutations.py b/saleor/graphql/account/mutations.py
--- a/saleor/graphql/account/mutations.py
+++ b/saleor/graphql/account/mutations.py
@@ -354,6 +354,32 @@ def mutate(cls, root, info, email):
         send_user_password_reset_email(user, site)
 
 
+class CustomerPasswordResetInput(graphene.InputObjectType):
+    email = graphene.String(
+        required=True, description=(
+            'Email of the user that will be used for password recovery.'))
+
+
+class CustomerPasswordReset(BaseMutation):
+    class Arguments:
+        input = CustomerPasswordResetInput(
+            description='Fields required to reset customer\'s password',
+            required=True)
+
+    class Meta:
+        description = 'Resets the customer\'s password.'
+
+    @classmethod
+    def mutate(cls, root, info, input):
+        email = input['email']
+        try:
+            user = models.User.objects.get(email=email)
+        except ObjectDoesNotExist:
+            return
+        site = info.context.site
+        send_user_password_reset_email(user, site)
+
+
 class AddressCreateInput(AddressInput):
     user_id = graphene.ID(
         description='ID of a user to create address for', required=True)
diff --git a/saleor/graphql/api.py b/saleor/graphql/api.py
--- a/saleor/graphql/api.py
+++ b/saleor/graphql/api.py
@@ -4,9 +4,9 @@
 from graphql_jwt.decorators import login_required, permission_required
 
 from .account.mutations import (
-    CustomerCreate, CustomerUpdate, CustomerRegister, PasswordReset,
-    SetPassword, StaffCreate, StaffDelete, StaffUpdate, AddressCreate,
-    AddressUpdate, AddressDelete)
+    CustomerCreate, CustomerUpdate, CustomerPasswordReset, CustomerRegister,
+    PasswordReset, SetPassword, StaffCreate, StaffDelete, StaffUpdate,
+    AddressCreate, AddressUpdate, AddressDelete)
 from .account.types import AddressValidationData, AddressValidationInput, User
 from .account.resolvers import (
     resolve_address_validator, resolve_customers, resolve_staff_users)
@@ -288,6 +288,7 @@ class Mutations(graphene.ObjectType):
 
     customer_create = CustomerCreate.Field()
     customer_update = CustomerUpdate.Field()
+    customer_password_reset = CustomerPasswordReset.Field()
     customer_register = CustomerRegister.Field()
 
     staff_create = StaffCreate.Field()


Test patch:
diff --git a/tests/api/test_account.py b/tests/api/test_account.py
--- a/tests/api/test_account.py
+++ b/tests/api/test_account.py
@@ -817,3 +817,33 @@ def test_address_validator_uses_geip_when_country_code_missing(
     data = content['data']['addressValidator']
     assert data['countryCode'] == 'US'
     assert data['countryName'] == 'UNITED STATES'
+
+
+@patch('saleor.account.emails.send_password_reset_email.delay')
+def test_customer_reset_password(
+        send_password_reset_mock, user_api_client, customer_user):
+    query = """
+        mutation CustomerPasswordReset($email: String!) {
+            customerPasswordReset(input: {email: $email}) {
+                errors {
+                    field
+                    message
+                }
+            }
+        }
+    """
+    # we have no user with given email
+    variables = json.dumps({'email': 'non-existing-email@email.com'})
+    response = user_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    assert not send_password_reset_mock.called
+
+    variables = json.dumps({'email': customer_user.email})
+    response = user_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    assert send_password_reset_mock.called
+    assert send_password_reset_mock.mock_calls[0][1][1] == customer_user.email