diff --git a/app/graphql/mutations/merge_requests/remove_attention_request.rb b/app/graphql/mutations/merge_requests/remove_attention_request.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab8617edb36ff2b468aa50dea0d0087ed8f67e37
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/remove_attention_request.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class RemoveAttentionRequest < Base
+ graphql_name 'MergeRequestRemoveAttentionRequest'
+
+ argument :user_id, ::Types::GlobalIDType[::User],
+ loads: Types::UserType,
+ required: true,
+ description: <<~DESC
+ User ID of the user for attention request removal.
+ DESC
+
+ def resolve(project_path:, iid:, user:)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+
+ result = ::MergeRequests::RemoveAttentionRequestedService.new(
+ project: merge_request.project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: user
+ ).execute
+
+ {
+ merge_request: merge_request,
+ errors: Array(result[:message])
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/request_attention.rb b/app/graphql/mutations/merge_requests/request_attention.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9a9f2f4cd435b656e9bb0a465819b8abd3586787
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/request_attention.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class RequestAttention < Base
+ graphql_name 'MergeRequestRequestAttention'
+
+ argument :user_id, ::Types::GlobalIDType[::User],
+ loads: Types::UserType,
+ required: true,
+ description: <<~DESC
+ User ID of the user to request attention.
+ DESC
+
+ def resolve(project_path:, iid:, user:)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+
+ result = ::MergeRequests::RequestAttentionService.new(
+ project: merge_request.project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: user
+ ).execute
+
+ {
+ merge_request: merge_request,
+ errors: Array(result[:message])
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 47d58e5dc60f9fa20cb44abcdc3016728b5abd7b..d6bfe6bf052dbd3a2b2783fae0511d1a0b015ee0 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -69,6 +69,8 @@ class MutationType < BaseObject
mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::MergeRequests::ReviewerRereview
+ mount_mutation Mutations::MergeRequests::RequestAttention, feature_flag: :mr_attention_requests
+ mount_mutation Mutations::MergeRequests::RemoveAttentionRequest, feature_flag: :mr_attention_requests
mount_mutation Mutations::MergeRequests::ToggleAttentionRequested, feature_flag: :mr_attention_requests
mount_mutation Mutations::Metrics::Dashboard::Annotations::Create
mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index d197c13378ae43e27cb32fe621bcc8d3d8d829a6..8d06a1020c423c993f8d9e14c622b6d3596db782 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -256,7 +256,7 @@ def remove_all_attention_requests(merge_request)
def remove_attention_requested(merge_request)
return unless merge_request.attention_requested_enabled?
- ::MergeRequests::RemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request).execute
+ ::MergeRequests::RemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request, user: current_user).execute
end
end
end
diff --git a/app/services/merge_requests/remove_attention_requested_service.rb b/app/services/merge_requests/remove_attention_requested_service.rb
index a32a80714710cdd53f071c19288768b4596e001a..8a410fda69184c7c09408ced917918d7b6853e76 100644
--- a/app/services/merge_requests/remove_attention_requested_service.rb
+++ b/app/services/merge_requests/remove_attention_requested_service.rb
@@ -2,22 +2,26 @@
module MergeRequests
class RemoveAttentionRequestedService < MergeRequests::BaseService
- attr_accessor :merge_request
+ attr_accessor :merge_request, :user
- def initialize(project:, current_user:, merge_request:)
+ def initialize(project:, current_user:, merge_request:, user:)
super(project: project, current_user: current_user)
@merge_request = merge_request
+ @user = user
end
def execute
return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
if reviewer || assignee
+ return success if reviewer&.reviewed? || assignee&.reviewed?
+
update_state(reviewer)
update_state(assignee)
- current_user.invalidate_attention_requested_count
+ user.invalidate_attention_requested_count
+ create_remove_attention_request_note
success
else
@@ -28,15 +32,19 @@ def execute
private
def assignee
- merge_request.find_assignee(current_user)
+ @assignee ||= merge_request.find_assignee(user)
end
def reviewer
- merge_request.find_reviewer(current_user)
+ @reviewer ||= merge_request.find_reviewer(user)
end
def update_state(reviewer_or_assignee)
reviewer_or_assignee&.update(state: :reviewed)
end
+
+ def create_remove_attention_request_note
+ SystemNoteService.remove_attention_request(merge_request, merge_request.project, current_user, user)
+ end
end
end
diff --git a/app/services/merge_requests/request_attention_service.rb b/app/services/merge_requests/request_attention_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..07e9996f87b134bdbb9e10482b94faf36bc11602
--- /dev/null
+++ b/app/services/merge_requests/request_attention_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class RequestAttentionService < MergeRequests::BaseService
+ attr_accessor :merge_request, :user
+
+ def initialize(project:, current_user:, merge_request:, user:)
+ super(project: project, current_user: current_user)
+
+ @merge_request = merge_request
+ @user = user
+ end
+
+ def execute
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ if reviewer || assignee
+ return success if reviewer&.attention_requested? || assignee&.attention_requested?
+
+ update_state(reviewer)
+ update_state(assignee)
+
+ user.invalidate_attention_requested_count
+ create_attention_request_note
+ notity_user
+
+ if current_user.id != user.id
+ remove_attention_requested(merge_request)
+ end
+
+ success
+ else
+ error("User is not a reviewer or assignee of the merge request")
+ end
+ end
+
+ private
+
+ def notity_user
+ notification_service.async.attention_requested_of_merge_request(merge_request, current_user, user)
+ todo_service.create_attention_requested_todo(merge_request, current_user, user)
+ end
+
+ def create_attention_request_note
+ SystemNoteService.request_attention(merge_request, merge_request.project, current_user, user)
+ end
+
+ def assignee
+ @assignee ||= merge_request.find_assignee(user)
+ end
+
+ def reviewer
+ @reviewer ||= merge_request.find_reviewer(user)
+ end
+
+ def update_state(reviewer_or_assignee)
+ reviewer_or_assignee&.update(state: :attention_requested, updated_state_by: current_user)
+ end
+ end
+end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index b88e52456c1f4a01b3372ab43d2b0cb11ad6a0cf..34e1e6f73972775b4ca919bf4f5e24d11d8ae28d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3459,6 +3459,52 @@ Input type: `MergeRequestCreateInput`
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| `mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. |
+### `Mutation.mergeRequestRemoveAttentionRequest`
+
+Available only when feature flag `mr_attention_requests` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.
+
+Input type: `MergeRequestRemoveAttentionRequestInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `iid` | [`String!`](#string) | IID of the merge request to mutate. |
+| `projectPath` | [`ID!`](#id) | Project the merge request to mutate is in. |
+| `userId` | [`UserID!`](#userid) | User ID of the user for attention request removal. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. |
+
+### `Mutation.mergeRequestRequestAttention`
+
+Available only when feature flag `mr_attention_requests` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.
+
+Input type: `MergeRequestRequestAttentionInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `iid` | [`String!`](#string) | IID of the merge request to mutate. |
+| `projectPath` | [`ID!`](#id) | Project the merge request to mutate is in. |
+| `userId` | [`UserID!`](#userid) | User ID of the user to request attention. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. |
+
### `Mutation.mergeRequestReviewerRereview`
Input type: `MergeRequestReviewerRereviewInput`
diff --git a/spec/requests/api/graphql/mutations/merge_requests/request_attention_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/request_attention_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e96f8ce55395d775493d1e243d132aa739a58ec7
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/request_attention_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Request attention' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request, reviewers: [user]) }
+ let_it_be(:project) { merge_request.project }
+
+ let(:input) { { user_id: global_id_of(user) } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: merge_request.iid.to_s
+ }
+ graphql_mutation(:merge_request_request_attention, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_request_attention)
+ end
+
+ def mutation_errors
+ mutation_response['errors']
+ end
+
+ before_all do
+ project.add_developer(current_user)
+ project.add_developer(user)
+ end
+
+ it 'is successful' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_errors).to be_empty
+ end
+
+ context 'when current user is not allowed to update the merge request' do
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+ end
+
+ context 'when user is not a reviewer' do
+ let(:input) { { user_id: global_id_of(create(:user)) } }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_errors).not_to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/remove_attention_request_spec.rb b/spec/requests/api/graphql/mutations/remove_attention_request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0a7ced1a30ce18946748d366aad1bda87fe94bb4
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/remove_attention_request_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Remove attention request' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request, reviewers: [user]) }
+ let_it_be(:project) { merge_request.project }
+
+ let(:input) { { user_id: global_id_of(user) } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: merge_request.iid.to_s
+ }
+ graphql_mutation(:merge_request_remove_attention_request, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_remove_attention_request)
+ end
+
+ def mutation_errors
+ mutation_response['errors']
+ end
+
+ before_all do
+ project.add_developer(current_user)
+ project.add_developer(user)
+ end
+
+ it 'is successful' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_errors).to be_empty
+ end
+
+ context 'when current user is not allowed to update the merge request' do
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+ end
+
+ context 'when user is not a reviewer' do
+ let(:input) { { user_id: global_id_of(create(:user)) } }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_errors).not_to be_empty
+ end
+ end
+end
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index 9b064da44b845f2f798d57b73c24821b121c986b..4d20d62b864403d01be17712d7e1ce948428dbf8 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -61,7 +61,7 @@
it 'removes attention requested state' do
expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: user, merge_request: merge_request)
+ .with(project: project, current_user: user, merge_request: merge_request, user: user)
.and_call_original
service.execute(merge_request)
diff --git a/spec/services/merge_requests/handle_assignees_change_service_spec.rb b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
index 26f53f55b0fcfd59ee8438ff956346a7b7b4b7af..34c598e6cc0830117e72445b1848e1283c71bc63 100644
--- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb
+++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
@@ -89,7 +89,7 @@ def execute
it 'removes attention requested state' do
expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: user, merge_request: merge_request)
+ .with(project: project, current_user: user, merge_request: merge_request, user: user)
.and_call_original
execute
diff --git a/spec/services/merge_requests/remove_attention_requested_service_spec.rb b/spec/services/merge_requests/remove_attention_requested_service_spec.rb
index 450204ebfdde988fb5cc3236ff474974361b3ba8..576049b9f1b58ce3ef4cd661af7259f393580348 100644
--- a/spec/services/merge_requests/remove_attention_requested_service_spec.rb
+++ b/spec/services/merge_requests/remove_attention_requested_service_spec.rb
@@ -3,64 +3,112 @@
require 'spec_helper'
RSpec.describe MergeRequests::RemoveAttentionRequestedService do
- let(:current_user) { create(:user) }
- let(:merge_request) { create(:merge_request, reviewers: [current_user], assignees: [current_user]) }
- let(:reviewer) { merge_request.find_reviewer(current_user) }
- let(:assignee) { merge_request.find_assignee(current_user) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:assignee_user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
+
+ let(:reviewer) { merge_request.find_reviewer(user) }
+ let(:assignee) { merge_request.find_assignee(assignee_user) }
let(:project) { merge_request.project }
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request) }
+
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: user
+ )
+ end
+
let(:result) { service.execute }
before do
+ allow(SystemNoteService).to receive(:remove_attention_request)
+
project.add_developer(current_user)
+ project.add_developer(user)
end
describe '#execute' do
- context 'invalid permissions' do
- let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request) }
+ context 'when current user cannot update merge request' do
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: create(:user),
+ merge_request: merge_request,
+ user: user
+ )
+ end
it 'returns an error' do
expect(result[:status]).to eq :error
end
end
- context 'reviewer does not exist' do
- let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request) }
+ context 'when user is not a reviewer nor assignee' do
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: create(:user)
+ )
+ end
it 'returns an error' do
expect(result[:status]).to eq :error
end
end
- context 'reviewer exists' do
+ context 'when user is a reviewer' do
+ before do
+ reviewer.update!(state: :attention_requested)
+ end
+
it 'returns success' do
expect(result[:status]).to eq :success
end
- it 'updates reviewers state' do
+ it 'updates reviewer state' do
service.execute
reviewer.reload
expect(reviewer.state).to eq 'reviewed'
end
+ it 'creates a remove attention request system note' do
+ expect(SystemNoteService)
+ .to receive(:remove_attention_request)
+ .with(merge_request, merge_request.project, current_user, user)
+
+ service.execute
+ end
+
it_behaves_like 'invalidates attention request cache' do
- let(:users) { [current_user] }
+ let(:users) { [user] }
end
end
- context 'assignee exists' do
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request) }
+ context 'when user is an assignee' do
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: assignee_user
+ )
+ end
before do
- assignee.update!(state: :reviewed)
+ assignee.update!(state: :attention_requested)
end
it 'returns success' do
expect(result[:status]).to eq :success
end
- it 'updates assignees state' do
+ it 'updates assignee state' do
service.execute
assignee.reload
@@ -68,14 +116,40 @@
end
it_behaves_like 'invalidates attention request cache' do
- let(:users) { [current_user] }
+ let(:users) { [assignee_user] }
+ end
+
+ it 'creates a remove attention request system note' do
+ expect(SystemNoteService)
+ .to receive(:remove_attention_request)
+ .with(merge_request, merge_request.project, current_user, assignee_user)
+
+ service.execute
end
end
- context 'assignee is the same as reviewer' do
- let(:merge_request) { create(:merge_request, reviewers: [current_user], assignees: [current_user]) }
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request) }
- let(:assignee) { merge_request.find_assignee(current_user) }
+ context 'when user is an assignee and reviewer at the same time' do
+ let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) }
+
+ let(:assignee) { merge_request.find_assignee(user) }
+
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: user
+ )
+ end
+
+ before do
+ reviewer.update!(state: :attention_requested)
+ assignee.update!(state: :attention_requested)
+ end
+
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
it 'updates reviewers and assignees state' do
service.execute
@@ -86,5 +160,24 @@
expect(assignee.state).to eq 'reviewed'
end
end
+
+ context 'when state is already not attention_requested' do
+ before do
+ reviewer.update!(state: :reviewed)
+ end
+
+ it 'does not change state' do
+ service.execute
+ reviewer.reload
+
+ expect(reviewer.state).to eq 'reviewed'
+ end
+
+ it 'does not create a remove attention request system note' do
+ expect(SystemNoteService).not_to receive(:remove_attention_request)
+
+ service.execute
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/request_attention_service_spec.rb b/spec/services/merge_requests/request_attention_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..813a8150625c90c76ac7aa301f95712dbeedc34a
--- /dev/null
+++ b/spec/services/merge_requests/request_attention_service_spec.rb
@@ -0,0 +1,220 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::RequestAttentionService do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:assignee_user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
+
+ let(:reviewer) { merge_request.find_reviewer(user) }
+ let(:assignee) { merge_request.find_assignee(assignee_user) }
+ let(:project) { merge_request.project }
+
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: user
+ )
+ end
+
+ let(:result) { service.execute }
+ let(:todo_svc) { instance_double('TodoService') }
+ let(:notification_svc) { instance_double('NotificationService') }
+
+ before do
+ allow(service).to receive(:todo_service).and_return(todo_svc)
+ allow(service).to receive(:notification_service).and_return(notification_svc)
+ allow(todo_svc).to receive(:create_attention_requested_todo)
+ allow(notification_svc).to receive_message_chain(:async, :attention_requested_of_merge_request)
+ allow(SystemNoteService).to receive(:request_attention)
+
+ project.add_developer(current_user)
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ context 'when current user cannot update merge request' do
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: create(:user),
+ merge_request: merge_request,
+ user: user
+ )
+ end
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'when user is not a reviewer nor assignee' do
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: create(:user)
+ )
+ end
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'when user is a reviewer' do
+ before do
+ reviewer.update!(state: :reviewed)
+ end
+
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates reviewers state' do
+ service.execute
+ reviewer.reload
+
+ expect(reviewer.state).to eq 'attention_requested'
+ end
+
+ it 'adds who toggled attention' do
+ service.execute
+ reviewer.reload
+
+ expect(reviewer.updated_state_by).to eq current_user
+ end
+
+ it 'creates a new todo for the reviewer' do
+ expect(todo_svc).to receive(:create_attention_requested_todo).with(merge_request, current_user, user)
+
+ service.execute
+ end
+
+ it 'sends email to reviewer' do
+ expect(notification_svc)
+ .to receive_message_chain(:async, :attention_requested_of_merge_request)
+ .with(merge_request, current_user, user)
+
+ service.execute
+ end
+
+ it 'removes attention requested state' do
+ expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
+ .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
+ .and_call_original
+
+ service.execute
+ end
+
+ it_behaves_like 'invalidates attention request cache' do
+ let(:users) { [user] }
+ end
+ end
+
+ context 'when user is an assignee' do
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: assignee_user
+ )
+ end
+
+ before do
+ assignee.update!(state: :reviewed)
+ end
+
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates assignees state' do
+ service.execute
+ assignee.reload
+
+ expect(assignee.state).to eq 'attention_requested'
+ end
+
+ it 'creates a new todo for the reviewer' do
+ expect(todo_svc).to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user)
+
+ service.execute
+ end
+
+ it 'creates a request attention system note' do
+ expect(SystemNoteService)
+ .to receive(:request_attention)
+ .with(merge_request, merge_request.project, current_user, assignee_user)
+
+ service.execute
+ end
+
+ it 'removes attention requested state' do
+ expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
+ .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
+ .and_call_original
+
+ service.execute
+ end
+
+ it_behaves_like 'invalidates attention request cache' do
+ let(:users) { [assignee_user] }
+ end
+ end
+
+ context 'when user is an assignee and reviewer at the same time' do
+ let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) }
+
+ let(:assignee) { merge_request.find_assignee(user) }
+
+ let(:service) do
+ described_class.new(
+ project: project,
+ current_user: current_user,
+ merge_request: merge_request,
+ user: user
+ )
+ end
+
+ before do
+ reviewer.update!(state: :reviewed)
+ assignee.update!(state: :reviewed)
+ end
+
+ it 'updates reviewers and assignees state' do
+ service.execute
+ reviewer.reload
+ assignee.reload
+
+ expect(reviewer.state).to eq 'attention_requested'
+ expect(assignee.state).to eq 'attention_requested'
+ end
+ end
+
+ context 'when state is attention_requested' do
+ before do
+ reviewer.update!(state: :attention_requested)
+ end
+
+ it 'does not change state' do
+ service.execute
+ reviewer.reload
+
+ expect(reviewer.state).to eq 'attention_requested'
+ end
+
+ it 'does not create a new todo for the reviewer' do
+ expect(todo_svc).not_to receive(:create_attention_requested_todo).with(merge_request, current_user, user)
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb b/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
index dcaac5d26996a4f2f29436dc7d98c37071938e10..20bc536b21e7c18483a65991d93cab7925703ec6 100644
--- a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
+++ b/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
@@ -80,7 +80,7 @@
it 'removes attention requested state' do
expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: current_user, merge_request: merge_request)
+ .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
.and_call_original
service.execute
@@ -129,7 +129,7 @@
it 'removes attention requested state' do
expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: current_user, merge_request: merge_request)
+ .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
.and_call_original
service.execute