Repository: saleor/saleor

Issue:
Add addValues and removeValues arguments to attributeUpdate mutation
We should be able to do a bulk update of attribute values. I'd like to propose adding `removeValues: [ID!]` i `addValues:[String!]` arguments to `attributeUpdate`.


Discussion:

Resolving patch:
diff --git a/saleor/graphql/product/mutations/attributes.py b/saleor/graphql/product/mutations/attributes.py
--- a/saleor/graphql/product/mutations/attributes.py
+++ b/saleor/graphql/product/mutations/attributes.py
@@ -1,37 +1,32 @@
 import graphene
+from django.core.exceptions import ValidationError
+from django.db.models import Q
+from django.template.defaultfilters import slugify
+from graphql_jwt.decorators import permission_required
 
 from ....product import models
-from ...core.mutations import ModelDeleteMutation, ModelMutation
-from django.core.exceptions import ValidationError
+from ...core.mutations import BaseMutation, ModelDeleteMutation, ModelMutation
+from ..types import Attribute, AttributeValue
 
 
-class AttributeValueCreateInput(graphene.InputObjectType):
-    attribute = graphene.ID(
-        required=False,
-        description='Attribute to which value will be assigned.',
-        name='attribute')
-    slug = graphene.String(
-        required=True, description='Internal name.')
+class AttributeCreateValueInput(graphene.InputObjectType):
     name = graphene.String(
         required=True, description='Name displayed in the interface.')
     value = graphene.String(
         required=True, description='Real value eg. HEX color.')
 
 
-class AttributeInput(graphene.InputObjectType):
-    slug = graphene.String(
-        required=True, description='Internal name.')
+class AttributeCreateInput(graphene.InputObjectType):
     name = graphene.String(
         required=True, description='Name displayed in the interface.')
-
-
-class AttributeCreateInput(AttributeInput):
     values = graphene.List(
-        AttributeValueCreateInput,
+        AttributeCreateValueInput,
         description='Attribute values to be created for this attribute.')
 
 
 class AttributeCreate(ModelMutation):
+    ATTRIBUTE_VALUES_FIELD = 'values'
+
     class Arguments:
         input = AttributeCreateInput(
             required=True,
@@ -42,45 +37,92 @@ class Meta:
         model = models.Attribute
 
     @classmethod
-    def user_is_allowed(cls, user, input):
-        return user.has_perm('product.manage_products')
+    def clean_attribute_value_uniqueness(cls, values, errors, error_msg):
+        """Checks if all provided values are unique."""
+        if len(set(values)) != len(values):
+            cls.add_error(errors, cls.ATTRIBUTE_VALUES_FIELD, error_msg)
+        return errors
 
     @classmethod
-    def clean_input(cls, info, instance, input, errors):
-        cleaned_input = super().clean_input(info, instance, input, errors)
-
-        values = cleaned_input.get('values', [])
-        names = [value['name'] for value in values]
-        if len(set(names)) != len(names):
-            cls.add_error(
-                errors, 'values', 'Duplicated attribute value names provided.')
+    def clean_attribute_values(cls, values, errors):
+        """Validates if a valid AttributeValue instance can be created using
+        the data provided.
+        """
         for value_data in values:
+            value_data['slug'] = slugify(value_data['name'])
             attribute_value = models.AttributeValue(**value_data)
             try:
                 attribute_value.full_clean()
             except ValidationError as validation_errors:
                 for field in validation_errors.message_dict:
-                    # Attribute instance is not created yet so we cannot add it
                     if field == 'attribute':
                         continue
                     for message in validation_errors.message_dict[field]:
-                        error_field = 'values:%(field)s' % {'field': field}
+                        error_field = '%(values_field)s:%(field)s' % {
+                            'values_field': cls.ATTRIBUTE_VALUES_FIELD,
+                            'field': field}
                         cls.add_error(errors, error_field, message)
+        return errors
+
+    @classmethod
+    def clean_input(cls, info, instance, input, errors):
+        cleaned_input = super().clean_input(info, instance, input, errors)
+        slug = slugify(cleaned_input['name'])
+        cleaned_input['slug'] = slug
+
+        # Slugs are created automatically from the names, but those should be
+        # unique.
+        attribute_with_same_slug_exists = instance._meta.model.objects.filter(
+            slug=slug).exclude(pk=getattr(instance, 'pk', None)).exists()
+        if attribute_with_same_slug_exists:
+            cls.add_error(errors, 'name', 'Attribute\'s name is not unique.')
+
+        values = cleaned_input.get(cls.ATTRIBUTE_VALUES_FIELD, [])
+        if not values:
+            return cleaned_input
+
+        # All provided names should be unique to each other
+        cls.clean_attribute_value_uniqueness(
+            [v['name'] for v in values], errors,
+            'Duplicated AttributeValue names provided.')
+
+        slugs = []
+        for value in values:
+            value['slug'] = slugify(value['name'])
+            slugs.append(value['slug'])
+        # All provided names should resolve to unique slugs
+        cls.clean_attribute_value_uniqueness(
+            slugs, errors, 'Provided AttributeValue names are not unique.')
+
+        cls.clean_attribute_values(values, errors)
         return cleaned_input
 
     @classmethod
     def _save_m2m(cls, info, instance, cleaned_data):
         super()._save_m2m(info, instance, cleaned_data)
-        values = cleaned_data.get('values', [])
+        values = cleaned_data.get(cls.ATTRIBUTE_VALUES_FIELD, [])
         for value in values:
             instance.values.create(**value)
 
 
+class AttributeUpdateInput(graphene.InputObjectType):
+    name = graphene.String(
+        description='Name displayed in the interface.')
+    remove_values = graphene.List(
+        graphene.ID, name='removeValues', required=True,
+        description='List of attributes to be removed from this attribute.')
+    add_values = graphene.List(
+        AttributeCreateValueInput, name='addValues', required=True,
+        description='Attribute values to be created for this attribute.')
+
+
 class AttributeUpdate(AttributeCreate):
+    ATTRIBUTE_VALUES_FIELD = 'add_values'
+
     class Arguments:
         id = graphene.ID(
             required=True, description='ID of an attribute to update.')
-        input = AttributeInput(
+        input = AttributeUpdateInput(
             required=True,
             description='Fields required to update an attribute.')
 
@@ -88,6 +130,57 @@ class Meta:
         description = 'Updates attribute.'
         model = models.Attribute
 
+    @classmethod
+    def clean_remove_values(cls, cleaned_input, instance, errors):
+        """Check if AttributeValues to be removed are assigned to given
+        Attribute.
+        """
+        remove_values = cleaned_input.get('remove_values', [])
+        if not remove_values:
+            return []
+
+        attribute_values = cls.get_nodes_or_error(
+            ids=remove_values, errors=errors,
+            only_type=AttributeValue, field=cls.ATTRIBUTE_VALUES_FIELD)
+        if attribute_values:
+            for value in attribute_values:
+                if value.attribute != instance:
+                    cls.add_error(
+                        errors, 'remove_values:%s' % value,
+                        'AttributeValue does not belong to this Attribute.')
+        return attribute_values
+
+    @classmethod
+    def clean_add_values(cls, cleaned_input, instance, errors):
+        """Check if AttributeValue with the same slug or name already exists.
+        """
+        existing_attribute_values = instance.values.values_list('slug', 'name')
+        existing_slugs, existing_names = zip(*existing_attribute_values)
+        for value in cleaned_input.get('add_values', []):
+            if value['name'] in existing_names:
+                cls.add_error(
+                    errors, 'add_values:%s' % value['name'],
+                    'AttributeValue with given name already exists.')
+            if value['slug'] in existing_slugs:
+                cls.add_error(
+                    errors, 'add_values:%s' % value['slug'],
+                    'AttributeValue name is not unique.')
+        return errors
+
+    @classmethod
+    def clean_input(cls, info, instance, input, errors):
+        cleaned_input = super().clean_input(info, instance, input, errors)
+        cleaned_input['remove_values'] = cls.clean_remove_values(
+            cleaned_input, instance, errors)
+        cls.clean_add_values(cleaned_input, instance, errors)
+        return cleaned_input
+
+    @classmethod
+    def _save_m2m(cls, info, instance, cleaned_data):
+        super()._save_m2m(info, instance, cleaned_data)
+        for attribute_value in cleaned_data.get('remove_values', []):
+            attribute_value.delete()
+
 
 class AttributeDelete(ModelDeleteMutation):
     class Arguments:
@@ -103,43 +196,106 @@ def user_is_allowed(cls, user, input):
         return user.has_perm('product.manage_products')
 
 
-class AttributeValueCreate(ModelMutation):
+class AttributeValueCreateInput(graphene.InputObjectType):
+    name = graphene.String(
+        required=True, description='Name displayed in the interface.')
+    value = graphene.String(
+        required=True, description='Real value eg. HEX color.')
+
+
+class AttributeValueCreate(BaseMutation):
+    attribute = graphene.Field(
+        Attribute, description='A related Attribute.')
+    attribute_value = graphene.Field(
+        AttributeValue, description='Created AttributeValue.')
+
     class Arguments:
+        id = graphene.ID(
+            required=True, name='attribute',
+            description='Attribute to which value will be assigned.')
         input = AttributeValueCreateInput(
             required=True,
-            description='Fields required to create an attribute choice value.')
+            description='Fields required to create an AttributeValue.')
 
     class Meta:
-        description = 'Creates an attribute choice value.'
-        model = models.AttributeValue
+        description = 'Creates an AttributeValue.'
 
     @classmethod
-    def user_is_allowed(cls, user, input):
-        return user.has_perm('product.manage_products')
+    def clean_input(
+            cls, name, slug, errors, attribute, attribute_value_pk=None):
+        other_values = attribute.values.exclude(pk=attribute_value_pk)
+        duplicated_values_exists = other_values.filter(
+            Q(name=name) | Q(slug=slug)).exists()
+        if duplicated_values_exists:
+            cls.add_error(errors, 'name', 'Provided name is not unique.')
+        return errors
+
+    @classmethod
+    @permission_required('product.manage_products')
+    def mutate(cls, root, info, id, input):
+        errors = []
+        attribute = cls.get_node_or_error(info, id, errors, 'id', Attribute)
+        if not attribute:
+            return AttributeValueCreate(errors=errors)
+
+        name = input['name']
+        slug = slugify(name)
+        cls.clean_input(name, slug, errors, attribute, attribute_value_pk=None)
+        if errors:
+            return AttributeValueCreate(errors=errors)
+
+        attribute_value = attribute.values.create(
+            name=name, value=input['value'], slug=slug)
+        return AttributeValueCreate(
+            attribute=attribute, attribute_value=attribute_value)
 
 
 class AttributeValueUpdate(AttributeValueCreate):
     class Arguments:
         id = graphene.ID(
             required=True,
-            description='ID of an attribute choice value to update.')
-        input = AttributeInput(
+            description='ID of an AttributeValue to update.')
+        input = AttributeValueCreateInput(
             required=True,
-            description='Fields required to update an attribute choice value.')
+            description='Fields required to update an AttributeValue.')
 
     class Meta:
-        description = 'Updates an attribute choice value.'
-        model = models.AttributeValue
+        description = 'Updates an AttributeValue.'
+
+    @classmethod
+    @permission_required('product.manage_products')
+    def mutate(cls, root, info, id, input):
+        errors = []
+        attribute_value = cls.get_node_or_error(
+            info, id, errors, 'id', AttributeValue)
+        if not attribute_value:
+            return AttributeValueUpdate(errors=errors)
+
+        attribute = attribute_value.attribute
+        name = input['name']
+        slug = slugify(name)
+        cls.clean_input(
+            name, slug, errors, attribute,
+            attribute_value_pk=attribute_value.pk)
+        if errors:
+            return AttributeValueUpdate(errors=errors)
+
+        attribute_value.name = name
+        attribute_value.value = input['value']
+        attribute_value.slug = slug
+        attribute_value.save(update_fields=['slug', 'name', 'value'])
+        return AttributeValueUpdate(
+            attribute=attribute, attribute_value=attribute_value)
 
 
 class AttributeValueDelete(ModelDeleteMutation):
     class Arguments:
         id = graphene.ID(
             required=True,
-            description='ID of an attribute choice value to delete.')
+            description='ID of an AttributeValue to delete.')
 
     class Meta:
-        description = 'Deletes an attribute choice value.'
+        description = 'Deletes an AttributeValue.'
         model = models.AttributeValue
 
     @classmethod


Test patch:
diff --git a/tests/api/test_attributes.py b/tests/api/test_attributes.py
--- a/tests/api/test_attributes.py
+++ b/tests/api/test_attributes.py
@@ -1,13 +1,15 @@
 import json
 
-import graphene
 import pytest
+
+import graphene
 from django.shortcuts import reverse
-from tests.api.utils import get_graphql_content
-from saleor.product.models import (
-    Category, Attribute, AttributeValue)
+from django.template.defaultfilters import slugify
+from saleor.graphql.product.types import (
+    AttributeValueType, resolve_attribute_value_type)
 from saleor.graphql.product.utils import attributes_to_hstore
-from saleor.graphql.product.types import resolve_attribute_value_type, AttributeValueType
+from saleor.product.models import Attribute, AttributeValue, Category
+from tests.api.utils import get_graphql_content
 
 
 def test_attributes_to_hstore(product, color_attribute):
@@ -90,10 +92,9 @@ def test_attributes_in_category_query(user_api_client, product):
 
 CREATE_ATTRIBUTES_QUERY = """
     mutation createAttribute(
-            $name: String!, $slug: String!,
-            $values: [AttributeValueCreateInput]) {
+            $name: String!, $values: [AttributeCreateValueInput]) {
         attributeCreate(
-                input: {name: $name, slug: $slug, values: $values}) {
+                input: {name: $name, values: $values}) {
             errors {
                 field
                 message
@@ -111,79 +112,162 @@ def test_attributes_in_category_query(user_api_client, product):
 """
 
 
-def test_create_attribute(admin_api_client):
-    query = CREATE_ATTRIBUTES_QUERY
-    name = 'test name'
-    slug = 'test-slug'
-    variables = json.dumps({'name': name, 'slug': slug, 'values': []})
-    response = admin_api_client.post(
-        reverse('api'), {'query': query, 'variables': variables})
-    content = get_graphql_content(response)
-    assert not content['data']['attributeCreate']['errors']
-    data = content['data']['attributeCreate']['attribute']
-    assert data['name'] == name
-    assert data['slug'] == slug
-    assert not data['values']
-
-
 def test_create_attribute_and_attribute_values(admin_api_client):
     query = CREATE_ATTRIBUTES_QUERY
+    attribute_name = 'Example name'
     name = 'Value name'
-    slug = 'value-slug'
     variables = json.dumps({
-        'name': 'Example name', 'slug': 'example-slug',
-        'values': [{'slug': slug, 'name': name, 'value': '#1231'}]})
+        'name': attribute_name, 'values': [{'name': name, 'value': '#1231'}]})
     response = admin_api_client.post(
         reverse('api'), {'query': query, 'variables': variables})
     content = get_graphql_content(response)
     assert not content['data']['attributeCreate']['errors']
-    data = content['data']['attributeCreate']['attribute']['values']
-    assert len(data) == 1
-    assert data[0]['name'] == name
-    assert data[0]['slug'] == slug
-
-
-def test_create_attribute_and_attribute_values_errors(admin_api_client):
+    data = content['data']['attributeCreate']['attribute']
+    assert data['name'] == attribute_name
+    assert data['slug'] == slugify(attribute_name)
+
+    assert len(data['values']) == 1
+    assert data['values'][0]['name'] == name
+    assert data['values'][0]['slug'] == slugify(name)
+
+
+@pytest.mark.parametrize(
+    'name_1, name_2, error_msg', (
+        (
+            'Red color', 'Red color',
+            'Duplicated AttributeValue names provided.'),
+        (
+            'Red color', 'red color',
+            'Provided AttributeValue names are not unique.')))
+def test_create_attribute_and_attribute_values_errors(
+        admin_api_client, name_1, name_2, error_msg):
     query = CREATE_ATTRIBUTES_QUERY
     variables = json.dumps({
-        'name': 'Example name', 'slug': 'example-slug',
+        'name': 'Example name',
         'values': [
-            {'slug': 'slug', 'name': 'Red Color', 'value': '#1231'},
-            {'slug': 'Incorrect slug', 'name': 'Red Color', 'value': '#121'}]})
+            {'name': name_1, 'value': '#1231'},
+            {'name': name_2, 'value': '#121'}]})
     response = admin_api_client.post(
         reverse('api'), {'query': query, 'variables': variables})
     content = get_graphql_content(response)
     errors = content['data']['attributeCreate']['errors']
     assert errors
     assert errors[0]['field'] == 'values'
-    assert errors[0]['message'] == 'Duplicated attribute value names provided.'
-    assert errors[1]['field'] == 'values:slug'
-    assert errors[1]['message'] == (
-        'Enter a valid \'slug\' consisting of letters, '
-        'numbers, underscores or hyphens.')
-
-
-def test_update_attribute(admin_api_client, color_attribute):
-    attribute = color_attribute
-    query = """
-    mutation updateAttribute($id: ID!, $name: String!, $slug: String!) {
-        attributeUpdate(id: $id, input: {name: $name, slug: $slug}) {
-            attribute {
+    assert errors[0]['message'] == error_msg
+
+
+UPDATE_ATTRIBUTE_QUERY = """
+    mutation updateAttribute(
+        $id: ID!, $name: String!, $addValues: [AttributeCreateValueInput]!,
+        $removeValues: [ID]!) {
+    attributeUpdate(
+            id: $id,
+            input: {
+                name: $name, addValues: $addValues,
+                removeValues: $removeValues}) {
+        errors {
+            field
+            message
+        }
+        attribute {
+            name
+            slug
+            values {
                 name
+                slug
             }
         }
     }
-    """
+}
+"""
+
+
+def test_update_attribute_name(admin_api_client, color_attribute):
+    query = UPDATE_ATTRIBUTE_QUERY
+    attribute = color_attribute
+    name = 'Wings name'
+    id = graphene.Node.to_global_id('Attribute', attribute.id)
+    variables = json.dumps({
+        'name': name, 'id': id, 'addValues': [], 'removeValues': []})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    attribute.refresh_from_db()
+    data = content['data']['attributeUpdate']
+    assert data['attribute']['name'] == name == attribute.name
+
+
+def test_update_attribute_remove_and_add_values(
+        admin_api_client, color_attribute):
+    query = UPDATE_ATTRIBUTE_QUERY
+    attribute = color_attribute
     name = 'Wings name'
-    slug = attribute.slug
+    attribute_value_name = 'Red Color'
     id = graphene.Node.to_global_id('Attribute', attribute.id)
-    variables = json.dumps({'name': name, 'id': id, 'slug': slug})
+    attribute_value_id = attribute.values.first().id
+    value_id = graphene.Node.to_global_id(
+        'AttributeValue', attribute_value_id)
+    variables = json.dumps({
+        'name': name, 'id': id,
+        'addValues': [{'name': attribute_value_name, 'value': '#1231'}],
+        'removeValues': [value_id]})
     response = admin_api_client.post(
         reverse('api'), {'query': query, 'variables': variables})
     content = get_graphql_content(response)
     attribute.refresh_from_db()
-    data = content['data']['attributeUpdate']['attribute']
-    assert data['name'] == name == attribute.name
+    data = content['data']['attributeUpdate']
+    assert not data['errors']
+    assert data['attribute']['name'] == name == attribute.name
+    assert not attribute.values.filter(pk=attribute_value_id).exists()
+    assert attribute.values.filter(name=attribute_value_name).exists()
+
+
+@pytest.mark.parametrize(
+    'name_1, name_2, error_msg', (
+        (
+            'Red color', 'Red color',
+            'Duplicated AttributeValue names provided.'),
+        (
+            'Red color', 'red color',
+            'Provided AttributeValue names are not unique.')))
+def test_update_attribute_and_add_attribute_values_errors(
+        admin_api_client, name_1, name_2, error_msg, color_attribute):
+    query = UPDATE_ATTRIBUTE_QUERY
+    attribute = color_attribute
+    id = graphene.Node.to_global_id('Attribute', attribute.id)
+    variables = json.dumps({
+        'name': 'Example name', 'id': id, 'removeValues': [],
+        'addValues': [
+            {'name': name_1, 'value': '#1'}, {'name': name_2, 'value': '#2'}]})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    errors = content['data']['attributeUpdate']['errors']
+    assert errors
+    assert errors[0]['field'] == 'addValues'
+    assert errors[0]['message'] == error_msg
+
+
+def test_update_attribute_and_remove_others_attribute_value(
+        admin_api_client, color_attribute, size_attribute):
+    query = UPDATE_ATTRIBUTE_QUERY
+    attribute = color_attribute
+    id = graphene.Node.to_global_id('Attribute', attribute.id)
+    size_attribute = size_attribute.values.first()
+    attr_id = graphene.Node.to_global_id(
+        'AttributeValue', size_attribute.pk)
+    variables = json.dumps({
+        'name': 'Example name', 'id': id, 'slug': 'example-slug',
+        'addValues': [], 'removeValues': [attr_id]})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    errors = content['data']['attributeUpdate']['errors']
+    assert errors
+    assert errors[0]['field'] == 'removeValues:%s' % str(
+        size_attribute).lower()
+    assert errors[0]['message'] == (
+        'AttributeValue does not belong to this Attribute.')
 
 
 def test_delete_attribute(admin_api_client, color_attribute):
@@ -206,63 +290,144 @@ def test_delete_attribute(admin_api_client, color_attribute):
         attribute.refresh_from_db()
 
 
-def test_create_attribute_value(admin_api_client, color_attribute):
-    attribute = color_attribute
-    query = """
-    mutation createChoice($attribute: ID!, $name: String!, $slug: String!, $value: String!) {
-        attributeValueCreate(
-        input: {attribute: $attribute, name: $name, slug: $slug, value: $value}) {
-            attributeValue {
+CREATE_ATTRIBUTE_VALUE_QUERY = """
+    mutation createAttributeValue(
+        $id: ID!, $name: String!, $value: String!) {
+    attributeValueCreate(
+        attribute: $id, input: {name: $name, value: $value}) {
+        errors {
+            field
+            message
+        }
+        attribute {
+            values {
                 name
-                slug
-                type
-                value
             }
         }
+        attributeValue {
+            name
+            type
+            slug
+            value
+        }
     }
-    """
+}
+"""
+
+
+def test_create_attribute_value(admin_api_client, color_attribute):
+    attribute = color_attribute
+    query = CREATE_ATTRIBUTE_VALUE_QUERY
     attribute_id = graphene.Node.to_global_id('Attribute', attribute.id)
     name = 'test name'
-    slug = 'test-slug'
     value = 'test-string'
     variables = json.dumps(
-        {'name': name, 'slug': slug, 'value': value, 'attribute': attribute_id})
+        {'name': name, 'value': value, 'id': attribute_id})
     response = admin_api_client.post(
         reverse('api'), {'query': query, 'variables': variables})
     content = get_graphql_content(response)
-    data = content[
-        'data']['attributeValueCreate']['attributeValue']
-    assert data['name'] == name
-    assert data['slug'] == slug
-    assert data['value'] == value
-    assert data['type'] == 'STRING'
+    data = content['data']['attributeValueCreate']
+    assert not data['errors']
 
+    attr_data = data['attributeValue']
+    assert attr_data['name'] == name
+    assert attr_data['slug'] == slugify(name)
+    assert attr_data['value'] == value
+    assert attr_data['type'] == 'STRING'
+    assert name in [value['name'] for value in data['attribute']['values']]
 
-def test_update_attribute_value(admin_api_client, pink_attribute_value):
-    value = pink_attribute_value
-    query = """
-    mutation updateChoice($id: ID!, $name: String!, $slug: String!) {
-        attributeValueUpdate(
-        id: $id, input: {name: $name, slug: $slug}) {
-            attributeValue {
+
+def test_create_attribute_value_not_unique_name(
+        admin_api_client, color_attribute):
+    attribute = color_attribute
+    query = CREATE_ATTRIBUTE_VALUE_QUERY
+    attribute_id = graphene.Node.to_global_id('Attribute', attribute.id)
+    value_name = attribute.values.first().name
+    variables = json.dumps(
+        {'name': value_name, 'value': 'test-string', 'id': attribute_id})
+    response = admin_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    data = content['data']['attributeValueCreate']
+    assert data['errors']
+    assert data['errors'][0]['field'] == 'name'
+    assert data['errors'][0]['message'] == 'Provided name is not unique.'
+
+
+UPDATE_ATTRIBUTE_VALUE_QUERY = """
+mutation updateChoice(
+        $id: ID!, $name: String!, $value: String!) {
+    attributeValueUpdate(
+    id: $id, input: {name: $name, value: $value}) {
+        errors {
+            field
+            message
+        }
+        attributeValue {
+            name
+            slug
+            value
+        }
+        attribute {
+            values {
                 name
-                slug
             }
         }
     }
-    """
+}
+"""
+
+
+def test_update_attribute_value(admin_api_client, pink_attribute_value):
+    query = UPDATE_ATTRIBUTE_VALUE_QUERY
+    value = pink_attribute_value
     id = graphene.Node.to_global_id('AttributeValue', value.id)
-    name = 'Crimson'
-    slug = value.slug
+    name = 'Crimson name'
     variables = json.dumps(
-        {'name': name, 'slug': slug, 'id': id})
+        {'name': name, 'value': '#RED', 'id': id})
     response = admin_api_client.post(
         reverse('api'), {'query': query, 'variables': variables})
     content = get_graphql_content(response)
     value.refresh_from_db()
-    data = content[
-        'data']['attributeValueUpdate']['attributeValue']
-    assert data['name'] == name == value.name
+    data = content['data']['attributeValueUpdate']
+    assert data['attributeValue']['name'] == name == value.name
+    assert data['attributeValue']['slug'] == slugify(name)
+    assert name in [value['name'] for value in data['attribute']['values']]
+
+
+def test_update_attribute_value_name_not_unique(
+        admin_api_client, pink_attribute_value):
+    query = UPDATE_ATTRIBUTE_VALUE_QUERY
+    value = pink_attribute_value.attribute.values.create(
+        name='Example Name', slug='example-name', value='#RED')
+    id = graphene.Node.to_global_id('AttributeValue', value.id)
+    variables = json.dumps(
+        {'name': pink_attribute_value.name, 'value': '#RED', 'id': 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']['attributeValueUpdate']
+    assert data['errors']
+    assert data['errors'][0]['field'] == 'name'
+    assert data['errors'][0]['message'] == 'Provided name is not unique.'
+
+
+def test_update_same_attribute_value(
+        admin_api_client, pink_attribute_value):
+    query = UPDATE_ATTRIBUTE_VALUE_QUERY
+    value = pink_attribute_value
+    id = graphene.Node.to_global_id('AttributeValue', value.id)
+    attr_value = '#BLUE'
+    variables = json.dumps(
+        {'name': value.name, 'value': attr_value, 'id': 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']['attributeValueUpdate']
+    assert not data['errors']
+    assert data['attributeValue']['value'] == attr_value
 
 
 def test_delete_attribute_value(