Commit 89f6584f authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch '63372-award-emoji-services' into 'master'

Add service classes for mutating AwardEmoji

Closes #63372

See merge request gitlab-org/gitlab-ce!29782
parents 9174d60b 926bf71e
...@@ -36,7 +36,7 @@ class AutocompleteController < ApplicationController ...@@ -36,7 +36,7 @@ class AutocompleteController < ApplicationController
end end
def award_emojis def award_emojis
render json: AwardedEmojiFinder.new(current_user).execute render json: AwardEmojis::CollectUserEmojiService.new(current_user).execute
end end
def merge_request_target_branches def merge_request_target_branches
......
...@@ -7,12 +7,9 @@ module ToggleAwardEmoji ...@@ -7,12 +7,9 @@ module ToggleAwardEmoji
authenticate_user! authenticate_user!
name = params.require(:name) name = params.require(:name)
if awardable.user_can_award?(current_user) service = AwardEmojis::ToggleService.new(awardable, name, current_user).execute
awardable.toggle_award_emoji(name, current_user)
todoable = to_todoable(awardable)
TodoService.new.new_award_emoji(todoable, current_user) if todoable
if service[:status] == :success
render json: { ok: true } render json: { ok: true }
else else
render json: { ok: false } render json: { ok: false }
...@@ -21,18 +18,6 @@ module ToggleAwardEmoji ...@@ -21,18 +18,6 @@ module ToggleAwardEmoji
private private
def to_todoable(awardable)
case awardable
when Note
# we don't create todos for personal snippet comments for now
awardable.for_personal_snippet? ? nil : awardable.noteable
when MergeRequest, Issue
awardable
when Snippet
nil
end
end
def awardable def awardable
raise NotImplementedError raise NotImplementedError
end end
......
# frozen_string_literal: true
class AwardEmojisFinder
attr_reader :awardable, :params
def initialize(awardable, params = {})
@awardable = awardable
@params = params
validate_params
end
def execute
awards = awardable.award_emoji
awards = by_name(awards)
awards = by_awarded_by(awards)
awards
end
private
def by_name(awards)
return awards unless params[:name]
awards.named(params[:name])
end
def by_awarded_by(awards)
return awards unless params[:awarded_by]
awards.awarded_by(params[:awarded_by])
end
def validate_params
return unless params.present?
validate_name_param
validate_awarded_by_param
end
def validate_name_param
return unless params[:name]
raise ArgumentError, 'Invalid name param' unless params[:name].in?(Gitlab::Emoji.emojis_names)
end
def validate_awarded_by_param
return unless params[:awarded_by]
# awarded_by can be a `User`, or an ID
unless params[:awarded_by].is_a?(User) || params[:awarded_by].to_s.match(/\A\d+\Z/)
raise ArgumentError, 'Invalid awarded_by param'
end
end
end
# frozen_string_literal: true
# Class for retrieving information about emoji awarded _by_ a particular user.
class AwardedEmojiFinder
attr_reader :current_user
# current_user - The User to generate the data for.
def initialize(current_user = nil)
@current_user = current_user
end
def execute
return [] unless current_user
# We want the resulting data set to be an Array containing the emoji names
# in descending order, based on how often they were awarded.
AwardEmoji
.award_counts_for_user(current_user)
.map { |name, _| { name: name } }
end
end
...@@ -10,14 +10,11 @@ module Mutations ...@@ -10,14 +10,11 @@ module Mutations
check_object_is_awardable!(awardable) check_object_is_awardable!(awardable)
# TODO this will be handled by AwardEmoji::AddService service = ::AwardEmojis::AddService.new(awardable, args[:name], current_user).execute
# 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?), award_emoji: (service[:award] if service[:status] == :success),
errors: errors_on_object(award) errors: service[:errors] || []
} }
end end
end end
......
...@@ -10,22 +10,11 @@ module Mutations ...@@ -10,22 +10,11 @@ module Mutations
check_object_is_awardable!(awardable) check_object_is_awardable!(awardable)
# TODO this check can be removed once AwardEmoji services are available. service = ::AwardEmojis::DestroyService.new(awardable, args[:name], current_user).execute
# 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 # Mutation response is always a `nil` award_emoji
errors: [] errors: service[:errors] || []
} }
end end
end end
......
...@@ -15,23 +15,15 @@ module Mutations ...@@ -15,23 +15,15 @@ module Mutations
check_object_is_awardable!(awardable) check_object_is_awardable!(awardable)
# TODO this will be handled by AwardEmoji::ToggleService service = ::AwardEmojis::ToggleService.new(awardable, args[:name], current_user).execute
# 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) toggled_on = awardable.awarded_emoji?(args[:name], current_user)
{ {
# For consistency with the AwardEmojis::Remove mutation, only return # For consistency with the AwardEmojis::Remove mutation, only return
# the AwardEmoji if it was created and not destroyed # the AwardEmoji if it was created and not destroyed
award_emoji: (award if toggled_on), award_emoji: (service[:award] if toggled_on),
errors: errors, errors: service[:errors] || [],
toggled_on: toggled_on toggled_on: toggled_on
} }
end end
......
...@@ -16,8 +16,10 @@ class AwardEmoji < ApplicationRecord ...@@ -16,8 +16,10 @@ class AwardEmoji < ApplicationRecord
participant :user participant :user
scope :downvotes, -> { where(name: DOWNVOTE_NAME) } scope :downvotes, -> { named(DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) } scope :upvotes, -> { named(UPVOTE_NAME) }
scope :named, -> (names) { where(name: names) }
scope :awarded_by, -> (users) { where(user: users) }
after_save :expire_etag_cache after_save :expire_etag_cache
after_destroy :expire_etag_cache after_destroy :expire_etag_cache
......
...@@ -106,30 +106,6 @@ module Awardable ...@@ -106,30 +106,6 @@ module Awardable
end end
def awarded_emoji?(emoji_name, current_user) def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists? award_emoji.named(emoji_name).awarded_by(current_user).exists?
end
def create_award_emoji(name, current_user)
return unless emoji_awardable?
award_emoji.create(name: normalize_name(name), user: current_user)
end
def remove_award_emoji(name, current_user)
award_emoji.where(name: name, user: current_user).destroy_all # rubocop: disable DestroyAll
end
def toggle_award_emoji(emoji_name, current_user)
if awarded_emoji?(emoji_name, current_user)
remove_award_emoji(emoji_name, current_user)
else
create_award_emoji(emoji_name, current_user)
end
end
private
def normalize_name(name)
Gitlab::Emoji.normalize_emoji_name(name)
end end
end end
# frozen_string_literal: true
module AwardEmojis
class AddService < AwardEmojis::BaseService
include Gitlab::Utils::StrongMemoize
def execute
unless awardable.user_can_award?(current_user)
return error('User cannot award emoji to awardable', status: :forbidden)
end
unless awardable.emoji_awardable?
return error('Awardable cannot be awarded emoji', status: :unprocessable_entity)
end
award = awardable.award_emoji.create(name: name, user: current_user)
if award.persisted?
TodoService.new.new_award_emoji(todoable, current_user) if todoable
success(award: award)
else
error(award.errors.full_messages, award: award)
end
end
private
def todoable
strong_memoize(:todoable) do
case awardable
when Note
# We don't create todos for personal snippet comments for now
awardable.noteable unless awardable.for_personal_snippet?
when MergeRequest, Issue
awardable
when Snippet
nil
end
end
end
end
end
# frozen_string_literal: true
module AwardEmojis
class BaseService < ::BaseService
attr_accessor :awardable, :name
def initialize(awardable, name, current_user)
@awardable = awardable
@name = normalize_name(name)
super(awardable.project, current_user)
end
private
def normalize_name(name)
Gitlab::Emoji.normalize_emoji_name(name)
end
# Provide more error state data than what BaseService allows.
# - An array of errors
# - The `AwardEmoji` if present
def error(errors, award: nil, status: nil)
errors = Array.wrap(errors)
super(errors.to_sentence.presence, status).merge({
award: award,
errors: errors
})
end
end
end
# frozen_string_literal: true
# Class for retrieving information about emoji awarded _by_ a particular user.
module AwardEmojis
class CollectUserEmojiService
attr_reader :current_user
# current_user - The User to generate the data for.
def initialize(current_user = nil)
@current_user = current_user
end
def execute
return [] unless current_user
# We want the resulting data set to be an Array containing the emoji names
# in descending order, based on how often they were awarded.
AwardEmoji
.award_counts_for_user(current_user)
.map { |name, _| { name: name } }
end
end
end
# frozen_string_literal: true
module AwardEmojis
class DestroyService < AwardEmojis::BaseService
def execute
unless awardable.user_can_award?(current_user)
return error('User cannot destroy emoji on the awardable', status: :forbidden)
end
awards = AwardEmojisFinder.new(awardable, name: name, awarded_by: current_user).execute
if awards.empty?
return error("User has not awarded emoji of type #{name} on the awardable", status: :forbidden)
end
award = awards.destroy_all.first # rubocop: disable DestroyAll
success(award: award)
end
end
end
# frozen_string_literal: true
module AwardEmojis
class ToggleService < AwardEmojis::BaseService
def execute
if awardable.awarded_emoji?(name, current_user)
DestroyService.new(awardable, name, current_user).execute
else
AddService.new(awardable, name, current_user).execute
end
end
end
end
...@@ -344,10 +344,7 @@ class IssuableBaseService < BaseService ...@@ -344,10 +344,7 @@ class IssuableBaseService < BaseService
def toggle_award(issuable) def toggle_award(issuable)
award = params.delete(:emoji_award) award = params.delete(:emoji_award)
if award AwardEmojis::ToggleService.new(issuable, award, current_user).execute if award
todo_service.new_award_emoji(issuable, current_user)
issuable.toggle_award_emoji(award, current_user)
end
end end
def associations_before_update(issuable) def associations_before_update(issuable)
......
require './spec/support/sidekiq' require './spec/support/sidekiq'
Gitlab::Seeder.quiet do Gitlab::Seeder.quiet do
emoji = Gitlab::Emoji.emojis.keys EMOJI = Gitlab::Emoji.emojis.keys
Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue| def seed_award_emoji(klass)
project = issue.project klass.order(Gitlab::Database.random).limit(klass.count / 2).each do |awardable|
awardable.project.authorized_users.where('project_authorizations.access_level > ?', Gitlab::Access::GUEST).sample(2).each do |user|
AwardEmojis::AddService.new(awardable, EMOJI.sample, user).execute
project.team.users.sample(2).each do |user| awardable.notes.user.sample(2).each do |note|
issue.create_award_emoji(emoji.sample, user) AwardEmojis::AddService.new(note, EMOJI.sample, user).execute
issue.notes.sample(2).each do |note|
next if note.system?
note.create_award_emoji(emoji.sample, user)
end end
print '.' print '.'
end end
end end
MergeRequest.order(Gitlab::Database.random).limit(MergeRequest.count / 2).each do |mr|
project = mr.project
project.team.users.sample(2).each do |user|
mr.create_award_emoji(emoji.sample, user)
mr.notes.sample(2).each do |note|
next if note.system?
note.create_award_emoji(emoji.sample, user)
end end
print '.' seed_award_emoji(Issue)
end seed_award_emoji(MergeRequest)
end
end end
...@@ -69,12 +69,12 @@ module API ...@@ -69,12 +69,12 @@ module API
post endpoint do post endpoint do
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user) service = AwardEmojis::AddService.new(awardable, params[:name], current_user).execute
if award.persisted? if service[:status] == :success
present award, with: Entities::AwardEmoji present service[:award], with: Entities::AwardEmoji
else else
not_found!("Award Emoji #{award.errors.messages}") not_found!("Award Emoji #{service[:message]}")
end end
end end
......
...@@ -1104,18 +1104,39 @@ describe Projects::IssuesController do ...@@ -1104,18 +1104,39 @@ describe Projects::IssuesController do
project.add_developer(user) project.add_developer(user)
end end
it "toggles the award emoji" do subject do
expect do
post(:toggle_award_emoji, params: { post(:toggle_award_emoji, params: {
namespace_id: project.namespace, namespace_id: project.namespace,
project_id: project, project_id: project,
id: issue.iid, id: issue.iid,
name: "thumbsup" name: emoji_name
}) })
end
let(:emoji_name) { 'thumbsup' }
it "toggles the award emoji" do
expect do
subject
end.to change { issue.award_emoji.count }.by(1) end.to change { issue.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it "removes the already awarded emoji" do
create(:award_emoji, awardable: issue, name: emoji_name, user: user)
expect { subject }.to change { AwardEmoji.count }.by(-1)
expect(response).to have_gitlab_http_status(200)
end
it 'marks Todos on the Issue as done' do
todo = create(:todo, target: issue, project: project, user: user)
subject
expect(todo.reload).to be_done
end
end end
describe 'POST create_merge_request' do describe 'POST create_merge_request' do
......
...@@ -543,23 +543,32 @@ describe Projects::NotesController do ...@@ -543,23 +543,32 @@ describe Projects::NotesController do
project.add_developer(user) project.add_developer(user)
end end
subject { post(:toggle_award_emoji, params: request_params.merge(name: emoji_name)) }
let(:emoji_name) { 'thumbsup' }
it "toggles the award emoji" do it "toggles the award emoji" do
expect do expect do
post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup")) subject
end.to change { note.award_emoji.count }.by(1) end.to change { note.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it "removes the already awarded emoji" do it "removes the already awarded emoji" do
post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup")) create(:award_emoji, awardable: note, name: emoji_name, user: user)
expect do expect { subject }.to change { AwardEmoji.count }.by(-1)
post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
end.to change { AwardEmoji.count }.by(-1)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it 'marks Todos on the Noteable as done' do
todo = create(:todo, target: note.noteable, project: project, user: user)
subject
expect(todo.reload).to be_done
end
end end
describe "resolving and unresolving" do describe "resolving and unresolving" do
......
...@@ -288,11 +288,13 @@ describe Snippets::NotesController do ...@@ -288,11 +288,13 @@ describe Snippets::NotesController do
describe 'POST toggle_award_emoji' do describe 'POST toggle_award_emoji' do
let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) } let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
let(:emoji_name) { 'thumbsup'}
before do before do
sign_in(user) sign_in(user)
end end
subject { post(:toggle_award_emoji, params: { snippet_id: public_snippet, id: note.id, name: "thumbsup" }) } subject { post(:toggle_award_emoji, params: { snippet_id: public_snippet, id: note.id, name: emoji_name }) }
it "toggles the award emoji" do it "toggles the award emoji" do
expect { subject }.to change { note.award_emoji.count }.by(1) expect { subject }.to change { note.award_emoji.count }.by(1)
...@@ -301,7 +303,7 @@ describe Snippets::NotesController do ...@@ -301,7 +303,7 @@ describe Snippets::NotesController do
end end
it "removes the already awarded emoji when it exists" do it "removes the already awarded emoji when it exists" do
note.toggle_award_emoji('thumbsup', user) # create award emoji before create(:award_emoji, awardable: note, name: emoji_name, user: user)
expect { subject }.to change { AwardEmoji.count }.by(-1) expect { subject }.to change { AwardEmoji.count }.by(-1)
......
# frozen_string_literal: true
require 'spec_helper'
describe AwardEmojisFinder do
set(:issue_1) { create(:issue) }
set(:issue_1_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_1) }
set(:issue_1_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_1) }
# Create a matching set of emoji for a second issue.
# These should never appear in our finder results
set(:issue_2) { create(:issue) }
set(:issue_2_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_2) }
set(:issue_2_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_2) }
describe 'param validation' do
it 'raises an error if `name` is invalid' do
expect { described_class.new(issue_1, { name: 'invalid' }).execute }.to raise_error(
ArgumentError,
'Invalid name param'
)
end
it 'raises an error if `awarded_by` is invalid' do
expectation = [ArgumentError, 'Invalid awarded_by param']
expect { described_class.new(issue_1, { awarded_by: issue_2 }).execute }.to raise_error(*expectation)
expect { described_class.new(issue_1, { awarded_by: 'not-an-id' }).execute }.to raise_error(*expectation)
expect { described_class.new(issue_1, { awarded_by: 1.123 }).execute }.to raise_error(*expectation)
end
end
describe '#execute' do
it 'scopes to the awardable' do
expect(described_class.new(issue_1).execute).to contain_exactly(
issue_1_thumbsup, issue_1_thumbsdown
)
end
it 'filters by emoji name' do
expect(described_class.new(issue_1, { name: 'thumbsup' }).execute).to contain_exactly(issue_1_thumbsup)
expect(described_class.new(issue_1, { name: '8ball' }).execute).to be_empty
end
it 'filters by user' do
expect(described_class.new(issue_1, { awarded_by: issue_1_thumbsup.user }).execute).to contain_exactly(issue_1_thumbsup)
expect(described_class.new(issue_1, { awarded_by: issue_2_thumbsup.user }).execute).to be_empty
end
end
end
...@@ -44,6 +44,29 @@ describe AwardEmoji do ...@@ -44,6 +44,29 @@ describe AwardEmoji do
end end
end end
describe 'scopes' do
set(:thumbsup) { create(:award_emoji, name: 'thumbsup') }
set(:thumbsdown) { create(:award_emoji, name: 'thumbsdown') }
describe '.upvotes' do
it { expect(described_class.upvotes).to contain_exactly(thumbsup) }
end
describe '.downvotes' do
it { expect(described_class.downvotes).to contain_exactly(thumbsdown) }
end
describe '.named' do
it { expect(described_class.named('thumbsup')).to contain_exactly(thumbsup) }
it { expect(described_class.named(%w[thumbsup thumbsdown])).to contain_exactly(thumbsup, thumbsdown) }
end
describe '.awarded_by' do
it { expect(described_class.awarded_by(thumbsup.user)).to contain_exactly(thumbsup) }
it { expect(described_class.awarded_by([thumbsup.user, thumbsdown.user])).to contain_exactly(thumbsup, thumbsdown) }
end
end
describe 'expiring ETag cache' do describe 'expiring ETag cache' do
context 'on a note' do context 'on a note' do
let(:note) { create(:note_on_issue) } let(:note) { create(:note_on_issue) }
......
...@@ -82,16 +82,6 @@ describe Awardable do ...@@ -82,16 +82,6 @@ describe Awardable do
end end
end end
describe "#toggle_award_emoji" do
it "adds an emoji if it isn't awarded yet" do
expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1)
end
it "toggles already awarded emoji" do
expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
end
end
describe 'querying award_emoji on an Awardable' do describe 'querying award_emoji on an Awardable' do
let(:issue) { create(:issue) } let(:issue) { create(:issue) }
......
...@@ -155,6 +155,14 @@ describe API::AwardEmoji do ...@@ -155,6 +155,14 @@ describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username) expect(json_response['user']['username']).to eq(user.username)
end end
it 'marks Todos on the Issue as done' do
todo = create(:todo, target: issue, project: project, user: user)
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), params: { name: '8ball' }
expect(todo.reload).to be_done
end
it "returns a 400 bad request error if the name is not given" do it "returns a 400 bad request error if the name is not given" do
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user) post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
...@@ -209,6 +217,14 @@ describe API::AwardEmoji do ...@@ -209,6 +217,14 @@ describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username) expect(json_response['user']['username']).to eq(user.username)
end end
it 'marks Todos on the Noteable as done' do
todo = create(:todo, target: note2.noteable, project: project, user: user)
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: 'rocket' }
expect(todo.reload).to be_done
end
it "normalizes +1 as thumbsup award" do it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: '+1' } post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: '+1' }
......
...@@ -5,9 +5,9 @@ require 'spec_helper' ...@@ -5,9 +5,9 @@ require 'spec_helper'
describe 'Adding an AwardEmoji' do describe 'Adding an AwardEmoji' do
include GraphqlHelpers include GraphqlHelpers
let(:current_user) { create(:user) } set(:current_user) { create(:user) }
let(:awardable) { create(:note) } set(:project) { create(:project) }
let(:project) { awardable.project } set(:awardable) { create(:note, project: project) }
let(:emoji_name) { 'thumbsup' } let(:emoji_name) { 'thumbsup' }
let(:mutation) do let(:mutation) do
variables = { variables = {
...@@ -43,7 +43,7 @@ describe 'Adding an AwardEmoji' do ...@@ -43,7 +43,7 @@ describe 'Adding an AwardEmoji' do
end end
context 'when the given awardable is not an Awardable' do context 'when the given awardable is not an Awardable' do
let(:awardable) { create(:label) } let(:awardable) { create(:label, project: project) }
it_behaves_like 'a mutation that does not create an AwardEmoji' it_behaves_like 'a mutation that does not create an AwardEmoji'
...@@ -52,7 +52,7 @@ describe 'Adding an AwardEmoji' do ...@@ -52,7 +52,7 @@ describe 'Adding an AwardEmoji' do
end end
context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do
let(:awardable) { create(:system_note) } let(:awardable) { create(:system_note, project: project) }
it_behaves_like 'a mutation that does not create an AwardEmoji' it_behaves_like 'a mutation that does not create an AwardEmoji'
...@@ -73,6 +73,13 @@ describe 'Adding an AwardEmoji' do ...@@ -73,6 +73,13 @@ describe 'Adding an AwardEmoji' do
expect(mutation_response['awardEmoji']['name']).to eq(emoji_name) expect(mutation_response['awardEmoji']['name']).to eq(emoji_name)
end end
describe 'marking Todos as done' do
let(:user) { current_user}
subject { post_graphql_mutation(mutation, current_user: user) }
include_examples 'creating award emojis marks Todos as done'
end
context 'when there were active record validation errors' do context 'when there were active record validation errors' do
before do before do
expect_next_instance_of(AwardEmoji) do |award| expect_next_instance_of(AwardEmoji) do |award|
......
...@@ -5,9 +5,9 @@ require 'spec_helper' ...@@ -5,9 +5,9 @@ require 'spec_helper'
describe 'Toggling an AwardEmoji' do describe 'Toggling an AwardEmoji' do
include GraphqlHelpers include GraphqlHelpers
let(:current_user) { create(:user) } set(:current_user) { create(:user) }
let(:awardable) { create(:note) } set(:project) { create(:project) }
let(:project) { awardable.project } set(:awardable) { create(:note, project: project) }
let(:emoji_name) { 'thumbsup' } let(:emoji_name) { 'thumbsup' }
let(:mutation) do let(:mutation) do
variables = { variables = {
...@@ -40,7 +40,7 @@ describe 'Toggling an AwardEmoji' do ...@@ -40,7 +40,7 @@ describe 'Toggling an AwardEmoji' do
end end
context 'when the given awardable is not an Awardable' do context 'when the given awardable is not an Awardable' do
let(:awardable) { create(:label) } let(:awardable) { create(:label, project: project) }
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji' it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
...@@ -49,7 +49,7 @@ describe 'Toggling an AwardEmoji' do ...@@ -49,7 +49,7 @@ describe 'Toggling an AwardEmoji' do
end end
context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do
let(:awardable) { create(:system_note) } let(:awardable) { create(:system_note, project: project) }
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji' it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
...@@ -81,6 +81,13 @@ describe 'Toggling an AwardEmoji' do ...@@ -81,6 +81,13 @@ describe 'Toggling an AwardEmoji' do
expect(mutation_response['toggledOn']).to eq(true) expect(mutation_response['toggledOn']).to eq(true)
end end
describe 'marking Todos as done' do
let(:user) { current_user}
subject { post_graphql_mutation(mutation, current_user: user) }
include_examples 'creating award emojis marks Todos as done'
end
context 'when there were active record validation errors' do context 'when there were active record validation errors' do
before do before do
expect_next_instance_of(AwardEmoji) do |award| expect_next_instance_of(AwardEmoji) do |award|
......
# frozen_string_literal: true
require 'spec_helper'
describe AwardEmojis::AddService do
set(:user) { create(:user) }
set(:project) { create(:project) }
set(:awardable) { create(:note, project: project) }
let(:name) { 'thumbsup' }
subject(:service) { described_class.new(awardable, name, user) }
describe '#execute' do
context 'when user is not authorized' do
it 'does not add an emoji' do
expect { service.execute }.not_to change { AwardEmoji.count }
end
it 'returns an error state' do
result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:forbidden)
end
end
context 'when user is authorized' do
before do
project.add_developer(user)
end
it 'creates an award emoji' do
expect { service.execute }.to change { AwardEmoji.count }.by(1)
end
it 'returns the award emoji' do
result = service.execute
expect(result[:award]).to be_kind_of(AwardEmoji)
end
it 'return a success status' do
result = service.execute
expect(result[:status]).to eq(:success)
end
it 'sets the correct properties on the award emoji' do
award = service.execute[:award]
expect(award.name).to eq(name)
expect(award.user).to eq(user)
end
describe 'marking Todos as done' do
subject { service.execute }
include_examples 'creating award emojis marks Todos as done'
end
context 'when the awardable cannot have emoji awarded to it' do
before do
expect(awardable).to receive(:emoji_awardable?).and_return(false)
end
it 'does not add an emoji' do
expect { service.execute }.not_to change { AwardEmoji.count }
end
it 'returns an error status' do
result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:unprocessable_entity)
end
end
context 'when the awardable is invalid' do
before do
expect_next_instance_of(AwardEmoji) do |award|
expect(award).to receive(:valid?).and_return(false)
expect(award).to receive_message_chain(:errors, :full_messages).and_return(['Error 1', 'Error 2'])
end
end
it 'does not add an emoji' do
expect { service.execute }.not_to change { AwardEmoji.count }
end
it 'returns an error status' do
result = service.execute
expect(result[:status]).to eq(:error)
end
it 'returns an error message' do
result = service.execute
expect(result[:message]).to eq('Error 1 and Error 2')
end
end
end
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe AwardedEmojiFinder do describe AwardEmojis::CollectUserEmojiService do
describe '#execute' do describe '#execute' do
it 'returns an Array containing the awarded emoji names' do it 'returns an Array containing the awarded emoji names' do
user = create(:user) user = create(:user)
......
# frozen_string_literal: true
require 'spec_helper'
describe AwardEmojis::DestroyService do
set(:user) { create(:user) }
set(:awardable) { create(:note) }
set(:project) { awardable.project }
let(:name) { 'thumbsup' }
let!(:award_from_other_user) do
create(:award_emoji, name: name, awardable: awardable, user: create(:user))
end
subject(:service) { described_class.new(awardable, name, user) }
describe '#execute' do
shared_examples_for 'a service that does not authorize the user' do |error:|
it 'does not remove the emoji' do
expect { service.execute }.not_to change { AwardEmoji.count }
end
it 'returns an error state' do
result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:forbidden)
end
it 'returns a nil award' do
result = service.execute
expect(result).to have_key(:award)
expect(result[:award]).to be_nil
end
it 'returns the error' do
result = service.execute
expect(result[:message]).to eq(error)
expect(result[:errors]).to eq([error])
end
end
context 'when user is not authorized' do
it_behaves_like 'a service that does not authorize the user',
error: 'User cannot destroy emoji on the awardable'
end
context 'when the user is authorized' do
before do
project.add_developer(user)
end
context 'when user has not awarded an emoji to the awardable' do
let!(:award_from_user) { create(:award_emoji, name: name, user: user) }
it_behaves_like 'a service that does not authorize the user',
error: 'User has not awarded emoji of type thumbsup on the awardable'
end
context 'when user has awarded an emoji to the awardable' do
let!(:award_from_user) { create(:award_emoji, name: name, awardable: awardable, user: user) }
it 'removes the emoji' do
expect { service.execute }.to change { AwardEmoji.count }.by(-1)
end
it 'returns a success status' do
result = service.execute
expect(result[:status]).to eq(:success)
end
it 'returns no errors' do
result = service.execute
expect(result).not_to have_key(:error)
expect(result).not_to have_key(:errors)
end
it 'returns the destroyed award' do
result = service.execute
expect(result[:award]).to eq(award_from_user)
expect(result[:award]).to be_destroyed
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AwardEmojis::ToggleService do
set(:user) { create(:user) }
set(:project) { create(:project, :public) }
set(:awardable) { create(:note, project: project) }
let(:name) { 'thumbsup' }
subject(:service) { described_class.new(awardable, name, user) }
describe '#execute' do
context 'when user has awarded an emoji' do
let!(:award_from_other_user) { create(:award_emoji, name: name, awardable: awardable, user: create(:user)) }
let!(:award) { create(:award_emoji, name: name, awardable: awardable, user: user) }
it 'calls AwardEmojis::DestroyService' do
expect(AwardEmojis::AddService).not_to receive(:new)
expect_next_instance_of(AwardEmojis::DestroyService) do |service|
expect(service).to receive(:execute)
end
service.execute
end
it 'destroys an AwardEmoji' do
expect { service.execute }.to change { AwardEmoji.count }.by(-1)
end
it 'returns the result of DestroyService#execute' do
mock_result = double(foo: true)
expect_next_instance_of(AwardEmojis::DestroyService) do |service|
expect(service).to receive(:execute).and_return(mock_result)
end
result = service.execute
expect(result).to eq(mock_result)
end
end
context 'when user has not awarded an emoji' do
it 'calls AwardEmojis::AddService' do
expect_next_instance_of(AwardEmojis::AddService) do |service|
expect(service).to receive(:execute)
end
expect(AwardEmojis::DestroyService).not_to receive(:new)
service.execute
end
it 'creates an AwardEmoji' do
expect { service.execute }.to change { AwardEmoji.count }.by(1)
end
it 'returns the result of AddService#execute' do
mock_result = double(foo: true)
expect_next_instance_of(AwardEmojis::AddService) do |service|
expect(service).to receive(:execute).and_return(mock_result)
end
result = service.execute
expect(result).to eq(mock_result)
end
end
end
end
...@@ -78,6 +78,7 @@ describe Projects::CreateService, '#execute' do ...@@ -78,6 +78,7 @@ describe Projects::CreateService, '#execute' do
expect(project).to be_valid expect(project).to be_valid
expect(project.owner).to eq(group) expect(project.owner).to eq(group)
expect(project.namespace).to eq(group) expect(project.namespace).to eq(group)
expect(project.team.owners).to include(user)
expect(user.authorized_projects).to include(project) expect(user.authorized_projects).to include(project)
end end
end end
......
# frozen_string_literal: true
# Shared examples to that test code that creates AwardEmoji also mark Todos
# as done.
#
# The examples expect these to be defined in the calling spec:
# - `subject` the callable code that executes the creation of an AwardEmoji
# - `user`
# - `project`
RSpec.shared_examples 'creating award emojis marks Todos as done' do
using RSpec::Parameterized::TableSyntax
before do
project.add_developer(user)
end
where(:type, :expectation) do
:issue | true
:merge_request | true
:project_snippet | false
end
with_them do
let(:project) { awardable.project }
let(:awardable) { create(type) }
let!(:todo) { create(:todo, target: awardable, project: project, user: user) }
it do
subject
expect(todo.reload.done?).to eq(expectation)
end
end
# Notes have more complicated rules than other Todoables
describe 'for notes' do
let!(:todo) { create(:todo, target: awardable.noteable, project: project, user: user) }
context 'regular Notes' do
let(:awardable) { create(:note, project: project) }
it 'marks the Todo as done' do
subject
expect(todo.reload.done?).to eq(true)
end
end
context 'PersonalSnippet Notes' do
let(:awardable) { create(:note, noteable: create(:personal_snippet, author: user)) }
it 'does not mark the Todo as done' do
subject
expect(todo.reload.done?).to eq(false)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment