diff --git a/app/graphql/mutations/.keep b/app/graphql/mutations/.keep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/app/graphql/mutations/award_emojis/add.rb b/app/graphql/mutations/award_emojis/add.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8e050dd6d29695ff851aeaf9d4c67d7646b42415
--- /dev/null
+++ b/app/graphql/mutations/award_emojis/add.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Mutations
+  module AwardEmojis
+    class Add < Base
+      graphql_name 'AddAwardEmoji'
+
+      def resolve(args)
+        awardable = authorized_find!(id: args[:awardable_id])
+
+        check_object_is_awardable!(awardable)
+
+        # TODO this will be handled by AwardEmoji::AddService
+        # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
+        # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
+        award = awardable.create_award_emoji(args[:name], current_user)
+
+        {
+          award_emoji: (award if award.persisted?),
+          errors: errors_on_object(award)
+        }
+      end
+    end
+  end
+end
diff --git a/app/graphql/mutations/award_emojis/base.rb b/app/graphql/mutations/award_emojis/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d868db84f9dfb48994bca5b10c821e0928c52ac9
--- /dev/null
+++ b/app/graphql/mutations/award_emojis/base.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Mutations
+  module AwardEmojis
+    class Base < BaseMutation
+      include Gitlab::Graphql::Authorize::AuthorizeResource
+
+      authorize :award_emoji
+
+      argument :awardable_id,
+               GraphQL::ID_TYPE,
+               required: true,
+               description: 'The global id of the awardable resource'
+
+      argument :name,
+               GraphQL::STRING_TYPE,
+               required: true,
+               description: copy_field_description(Types::AwardEmojis::AwardEmojiType, :name)
+
+      field :award_emoji,
+            Types::AwardEmojis::AwardEmojiType,
+            null: true,
+            description: 'The award emoji after mutation'
+
+      private
+
+      def find_object(id:)
+        GitlabSchema.object_from_id(id)
+      end
+
+      # Called by mutations methods after performing an authorization check
+      # of an awardable object.
+      def check_object_is_awardable!(object)
+        unless object.is_a?(Awardable) && object.emoji_awardable?
+          raise Gitlab::Graphql::Errors::ResourceNotAvailable,
+                'Cannot award emoji to this resource'
+        end
+      end
+    end
+  end
+end
diff --git a/app/graphql/mutations/award_emojis/remove.rb b/app/graphql/mutations/award_emojis/remove.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3ba85e445b811c467b0b4d955e3d0414c8857b19
--- /dev/null
+++ b/app/graphql/mutations/award_emojis/remove.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Mutations
+  module AwardEmojis
+    class Remove < Base
+      graphql_name 'RemoveAwardEmoji'
+
+      def resolve(args)
+        awardable = authorized_find!(id: args[:awardable_id])
+
+        check_object_is_awardable!(awardable)
+
+        # TODO this check can be removed once AwardEmoji services are available.
+        # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
+        # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
+        unless awardable.awarded_emoji?(args[:name], current_user)
+          raise Gitlab::Graphql::Errors::ResourceNotAvailable,
+                'You have not awarded emoji of type name to the awardable'
+        end
+
+        # TODO this will be handled by AwardEmoji::DestroyService
+        # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
+        # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
+        awardable.remove_award_emoji(args[:name], current_user)
+
+        {
+          # Mutation response is always a `nil` award_emoji
+          errors: []
+        }
+      end
+    end
+  end
+end
diff --git a/app/graphql/mutations/award_emojis/toggle.rb b/app/graphql/mutations/award_emojis/toggle.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c03902e80355c4057b7da7df0b36158e4b20f767
--- /dev/null
+++ b/app/graphql/mutations/award_emojis/toggle.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+  module AwardEmojis
+    class Toggle < Base
+      graphql_name 'ToggleAwardEmoji'
+
+      field :toggledOn,
+            GraphQL::BOOLEAN_TYPE,
+            null: false,
+            description: 'True when the emoji was awarded, false when it was removed'
+
+      def resolve(args)
+        awardable = authorized_find!(id: args[:awardable_id])
+
+        check_object_is_awardable!(awardable)
+
+        # TODO this will be handled by AwardEmoji::ToggleService
+        # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
+        # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
+        award = awardable.toggle_award_emoji(args[:name], current_user)
+
+        # Destroy returns a collection :(
+        award = award.first if award.is_a?(Array)
+
+        errors = errors_on_object(award)
+
+        toggled_on = awardable.awarded_emoji?(args[:name], current_user)
+
+        {
+          # For consistency with the AwardEmojis::Remove mutation, only return
+          # the AwardEmoji if it was created and not destroyed
+          award_emoji: (award if toggled_on),
+          errors: errors,
+          toggled_on: toggled_on
+        }
+      end
+    end
+  end
+end
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index eb03dfe16242f3a3f3c5412a7e98d0a94a9e5c7f..08d2a1f18a3e0337df5dbdec4b78c26b08ee48c1 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -2,6 +2,8 @@
 
 module Mutations
   class BaseMutation < GraphQL::Schema::RelayClassicMutation
+    prepend Gitlab::Graphql::CopyFieldDescription
+
     field :errors, [GraphQL::STRING_TYPE],
           null: false,
           description: "Reasons why the mutation failed."
@@ -9,5 +11,10 @@ class BaseMutation < GraphQL::Schema::RelayClassicMutation
     def current_user
       context[:current_user]
     end
+
+    # Returns Array of errors on an ActiveRecord object
+    def errors_on_object(record)
+      record.errors.full_messages
+    end
   end
 end
diff --git a/app/graphql/types/award_emojis/award_emoji_type.rb b/app/graphql/types/award_emojis/award_emoji_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8daf699a1126d9b81d999f5d7ee12af8ded00a9e
--- /dev/null
+++ b/app/graphql/types/award_emojis/award_emoji_type.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Types
+  module AwardEmojis
+    class AwardEmojiType < BaseObject
+      graphql_name 'AwardEmoji'
+
+      authorize :read_emoji
+
+      present_using AwardEmojiPresenter
+
+      field :name,
+            GraphQL::STRING_TYPE,
+            null: false,
+            description: 'The emoji name'
+
+      field :description,
+            GraphQL::STRING_TYPE,
+            null: false,
+            description: 'The emoji description'
+
+      field :unicode,
+            GraphQL::STRING_TYPE,
+            null: false,
+            description: 'The emoji in unicode'
+
+      field :emoji,
+            GraphQL::STRING_TYPE,
+            null: false,
+            description: 'The emoji as an icon'
+
+      field :unicode_version,
+            GraphQL::STRING_TYPE,
+            null: false,
+            description: 'The unicode version for this emoji'
+
+      field :user,
+            Types::UserType,
+            null: false,
+            description: 'The user who awarded the emoji',
+            resolve: -> (award_emoji, _args, _context) {
+              Gitlab::Graphql::Loaders::BatchModelLoader.new(User, award_emoji.user_id).find
+            }
+    end
+  end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 2b4ef299296250a0cd9420ec9745a214f30be0e7..6ef1d816b7c31015d9ec8bcec28b08cb8e403730 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -6,6 +6,9 @@ class MutationType < BaseObject
 
     graphql_name "Mutation"
 
+    mount_mutation Mutations::AwardEmojis::Add
+    mount_mutation Mutations::AwardEmojis::Remove
+    mount_mutation Mutations::AwardEmojis::Toggle
     mount_mutation Mutations::MergeRequests::SetWip
   end
 end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index f4fdac2558cafd4d2f0c187e3ea445b0157b759d..009314573444ecab75f11fa6c5936872ffcc4ed8 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -194,6 +194,10 @@ def spammable_entity_type
     'snippet'
   end
 
+  def to_ability_name
+    model_name.singular
+  end
+
   class << self
     # Searches for snippets with a matching title or file name.
     #
diff --git a/app/policies/award_emoji_policy.rb b/app/policies/award_emoji_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..21e382e24b31482bf680212a864874b3d09d0673
--- /dev/null
+++ b/app/policies/award_emoji_policy.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AwardEmojiPolicy < BasePolicy
+  delegate { @subject.awardable if DeclarativePolicy.has_policy?(@subject.awardable) }
+
+  condition(:can_read_awardable) do
+    can?(:"read_#{@subject.awardable.to_ability_name}")
+  end
+
+  rule { can_read_awardable }.enable :read_emoji
+end
diff --git a/app/presenters/award_emoji_presenter.rb b/app/presenters/award_emoji_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..98713855d35ddc894c85f8ef570ff1a5c3bcdb2f
--- /dev/null
+++ b/app/presenters/award_emoji_presenter.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class AwardEmojiPresenter < Gitlab::View::Presenter::Delegated
+  presents :award_emoji
+
+  def description
+    as_emoji['description']
+  end
+
+  def unicode
+    as_emoji['unicode']
+  end
+
+  def emoji
+    as_emoji['moji']
+  end
+
+  def unicode_version
+    Gitlab::Emoji.emoji_unicode_version(award_emoji.name)
+  end
+
+  private
+
+  def as_emoji
+    @emoji ||= Gitlab::Emoji.emojis[award_emoji.name] || {}
+  end
+end
diff --git a/changelogs/unreleased/62826-graphql-emoji-mutations.yml b/changelogs/unreleased/62826-graphql-emoji-mutations.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0c0aaedf844a82239e3b0b8b305747f10f3e2021
--- /dev/null
+++ b/changelogs/unreleased/62826-graphql-emoji-mutations.yml
@@ -0,0 +1,5 @@
+---
+title: GraphQL mutations for add, remove and toggle emoji
+merge_request: 29919
+author:
+type: added
diff --git a/lib/gitlab/graphql/copy_field_description.rb b/lib/gitlab/graphql/copy_field_description.rb
new file mode 100644
index 0000000000000000000000000000000000000000..edd73083ff26eb6e4fc8b2cea143478d8568246a
--- /dev/null
+++ b/lib/gitlab/graphql/copy_field_description.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Graphql
+    module CopyFieldDescription
+      extend ActiveSupport::Concern
+
+      class_methods do
+        # Returns the `description` for property of field `field_name` on type.
+        # This can be used to ensure, for example, that mutation argument descriptions
+        # are always identical to the corresponding query field descriptions.
+        #
+        # E.g.:
+        #   argument :name, GraphQL::STRING_TYPE, description: copy_field_description(Types::UserType, :name)
+        def copy_field_description(type, field_name)
+          type.fields[field_name.to_s.camelize(:lower)].description
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/graphql/errors.rb b/lib/gitlab/graphql/errors.rb
index fe74549e322bd7a9beb1c0313495ed1a1b243595..40b90310e8bc2fe2146630f1cb834361f06213cd 100644
--- a/lib/gitlab/graphql/errors.rb
+++ b/lib/gitlab/graphql/errors.rb
@@ -6,6 +6,7 @@ module Errors
       BaseError = Class.new(GraphQL::ExecutionError)
       ArgumentError = Class.new(BaseError)
       ResourceNotAvailable = Class.new(BaseError)
+      MutationError = Class.new(BaseError)
     end
   end
 end
diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb
index d37e2bf511e26ecc9c733ff32fd7bbbdfee1bc55..43753fa650ccdc70167966ccaed8e7e928674259 100644
--- a/spec/factories/award_emoji.rb
+++ b/spec/factories/award_emoji.rb
@@ -5,7 +5,7 @@
     awardable factory: :issue
 
     after(:create) do |award, evaluator|
-      award.awardable.project.add_guest(evaluator.user)
+      award.awardable.project&.add_guest(evaluator.user)
     end
 
     trait :upvote
diff --git a/spec/graphql/types/award_emojis/award_emoji_type_spec.rb b/spec/graphql/types/award_emojis/award_emoji_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5663a3d7195034cdf4c8dc7875088ce6b6e9c9ce
--- /dev/null
+++ b/spec/graphql/types/award_emojis/award_emoji_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['AwardEmoji'] do
+  it { expect(described_class.graphql_name).to eq('AwardEmoji') }
+
+  it { is_expected.to require_graphql_authorizations(:read_emoji) }
+
+  it { expect(described_class).to have_graphql_fields(:description, :unicode_version, :emoji, :name, :unicode, :user) }
+end
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
index 20842f55014faa339b21907610e9a53bf37962e5..50138d272c4a2264666e5a75bf7f839e2b749de0 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
@@ -67,7 +67,7 @@ def current_user
     end
 
     describe '#authorize!' do
-      it 'does not raise an error' do
+      it 'raises an error' do
         expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
       end
     end
diff --git a/spec/lib/gitlab/graphql/copy_field_description_spec.rb b/spec/lib/gitlab/graphql/copy_field_description_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e7462c5b9547c676c6e097f6ab88d96bc18cdbf1
--- /dev/null
+++ b/spec/lib/gitlab/graphql/copy_field_description_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::CopyFieldDescription do
+  subject { Class.new.include(described_class) }
+
+  describe '.copy_field_description' do
+    let(:type) do
+      Class.new(Types::BaseObject) do
+        graphql_name "TestType"
+
+        field :field_name, GraphQL::STRING_TYPE, null: true, description: 'Foo'
+      end
+    end
+
+    it 'returns the correct description' do
+      expect(subject.copy_field_description(type, :field_name)).to eq('Foo')
+    end
+  end
+end
diff --git a/spec/policies/award_emoji_policy_spec.rb b/spec/policies/award_emoji_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2e3693c58d7755e39e2378a586073bf07971ba65
--- /dev/null
+++ b/spec/policies/award_emoji_policy_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojiPolicy do
+  let(:user) { create(:user) }
+  let(:award_emoji) { create(:award_emoji, awardable: awardable) }
+
+  subject { described_class.new(user, award_emoji) }
+
+  shared_examples 'when the user can read the awardable' do
+    context do
+      let(:project) { create(:project, :public) }
+
+      it { expect_allowed(:read_emoji) }
+    end
+  end
+
+  shared_examples 'when the user cannot read the awardable' do
+    context do
+      let(:project) { create(:project, :private) }
+
+      it { expect_disallowed(:read_emoji) }
+    end
+  end
+
+  context 'when the awardable is an issue' do
+    let(:awardable) { create(:issue, project: project) }
+
+    include_examples 'when the user can read the awardable'
+    include_examples 'when the user cannot read the awardable'
+  end
+
+  context 'when the awardable is a merge request' do
+    let(:awardable) { create(:merge_request, source_project: project) }
+
+    include_examples 'when the user can read the awardable'
+    include_examples 'when the user cannot read the awardable'
+  end
+
+  context 'when the awardable is a note' do
+    let(:awardable) { create(:note_on_merge_request, project: project) }
+
+    include_examples 'when the user can read the awardable'
+    include_examples 'when the user cannot read the awardable'
+  end
+
+  context 'when the awardable is a snippet' do
+    let(:awardable) { create(:project_snippet, :public, project: project) }
+
+    include_examples 'when the user can read the awardable'
+    include_examples 'when the user cannot read the awardable'
+  end
+end
diff --git a/spec/presenters/award_emoji_presenter_spec.rb b/spec/presenters/award_emoji_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e2ada2a3c93e3603d6cafacf64e1988ef3e0989d
--- /dev/null
+++ b/spec/presenters/award_emoji_presenter_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojiPresenter do
+  let(:emoji_name) { 'thumbsup' }
+  let(:award_emoji) { build(:award_emoji, name: emoji_name) }
+  let(:presenter) { described_class.new(award_emoji) }
+
+  describe '#description' do
+    it { expect(presenter.description).to eq Gitlab::Emoji.emojis[emoji_name]['description'] }
+  end
+
+  describe '#unicode' do
+    it { expect(presenter.unicode).to eq Gitlab::Emoji.emojis[emoji_name]['unicode'] }
+  end
+
+  describe '#unicode_version' do
+    it { expect(presenter.unicode_version).to eq Gitlab::Emoji.emoji_unicode_version(emoji_name) }
+  end
+
+  describe '#emoji' do
+    it { expect(presenter.emoji).to eq Gitlab::Emoji.emojis[emoji_name]['moji'] }
+  end
+
+  describe 'when presenting an award emoji with an invalid name' do
+    let(:emoji_name) { 'invalid-name' }
+
+    it 'returns nil for all properties' do
+      expect(presenter.description).to be_nil
+      expect(presenter.emoji).to be_nil
+      expect(presenter.unicode).to be_nil
+      expect(presenter.unicode_version).to be_nil
+    end
+  end
+end
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3982125a38a9216c06c11a555cb7d4aaa1b293fe
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Adding an AwardEmoji' do
+  include GraphqlHelpers
+
+  let(:current_user) { create(:user) }
+  let(:awardable) { create(:note) }
+  let(:project) { awardable.project }
+  let(:emoji_name) { 'thumbsup' }
+  let(:mutation) do
+    variables = {
+      awardable_id: GitlabSchema.id_from_object(awardable).to_s,
+      name: emoji_name
+    }
+
+    graphql_mutation(:add_award_emoji, variables)
+  end
+
+  def mutation_response
+    graphql_mutation_response(:add_award_emoji)
+  end
+
+  shared_examples 'a mutation that does not create an AwardEmoji' do
+    it do
+      expect do
+        post_graphql_mutation(mutation, current_user: current_user)
+      end.not_to change { AwardEmoji.count }
+    end
+  end
+
+  context 'when the user does not have permission' do
+    it_behaves_like 'a mutation that does not create an AwardEmoji'
+
+    it_behaves_like 'a mutation that returns top-level errors',
+                    errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+  end
+
+  context 'when the user has permission' do
+    before do
+      project.add_developer(current_user)
+    end
+
+    context 'when the given awardable is not an Awardable' do
+      let(:awardable) { create(:label) }
+
+      it_behaves_like 'a mutation that does not create an AwardEmoji'
+
+      it_behaves_like 'a mutation that returns top-level errors',
+                      errors: ['Cannot award emoji to this resource']
+    end
+
+    context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do
+      let(:awardable) { create(:system_note) }
+
+      it_behaves_like 'a mutation that does not create an AwardEmoji'
+
+      it_behaves_like 'a mutation that returns top-level errors',
+                      errors: ['Cannot award emoji to this resource']
+    end
+
+    context 'when the given awardable an Awardable' do
+      it 'creates an emoji' do
+        expect do
+          post_graphql_mutation(mutation, current_user: current_user)
+        end.to change { AwardEmoji.count }.by(1)
+      end
+
+      it 'returns the emoji' do
+        post_graphql_mutation(mutation, current_user: current_user)
+
+        expect(mutation_response['awardEmoji']['name']).to eq(emoji_name)
+      end
+
+      context 'when there were active record validation errors' do
+        before do
+          expect_next_instance_of(AwardEmoji) do |award|
+            expect(award).to receive(:valid?).at_least(:once).and_return(false)
+            expect(award).to receive_message_chain(
+              :errors,
+              :full_messages
+            ).and_return(['Error 1', 'Error 2'])
+          end
+        end
+
+        it_behaves_like 'a mutation that does not create an AwardEmoji'
+
+        it_behaves_like 'a mutation that returns errors in the response', errors: ['Error 1', 'Error 2']
+
+        it 'returns an empty awardEmoji' do
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(mutation_response).to have_key('awardEmoji')
+          expect(mutation_response['awardEmoji']).to be_nil
+        end
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c78f0c7ca27df41ff4f0da8e77562e87afdc24fb
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Removing an AwardEmoji' do
+  include GraphqlHelpers
+
+  let(:current_user) { create(:user) }
+  let(:awardable) { create(:note) }
+  let(:project) { awardable.project }
+  let(:emoji_name) { 'thumbsup' }
+  let(:input) { { awardable_id: GitlabSchema.id_from_object(awardable).to_s, name: emoji_name } }
+
+  let(:mutation) do
+    graphql_mutation(:remove_award_emoji, input)
+  end
+
+  def mutation_response
+    graphql_mutation_response(:remove_award_emoji)
+  end
+
+  def create_award_emoji(user)
+    create(:award_emoji, name: emoji_name, awardable: awardable, user: user )
+  end
+
+  shared_examples 'a mutation that does not destroy an AwardEmoji' do
+    it do
+      expect do
+        post_graphql_mutation(mutation, current_user: current_user)
+      end.not_to change { AwardEmoji.count }
+    end
+  end
+
+  shared_examples 'a mutation that does not authorize the user' do
+    it_behaves_like 'a mutation that does not destroy an AwardEmoji'
+
+    it_behaves_like 'a mutation that returns top-level errors',
+                    errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+  end
+
+  context 'when the current_user does not own the award emoji' do
+    let!(:award_emoji) { create_award_emoji(create(:user)) }
+
+    it_behaves_like 'a mutation that does not authorize the user'
+  end
+
+  context 'when the current_user owns the award emoji' do
+    let!(:award_emoji) { create_award_emoji(current_user) }
+
+    context 'when the given awardable is not an Awardable' do
+      let(:awardable) { create(:label) }
+
+      it_behaves_like 'a mutation that does not destroy an AwardEmoji'
+
+      it_behaves_like 'a mutation that returns top-level errors',
+                      errors: ['Cannot award emoji to this resource']
+    end
+
+    context 'when the given awardable is an Awardable' do
+      it 'removes the emoji' do
+        expect do
+          post_graphql_mutation(mutation, current_user: current_user)
+        end.to change { AwardEmoji.count }.by(-1)
+      end
+
+      it 'returns no errors' do
+        post_graphql_mutation(mutation, current_user: current_user)
+
+        expect(graphql_errors).to be_nil
+      end
+
+      it 'returns an empty awardEmoji' do
+        post_graphql_mutation(mutation, current_user: current_user)
+
+        expect(mutation_response).to have_key('awardEmoji')
+        expect(mutation_response['awardEmoji']).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..31145730f109e0e1e91ca5a76fb2462915f07465
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Toggling an AwardEmoji' do
+  include GraphqlHelpers
+
+  let(:current_user) { create(:user) }
+  let(:awardable) { create(:note) }
+  let(:project) { awardable.project }
+  let(:emoji_name) { 'thumbsup' }
+  let(:mutation) do
+    variables = {
+      awardable_id: GitlabSchema.id_from_object(awardable).to_s,
+      name: emoji_name
+    }
+
+    graphql_mutation(:toggle_award_emoji, variables)
+  end
+
+  def mutation_response
+    graphql_mutation_response(:toggle_award_emoji)
+  end
+
+  shared_examples 'a mutation that does not create or destroy an AwardEmoji' do
+    it do
+      expect do
+        post_graphql_mutation(mutation, current_user: current_user)
+      end.not_to change { AwardEmoji.count }
+    end
+  end
+
+  def create_award_emoji(user)
+    create(:award_emoji, name: emoji_name, awardable: awardable, user: user )
+  end
+
+  context 'when the user has permission' do
+    before do
+      project.add_developer(current_user)
+    end
+
+    context 'when the given awardable is not an Awardable' do
+      let(:awardable) { create(:label) }
+
+      it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
+
+      it_behaves_like 'a mutation that returns top-level errors',
+                      errors: ['Cannot award emoji to this resource']
+    end
+
+    context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do
+      let(:awardable) { create(:system_note) }
+
+      it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
+
+      it_behaves_like 'a mutation that returns top-level errors',
+                      errors: ['Cannot award emoji to this resource']
+    end
+
+    context 'when the given awardable is an Awardable' do
+      context 'when no emoji has been awarded by the current_user yet' do
+        # Create an award emoji for another user. This therefore tests that
+        # toggling is correctly scoped to the user's emoji only.
+        let!(:award_emoji) { create_award_emoji(create(:user)) }
+
+        it 'creates an emoji' do
+          expect do
+            post_graphql_mutation(mutation, current_user: current_user)
+          end.to change { AwardEmoji.count }.by(1)
+        end
+
+        it 'returns the emoji' do
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(mutation_response['awardEmoji']['name']).to eq(emoji_name)
+        end
+
+        it 'returns toggledOn as true' do
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(mutation_response['toggledOn']).to eq(true)
+        end
+
+        context 'when there were active record validation errors' do
+          before do
+            expect_next_instance_of(AwardEmoji) do |award|
+              expect(award).to receive(:valid?).at_least(:once).and_return(false)
+              expect(award).to receive_message_chain(:errors, :full_messages).and_return(['Error 1', 'Error 2'])
+            end
+          end
+
+          it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
+
+          it_behaves_like 'a mutation that returns errors in the response', errors: ['Error 1', 'Error 2']
+
+          it 'returns an empty awardEmoji' do
+            post_graphql_mutation(mutation, current_user: current_user)
+
+            expect(mutation_response).to have_key('awardEmoji')
+            expect(mutation_response['awardEmoji']).to be_nil
+          end
+        end
+      end
+
+      context 'when an emoji has been awarded by the current_user' do
+        let!(:award_emoji) { create_award_emoji(current_user) }
+
+        it 'removes the emoji' do
+          expect do
+            post_graphql_mutation(mutation, current_user: current_user)
+          end.to change { AwardEmoji.count }.by(-1)
+        end
+
+        it 'returns no errors' do
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(graphql_errors).to be_nil
+        end
+
+        it 'returns an empty awardEmoji' do
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(mutation_response).to have_key('awardEmoji')
+          expect(mutation_response['awardEmoji']).to be_nil
+        end
+
+        it 'returns toggledOn as false' do
+          post_graphql_mutation(mutation, current_user: current_user)
+
+          expect(mutation_response['toggledOn']).to eq(false)
+        end
+      end
+    end
+  end
+
+  context 'when the user does not have permission' do
+    it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
+
+    it_behaves_like 'a mutation that returns top-level errors',
+                    errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+  end
+end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index bcf6669f37d4bf621209e4043c5000523f04d111..1a09d48f4cdebdf6dc0b4e8ecbf2308490c37676 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -4,10 +4,7 @@ module GraphqlHelpers
   # makes an underscored string look like a fieldname
   # "merge_request" => "mergeRequest"
   def self.fieldnamerize(underscored_field_name)
-    graphql_field_name = underscored_field_name.to_s.camelize
-    graphql_field_name[0] = graphql_field_name[0].downcase
-
-    graphql_field_name
+    underscored_field_name.to_s.camelize(:lower)
   end
 
   # Run a loader's named resolver
diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..022d41c0bdd1beadf158fc8c7e8082fe310efd9d
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# Shared example for expecting top-level errors.
+# See https://graphql-ruby.org/mutations/mutation_errors#raising-errors
+#
+#   { errors: [] }
+#
+# There must be a method or let called `mutation` defined that executes
+# the mutation.
+RSpec.shared_examples 'a mutation that returns top-level errors' do |errors:|
+  it do
+    post_graphql_mutation(mutation, current_user: current_user)
+
+    error_messages = graphql_errors.map { |e| e['message'] }
+
+    expect(error_messages).to eq(errors)
+  end
+end
+
+# Shared example for expecting schema-level errors.
+# See https://graphql-ruby.org/mutations/mutation_errors#errors-as-data
+#
+#   { data: { mutationName: { errors: [] } } }
+#
+# There must be:
+# - a method or let called `mutation` defined that executes the mutation
+# - a `mutation_response` method defined that returns the data of the mutation response.
+RSpec.shared_examples 'a mutation that returns errors in the response' do |errors:|
+  it do
+    post_graphql_mutation(mutation, current_user: current_user)
+
+    expect(mutation_response['errors']).to eq(errors)
+  end
+end