Repository: saleor/saleor

Issue:
`Attributes` field in product type should not be nullable
### What I'm trying to achieve
To get product's attribute list. 

### Steps to reproduce the problem
1. Execute the following query on a product with no attributes
```
{
  product {
    attributes {
      id
    }
  }
}
```

### What I expected to happen
Instead of `null`, `[]` should be returned.


Discussion:

Resolving patch:
diff --git a/saleor/graphql/product/types.py b/saleor/graphql/product/types.py
--- a/saleor/graphql/product/types.py
+++ b/saleor/graphql/product/types.py
@@ -57,10 +57,13 @@ def resolve_attribute_list(attributes_hstore, attributes_qs):
         for val in attr.values.all():
             values_map[val.pk] = val
 
-    attributes_list = [SelectedAttribute(
-        attribute=attributes_map.get(int(k)),
-        value=values_map.get(int(v)))
-        for k, v in attributes_hstore.items()]
+    attributes_list = []
+    for k, v in attributes_hstore.items():
+        attribute = attributes_map.get(int(k))
+        value = values_map.get(int(v))
+        if attribute and value:
+            attributes_list.append(
+                SelectedAttribute(attribute=attribute, value=value))
     return attributes_list
 
 
@@ -116,10 +119,11 @@ class Margin(graphene.ObjectType):
 
 class SelectedAttribute(graphene.ObjectType):
     attribute = graphene.Field(
-        Attribute, default_value=None, description=AttributeDescriptions.NAME)
+        Attribute, default_value=None, description=AttributeDescriptions.NAME,
+        required=True)
     value = graphene.Field(
-        AttributeValue,
-        default_value=None, description='Value of an attribute.')
+        AttributeValue, default_value=None,
+        description='Value of an attribute.', required=True)
 
     class Meta:
         description = 'Represents a custom attribute.'
@@ -231,7 +235,7 @@ class Product(CountableDjangoObjectType):
         description=dedent("""The product's base price (without any discounts
         applied)."""))
     attributes = graphene.List(
-        SelectedAttribute,
+        graphene.NonNull(SelectedAttribute), required=True,
         description='List of attributes assigned to this product.')
     purchase_cost = graphene.Field(MoneyRange)
     margin = graphene.Field(Margin)


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
@@ -9,14 +9,32 @@
 from tests.utils import create_image, create_pdf_file_with_image_ext
 
 from saleor.graphql.core.types import ReportingPeriod
-from saleor.graphql.product.types import StockAvailability
+from saleor.graphql.product.types import (
+    StockAvailability, resolve_attribute_list)
 from saleor.product.models import (
-    Category, Product, ProductImage, ProductType, ProductVariant)
+    Attribute, AttributeValue, Category, Product, ProductImage, ProductType,
+    ProductVariant)
 from saleor.product.tasks import update_variants_names
 
 from .utils import assert_no_permission, get_multipart_request_body
 
 
+def test_resolve_attribute_list(color_attribute):
+    value = color_attribute.values.first()
+    attributes_hstore = {str(color_attribute.pk): str(value.pk)}
+    res = resolve_attribute_list(attributes_hstore, Attribute.objects.all())
+    assert len(res) == 1
+    assert res[0].attribute.name == color_attribute.name
+    assert res[0].value.name == value.name
+
+    # test passing invalid hstore should resolve to empty list
+    attr_pk = str(Attribute.objects.order_by('pk').last().pk + 1)
+    val_pk = str(AttributeValue.objects.order_by('pk').last().pk + 1)
+    attributes_hstore = {attr_pk: val_pk}
+    res = resolve_attribute_list(attributes_hstore, Attribute.objects.all())
+    assert res == []
+
+
 def test_fetch_all_products(user_api_client, product):
     query = """
     query {