Skip to content
Snippets Groups Projects
Commit 4b9b2a43 authored by Luke Duncalfe's avatar Luke Duncalfe
Browse files

GraphQL mutations for add, remove and toggle emoji

Adding new `AddAwardEmoji`, `RemoveAwardEmoji` and `ToggleAwardEmoji`
GraphQL mutations.

Adding new `#authorized_find_with_pre_checks!` and (unused, but for
completeness `#authorized_find_with_post_checks!`) authorization
methods. These allow us to perform an authorized find, and run our own
additional checks before or after the authorization runs.

https://gitlab.com/gitlab-org/gitlab-ce/issues/62826
parent 62a40c51
No related branches found
No related tags found
No related merge requests found
Showing
with 388 additions and 2 deletions
# 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
# 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
# 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
# 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
Loading
Loading
@@ -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."
Loading
Loading
@@ -9,5 +11,10 @@ module Mutations
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
# 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
Loading
Loading
@@ -6,6 +6,9 @@ module Types
 
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
Loading
Loading
@@ -194,6 +194,10 @@ class Snippet < ApplicationRecord
'snippet'
end
 
def to_ability_name
model_name.singular
end
class << self
# Searches for snippets with a matching title or file name.
#
Loading
Loading
# 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
# 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
---
title: GraphQL mutations for add, remove and toggle emoji
merge_request: 29919
author:
type: added
# 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
Loading
Loading
@@ -6,6 +6,7 @@ module Gitlab
BaseError = Class.new(GraphQL::ExecutionError)
ArgumentError = Class.new(BaseError)
ResourceNotAvailable = Class.new(BaseError)
MutationError = Class.new(BaseError)
end
end
end
Loading
Loading
@@ -5,7 +5,7 @@ FactoryBot.define do
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
Loading
Loading
# 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
Loading
Loading
@@ -67,7 +67,7 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
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
Loading
Loading
# 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
# 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
# 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment