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

Add service classes for mutating AwardEmoji

Closes #63372

See merge request gitlab-org/gitlab-ce!29782
This commit is contained in:
Grzegorz Bizon 2019-08-21 09:38:40 +00:00
commit 89f6584fb3
32 changed files with 700 additions and 162 deletions

View File

@ -36,7 +36,7 @@ class AutocompleteController < ApplicationController
end
def award_emojis
render json: AwardedEmojiFinder.new(current_user).execute
render json: AwardEmojis::CollectUserEmojiService.new(current_user).execute
end
def merge_request_target_branches

View File

@ -7,12 +7,9 @@ module ToggleAwardEmoji
authenticate_user!
name = params.require(:name)
if awardable.user_can_award?(current_user)
awardable.toggle_award_emoji(name, current_user)
todoable = to_todoable(awardable)
TodoService.new.new_award_emoji(todoable, current_user) if todoable
service = AwardEmojis::ToggleService.new(awardable, name, current_user).execute
if service[:status] == :success
render json: { ok: true }
else
render json: { ok: false }
@ -21,18 +18,6 @@ module ToggleAwardEmoji
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
raise NotImplementedError
end

View File

@ -0,0 +1,55 @@
# 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

View File

@ -1,21 +0,0 @@
# 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

View File

@ -10,14 +10,11 @@ module Mutations
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)
service = ::AwardEmojis::AddService.new(awardable, args[:name], current_user).execute
{
award_emoji: (award if award.persisted?),
errors: errors_on_object(award)
award_emoji: (service[:award] if service[:status] == :success),
errors: service[:errors] || []
}
end
end

View File

@ -10,22 +10,11 @@ module Mutations
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)
service = ::AwardEmojis::DestroyService.new(awardable, args[:name], current_user).execute
{
# Mutation response is always a `nil` award_emoji
errors: []
errors: service[:errors] || []
}
end
end

View File

@ -15,23 +15,15 @@ module Mutations
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)
service = ::AwardEmojis::ToggleService.new(awardable, args[:name], current_user).execute
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,
award_emoji: (service[:award] if toggled_on),
errors: service[:errors] || [],
toggled_on: toggled_on
}
end

View File

@ -16,8 +16,10 @@ class AwardEmoji < ApplicationRecord
participant :user
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
scope :downvotes, -> { named(DOWNVOTE_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_destroy :expire_etag_cache

View File

@ -106,30 +106,6 @@ module Awardable
end
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: 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)
award_emoji.named(emoji_name).awarded_by(current_user).exists?
end
end

View File

@ -0,0 +1,42 @@
# 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

View File

@ -0,0 +1,32 @@
# 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

View File

@ -0,0 +1,23 @@
# 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

View File

@ -0,0 +1,21 @@
# 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

View File

@ -0,0 +1,13 @@
# 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

View File

@ -344,10 +344,7 @@ class IssuableBaseService < BaseService
def toggle_award(issuable)
award = params.delete(:emoji_award)
if award
todo_service.new_award_emoji(issuable, current_user)
issuable.toggle_award_emoji(award, current_user)
end
AwardEmojis::ToggleService.new(issuable, award, current_user).execute if award
end
def associations_before_update(issuable)

View File

@ -1,35 +1,22 @@
require './spec/support/sidekiq'
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|
project = issue.project
def seed_award_emoji(klass)
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|
issue.create_award_emoji(emoji.sample, user)
awardable.notes.user.sample(2).each do |note|
AwardEmojis::AddService.new(note, EMOJI.sample, user).execute
end
issue.notes.sample(2).each do |note|
next if note.system?
note.create_award_emoji(emoji.sample, user)
print '.'
end
print '.'
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
print '.'
end
end
seed_award_emoji(Issue)
seed_award_emoji(MergeRequest)
end

View File

@ -69,12 +69,12 @@ module API
post endpoint do
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?
present award, with: Entities::AwardEmoji
if service[:status] == :success
present service[:award], with: Entities::AwardEmoji
else
not_found!("Award Emoji #{award.errors.messages}")
not_found!("Award Emoji #{service[:message]}")
end
end

View File

@ -1104,18 +1104,39 @@ describe Projects::IssuesController do
project.add_developer(user)
end
subject do
post(:toggle_award_emoji, params: {
namespace_id: project.namespace,
project_id: project,
id: issue.iid,
name: emoji_name
})
end
let(:emoji_name) { 'thumbsup' }
it "toggles the award emoji" do
expect do
post(:toggle_award_emoji, params: {
namespace_id: project.namespace,
project_id: project,
id: issue.iid,
name: "thumbsup"
})
subject
end.to change { issue.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200)
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
describe 'POST create_merge_request' do

View File

@ -543,23 +543,32 @@ describe Projects::NotesController do
project.add_developer(user)
end
subject { post(:toggle_award_emoji, params: request_params.merge(name: emoji_name)) }
let(:emoji_name) { 'thumbsup' }
it "toggles the award emoji" do
expect do
post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
subject
end.to change { note.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200)
end
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
post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
end.to change { AwardEmoji.count }.by(-1)
expect { subject }.to change { AwardEmoji.count }.by(-1)
expect(response).to have_gitlab_http_status(200)
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
describe "resolving and unresolving" do

View File

@ -288,11 +288,13 @@ describe Snippets::NotesController do
describe 'POST toggle_award_emoji' do
let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
let(:emoji_name) { 'thumbsup'}
before do
sign_in(user)
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
expect { subject }.to change { note.award_emoji.count }.by(1)
@ -301,7 +303,7 @@ describe Snippets::NotesController do
end
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)

View File

@ -0,0 +1,49 @@
# 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

View File

@ -44,6 +44,29 @@ describe AwardEmoji do
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
context 'on a note' do
let(:note) { create(:note_on_issue) }

View File

@ -82,16 +82,6 @@ describe Awardable do
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
let(:issue) { create(:issue) }

View File

@ -155,6 +155,14 @@ describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username)
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
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
@ -209,6 +217,14 @@ describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username)
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
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: '+1' }

View File

@ -5,9 +5,9 @@ require 'spec_helper'
describe 'Adding an AwardEmoji' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:awardable) { create(:note) }
let(:project) { awardable.project }
set(:current_user) { create(:user) }
set(:project) { create(:project) }
set(:awardable) { create(:note, project: project) }
let(:emoji_name) { 'thumbsup' }
let(:mutation) do
variables = {
@ -43,7 +43,7 @@ describe 'Adding an AwardEmoji' do
end
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'
@ -52,7 +52,7 @@ describe 'Adding an AwardEmoji' do
end
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'
@ -73,6 +73,13 @@ describe 'Adding an AwardEmoji' do
expect(mutation_response['awardEmoji']['name']).to eq(emoji_name)
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
before do
expect_next_instance_of(AwardEmoji) do |award|

View File

@ -5,9 +5,9 @@ require 'spec_helper'
describe 'Toggling an AwardEmoji' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:awardable) { create(:note) }
let(:project) { awardable.project }
set(:current_user) { create(:user) }
set(:project) { create(:project) }
set(:awardable) { create(:note, project: project) }
let(:emoji_name) { 'thumbsup' }
let(:mutation) do
variables = {
@ -40,7 +40,7 @@ describe 'Toggling an AwardEmoji' do
end
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'
@ -49,7 +49,7 @@ describe 'Toggling an AwardEmoji' do
end
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'
@ -81,6 +81,13 @@ describe 'Toggling an AwardEmoji' do
expect(mutation_response['toggledOn']).to eq(true)
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
before do
expect_next_instance_of(AwardEmoji) do |award|

View File

@ -0,0 +1,103 @@
# 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

View File

@ -2,7 +2,7 @@
require 'spec_helper'
describe AwardedEmojiFinder do
describe AwardEmojis::CollectUserEmojiService do
describe '#execute' do
it 'returns an Array containing the awarded emoji names' do
user = create(:user)

View File

@ -0,0 +1,89 @@
# 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

View File

@ -0,0 +1,72 @@
# 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

View File

@ -78,6 +78,7 @@ describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.owner).to eq(group)
expect(project.namespace).to eq(group)
expect(project.team.owners).to include(user)
expect(user.authorized_projects).to include(project)
end
end

View File

@ -0,0 +1,59 @@
# 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