From e3970a7de431f76f223a486194fd203928150adf Mon Sep 17 00:00:00 2001 From: Keeyan Nejad <knejad@gitlab.com> Date: Wed, 9 Oct 2024 09:24:31 +0000 Subject: [PATCH] Wrap source user reassignments in a lock This prevents placeholder contributions being remapped to the wrong user after a reassignment has already been accepted. Changelog: fixed --- .../import/source_users_controller.rb | 14 ++++++---- app/models/import/source_user.rb | 10 +++++++ .../accept_reassignment_service.rb | 24 ++++++++++++++--- .../cancel_reassignment_service.rb | 16 +++++++++-- .../import/source_users/reassign_service.rb | 16 +++++++++-- .../reject_reassignment_service.rb | 25 ++++++++++++----- app/views/import/source_users/show.html.haml | 4 +-- .../import_source_user_reassign.html.haml | 2 +- .../import_source_user_reassign.text.erb | 2 +- config/routes/import.rb | 2 +- ...assignment_token_to_import_source_users.rb | 16 +++++++++++ db/schema_migrations/20240930133006 | 1 + db/structure.sql | 4 +++ spec/factories/import_source_users.rb | 1 + spec/mailers/emails/imports_spec.rb | 6 +++-- spec/models/import/source_user_spec.rb | 25 +++++++++++++++-- .../import/source_users_controller_spec.rb | 22 ++++++++------- ...n_placeholder_user_records_service_spec.rb | 2 +- .../accept_reassignment_service_spec.rb | 17 +++++++++++- .../reject_reassignment_service_spec.rb | 27 ++++++++++++++++--- 20 files changed, 193 insertions(+), 43 deletions(-) create mode 100644 db/migrate/20240930133006_add_reassignment_token_to_import_source_users.rb create mode 100644 db/schema_migrations/20240930133006 diff --git a/app/controllers/import/source_users_controller.rb b/app/controllers/import/source_users_controller.rb index 5972511dd5f81..75989c38a2571 100644 --- a/app/controllers/import/source_users_controller.rb +++ b/app/controllers/import/source_users_controller.rb @@ -10,7 +10,9 @@ class SourceUsersController < ApplicationController feature_category :importers def accept - result = ::Import::SourceUsers::AcceptReassignmentService.new(source_user, current_user: current_user).execute + result = ::Import::SourceUsers::AcceptReassignmentService.new( + source_user, current_user: current_user, reassignment_token: params[:reassignment_token] + ).execute if result.success? flash[:raw] = banner('accept_invite') @@ -21,7 +23,9 @@ def accept end def decline - result = ::Import::SourceUsers::RejectReassignmentService.new(source_user, current_user: current_user).execute + result = ::Import::SourceUsers::RejectReassignmentService.new( + source_user, current_user: current_user, reassignment_token: params[:reassignment_token] + ).execute if result.success? flash[:raw] = banner('reject_invite') @@ -36,7 +40,7 @@ def show; end private def check_source_user_valid! - return if source_user.awaiting_approval? && current_user_matches_invite? + return if source_user&.awaiting_approval? && current_user_matches_invite? flash[:raw] = banner('invalid_invite') redirect_to(root_path) @@ -47,12 +51,12 @@ def current_user_matches_invite? end def source_user - Import::SourceUser.find(params[:id]) + Import::SourceUser.find_by_reassignment_token(params[:reassignment_token]) end strong_memoize_attr :source_user def check_feature_flag! - not_found unless Feature.enabled?(:importer_user_mapping, source_user.reassigned_by_user) + not_found unless source_user.nil? || Feature.enabled?(:importer_user_mapping, source_user.reassigned_by_user) end def banner(partial) diff --git a/app/models/import/source_user.rb b/app/models/import/source_user.rb index 5c34366882707..0fdbf216aa288 100644 --- a/app/models/import/source_user.rb +++ b/app/models/import/source_user.rb @@ -22,6 +22,8 @@ class SourceUser < ApplicationRecord validates :namespace_id, :import_type, :source_hostname, :source_user_identifier, :status, presence: true validates :source_user_identifier, uniqueness: { scope: [:namespace_id, :source_hostname, :import_type] } validates :placeholder_user_id, presence: true, unless: :completed? + validates :reassignment_token, absence: true, unless: :awaiting_approval? + validates :reassignment_token, length: { is: 32 }, if: :awaiting_approval? validates :reassign_to_user_id, presence: true, if: -> { awaiting_approval? || reassignment_in_progress? || completed? } @@ -61,6 +63,14 @@ class SourceUser < ApplicationRecord state status_name, value: value end + before_transition awaiting_approval: any do |source_user| + source_user.reassignment_token = nil + end + + before_transition any => :awaiting_approval do |source_user| + source_user.reassignment_token = SecureRandom.hex + end + event :reassign do transition REASSIGNABLE_STATUSES => :awaiting_approval end diff --git a/app/services/import/source_users/accept_reassignment_service.rb b/app/services/import/source_users/accept_reassignment_service.rb index f0bbab94c3540..b6c8810f9a101 100644 --- a/app/services/import/source_users/accept_reassignment_service.rb +++ b/app/services/import/source_users/accept_reassignment_service.rb @@ -3,15 +3,25 @@ module Import module SourceUsers class AcceptReassignmentService < BaseService - def initialize(import_source_user, current_user:) + def initialize(import_source_user, current_user:, reassignment_token:) @import_source_user = import_source_user @current_user = current_user + @reassignment_token = reassignment_token end def execute - return error_invalid_permissions unless current_user_matches_reassign_to_user + invalid_permissions = false + accept_successful = false - if import_source_user.accept + import_source_user.with_lock do + next invalid_permissions = true unless current_user_matches_reassign_to_user? && reassignment_token_is_valid? + + accept_successful = import_source_user.accept + end + + return error_invalid_permissions if invalid_permissions + + if accept_successful Import::ReassignPlaceholderUserRecordsWorker.perform_async(import_source_user.id) ServiceResponse.success(payload: import_source_user) else @@ -21,11 +31,17 @@ def execute private - def current_user_matches_reassign_to_user + attr_reader :reassignment_token + + def current_user_matches_reassign_to_user? return false if current_user.nil? current_user.id == import_source_user.reassign_to_user_id end + + def reassignment_token_is_valid? + reassignment_token == import_source_user.reassignment_token + end end end end diff --git a/app/services/import/source_users/cancel_reassignment_service.rb b/app/services/import/source_users/cancel_reassignment_service.rb index 65069d297428a..b3ee14fee7db7 100644 --- a/app/services/import/source_users/cancel_reassignment_service.rb +++ b/app/services/import/source_users/cancel_reassignment_service.rb @@ -10,9 +10,21 @@ def initialize(import_source_user, current_user:) def execute return error_invalid_permissions unless current_user.can?(:admin_import_source_user, import_source_user) - return error_invalid_status unless import_source_user.cancelable_status? - if cancel_reassignment + invalid_status = false + cancel_successful = false + + import_source_user.with_lock do + if import_source_user.cancelable_status? + cancel_successful = cancel_reassignment + else + invalid_status = true + end + end + + return error_invalid_status if invalid_status + + if cancel_successful ServiceResponse.success(payload: import_source_user) else ServiceResponse.error(payload: import_source_user, message: import_source_user.errors.full_messages) diff --git a/app/services/import/source_users/reassign_service.rb b/app/services/import/source_users/reassign_service.rb index 9a6020119c5d2..2172f3e0fec12 100644 --- a/app/services/import/source_users/reassign_service.rb +++ b/app/services/import/source_users/reassign_service.rb @@ -11,10 +11,22 @@ def initialize(import_source_user, assignee_user, current_user:) def execute return error_invalid_permissions unless current_user.can?(:admin_import_source_user, import_source_user) - return error_invalid_status unless import_source_user.reassignable_status? return error_invalid_assignee unless valid_assignee?(assignee_user) - if reassign_user + invalid_status = false + reassign_successful = false + + import_source_user.with_lock do + if import_source_user.reassignable_status? + reassign_successful = reassign_user + else + invalid_status = true + end + end + + return error_invalid_status if invalid_status + + if reassign_successful send_user_reassign_email ServiceResponse.success(payload: import_source_user) diff --git a/app/services/import/source_users/reject_reassignment_service.rb b/app/services/import/source_users/reject_reassignment_service.rb index ad99163ad6d9f..212354bb7feaf 100644 --- a/app/services/import/source_users/reject_reassignment_service.rb +++ b/app/services/import/source_users/reject_reassignment_service.rb @@ -3,16 +3,27 @@ module Import module SourceUsers class RejectReassignmentService < BaseService - def initialize(import_source_user, current_user:) + def initialize(import_source_user, current_user:, reassignment_token:) @import_source_user = import_source_user @current_user = current_user + @reassignment_token = reassignment_token end def execute - return error_invalid_permissions unless current_user_matches_reassign_to_user return error_invalid_status unless import_source_user.awaiting_approval? - if reject + invalid_permissions = false + reject_successful = false + + import_source_user.with_lock do + next invalid_permissions = true unless current_user_matches_reassign_to_user? && reassignment_token_is_valid? + + reject_successful = import_source_user.reject + end + + return error_invalid_permissions if invalid_permissions + + if reject_successful send_user_reassign_rejected_email ServiceResponse.success(payload: import_source_user) @@ -27,14 +38,16 @@ def send_user_reassign_rejected_email private - def current_user_matches_reassign_to_user + attr_reader :reassignment_token + + def current_user_matches_reassign_to_user? return false if current_user.nil? current_user.id == import_source_user.reassign_to_user_id end - def reject - import_source_user.reject + def reassignment_token_is_valid? + reassignment_token == import_source_user.reassignment_token end end end diff --git a/app/views/import/source_users/show.html.haml b/app/views/import/source_users/show.html.haml index 56e165e35540b..1fec771ec7514 100644 --- a/app/views/import/source_users/show.html.haml +++ b/app/views/import/source_users/show.html.haml @@ -48,7 +48,7 @@ - c.with_footer do .gl-flex.gl-gap-3 - = render Pajamas::ButtonComponent.new(variant: :danger, method: :post, href: accept_import_source_user_path(@source_user)) do + = render Pajamas::ButtonComponent.new(variant: :danger, method: :post, href: accept_import_source_user_path(@source_user.reassignment_token)) do = s_('UserMapping|Approve reassignment') - = render Pajamas::ButtonComponent.new(method: :post, href: decline_import_source_user_path(@source_user)) do + = render Pajamas::ButtonComponent.new(method: :post, href: decline_import_source_user_path(@source_user.reassignment_token)) do = s_('UserMapping|Reject') diff --git a/app/views/notify/import_source_user_reassign.html.haml b/app/views/notify/import_source_user_reassign.html.haml index 362b64e9dc301..72946cabf408d 100644 --- a/app/views/notify/import_source_user_reassign.html.haml +++ b/app/views/notify/import_source_user_reassign.html.haml @@ -27,7 +27,7 @@ destination_group: destination_group) %p{ style: text_style } - = link_to import_source_user_url(@source_user), target: '_blank', rel: 'noopener noreferrer' do + = link_to import_source_user_url(@source_user.reassignment_token), target: '_blank', rel: 'noopener noreferrer' do %button{ type: 'button', style: button_style } = s_('UserMapping|Review reassignment details') diff --git a/app/views/notify/import_source_user_reassign.text.erb b/app/views/notify/import_source_user_reassign.text.erb index 9ba3af278ac46..2d53d5399f214 100644 --- a/app/views/notify/import_source_user_reassign.text.erb +++ b/app/views/notify/import_source_user_reassign.text.erb @@ -16,7 +16,7 @@ source_hostname: source_hostname, destination_group: destination_group } %> -<%= s_('UserMapping|Review reassignment details') %>: <%= import_source_user_url(@source_user) %> +<%= s_('UserMapping|Review reassignment details') %>: <%= import_source_user_url(@source_user.reassignment_token) %> <%= s_('UserMapping|Import details:') %> <%= safe_format(s_('UserMapping|Imported from: %{source_hostname}'), source_hostname: source_hostname) %> diff --git a/config/routes/import.rb b/config/routes/import.rb index 5962a68c64a8b..f0e4c376ba2e7 100644 --- a/config/routes/import.rb +++ b/config/routes/import.rb @@ -89,7 +89,7 @@ post :upload end - resources :source_users, only: [] do + resources :source_users, param: :reassignment_token, only: [] do member do get :show post :accept diff --git a/db/migrate/20240930133006_add_reassignment_token_to_import_source_users.rb b/db/migrate/20240930133006_add_reassignment_token_to_import_source_users.rb new file mode 100644 index 0000000000000..7d0fe8fd37648 --- /dev/null +++ b/db/migrate/20240930133006_add_reassignment_token_to_import_source_users.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddReassignmentTokenToImportSourceUsers < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.5' + + def up + add_column :import_source_users, :reassignment_token, :text + add_concurrent_index :import_source_users, :reassignment_token, unique: true + add_text_limit :import_source_users, :reassignment_token, 32 + end + + def down + remove_column :import_source_users, :reassignment_token, :text + end +end diff --git a/db/schema_migrations/20240930133006 b/db/schema_migrations/20240930133006 new file mode 100644 index 0000000000000..216067307a987 --- /dev/null +++ b/db/schema_migrations/20240930133006 @@ -0,0 +1 @@ +1a74228cbfeb2795e3ea6c544cfedee3b3f809e86bd379cd89bfec748be17ded \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b5f71e89968c0..a637245151086 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12247,11 +12247,13 @@ CREATE TABLE import_source_users ( import_type text NOT NULL, reassigned_by_user_id bigint, reassignment_error text, + reassignment_token text, CONSTRAINT check_05708218cd CHECK ((char_length(reassignment_error) <= 255)), CONSTRAINT check_0d7295a307 CHECK ((char_length(import_type) <= 255)), CONSTRAINT check_199c28ec54 CHECK ((char_length(source_username) <= 255)), CONSTRAINT check_562655155f CHECK ((char_length(source_name) <= 255)), CONSTRAINT check_cc9d4093b5 CHECK ((char_length(source_user_identifier) <= 255)), + CONSTRAINT check_cd2edb9334 CHECK ((char_length(reassignment_token) <= 32)), CONSTRAINT check_e2039840c5 CHECK ((char_length(source_hostname) <= 255)) ); @@ -29248,6 +29250,8 @@ CREATE INDEX index_import_source_users_on_placeholder_user_id ON import_source_u CREATE INDEX index_import_source_users_on_reassigned_by_user_id ON import_source_users USING btree (reassigned_by_user_id); +CREATE UNIQUE INDEX index_import_source_users_on_reassignment_token ON import_source_users USING btree (reassignment_token); + CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON projects USING btree (import_type, creator_id, created_at) WHERE (import_type IS NOT NULL); CREATE INDEX index_imported_projects_on_import_type_id ON projects USING btree (import_type, id) WHERE (import_type IS NOT NULL); diff --git a/spec/factories/import_source_users.rb b/spec/factories/import_source_users.rb index 1575ebd7f341e..6fbe9df4ddbf9 100644 --- a/spec/factories/import_source_users.rb +++ b/spec/factories/import_source_users.rb @@ -24,6 +24,7 @@ trait :awaiting_approval do with_reassign_to_user + reassignment_token { SecureRandom.hex } status { 1 } end diff --git a/spec/mailers/emails/imports_spec.rb b/spec/mailers/emails/imports_spec.rb index b5b1600691230..2ad19d5f2260d 100644 --- a/spec/mailers/emails/imports_spec.rb +++ b/spec/mailers/emails/imports_spec.rb @@ -91,7 +91,9 @@ let(:user) { build_stubbed(:user) } let(:group) { build_stubbed(:group) } let(:source_user) do - build_stubbed(:import_source_user, :with_reassigned_by_user, namespace: group, reassign_to_user: user) + build_stubbed( + :import_source_user, :awaiting_approval, :with_reassigned_by_user, namespace: group, reassign_to_user: user + ) end subject { Notify.import_source_user_reassign('user_id') } @@ -109,7 +111,7 @@ is_expected.to have_content( "Reassigned by: #{source_user.reassigned_by_user.name} (@#{source_user.reassigned_by_user.username})" ) - is_expected.to have_body_text(import_source_user_url(source_user)) + is_expected.to have_body_text(import_source_user_url(source_user.reassignment_token)) end it_behaves_like 'appearance header and footer enabled' diff --git a/spec/models/import/source_user_spec.rb b/spec/models/import/source_user_spec.rb index 069939428db2a..f0cdbf1951f0a 100644 --- a/spec/models/import/source_user_spec.rb +++ b/spec/models/import/source_user_spec.rb @@ -16,7 +16,7 @@ it { is_expected.to validate_presence_of(:source_hostname) } it { is_expected.to validate_presence_of(:source_user_identifier) } it { is_expected.to validate_presence_of(:status) } - + it { is_expected.to validate_absence_of(:reassignment_token) } it { is_expected.not_to validate_presence_of(:reassign_to_user_id) } it 'validates source_hostname has port and scheme' do @@ -52,6 +52,8 @@ subject { build(:import_source_user, :awaiting_approval) } it { is_expected.to validate_presence_of(:reassign_to_user_id) } + it { is_expected.to validate_length_of(:reassignment_token).is_equal_to(32) } + it { is_expected.to validate_presence_of(:reassignment_token).with_message(/is the wrong length/) } end context 'when reassignment_in_progress' do @@ -120,6 +122,22 @@ it 'begins in pending state' do expect(described_class.new.pending_reassignment?).to eq(true) end + + context 'when switching to awaiting_approval' do + subject(:source_user) { create(:import_source_user, :pending_reassignment) } + + it 'assigns a reassignment_token' do + expect { source_user.reassign }.to change { source_user.reassignment_token }.from(nil) + end + end + + context 'when switching from awaiting_approval' do + subject(:source_user) { create(:import_source_user, :awaiting_approval) } + + it 'removes the reassignment_token' do + expect { source_user.cancel_reassignment }.to change { source_user.reassignment_token }.to(nil) + end + end end describe '.find_source_user' do @@ -194,7 +212,10 @@ let_it_be(:source_user_1) { create(:import_source_user, namespace: namespace, status: 4, source_name: 'd') } let_it_be(:source_user_2) { create(:import_source_user, namespace: namespace, status: 3, source_name: 'c') } let_it_be(:source_user_3) do - create(:import_source_user, :with_reassign_to_user, namespace: namespace, status: 1, source_name: 'a') + create( + :import_source_user, :with_reassign_to_user, + namespace: namespace, status: 1, source_name: 'a', reassignment_token: SecureRandom.hex + ) end let_it_be(:source_user_4) do diff --git a/spec/requests/import/source_users_controller_spec.rb b/spec/requests/import/source_users_controller_spec.rb index ea3915f14583a..cfdee1b2b0768 100644 --- a/spec/requests/import/source_users_controller_spec.rb +++ b/spec/requests/import/source_users_controller_spec.rb @@ -25,7 +25,7 @@ end end - shared_examples 'it requires awaiting approval status' do + shared_examples 'it notifies about unavailable reassignments' do it 'shows error message' do source_user.accept! @@ -51,8 +51,10 @@ create(:import_source_user, :with_reassigned_by_user, :awaiting_approval) end + let!(:reassignment_token) { source_user.reassignment_token } + describe 'POST /accept' do - let(:path) { accept_import_source_user_path(source_user) } + let(:path) { accept_import_source_user_path(reassignment_token: reassignment_token) } subject(:accept_invite) { post path } @@ -77,7 +79,7 @@ end it 'cannot be accepted twice' do - allow(Import::SourceUser).to receive(:find).and_return(source_user) + allow(Import::SourceUser).to receive(:find_by_reassignment_token).and_return(source_user) allow(source_user).to receive(:accept).and_return(false) accept_invite @@ -86,7 +88,7 @@ expect(flash[:alert]).to match(/The invitation could not be accepted/) end - it_behaves_like 'it requires awaiting approval status' + it_behaves_like 'it notifies about unavailable reassignments' it_behaves_like 'it requires the user is the reassign to user' end @@ -95,7 +97,8 @@ end describe 'POST /decline' do - let(:path) { decline_import_source_user_path(source_user) } + let(:path) { decline_import_source_user_path(reassignment_token: reassignment_token) } + let(:message_delivery) { instance_double(ActionMailer::MessageDelivery) } subject(:reject_invite) { post path } @@ -117,7 +120,7 @@ end it 'cannot be declined twice' do - allow(Import::SourceUser).to receive(:find).and_return(source_user) + allow(Import::SourceUser).to receive(:find_by_reassignment_token).and_return(source_user) allow(source_user).to receive(:reject).and_return(false) reject_invite @@ -126,7 +129,7 @@ expect(flash[:alert]).to match(/The invitation could not be declined/) end - it_behaves_like 'it requires awaiting approval status' + it_behaves_like 'it notifies about unavailable reassignments' it_behaves_like 'it requires the user is the reassign to user' end @@ -135,7 +138,8 @@ end describe 'GET /show' do - let(:path) { import_source_user_path(source_user) } + let(:path) { import_source_user_path(reassignment_token: reassignment_token) } + let(:reassignment_token) { source_user.reassignment_token } subject(:show_invite) { get path } @@ -150,7 +154,7 @@ expect(response).to have_gitlab_http_status(:success) end - it_behaves_like 'it requires awaiting approval status' + it_behaves_like 'it notifies about unavailable reassignments' it_behaves_like 'it requires the user is the reassign to user' end diff --git a/spec/services/import/reassign_placeholder_user_records_service_spec.rb b/spec/services/import/reassign_placeholder_user_records_service_spec.rb index cace45aab4f95..be99e1c38aa80 100644 --- a/spec/services/import/reassign_placeholder_user_records_service_spec.rb +++ b/spec/services/import/reassign_placeholder_user_records_service_spec.rb @@ -467,7 +467,7 @@ def expect_skipped_membership_log(message, placeholder_membership, existing_memb context 'when the source user is not in reassignment_in_progress status' do before do - source_user.update!(status: 1) + source_user.complete! end it 'does not reassign any contributions or create memberships' do diff --git a/spec/services/import/source_users/accept_reassignment_service_spec.rb b/spec/services/import/source_users/accept_reassignment_service_spec.rb index 74ee78c05706a..bb9a2b9b1c79f 100644 --- a/spec/services/import/source_users/accept_reassignment_service_spec.rb +++ b/spec/services/import/source_users/accept_reassignment_service_spec.rb @@ -4,8 +4,11 @@ RSpec.describe Import::SourceUsers::AcceptReassignmentService, feature_category: :importers do let(:import_source_user) { create(:import_source_user, :awaiting_approval) } + let(:reassignment_token) { import_source_user.reassignment_token } let(:current_user) { import_source_user.reassign_to_user } - let(:service) { described_class.new(import_source_user, current_user: current_user) } + let(:service) do + described_class.new(import_source_user, current_user: current_user, reassignment_token: reassignment_token) + end describe '#execute' do it 'returns success' do @@ -51,6 +54,18 @@ it_behaves_like 'current user does not have permission to accept reassignment' end + context 'when passing the wrong reassignment_token' do + let(:reassignment_token) { '1234567890abcdef' } + + it_behaves_like 'current user does not have permission to accept reassignment' + end + + context 'when not passing a reassignment_token' do + let(:reassignment_token) { nil } + + it_behaves_like 'current user does not have permission to accept reassignment' + end + context 'when the source user is not awaiting approval' do let(:import_source_user) { create(:import_source_user, :reassignment_in_progress) } diff --git a/spec/services/import/source_users/reject_reassignment_service_spec.rb b/spec/services/import/source_users/reject_reassignment_service_spec.rb index 04d21f2359da5..1b8aba193980e 100644 --- a/spec/services/import/source_users/reject_reassignment_service_spec.rb +++ b/spec/services/import/source_users/reject_reassignment_service_spec.rb @@ -4,8 +4,11 @@ RSpec.describe Import::SourceUsers::RejectReassignmentService, feature_category: :importers do let(:import_source_user) { create(:import_source_user, :awaiting_approval) } + let(:reassignment_token) { import_source_user.reassignment_token } let(:current_user) { import_source_user.reassign_to_user } - let(:service) { described_class.new(import_source_user, current_user: current_user) } + let(:service) do + described_class.new(import_source_user, current_user: current_user, reassignment_token: reassignment_token) + end describe '#execute' do let(:message_delivery) { instance_double(ActionMailer::MessageDelivery) } @@ -25,9 +28,7 @@ expect(import_source_user.reload).to be_rejected end - context 'when current user does not have permission to reject' do - let(:current_user) { create(:user) } - + shared_examples 'current user does not have permission to reject reassignment' do it 'returns error no permissions' do result = service.execute @@ -38,6 +39,24 @@ end end + context 'when passing the wrong reassignment_token' do + let(:reassignment_token) { '1234567890abcdef' } + + it_behaves_like 'current user does not have permission to reject reassignment' + end + + context 'when not passing a reassignment_token' do + let(:reassignment_token) { nil } + + it_behaves_like 'current user does not have permission to reject reassignment' + end + + context 'when current user is not the assigned user' do + let(:current_user) { create(:user) } + + it_behaves_like 'current user does not have permission to reject reassignment' + end + context 'when import source user does not have a rejectable status' do let(:import_source_user) { create(:import_source_user, :reassignment_in_progress) } -- GitLab