Commit 670a84b6 authored by Jackie Fraser's avatar Jackie Fraser Committed by Mayra Cabrera

Change modal error message for restricted emails

parent 02ad2733
......@@ -11,9 +11,10 @@ import {
GlFormInput,
GlFormCheckboxGroup,
} from '@gitlab/ui';
import { partition, isString } from 'lodash';
import { partition, isString, unescape } from 'lodash';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { sanitize } from '~/lib/dompurify';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale';
import {
......@@ -293,7 +294,7 @@ export default {
};
},
conditionallyShowToastSuccess(response) {
const message = responseMessageFromSuccess(response);
const message = this.unescapeMsg(responseMessageFromSuccess(response));
if (message === '') {
this.showToastMessageSuccess();
......@@ -309,13 +310,17 @@ export default {
this.closeModal();
},
showInvalidFeedbackMessage(response) {
const message = this.unescapeMsg(responseMessageFromError(response));
this.isLoading = false;
this.invalidFeedbackMessage =
responseMessageFromError(response) || this.$options.labels.invalidFeedbackMessageDefault;
this.invalidFeedbackMessage = message || this.$options.labels.invalidFeedbackMessageDefault;
},
handleMembersTokenSelectClear() {
this.invalidFeedbackMessage = '';
},
unescapeMsg(message) {
return unescape(sanitize(message, { ALLOWED_TAGS: [] }));
},
},
labels: {
members: {
......
......@@ -18,7 +18,10 @@ function responseMessageStringForMultiple(message) {
return message.includes(':');
}
function responseMessageStringFirstPart(message) {
return message.split(' and ')[0];
const firstPart = message.split(':')[1];
const firstMsg = firstPart.split(/ and [\w-]*$/)[0].trim();
return firstMsg;
}
export function responseMessageFromError(response) {
......
......@@ -7,15 +7,49 @@ module RestrictedSignup
def validate_admin_signup_restrictions(email)
return if allowed_domain?(email)
error_type = fetch_error_type(email)
return unless error_type.present?
[
signup_email_invalid_message,
error_message[created_by_key][error_type]
].join(' ')
end
def fetch_error_type(email)
if allowlist_present?
return _('domain is not authorized for sign-up.')
:allowlist
elsif denied_domain?(email)
return _('is not from an allowed domain.')
:denylist
elsif restricted_email?(email)
return _('is not allowed. Try again with a different email address, or contact your GitLab admin.')
:restricted
end
end
def error_message
{
admin: {
allowlist: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check 'Allowed domains for sign-ups'.")).html_safe,
denylist: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check the 'Domain denylist'.")).html_safe,
restricted: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check 'Email restrictions for sign-ups'.")).html_safe,
group_setting: html_escape_once(_("Go to the group’s 'Settings > General' page, and check 'Restrict membership by email domain'.")).html_safe
},
nonadmin: {
allowlist: error_nonadmin,
denylist: error_nonadmin,
restricted: error_nonadmin,
group_setting: error_nonadmin
}
}
end
def error_nonadmin
_("Check with your administrator.")
end
nil
def created_by_key
created_by&.can_admin_all_resources? ? :admin : :nonadmin
end
def denied_domain?(email)
......
......@@ -448,6 +448,14 @@ class Member < ApplicationRecord
errors.add(:user, error) if error
end
def signup_email_invalid_message
if source_type == 'Project'
_("is not allowed for this project.")
else
_("is not allowed for this group.")
end
end
def update_highest_role?
return unless user_id.present?
......
......@@ -2102,6 +2102,10 @@ class User < ApplicationRecord
errors.add(:email, error) if error
end
def signup_email_invalid_message
_('is not allowed for sign-up.')
end
def check_username_format
return if username.blank? || Mime::EXTENSION_LOOKUP.keys.none? { |type| username.end_with?(".#{type}") }
......
......@@ -10,6 +10,9 @@ en:
target: Target issue
group:
path: Group URL
member:
user: "The member's email address"
invite_email: "The member's email address"
project/error_tracking_setting:
token: "Auth Token"
project: "Project"
......
......@@ -103,8 +103,11 @@ module EE
end
def email_does_not_match_any_allowed_domains(email)
n_("email does not match the allowed domain of %{email_domains}", "email does not match the allowed domains: %{email_domains}", group_allowed_email_domains.size) %
{ email_domains: group_allowed_email_domains.map(&:domain).join(', ') }
msg_1 = signup_email_invalid_message
msg_2 = error_message[created_by_key][:group_setting]
[msg_1, msg_2].join(' ')
end
def matches_at_least_one_group_allowed_email_domain?(email)
......
......@@ -189,7 +189,7 @@ RSpec.describe Groups::GroupMembersController do
post :request_access, params: { group_id: group }
expect(controller).to set_flash.to "Your request for access could not be processed: "\
"User email 'unverified@gitlab.com' is not a verified email."
"The member's email address email 'unverified@gitlab.com' is not a verified email."
expect(response).to redirect_to(group_path(group))
expect(group.requesters.exists?(user_id: requesting_user)).to be_falsey
expect(group.users).not_to include requesting_user
......
......@@ -12,7 +12,7 @@ RSpec.describe GroupMember do
let(:source) { group }
let(:nested_source) { create(:group, parent: group) }
it_behaves_like 'member group domain validations'
it_behaves_like 'member group domain validations', 'group'
end
describe 'access level inclusion' do
......
......@@ -95,7 +95,7 @@ RSpec.describe ProjectMember do
let(:subgroup) { create(:group, parent: group) }
let(:nested_source) { create(:project, namespace: subgroup) }
it_behaves_like 'member group domain validations'
it_behaves_like 'member group domain validations', 'project'
it 'does not validate personal projects' do
unconfirmed_gitlab_user = create(:user, :unconfirmed, email: 'unverified@gitlab.com')
......
......@@ -21,40 +21,37 @@ RSpec.describe API::Invitations, 'EE Invitations' do
end
end
shared_examples 'admin signup restrictions email error' do
context 'when restricted by admin signup restriction - denylist' do
before do
stub_application_setting(domain_denylist_enabled: true)
stub_application_setting(domain_denylist: ['example.org'])
end
# this response code should be changed to 4xx: https://gitlab.com/gitlab-org/gitlab/-/issues/321706
it_behaves_like 'restricted email error', 'User is not from an allowed domain.', :created
shared_examples 'admin signup restrictions email error - denylist' do |message, code|
before do
stub_application_setting(domain_denylist_enabled: true)
stub_application_setting(domain_denylist: ['example.org'])
end
context 'when restricted by admin signup restriction - allowlist' do
before do
stub_application_setting(domain_allowlist: ['example.com'])
end
it_behaves_like 'restricted email error', message, code
end
# this response code should be changed to 4xx: https://gitlab.com/gitlab-org/gitlab/-/issues/321706
it_behaves_like 'restricted email error', 'User domain is not authorized for sign-up.', :created
shared_examples 'admin signup restrictions email error - allowlist' do |message, code|
before do
stub_application_setting(domain_allowlist: ['example.com'])
end
context 'when restricted by admin signup restriction - email restrictions' do
before do
stub_application_setting(email_restrictions_enabled: true)
stub_application_setting(email_restrictions: '([\+]|\b(\w*example.org\w*)\b)')
end
it_behaves_like 'restricted email error', message, code
end
# this response code should be changed to 4xx: https://gitlab.com/gitlab-org/gitlab/-/issues/321706
it_behaves_like 'restricted email error', 'User is not allowed. Try again with a different email address, or contact your GitLab admin.', :created
shared_examples 'admin signup restrictions email error - email restrictions' do |message, code|
before do
stub_application_setting(email_restrictions_enabled: true)
stub_application_setting(email_restrictions: '([\+]|\b(\w*example.org\w*)\b)')
end
it_behaves_like 'restricted email error', message, code
end
describe 'POST /groups/:id/invitations' do
it_behaves_like 'admin signup restrictions email error - denylist', "The member's email address is not allowed for this group. Go to the &#39;Admin area &gt; Sign-up restrictions&#39;, and check the &#39;Domain denylist&#39;.", :created
context 'when the group is restricted by admin signup restrictions' do
it_behaves_like 'admin signup restrictions email error'
it_behaves_like 'admin signup restrictions email error - allowlist', "The member's email address is not allowed for this group. Go to the &#39;Admin area &gt; Sign-up restrictions&#39;, and check &#39;Allowed domains for sign-ups&#39;.", :created
it_behaves_like 'admin signup restrictions email error - email restrictions', "The member's email address is not allowed for this group. Go to the &#39;Admin area &gt; Sign-up restrictions&#39;, and check &#39;Email restrictions for sign-ups&#39;.", :created
end
context 'when the group is restricted by group signup restriction - allowed domains for signup' do
......@@ -63,8 +60,7 @@ RSpec.describe API::Invitations, 'EE Invitations' do
create(:allowed_email_domain, group: group, domain: 'example.com')
end
# this response code should be changed to 4xx: https://gitlab.com/gitlab-org/gitlab/-/issues/321706
it_behaves_like 'restricted email error', "Invite email email does not match the allowed domain of example.com", :success
it_behaves_like 'restricted email error', "The member's email address is not allowed for this group. Go to the group’s &#39;Settings &gt; General&#39; page, and check &#39;Restrict membership by email domain&#39;.", :success
end
end
......@@ -74,7 +70,11 @@ RSpec.describe API::Invitations, 'EE Invitations' do
let(:url) { "/projects/#{project.id}/invitations" }
context 'when the project is restricted by admin signup restrictions' do
it_behaves_like 'admin signup restrictions email error'
it_behaves_like 'admin signup restrictions email error - denylist', "The member's email address is not allowed for this project. Go to the &#39;Admin area &gt; Sign-up restrictions&#39;, and check the &#39;Domain denylist&#39;.", :created
context 'when the group is restricted by admin signup restrictions' do
it_behaves_like 'admin signup restrictions email error - allowlist', "The member's email address is not allowed for this project. Go to the &#39;Admin area &gt; Sign-up restrictions&#39;, and check &#39;Allowed domains for sign-ups&#39;.", :created
it_behaves_like 'admin signup restrictions email error - email restrictions', "The member's email address is not allowed for this project. Go to the &#39;Admin area &gt; Sign-up restrictions&#39;, and check &#39;Email restrictions for sign-ups&#39;.", :created
end
end
end
end
......@@ -53,7 +53,7 @@ RSpec.describe Groups::GroupMembersController do
it 'returns error message' do
subject
expect(json_response).to eq({ 'message' => "User email does not match the allowed domain of gitlab.com" })
expect(json_response['message']).to eq("The member's email address is not allowed for this group. Check with your administrator.")
end
end
end
......
......@@ -53,7 +53,7 @@ RSpec.shared_examples 'member validations' do
end
end
RSpec.shared_examples 'member group domain validations' do
RSpec.shared_examples 'member group domain validations' do |source_type|
context 'validates group domain limitations' do
let(:group) { create(:group) }
let(:gitlab_user) { create(:user, email: 'test@gitlab.com') }
......@@ -77,11 +77,20 @@ RSpec.shared_examples 'member group domain validations' do
expect(build(member_type, source: source, user: acme_user)).to be_valid
end
it 'shows proper error message' do
it 'shows proper error message when not invited by admin' do
member = build(member_type, source: source, user: gmail_user)
allow(member).to receive_message_chain(:created_by, :can_admin_all_resources?).and_return(false)
expect(member).to be_invalid
expect(member.errors[:user]).to include("email does not match the allowed domains: gitlab.com, acme.com")
expect(member.errors[:user]).to include("is not allowed for this #{source_type}. Check with your administrator.")
end
it 'shows proper error message when invited by admin' do
member = build(member_type, source: source, user: gmail_user)
allow(member).to receive_message_chain(:created_by, :can_admin_all_resources?).and_return(true)
expect(member).to be_invalid
expect(member.errors[:user]).to include("is not allowed for this #{source_type}. Go to the group’s &#39;Settings &gt; General&#39; page, and check &#39;Restrict membership by email domain&#39;.")
end
it 'shows proper error message for single domain limitation' do
......@@ -89,7 +98,7 @@ RSpec.shared_examples 'member group domain validations' do
member = build(member_type, source: source, user: gmail_user)
expect(member).to be_invalid
expect(member.errors[:user]).to include("email does not match the allowed domain of gitlab.com")
expect(member.errors[:user]).to include("is not allowed for this #{source_type}. Check with your administrator.")
end
it 'invited email must match at least one of the allowed domain emails' do
......
......@@ -6546,6 +6546,9 @@ msgstr ""
msgid "Check the elasticsearch.log file to debug why the migration was halted and make any changes before retrying the migration. When you fix the cause of the failure, click \"Retry migration\", and the migration will be scheduled to be retried in the background."
msgstr ""
msgid "Check with your administrator."
msgstr ""
msgid "Check your Docker images for known vulnerabilities."
msgstr ""
......@@ -15823,9 +15826,21 @@ msgstr ""
msgid "Go to snippets"
msgstr ""
msgid "Go to the 'Admin area &gt; Sign-up restrictions', and check 'Allowed domains for sign-ups'."
msgstr ""
msgid "Go to the 'Admin area &gt; Sign-up restrictions', and check 'Email restrictions for sign-ups'."
msgstr ""
msgid "Go to the 'Admin area &gt; Sign-up restrictions', and check the 'Domain denylist'."
msgstr ""
msgid "Go to the activity feed"
msgstr ""
msgid "Go to the group’s 'Settings &gt; General' page, and check 'Restrict membership by email domain'."
msgstr ""
msgid "Go to the milestone list"
msgstr ""
......@@ -40034,9 +40049,6 @@ msgstr ""
msgid "does not have a supported extension. Only %{extension_list} are supported"
msgstr ""
msgid "domain is not authorized for sign-up."
msgstr ""
msgid "download it"
msgstr ""
......@@ -40054,11 +40066,6 @@ msgstr ""
msgid "email '%{email}' is not a verified email."
msgstr ""
msgid "email does not match the allowed domain of %{email_domains}"
msgid_plural "email does not match the allowed domains: %{email_domains}"
msgstr[0] ""
msgstr[1] ""
msgid "enabled"
msgstr ""
......@@ -40272,16 +40279,19 @@ msgstr ""
msgid "is not a valid X509 certificate."
msgstr ""
msgid "is not allowed since the group is not top-level group."
msgid "is not allowed for sign-up."
msgstr ""
msgid "is not allowed. Try again with a different email address, or contact your GitLab admin."
msgid "is not allowed for this group."
msgstr ""
msgid "is not allowed. We do not currently support project-level iterations"
msgid "is not allowed for this project."
msgstr ""
msgid "is not from an allowed domain."
msgid "is not allowed since the group is not top-level group."
msgstr ""
msgid "is not allowed. We do not currently support project-level iterations"
msgstr ""
msgid "is not in the group enforcing Group Managed Account"
......
......@@ -242,7 +242,7 @@ describe('InviteMembersModal', () => {
};
const expectedEmailRestrictedError =
"email 'email@example.com' does not match the allowed domains: example1.org";
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
const expectedSyntaxError = 'email contains an invalid email address';
it('calls the API with the expected focus data when an areas_of_focus checkbox is clicked', () => {
......@@ -421,7 +421,7 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe(
"root: User email 'admin@example.com' does not match the allowed domain of example2.com",
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
);
expect(findMembersSelect().props('validationState')).toBe(false);
});
......
......@@ -9,7 +9,7 @@ const INVITATIONS_API_ERROR_EMAIL_INVALID = {
const INVITATIONS_API_EMAIL_RESTRICTED = {
message: {
'email@example.com':
"Invite email 'email@example.com' does not match the allowed domains: example1.org",
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
},
status: 'error',
};
......@@ -17,9 +17,9 @@ const INVITATIONS_API_EMAIL_RESTRICTED = {
const INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED = {
message: {
'email@example.com':
"Invite email email 'email@example.com' does not match the allowed domains: example1.org",
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
'email4@example.com':
"Invite email email 'email4@example.com' does not match the allowed domains: example1.org",
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
},
status: 'error',
};
......@@ -36,7 +36,11 @@ const MEMBERS_API_MEMBER_ALREADY_EXISTS = {
};
const MEMBERS_API_SINGLE_USER_RESTRICTED = {
message: { user: ["email 'email@example.com' does not match the allowed domains: example1.org"] },
message: {
user: [
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
],
},
};
const MEMBERS_API_SINGLE_USER_ACCESS_LEVEL = {
......@@ -49,7 +53,7 @@ const MEMBERS_API_SINGLE_USER_ACCESS_LEVEL = {
const MEMBERS_API_MULTIPLE_USERS_RESTRICTED = {
message:
"root: User email 'admin@example.com' does not match the allowed domain of example2.com and user18: User email 'user18@example.org' does not match the allowed domain of example2.com",
"root: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups. and user18: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist. and john_doe31: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Email restrictions for sign-ups.",
status: 'error',
};
......
......@@ -2,18 +2,20 @@ import {
responseMessageFromSuccess,
responseMessageFromError,
} from '~/invite_members/utils/response_message_parser';
import { membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
describe('Response message parser', () => {
const expectedMessage = 'expected display message';
const expectedMessage = 'expected display and message.';
describe('parse message from successful response', () => {
const exampleKeyedMsg = { 'email@example.com': expectedMessage };
const exampleFirstPartMultiple = 'username1: expected display and message.';
const exampleUserMsgMultiple =
' and username1: id not found and username2: email is restricted';
' and username2: id not found and restricted email. and username3: email is restricted.';
it.each([
[[{ data: { message: expectedMessage } }]],
[[{ data: { message: expectedMessage + exampleUserMsgMultiple } }]],
[[{ data: { message: exampleFirstPartMultiple + exampleUserMsgMultiple } }]],
[[{ data: { error: expectedMessage } }]],
[[{ data: { message: [expectedMessage] } }]],
[[{ data: { message: exampleKeyedMsg } }]],
......@@ -33,4 +35,24 @@ describe('Response message parser', () => {
expect(responseMessageFromError(errorResponse)).toBe(expectedMessage);
});
});
describe('displaying only the first error when a response has messages for multiple users', () => {
const expected =
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
it.each([
[[{ data: membersApiResponse.MULTIPLE_USERS_RESTRICTED }]],
[[{ data: invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED }]],
[[{ data: invitationsApiResponse.EMAIL_RESTRICTED }]],
])(`returns "${expectedMessage}" from success response: %j`, (restrictedResponse) => {
expect(responseMessageFromSuccess(restrictedResponse)).toBe(expected);
});
it.each([[{ response: { data: membersApiResponse.SINGLE_USER_RESTRICTED } }]])(
`returns "${expectedMessage}" from error response: %j`,
(singleRestrictedResponse) => {
expect(responseMessageFromError(singleRestrictedResponse)).toBe(expected);
},
);
});
});
......@@ -65,6 +65,8 @@ RSpec.describe Member do
end
context 'with admin signup restrictions' do
let(:expected_message) { _('is not allowed for this group. Check with your administrator.') }
context 'when allowed domains for signup is enabled' do
before do
stub_application_setting(domain_allowlist: ['example.com'])
......@@ -74,7 +76,7 @@ RSpec.describe Member do
member = build(:group_member, :invited, invite_email: 'info@gitlab.com')
expect(member).not_to be_valid
expect(member.errors.messages[:user].first).to eq(_('domain is not authorized for sign-up.'))
expect(member.errors.messages[:user].first).to eq(expected_message)
end
end
......@@ -88,7 +90,7 @@ RSpec.describe Member do
member = build(:group_member, :invited, invite_email: 'denylist@example.org')
expect(member).not_to be_valid
expect(member.errors.messages[:user].first).to eq(_('is not from an allowed domain.'))
expect(member.errors.messages[:user].first).to eq(expected_message)
end
end
......@@ -102,7 +104,7 @@ RSpec.describe Member do
member = build(:group_member, :invited, invite_email: 'info@gitlab.com')
expect(member).not_to be_valid
expect(member.errors.messages[:user].first).to eq(_('is not allowed. Try again with a different email address, or contact your GitLab admin.'))
expect(member.errors.messages[:user].first).to eq(expected_message)
end
end
end
......
......@@ -494,6 +494,8 @@ RSpec.describe User do
end
describe 'email' do
let(:expected_error) { _('is not allowed for sign-up. Check with your administrator.') }
context 'when no signup domains allowed' do
before do
stub_application_setting(domain_allowlist: [])
......@@ -537,7 +539,7 @@ RSpec.describe User do
it 'rejects example@test.com' do
user = build(:user, email: "example@test.com")
expect(user).to be_invalid
expect(user.errors.messages[:email].first).to eq(_('domain is not authorized for sign-up.'))
expect(user.errors.messages[:email].first).to eq(expected_error)
end
end
......@@ -554,13 +556,13 @@ RSpec.describe User do
it 'rejects info@test.example.com' do
user = build(:user, email: "info@test.example.com")
expect(user).to be_invalid
expect(user.errors.messages[:email].first).to eq(_('domain is not authorized for sign-up.'))
expect(user.errors.messages[:email].first).to eq(expected_error)
end
it 'rejects example@test.com' do
user = build(:user, email: "example@test.com")
expect(user).to be_invalid
expect(user.errors.messages[:email].first).to eq(_('domain is not authorized for sign-up.'))
expect(user.errors.messages[:email].first).to eq(expected_error)
end
it 'accepts example@test.com when added by another user' do
......@@ -598,7 +600,7 @@ RSpec.describe User do
it 'rejects info@example.com' do
user = build(:user, email: 'info@example.com')
expect(user).not_to be_valid
expect(user.errors.messages[:email].first).to eq(_('is not from an allowed domain.'))
expect(user.errors.messages[:email].first).to eq(expected_error)
end
it 'accepts info@example.com when added by another user' do
......@@ -632,7 +634,7 @@ RSpec.describe User do
it 'rejects info@example.com' do
user = build(:user, email: 'info@example.com')
expect(user).not_to be_valid
expect(user.errors.messages[:email].first).to eq(_('domain is not authorized for sign-up.'))
expect(user.errors.messages[:email].first).to eq(expected_error)
end
end
end
......@@ -673,7 +675,7 @@ RSpec.describe User do
user = build(:user, email: 'info@gitlab.com')
expect(user).not_to be_valid
expect(user.errors.messages[:email].first).to eq(_('is not allowed. Try again with a different email address, or contact your GitLab admin.'))
expect(user.errors.messages[:email].first).to eq(expected_error)
end
it 'does accept a valid email address' do
......
......@@ -80,7 +80,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'does not add a member' do
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to eq('Invite email has already been taken')
expect(execute_service[:message]).to eq("The member's email address has already been taken")
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
end
end
......
......@@ -150,7 +150,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
expect_to_create_members(count: 1)
expect(result[:status]).to eq(:error)
expect(result[:message][invited_member.invite_email])
.to eq("Invite email has already been taken")
.to eq("The member's email address has already been taken")
expect(project.users).to include project_user
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment