Repository: saleor/saleor

Issue:
Improve input of ProductImageUpdate mutation.
Currently, `product` and `image` fields are required, while they shouldn't be modified once an image is created. To change the image user should remove the image and upload it again. The only field exposed for editing should be `alt`.


Discussion:

Resolving patch:
diff --git a/saleor/graphql/file_upload/types.py b/saleor/graphql/file_upload/types.py
--- a/saleor/graphql/file_upload/types.py
+++ b/saleor/graphql/file_upload/types.py
@@ -2,6 +2,13 @@
 
 
 class Upload(graphene.types.Scalar):
+
+    class Meta:
+        description = '''Variables of this type must be set to null in
+        mutations. They will be replaced with a filename from a following
+        multipart part containing a binary file. See:
+        https://github.com/jaydenseric/graphql-multipart-request-spec'''
+
     @staticmethod
     def serialize(value):
         return value
diff --git a/saleor/graphql/product/mutations/products.py b/saleor/graphql/product/mutations/products.py
--- a/saleor/graphql/product/mutations/products.py
+++ b/saleor/graphql/product/mutations/products.py
@@ -365,7 +365,6 @@ def user_is_allowed(cls, user, input):
 
 
 class ProductTypeInput(graphene.InputObjectType):
-
     name = graphene.String(description='Name of the product type.')
     has_variants = graphene.Boolean(
         required=True,
@@ -428,20 +427,24 @@ def user_is_allowed(cls, user, input):
         return user.has_perm('product.manage_products')
 
 
-class ProductImageInput(graphene.InputObjectType):
+class ProductImageCreateInput(graphene.InputObjectType):
     alt = graphene.String(description='Alt text for an image.')
-    image = Upload(required=True, description='Image file.')
+    image = Upload(
+        required=True,
+        description='Represents an image file in a multipart request.')
     product = graphene.ID(description='ID of an product.')
 
 
 class ProductImageCreate(ModelMutation):
     class Arguments:
-        input = ProductImageInput(
+        input = ProductImageCreateInput(
             required=True,
             description='Fields required to create a product image.')
 
     class Meta:
-        description = 'Creates a product image.'
+        description = '''Create a product image. This mutation must be sent
+        as a `multipart` request. More detailed specs of the upload format can
+        be found here: https://github.com/jaydenseric/graphql-multipart-request-spec'''
         model = models.ProductImage
 
     @classmethod
@@ -457,11 +460,15 @@ def user_is_allowed(cls, user, input):
         return user.has_perm('product.manage_products')
 
 
-class ProductImageUpdate(ProductImageCreate):
+class ProductImageUpdateInput(graphene.InputObjectType):
+    alt = graphene.String(description='Alt text for an image.')
+
+
+class ProductImageUpdate(ModelMutation):
     class Arguments:
         id = graphene.ID(
             required=True, description='ID of a product image to update.')
-        input = ProductImageInput(
+        input = ProductImageUpdateInput(
             required=True,
             description='Fields required to update a product image.')
 


Test patch:
diff --git a/tests/api/test_product.py b/tests/api/test_product.py
--- a/tests/api/test_product.py
+++ b/tests/api/test_product.py
@@ -862,10 +862,9 @@ def test_invalid_product_image_create_mutation(admin_api_client, product):
 
 
 def test_product_image_update_mutation(admin_api_client, product_with_image):
-    product = product_with_image
     query = """
-    mutation updateProductImage($imageId: ID!, $image: Upload!, $alt: String, $product: ID!) {
-        productImageUpdate(id: $imageId, input: {image: $image, alt: $alt, product: $product}) {
+    mutation updateProductImage($imageId: ID!, $alt: String) {
+        productImageUpdate(id: $imageId, input: {alt: $alt}) {
             productImage {
                 alt
             }
@@ -873,55 +872,15 @@ def test_product_image_update_mutation(admin_api_client, product_with_image):
     }
     """
     image_obj = product_with_image.images.first()
-    image = image_obj.image
-    assert not image_obj.alt
     alt = 'damage alt'
-    variables = {
-        'product': graphene.Node.to_global_id('Product', product.id),
-        'image': image.name, 'alt': alt,
-        'imageId': graphene.Node.to_global_id('ProductImage', image_obj.id)}
-    body = get_multipart_request_body(query, variables, image.file, image.name)
-    response = admin_api_client.post_multipart(reverse('api'), body)
+    variables = json.dumps({
+        'alt': alt,
+        'imageId': graphene.Node.to_global_id('ProductImage', image_obj.id)})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
     content = get_graphql_content(response)
     assert 'errors' not in content
-    data = content['data']['productImageUpdate']
-    assert data['productImage']['alt'] == alt
-
-
-def test_invalid_product_image_update_mutation(
-        admin_api_client, product_with_image):
-    product = product_with_image
-    query = """
-    mutation updateProductImage($image: Upload!, $alt: String, $product: ID!, $id: ID!) {
-        productImageUpdate(id: $id, input: {image: $image, alt: $alt, product: $product}) {
-            productImage {
-                image
-            }
-            errors {
-                field
-                message
-            }
-        }
-    }
-    """
-    image_obj = product_with_image.images.first()
-    image = image_obj.image
-    new_image_file, new_image_name = create_pdf_file_with_image_ext()
-    variables = {
-        'product': graphene.Node.to_global_id('Product', product.id),
-        'image': new_image_name,
-        'id': graphene.Node.to_global_id('ProductImage', image_obj.id),
-    }
-    body = get_multipart_request_body(
-        query, variables, new_image_file, new_image_name)
-    response = admin_api_client.post_multipart(reverse('api'), body)
-    content = get_graphql_content(response)
-    assert content['data']['productImageUpdate']['errors'] == [{
-        'field': 'image',
-        'message': 'Invalid file type'}]
-    product_with_image.refresh_from_db()
-    assert product_with_image.images.count() == 1
-    assert product_with_image.images.first().image == image
+    data = content['data']['productImageUpdate']['productImage']['alt'] == alt
 
 
 def test_product_image_delete(admin_api_client, product_with_image):