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:

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
@@ -331,6 +331,17 @@ class Meta:
         description = 'Creates a new variant for a product'
         model = models.ProductVariant
 
+    @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))
+
     @classmethod
     def clean_input(cls, info, instance, input, errors):
         cleaned_input = super().clean_input(info, instance, input, errors)
@@ -348,6 +359,7 @@ def clean_input(cls, info, instance, input, errors):
         if attributes and product_type:
             try:
                 qs = product_type.variant_attributes.prefetch_related('values')
+                cls.clean_product_type_attributes(qs, attributes, errors)
                 attributes = attributes_to_hstore(attributes, qs)
             except ValueError as e:
                 cls.add_error(errors, 'attributes', str(e))


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
@@ -150,6 +150,47 @@ def test_create_variant(admin_api_client, product, product_type):
     assert data['weight']['value'] == weight
 
 
+def test_create_product_variant_not_all_attributes(
+        admin_api_client, product, product_type, color_attribute):
+    query = """
+            mutation createVariant (
+                $productId: ID!,
+                $sku: String!,
+                $attributes: [AttributeValueInput]!) {
+                    productVariantCreate(
+                        input: {
+                            product: $productId,
+                            sku: $sku,
+                            attributes: $attributes
+                        }) {
+                        errors {
+                            field
+                            message
+                        }
+                    }
+                }
+
+        """
+    product_id = graphene.Node.to_global_id('Product', product.pk)
+    sku = "1"
+    variant_slug = product_type.variant_attributes.first().slug
+    variant_value = 'test-value'
+    product_type.variant_attributes.add(color_attribute)
+
+    variables = json.dumps({
+        'productId': product_id,
+        'sku': sku,
+        'attributes': [{'slug': variant_slug, 'value': variant_value}]})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    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 not product.variants.filter(sku=sku).exists()
+
+
 def test_update_product_variant(admin_api_client, product):
     query = """
         mutation updateVariant (
@@ -205,6 +246,50 @@ def test_update_product_variant(admin_api_client, product):
     assert data['sku'] == sku
 
 
+def test_update_product_variant_not_all_attributes(
+        admin_api_client, product, product_type, color_attribute):
+    query = """
+        mutation updateVariant (
+            $id: ID!,
+            $sku: String!,
+            $attributes: [AttributeValueInput]!) {
+                productVariantUpdate(
+                    id: $id,
+                    input: {
+                        sku: $sku,
+                        attributes: $attributes
+                    }) {
+                    errors {
+                        field
+                        message
+                    }
+                }
+            }
+
+    """
+    variant = product.variants.first()
+    variant_id = graphene.Node.to_global_id('ProductVariant', variant.pk)
+    sku = "test sku"
+    variant_slug = product_type.variant_attributes.first().slug
+    variant_value = 'test-value'
+    product_type.variant_attributes.add(color_attribute)
+
+    variables = json.dumps({
+        'id': variant_id,
+        'sku': sku,
+        'attributes': [{'slug': variant_slug, 'value': variant_value}]})
+
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    variant.refresh_from_db()
+    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 not product.variants.filter(sku=sku).exists()
+
+
 def test_delete_variant(admin_api_client, product):
     query = """
         mutation variantDelete($id: ID!) {