diff --git a/app/events/members/approved_invite_event.rb b/app/events/members/approved_invite_event.rb new file mode 100644 index 0000000000000000000000000000000000000000..e98754b66510a373d2b5ef96434e5ff31d613d22 --- /dev/null +++ b/app/events/members/approved_invite_event.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Members + class ApprovedInviteEvent < ::Gitlab::EventStore::Event + def schema + { + 'type' => 'object', + 'required' => %w[source_id source_type user_ids member_ids], + 'properties' => { + 'source_id' => { 'type' => 'integer' }, + 'source_type' => { 'type' => 'string' }, + 'user_ids' => { + 'type' => 'array', + 'items' => { 'type' => 'integer' } + }, + 'member_ids' => { + 'type' => 'array', + 'items' => { 'type' => 'integer' } + } + } + } + end + end +end diff --git a/app/services/members/approve_invite_service.rb b/app/services/members/approve_invite_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..c3f6dbc1d349c06774460365e79342da61e9b2fd --- /dev/null +++ b/app/services/members/approve_invite_service.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Members + class ApproveInviteService < Members::BaseService + def initialize(...) + super + + @member = @params[:member] + end + + def execute + return unless member + return unless user + + return error_email_mismatch unless user.verified_email?(member_email) + return error_approval unless member.accept_invite!(user) + + log_approval + publish_approval_event + success + end + + private + + attr_reader :member + alias_method :user, :current_user + + def error_email_mismatch + log_error( + "#{error_email_mismatch_message} Member email address #{member_email} and user email addresses #{user_emails}." + ) + error(error_email_mismatch_message) + end + + def error_approval + log_error("#{error_approval_message} Member #{member.id} and user #{user.id}.") + error(error_approval_message) + end + + def log_approval + log_info("The invitation to become member #{member.id} was accepted by user #{user.id}.") + end + + def publish_approval_event + Gitlab::EventStore.publish( + Members::ApprovedInviteEvent.new(data: { + source_id: member.source_id, + source_type: member.source_type, + user_ids: [user.id], + member_ids: [member.id] + }) + ) + end + + def member_email + member.invite_email + end + + def user_emails + user.verified_emails.join(', ') + end + + def error_email_mismatch_message + _("The invitation could not be accepted because the email addresses of the user and invited member don't match.") + end + + def error_approval_message + _("The invitation could not be accepted.") + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e1e1b2c2aa223e0c56565d822d50eecae25201e4..c3252156d0aa4511df7fb8745cb74f62c61b300a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -65394,6 +65394,9 @@ msgstr "" msgid "The invitation can not be found with the provided invite token." msgstr "" +msgid "The invitation could not be accepted because the email addresses of the user and invited member don't match." +msgstr "" + msgid "The invitation could not be accepted." msgstr "" diff --git a/spec/services/members/approve_invite_service_spec.rb b/spec/services/members/approve_invite_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ea5c85980f84085bbbf6b6087f48674804a7ed14 --- /dev/null +++ b/spec/services/members/approve_invite_service_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Members::ApproveInviteService, :aggregate_failures, :clean_gitlab_redis_shared_state, :sidekiq_inline, + feature_category: :groups_and_projects do + let(:user) { create(:user, email: 'user1@example.com') } + let(:member) { create(:project_member, :invited, invite_email: user.email) } + let(:service) { described_class.new(user, member: member) } + let(:error_email_mismatch_message) do + "The invitation could not be accepted because the email addresses of the user and invited member don't match." + end + + subject(:result) { service.execute } + + describe '#execute' do + context 'when member is nil' do + let(:service) { described_class.new(user, member: nil) } + + it 'returns nil without processing' do + expect(result).to be_nil + end + end + + context 'when user is nil' do + let(:service) { described_class.new(nil, member: member) } + + it 'returns nil without processing' do + expect(result).to be_nil + end + end + + context 'when user email does not match member invite email' do + let(:member) { create(:project_member, :invited, invite_email: 'user2@example.com') } + + it 'returns an error response' do + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_email_mismatch_message) + end + + it 'logs the email mismatch error' do + error = error_email_mismatch_message + user_email1 = 'user1@example.com' + user_email2 = Gitlab::PrivateCommitEmail.for_user(user) + member_email = 'user2@example.com' + log = "#{error} Member email address #{member_email} and user email addresses #{user_email1}, #{user_email2}." + + expect(service).to receive(:log_error).with(log) + result + end + + it 'does not accept the invitation' do + expect { result }.not_to change { member.user } + end + end + + context 'when member fails to accept invitation' do + before do + allow(member).to receive(:accept_invite!).with(user).and_return(false) + end + + it 'returns an error response' do + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('The invitation could not be accepted.') + end + + it 'logs the approval error' do + expect(service).to receive(:log_error).with( + "The invitation could not be accepted. Member #{member.id} and user #{user.id}." + ) + result + end + + it 'does not accept the invitation' do + expect { result }.not_to change { member.user } + end + end + + context 'when invitation is accepted' do + it 'returns a success response' do + expect(result[:status]).to eq :success + end + + it 'accepts the invitation' do + expect { result }.to change { member.user }.from(nil).to(user) + end + + it 'logs the successful approval' do + expect(service).to receive(:log_info).with( + "The invitation to become member #{member.id} was accepted by user #{user.id}." + ) + result + end + + it 'publishes the approval event' do + expect(Gitlab::EventStore).to receive(:publish) do |event| + expect(event).to be_an_instance_of(Members::ApprovedInviteEvent) + expect(event.data).to eq({ + "source_id" => member.source_id, + "source_type" => 'Project', + "user_ids" => [user.id], + "member_ids" => [member.id] + }) + end + + result + end + end + end +end