Repository: saleor/saleor

Issue:
User can create a variant without attributes
Currently user is able to create a variant with partly set attributes or without attributes at all. 

### What I expected to happen
All attributes set in product type should be required in variant creating form. 





Discussion:
PR #2911 partially solved the issue because it added checking if all attribute slugs required by the given product type were provided. But it is still possible to provide an empty value. I think we should change that and require values to be not empty as well.

Resolving patch:
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
@@ -299,7 +299,7 @@ def user_is_allowed(cls, user, input):
 
 class ProductVariantInput(graphene.InputObjectType):
     attributes = graphene.List(
-        AttributeValueInput,
+        AttributeValueInput, required=False,
         description='List of attributes specific to this variant.')
     cost_price = Decimal(description='Cost price of the variant.')
     price_override = Decimal(
@@ -316,6 +316,9 @@ class ProductVariantInput(graphene.InputObjectType):
 
 
 class ProductVariantCreateInput(ProductVariantInput):
+    attributes = graphene.List(
+        AttributeValueInput, required=True,
+        description='List of attributes specific to this variant.')
     product = graphene.ID(
         description='Product ID of which type is the variant.',
         name='product', required=True)
@@ -334,13 +337,15 @@ class Meta:
     @classmethod
     def clean_product_type_attributes(
             cls, attributes_qs, attributes_input, errors):
-        product_type_attr_slugs = {attr.slug for attr in attributes_qs}
-        attributes_input = {attr.slug for attr in attributes_input}
-        missing_attributes = product_type_attr_slugs - attributes_input
-        if missing_attributes:
-            cls.add_error(
-                errors, 'attributes',
-                'Missing attributes: %s' % ', '.join(missing_attributes))
+        # transform attributes_input list to a dict of slug:value pairs
+        attributes_input = {
+            item['slug']: item['value'] for item in attributes_input}
+
+        for attr in attributes_qs:
+            value = attributes_input.get(attr.slug, None)
+            if not value:
+                fieldname = 'attributes:%s' % attr.slug
+                cls.add_error(errors, fieldname, 'This field cannot be blank.')
 
     @classmethod
     def clean_input(cls, info, instance, input, errors):
@@ -351,16 +356,18 @@ def clean_input(cls, info, instance, input, errors):
         # `Product` model, which is HStore field that maps attribute's PK to
         # the value's PK.
 
-        attributes = cleaned_input.pop('attributes', [])
-        product = instance.product if instance.pk else cleaned_input.get(
-            'product')
-        product_type = product.product_type
-
-        if attributes and product_type:
+        if 'attributes' in input:
+            attributes_input = cleaned_input.pop('attributes')
+            product = instance.product if instance.pk else cleaned_input.get(
+                'product')
+            product_type = product.product_type
+            variant_attrs = product_type.variant_attributes.prefetch_related(
+                'values')
             try:
-                qs = product_type.variant_attributes.prefetch_related('values')
-                cls.clean_product_type_attributes(qs, attributes, errors)
-                attributes = attributes_to_hstore(attributes, qs)
+                cls.clean_product_type_attributes(
+                    variant_attrs, attributes_input, errors)
+                attributes = attributes_to_hstore(
+                    attributes_input, variant_attrs)
             except ValueError as e:
                 cls.add_error(errors, 'attributes', str(e))
             else:


Test patch:
diff --git a/tests/api/test_variant.py b/tests/api/test_variant.py
--- a/tests/api/test_variant.py
+++ b/tests/api/test_variant.py
@@ -70,7 +70,7 @@ def test_create_variant(admin_api_client, product, product_type):
             $priceOverride: Decimal,
             $costPrice: Decimal,
             $quantity: Int!,
-            $attributes: [AttributeValueInput],
+            $attributes: [AttributeValueInput]!,
             $weight: WeightScalar,
             $trackInventory: Boolean!) {
                 productVariantCreate(
@@ -186,8 +186,7 @@ def test_create_product_variant_not_all_attributes(
     content = get_graphql_content(response)
     assert 'errors' not in content
     assert 'errors' in content['data']['productVariantCreate']
-    assert content['data']['productVariantCreate']['errors'] == [{
-        'field': 'attributes', 'message': 'Missing attributes: color'}]
+    assert content['data']['productVariantCreate']['errors'][0]['field'] == 'attributes:color'
     assert not product.variants.filter(sku=sku).exists()
 
 
@@ -285,8 +284,7 @@ def test_update_product_variant_not_all_attributes(
     content = get_graphql_content(response)
     assert 'errors' not in content
     assert 'errors' in content['data']['productVariantUpdate']
-    assert content['data']['productVariantUpdate']['errors'] == [{
-        'field': 'attributes', 'message': 'Missing attributes: color'}]
+    assert content['data']['productVariantUpdate']['errors'][0]['field'] == 'attributes:color'
     assert not product.variants.filter(sku=sku).exists()