From 49b1eaf113a0f96e3e887585010a56805ec9503b Mon Sep 17 00:00:00 2001 From: Lukas Wanko Date: Thu, 25 Sep 2025 15:01:32 +0200 Subject: [PATCH 1/3] Create members approve invite service --- app/events/members/approved_invite_event.rb | 24 ++++ .../members/approve_invite_service.rb | 69 +++++++++++ locale/gitlab.pot | 3 + .../members/approve_invite_service_spec.rb | 108 ++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 app/events/members/approved_invite_event.rb create mode 100644 app/services/members/approve_invite_service.rb create mode 100644 spec/services/members/approve_invite_service_spec.rb diff --git a/app/events/members/approved_invite_event.rb b/app/events/members/approved_invite_event.rb new file mode 100644 index 00000000000000..e98754b66510a3 --- /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 00000000000000..59b7ac8627ccdb --- /dev/null +++ b/app/services/members/approve_invite_service.rb @@ -0,0 +1,69 @@ +# 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 e-mail #{member_email} and user e-mails #{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 e-mail of the user and 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 e1e1b2c2aa223e..1366755a4e0382 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 e-mail of the user and 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 00000000000000..c24c83d0957a44 --- /dev/null +++ b/spec/services/members/approve_invite_service_spec.rb @@ -0,0 +1,108 @@ +# 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) } + + 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("The invitation could not be accepted, because e-mail of the user and member don't match.") + end + + it 'logs the email mismatch error' do + error = "The invitation could not be accepted, because e-mail of the user and member don't match." + user_email1 = 'user1@example.com' + user_email2 = Gitlab::PrivateCommitEmail.for_user(user) + member_email = 'user2@example.com' + + expect(service).to receive(:log_error) + .with("#{error} Member e-mail #{member_email} and user e-mails #{user_email1}, #{user_email2}.") + 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 -- GitLab From 8faf2d37965bf6244f1e90900840077c937c23a4 Mon Sep 17 00:00:00 2001 From: Lukas Wanko Date: Fri, 26 Sep 2025 13:30:41 +0200 Subject: [PATCH 2/3] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Katherine Richards --- app/services/members/approve_invite_service.rb | 2 +- locale/gitlab.pot | 2 +- spec/services/members/approve_invite_service_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/members/approve_invite_service.rb b/app/services/members/approve_invite_service.rb index 59b7ac8627ccdb..8dcdf4eb1d8abd 100644 --- a/app/services/members/approve_invite_service.rb +++ b/app/services/members/approve_invite_service.rb @@ -59,7 +59,7 @@ def user_emails end def error_email_mismatch_message - _("The invitation could not be accepted, because e-mail of the user and member don't match.") + _("The invitation could not be accepted, because the e-mails of the user and member don't match.") end def error_approval_message diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1366755a4e0382..a711dad95e2720 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -65394,7 +65394,7 @@ msgstr "" msgid "The invitation can not be found with the provided invite token." msgstr "" -msgid "The invitation could not be accepted, because e-mail of the user and member don't match." +msgid "The invitation could not be accepted, because the e-mails of the user and member don't match." msgstr "" msgid "The invitation could not be accepted." diff --git a/spec/services/members/approve_invite_service_spec.rb b/spec/services/members/approve_invite_service_spec.rb index c24c83d0957a44..cfe08f1119a5a5 100644 --- a/spec/services/members/approve_invite_service_spec.rb +++ b/spec/services/members/approve_invite_service_spec.rb @@ -33,11 +33,11 @@ it 'returns an error response' do expect(result[:status]).to eq(:error) expect(result[:message]) - .to eq("The invitation could not be accepted, because e-mail of the user and member don't match.") + .to eq("The invitation could not be accepted, because the e-mails of the user and member don't match.") end it 'logs the email mismatch error' do - error = "The invitation could not be accepted, because e-mail of the user and member don't match." + error = "The invitation could not be accepted, because the e-mails of the user and member don't match." user_email1 = 'user1@example.com' user_email2 = Gitlab::PrivateCommitEmail.for_user(user) member_email = 'user2@example.com' -- GitLab From fa3d53525968913dd05cb979796dcd1f729e4687 Mon Sep 17 00:00:00 2001 From: Lukas Wanko Date: Fri, 26 Sep 2025 16:31:22 +0200 Subject: [PATCH 3/3] Integrate writing review --- app/services/members/approve_invite_service.rb | 6 ++++-- locale/gitlab.pot | 2 +- spec/services/members/approve_invite_service_spec.rb | 12 +++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/services/members/approve_invite_service.rb b/app/services/members/approve_invite_service.rb index 8dcdf4eb1d8abd..c3f6dbc1d349c0 100644 --- a/app/services/members/approve_invite_service.rb +++ b/app/services/members/approve_invite_service.rb @@ -26,7 +26,9 @@ def execute alias_method :user, :current_user def error_email_mismatch - log_error("#{error_email_mismatch_message} Member e-mail #{member_email} and user e-mails #{user_emails}.") + log_error( + "#{error_email_mismatch_message} Member email address #{member_email} and user email addresses #{user_emails}." + ) error(error_email_mismatch_message) end @@ -59,7 +61,7 @@ def user_emails end def error_email_mismatch_message - _("The invitation could not be accepted, because the e-mails of the user and member don't match.") + _("The invitation could not be accepted because the email addresses of the user and invited member don't match.") end def error_approval_message diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a711dad95e2720..c3252156d0aa45 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -65394,7 +65394,7 @@ msgstr "" msgid "The invitation can not be found with the provided invite token." msgstr "" -msgid "The invitation could not be accepted, because the e-mails of the user and member don't match." +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." diff --git a/spec/services/members/approve_invite_service_spec.rb b/spec/services/members/approve_invite_service_spec.rb index cfe08f1119a5a5..ea5c85980f8408 100644 --- a/spec/services/members/approve_invite_service_spec.rb +++ b/spec/services/members/approve_invite_service_spec.rb @@ -7,6 +7,9 @@ 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 } @@ -32,18 +35,17 @@ it 'returns an error response' do expect(result[:status]).to eq(:error) - expect(result[:message]) - .to eq("The invitation could not be accepted, because the e-mails of the user and member don't match.") + expect(result[:message]).to eq(error_email_mismatch_message) end it 'logs the email mismatch error' do - error = "The invitation could not be accepted, because the e-mails of the user and member don't match." + 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("#{error} Member e-mail #{member_email} and user e-mails #{user_email1}, #{user_email2}.") + expect(service).to receive(:log_error).with(log) result end -- GitLab