From 05bf47f96a22ecc0121d4a5a10f665b71f16c2e1 Mon Sep 17 00:00:00 2001
From: Alex Buijs <abuijs@gitlab.com>
Date: Thu, 16 May 2024 15:30:44 +0000
Subject: [PATCH] Display custom role title in invitation emails

When a custom role is assigned, display the
custom role's title in access granted notifications.

Changelog: changed
---
 .rubocop_todo/style/format_string.yml         |  1 -
 app/helpers/invite_members_helper.rb          | 14 +++++------
 app/presenters/member_presenter.rb            |  4 +++
 .../member_access_granted_email.html.haml     | 14 +++++------
 .../member_access_granted_email.text.erb      |  5 +++-
 .../notify/member_invited_email.html.haml     |  7 +++---
 .../notify/member_invited_email.text.erb      |  7 +++---
 ee/app/presenters/ee/member_presenter.rb      |  7 ++++++
 .../identity_verification_spec.rb             |  8 ++++--
 .../saas/standard_flow_with_2fa_spec.rb       |  3 ++-
 ee/spec/presenters/member_presenter_spec.rb   | 16 ++++++++++++
 .../helpers/saas_registration_helpers.rb      |  3 ++-
 locale/gitlab.pot                             | 25 ++++++++-----------
 spec/features/invites_spec.rb                 |  9 ++++---
 .../registrations/oauth_registration_spec.rb  |  3 ++-
 spec/helpers/invite_members_helper_spec.rb    | 22 ++++++++++++++++
 spec/mailers/notify_spec.rb                   | 11 +++++---
 spec/presenters/member_presenter_spec.rb      |  6 +++++
 18 files changed, 116 insertions(+), 49 deletions(-)

diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml
index e7827f1d82c9..1126e08f8148 100644
--- a/.rubocop_todo/style/format_string.yml
+++ b/.rubocop_todo/style/format_string.yml
@@ -39,7 +39,6 @@ Style/FormatString:
     - 'app/helpers/groups/group_members_helper.rb'
     - 'app/helpers/groups_helper.rb'
     - 'app/helpers/import_helper.rb'
-    - 'app/helpers/invite_members_helper.rb'
     - 'app/helpers/issuables_helper.rb'
     - 'app/helpers/issues_helper.rb'
     - 'app/helpers/members_helper.rb'
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index 0443861903b0..f381a54a208e 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -10,14 +10,12 @@ def can_invite_members_for_project?(project)
   end
 
   def invite_accepted_notice(member)
-    case member.source
-    when Project
-      _("You have been granted %{member_human_access} access to project %{name}.") %
-        { member_human_access: member.human_access, name: member.source.name }
-    when Group
-      _("You have been granted %{member_human_access} access to group %{name}.") %
-        { member_human_access: member.human_access, name: member.source.name }
-    end
+    format(
+      _('You have been granted access to the %{source_name} %{source_type} with the following role: %{role_name}.'),
+      source_name: member.source.name,
+      source_type: member.source.model_name.singular,
+      role_name: member.present.human_access
+    )
   end
 
   # Overridden in EE
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 42b49a80b8a6..d0c075c5b7e5 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -19,6 +19,10 @@ def valid_member_roles
     []
   end
 
+  def role_type
+    'default'
+  end
+
   def can_resend_invite?
     invite? &&
       can?(current_user, admin_member_permission, source)
diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml
index 49a571f154eb..9835883e5809 100644
--- a/app/views/notify/member_access_granted_email.html.haml
+++ b/app/views/notify/member_access_granted_email.html.haml
@@ -1,13 +1,13 @@
-- link_end = '</a>'.html_safe
 - source_type = member_source.model_name.singular
-- leave_link = polymorphic_url([member_source], leave: 1)
-- source_link = link_to(member_source.human_name, member_source.web_url, target: '_blank', rel: 'noopener noreferrer', class: :highlight)
-- access_level = content_tag(:span, member.human_access, class: :highlight)
+- source_name = link_to(member_source.human_name, member_source.web_url, target: '_blank', rel: 'noopener noreferrer', class: :highlight)
+- role_name = content_tag(:span, member.present.human_access, class: :highlight)
+- role_type = content_tag(:span, "#{member.present.role_type} role", class: :highlight)
 
 %tr
   %td.text-content
     %p
-      = _('You have been granted %{access_level} access to the %{source_link} %{source_type}.').html_safe % { access_level: access_level, source_link: source_link, source_type: source_type }
+      = safe_format(_('You have been granted access to the %{source_name} %{source_type} with the following role: %{role_name}.'), source_name: source_name, source_type: source_type, role_name: role_name)
+      = safe_format(_('This is a %{role_type}.'), role_type: role_type)
     %p
-      - leave_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: leave_link }
-      = _('If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}.').html_safe % { source_type: source_type, leave_link_start: leave_link_start, link_end: link_end }
+      - leave_link = link_to('', polymorphic_url([member_source], leave: 1), target: '_blank', rel: 'noopener noreferrer')
+      = safe_format(_('If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}.'), tag_pair(leave_link, :leave_link_start, :link_end), source_type: source_type)
diff --git a/app/views/notify/member_access_granted_email.text.erb b/app/views/notify/member_access_granted_email.text.erb
index 445009bb413d..5bb879fea604 100644
--- a/app/views/notify/member_access_granted_email.text.erb
+++ b/app/views/notify/member_access_granted_email.text.erb
@@ -1,5 +1,8 @@
 <% source_type = member_source.model_name.singular %>
-<%= _('You have been granted %{access_level} access to the %{source_name} %{source_type}.') % { access_level: member.human_access, source_name: member_source.human_name, source_type: source_type  } %>
+<% role_name = member.present.human_access %>
+<% role_type = member.present.role_type %>
+<%= _('You have been granted access to the %{source_name} %{source_type} with the following role: %{role_name}.') % { source_name: member_source.human_name, source_type: source_type, role_name: role_name  } %>
+<%= _('This is a %{role_type} role.') % { role_type: role_type }  %>
 
 <%= member_source.web_url %>
 
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index 21d0f8b9108f..06e1ee836b1b 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -3,7 +3,8 @@
   project_or_group_name: member_source.human_name,
   project_or_group: member_source.model_name.singular,
   br_tag: '<br/>'.html_safe,
-  role: member.human_access.downcase }
+  role: member.present.human_access,
+  role_type: member.present.role_type }
 - join_text =  s_('InviteEmail|Join now')
 - inviter_name = member.created_by.name if member.created_by
 - join_url = invite_url(@token, invite_type: Emails::Members::INITIAL_INVITE)
@@ -21,9 +22,9 @@
     %img.mail-avatar{ height: "60", src: avatar_icon_for_user(member.created_by, 60, only_path: false), width: "60", alt: "" }
     %p
       - if member.created_by
-        = html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders.merge({ inviter: (link_to inviter_name, user_url(member.created_by)).html_safe })
+        = html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} with the following role: %{role}.%{br_tag}This is a %{role_type} role.")) % placeholders.merge({ inviter: (link_to inviter_name, user_url(member.created_by)).html_safe })
       - else
-        = html_escape(s_("InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders
+        = html_escape(s_("InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} with the following role: %{role}.%{br_tag}This is a %{role_type} role.")) % placeholders
     %p.invite-actions
       = link_to join_text, join_url, class: 'invite-btn-join'
 %tr.border-top
diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb
index e58dfc10810a..b82c4c3faff2 100644
--- a/app/views/notify/member_invited_email.text.erb
+++ b/app/views/notify/member_invited_email.text.erb
@@ -1,9 +1,10 @@
-<% placeholders = { project_or_group_name: member_source.human_name, project_or_group: member_source.model_name.singular, role: member.human_access.downcase } %>
+<% placeholders = { project_or_group_name: member_source.human_name, project_or_group: member_source.model_name.singular, role: member.present.human_access} %>
 
 <% if member.created_by %>
-<%= s_('InviteEmail|%{inviter} invited you to join the %{project_or_group_name} %{project_or_group} as a %{role}') % placeholders.merge({ inviter: sanitize_name(member.created_by.name) }) %>
+<%= s_('InviteEmail|%{inviter} invited you to join the %{project_or_group_name} %{project_or_group} with the following role: %{role}.') % placeholders.merge({ inviter: sanitize_name(member.created_by.name) }) %>
 <% else %>
-<%= s_('InviteEmail|You have been invited to join the %{project_or_group_name} %{project_or_group} as a %{role}') % placeholders %>
+<%= s_('InviteEmail|You have been invited to join the %{project_or_group_name} %{project_or_group} with the following role: %{role}.') % placeholders %>
 <% end %>
+<%= _('This is a %{role_type} role.') % { role_type: member.present.role_type }  %>
 
 <%= s_('InviteEmail|Join now') %>: <%= invite_url(@token) %>
diff --git a/ee/app/presenters/ee/member_presenter.rb b/ee/app/presenters/ee/member_presenter.rb
index 911f03c8e0ad..568a00a67d1f 100644
--- a/ee/app/presenters/ee/member_presenter.rb
+++ b/ee/app/presenters/ee/member_presenter.rb
@@ -21,6 +21,13 @@ def human_access
       super
     end
 
+    override :role_type
+    def role_type
+      return 'custom' if member_role
+
+      super
+    end
+
     delegator_override :valid_member_roles
     def valid_member_roles
       source = member.source
diff --git a/ee/spec/features/registrations/identity_verification_spec.rb b/ee/spec/features/registrations/identity_verification_spec.rb
index 473515868c15..6219e8eef53a 100644
--- a/ee/spec/features/registrations/identity_verification_spec.rb
+++ b/ee/spec/features/registrations/identity_verification_spec.rb
@@ -323,7 +323,9 @@ def send_request(session, method, path, headers:)
 
         it 'does not verify the user and lands on group page' do
           expect(page).to have_current_path(group_path(invitation.group))
-          expect(page).to have_content("You have been granted Developer access to group #{invitation.group.name}.")
+          expect(page).to have_content(
+            "You have been granted access to the #{invitation.group.name} group with the following role: Developer."
+          )
         end
       end
 
@@ -342,7 +344,9 @@ def send_request(session, method, path, headers:)
 
       it 'allows the user to complete registration' do
         expect(page).to have_current_path(group_path(invitation.group))
-        expect(page).to have_content("You have been granted Developer access to group #{invitation.group.name}.")
+        expect(page).to have_content(
+          "You have been granted access to the #{invitation.group.name} group with the following role: Developer."
+        )
       end
     end
   end
diff --git a/ee/spec/features/registrations/saas/standard_flow_with_2fa_spec.rb b/ee/spec/features/registrations/saas/standard_flow_with_2fa_spec.rb
index bd21ad2f8e11..73eb2173334f 100644
--- a/ee/spec/features/registrations/saas/standard_flow_with_2fa_spec.rb
+++ b/ee/spec/features/registrations/saas/standard_flow_with_2fa_spec.rb
@@ -101,7 +101,8 @@ def expect_to_be_on_2fa_verification(with_invite_notification: false)
 
     return unless with_invite_notification # rubocop:disable RSpec/AvoidConditionalStatements
 
-    expect(page).to have_content('You have been granted Developer access to group Test Group')
+    expect(page)
+      .to have_content('You have been granted access to the Test Group group with the following role: Developer.')
   end
 
   def expect_to_be_on_profile_account_page
diff --git a/ee/spec/presenters/member_presenter_spec.rb b/ee/spec/presenters/member_presenter_spec.rb
index c48a336872da..3cf9155b205c 100644
--- a/ee/spec/presenters/member_presenter_spec.rb
+++ b/ee/spec/presenters/member_presenter_spec.rb
@@ -46,6 +46,22 @@
     end
   end
 
+  describe '#role_type' do
+    context 'when a default role is assigned' do
+      it "returns 'default'" do
+        expect(presenter.role_type).to eq('default')
+      end
+    end
+
+    context 'when a custom role is assigned' do
+      it "returns 'custom'" do
+        member_root.member_role = create(:member_role, namespace: root_group)
+
+        expect(presenter.role_type).to eq('custom')
+      end
+    end
+  end
+
   describe '#valid_member_roles' do
     let_it_be(:member_role_guest) { create(:member_role, :guest, name: 'guest plus', namespace: root_group) }
     let_it_be(:member_role_reporter) do
diff --git a/ee/spec/support/helpers/saas_registration_helpers.rb b/ee/spec/support/helpers/saas_registration_helpers.rb
index 3b41679a17d8..f5cdfd605fa7 100644
--- a/ee/spec/support/helpers/saas_registration_helpers.rb
+++ b/ee/spec/support/helpers/saas_registration_helpers.rb
@@ -41,7 +41,8 @@ def expect_to_see_welcome_form_for_invites
 
   def expect_to_be_on_page_for(group)
     expect(page).to have_current_path(group_path(group), ignore_query: true)
-    expect(page).to have_content('You have been granted Developer access to group Test Group')
+    expect(page)
+      .to have_content('You have been granted access to the Test Group group with the following role: Developer')
   end
 
   def confirm_account
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2605e33c0c79..b521bf8d01b5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -27940,10 +27940,10 @@ msgstr ""
 msgid "Invite members"
 msgstr ""
 
-msgid "InviteEmail|%{inviter} invited you to join the %{project_or_group_name} %{project_or_group} as a %{role}"
+msgid "InviteEmail|%{inviter} invited you to join the %{project_or_group_name} %{project_or_group} with the following role: %{role}."
 msgstr ""
 
-msgid "InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}"
+msgid "InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} with the following role: %{role}.%{br_tag}This is a %{role_type} role."
 msgstr ""
 
 msgid "InviteEmail|%{project_or_group} details"
@@ -27967,10 +27967,10 @@ msgstr ""
 msgid "InviteEmail|What's it about?"
 msgstr ""
 
-msgid "InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}"
+msgid "InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} with the following role: %{role}.%{br_tag}This is a %{role_type} role."
 msgstr ""
 
-msgid "InviteEmail|You have been invited to join the %{project_or_group_name} %{project_or_group} as a %{role}"
+msgid "InviteEmail|You have been invited to join the %{project_or_group_name} %{project_or_group} with the following role: %{role}."
 msgstr ""
 
 msgid "InviteMembersBanner|Collaborate with your team"
@@ -53093,6 +53093,12 @@ msgstr ""
 msgid "This is a \"Ghost User\", created to hold all issues authored by users that have since been deleted. This user cannot be removed."
 msgstr ""
 
+msgid "This is a %{role_type} role."
+msgstr ""
+
+msgid "This is a %{role_type}."
+msgstr ""
+
 msgid "This is a Jira user."
 msgstr ""
 
@@ -59771,16 +59777,7 @@ msgstr ""
 msgid "You have been added to ticket #%{issue_iid}."
 msgstr ""
 
-msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}."
-msgstr ""
-
-msgid "You have been granted %{access_level} access to the %{source_name} %{source_type}."
-msgstr ""
-
-msgid "You have been granted %{member_human_access} access to group %{name}."
-msgstr ""
-
-msgid "You have been granted %{member_human_access} access to project %{name}."
+msgid "You have been granted access to the %{source_name} %{source_type} with the following role: %{role_name}."
 msgstr ""
 
 msgid "You have been invited by %{link_to_inviter} to join %{source_name} %{strong_open}%{link_to_source}%{strong_close} as %{role}"
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 1ea3ad019d92..b63705affbf0 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -87,7 +87,8 @@
             end
 
             it 'accepts invite' do
-              expect(page).to have_content('You have been granted Developer access to group Owned.')
+              expect(page)
+                .to have_content('You have been granted access to the Owned group with the following role: Developer.')
             end
           end
         end
@@ -167,7 +168,8 @@ def expect_to_be_on_group_page(group)
             fill_in_sign_up_form(new_user, invite: true)
 
             expect(page).to have_current_path(group_path(group), ignore_query: true)
-            expect(page).to have_content('You have been granted Owner access to group Owned.')
+            expect(page)
+              .to have_content('You have been granted access to the Owned group with the following role: Owner.')
           end
         end
       end
@@ -230,7 +232,8 @@ def expect_to_be_on_group_page(group)
         fill_in_sign_up_form(new_user, 'Register', invite: true)
 
         expect(page).to have_current_path(group_path(group))
-        expect(page).to have_content('You have been granted Owner access to group Owned.')
+        expect(page)
+          .to have_content('You have been granted access to the Owned group with the following role: Owner.')
       end
     end
 
diff --git a/spec/features/registrations/oauth_registration_spec.rb b/spec/features/registrations/oauth_registration_spec.rb
index 5265a05c66f6..ca335059bf75 100644
--- a/spec/features/registrations/oauth_registration_spec.rb
+++ b/spec/features/registrations/oauth_registration_spec.rb
@@ -106,7 +106,8 @@
           visit invite_path(group_invite.raw_invite_token, extra_params)
           click_link_or_button Gitlab::Auth::OAuth::Provider.label_for(provider)
 
-          expect(page).to have_content('You have been granted Owner access to group Owned.')
+          expect(page)
+            .to have_content('You have been granted access to the Owned group with the following role: Owner.')
           expect(page).to have_current_path(group_path(group), ignore_query: true)
         end
       end
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index 16e4bee47c07..8f4e288d83a0 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -94,4 +94,26 @@
       end
     end
   end
+
+  describe '#invite_accepted_notice' do
+    context 'for group invites' do
+      let_it_be(:group) { create(:group, name: 'My group') }
+      let_it_be(:member) { build(:group_member, :guest, group: group) }
+
+      it 'returns the expected message' do
+        expect(helper.invite_accepted_notice(member))
+          .to eq('You have been granted access to the My group group with the following role: Guest.')
+      end
+    end
+
+    context 'for project invites' do
+      let_it_be(:project) { create(:project, name: 'My project') }
+      let_it_be(:member) { build(:project_member, :guest, project: project) }
+
+      it 'returns the expected message' do
+        expect(helper.invite_accepted_notice(member))
+          .to eq('You have been granted access to the My project project with the following role: Guest.')
+      end
+    end
+  end
 end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index bbb5a77e89c5..b285aaa0ee38 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1085,6 +1085,7 @@ def id
         is_expected.to have_body_text project.full_name
         is_expected.to have_body_text project.web_url
         is_expected.to have_body_text project_member.human_access
+        is_expected.to have_body_text 'default role'
         is_expected.to have_body_text 'leave the project'
         is_expected.to have_body_text project_url(project, leave: 1)
       end
@@ -1122,7 +1123,8 @@ def invite_to_project(project, inviter:, user: nil)
         it 'contains all the useful information' do
           is_expected.to have_subject "#{inviter.name} invited you to join GitLab"
           is_expected.to have_body_text project.full_name
-          is_expected.to have_body_text project_member.human_access.downcase
+          is_expected.to have_body_text project_member.human_access
+          is_expected.to have_body_text 'default role'
           is_expected.to have_body_text project_member.invite_token
           is_expected.to have_link(
             'Join now',
@@ -1140,7 +1142,8 @@ def invite_to_project(project, inviter:, user: nil)
         it 'contains all the useful information' do
           is_expected.to have_subject "Invitation to join the #{project.full_name} project"
           is_expected.to have_body_text project.full_name
-          is_expected.to have_body_text project_member.human_access.downcase
+          is_expected.to have_body_text project_member.human_access
+          is_expected.to have_body_text 'default role'
           is_expected.to have_body_text project_member.invite_token
           is_expected.to have_link(
             'Join now',
@@ -1796,7 +1799,7 @@ def invite_to_group(group, inviter:, user: nil)
         it 'contains all the useful information' do
           is_expected.to have_subject "#{group_member.created_by.name} invited you to join GitLab"
           is_expected.to have_body_text group.name
-          is_expected.to have_body_text group_member.human_access.downcase
+          is_expected.to have_body_text group_member.human_access
           is_expected.to have_body_text group_member.invite_token
         end
       end
@@ -1807,7 +1810,7 @@ def invite_to_group(group, inviter:, user: nil)
         it 'contains all the useful information' do
           is_expected.to have_subject "Invitation to join the #{group.name} group"
           is_expected.to have_body_text group.name
-          is_expected.to have_body_text group_member.human_access.downcase
+          is_expected.to have_body_text group_member.human_access
           is_expected.to have_body_text group_member.invite_token
         end
       end
diff --git a/spec/presenters/member_presenter_spec.rb b/spec/presenters/member_presenter_spec.rb
index 7223c98d5f7a..898f4288936f 100644
--- a/spec/presenters/member_presenter_spec.rb
+++ b/spec/presenters/member_presenter_spec.rb
@@ -21,6 +21,12 @@
     end
   end
 
+  describe '#role_type' do
+    it "returns 'default'" do
+      expect(presenter.role_type).to eq('default')
+    end
+  end
+
   describe '#valid_level_roles' do
     it 'does not return levels lower than user highest membership in the hierarchy' do
       expect(described_class.new(subgroup_member).valid_level_roles).to eq(
-- 
GitLab