Commit 86a8eee1 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@13-6-stable-ee

parent 1b517a5a
import { joinPaths } from '~/lib/utils/url_utility';
export const pathGenerator = (imageDetails, ending = '?format=json') => { export const pathGenerator = (imageDetails, ending = '?format=json') => {
// this method is a temporary workaround, to be removed with graphql implementation // this method is a temporary workaround, to be removed with graphql implementation
// https://gitlab.com/gitlab-org/gitlab/-/issues/276432 // https://gitlab.com/gitlab-org/gitlab/-/issues/276432
...@@ -12,5 +14,12 @@ export const pathGenerator = (imageDetails, ending = '?format=json') => { ...@@ -12,5 +14,12 @@ export const pathGenerator = (imageDetails, ending = '?format=json') => {
return acc; return acc;
}, []) }, [])
.join('/'); .join('/');
return `/${basePath}/registry/repository/${imageDetails.id}/tags${ending}`;
return joinPaths(
window.gon.relative_url_root,
`/${basePath}`,
'/registry/repository/',
`${imageDetails.id}`,
`tags${ending}`,
);
}; };
...@@ -202,6 +202,9 @@ export default { ...@@ -202,6 +202,9 @@ export default {
shouldShowAccessibilityReport() { shouldShowAccessibilityReport() {
return this.mr.accessibilityReportPath; return this.mr.accessibilityReportPath;
}, },
formattedHumanAccess() {
return (this.mr.humanAccess || '').toLowerCase();
},
}, },
watch: { watch: {
state(newVal, oldVal) { state(newVal, oldVal) {
...@@ -439,7 +442,7 @@ export default { ...@@ -439,7 +442,7 @@ export default {
class="mr-widget-workflow" class="mr-widget-workflow"
:pipeline-path="mr.mergeRequestAddCiConfigPath" :pipeline-path="mr.mergeRequestAddCiConfigPath"
:pipeline-svg-path="mr.pipelinesEmptySvgPath" :pipeline-svg-path="mr.pipelinesEmptySvgPath"
:human-access="mr.humanAccess.toLowerCase()" :human-access="formattedHumanAccess"
:user-callouts-path="mr.userCalloutsPath" :user-callouts-path="mr.userCalloutsPath"
:user-callout-feature-id="mr.suggestPipelineFeatureId" :user-callout-feature-id="mr.suggestPipelineFeatureId"
@dismiss="dismissSuggestPipelines" @dismiss="dismissSuggestPipelines"
......
...@@ -6,14 +6,14 @@ module MembersHelper ...@@ -6,14 +6,14 @@ module MembersHelper
text = 'Are you sure you want to' text = 'Are you sure you want to'
action = action =
if member.request? if member.invite?
"revoke the invitation for #{member.invite_email} to join"
elsif member.request?
if member.user == user if member.user == user
'withdraw your access request for' 'withdraw your access request for'
else else
"deny #{member.user.name}'s request to join" "deny #{member.user.name}'s request to join"
end end
elsif member.invite?
"revoke the invitation for #{member.invite_email} to join"
else else
if member.user if member.user
"remove #{member.user.name} from" "remove #{member.user.name} from"
......
...@@ -20,8 +20,8 @@ module Members ...@@ -20,8 +20,8 @@ module Members
emails.each do |email| emails.each do |email|
next if existing_member?(source, email) next if existing_member?(source, email)
next if existing_invite?(source, email) next if existing_invite?(source, email)
next if existing_request?(source, email)
if existing_user?(email) if existing_user?(email)
add_existing_user_as_member(current_user, source, params, email) add_existing_user_as_member(current_user, source, params, email)
...@@ -44,8 +44,7 @@ module Members ...@@ -44,8 +44,7 @@ module Members
access_level: params[:access_level], access_level: params[:access_level],
invite_email: email, invite_email: email,
created_by_id: current_user.id, created_by_id: current_user.id,
expires_at: params[:expires_at], expires_at: params[:expires_at])
requested_at: Time.current.utc)
unless new_member.valid? && new_member.persisted? unless new_member.valid? && new_member.persisted?
errors[params[:email]] = new_member.errors.full_messages.to_sentence errors[params[:email]] = new_member.errors.full_messages.to_sentence
...@@ -92,6 +91,17 @@ module Members ...@@ -92,6 +91,17 @@ module Members
false false
end end
def existing_request?(source, email)
existing_request = source.requesters.with_user_by_email(email).exists?
if existing_request
errors[email] = "Member cannot be invited because they already requested to join #{source.name}"
return true
end
false
end
def existing_user(email) def existing_user(email)
User.find_by_email(email) User.find_by_email(email)
end end
......
...@@ -67,7 +67,7 @@ module Projects ...@@ -67,7 +67,7 @@ module Projects
@project @project
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} " message = "Unable to save #{e.inspect}: #{e.record.errors.full_messages.join(", ")}"
fail(error: message) fail(error: message)
rescue => e rescue => e
@project.errors.add(:base, e.message) if @project @project.errors.add(:base, e.message) if @project
...@@ -122,8 +122,9 @@ module Projects ...@@ -122,8 +122,9 @@ module Projects
only_concrete_membership: true) only_concrete_membership: true)
if group_access_level > GroupMember::NO_ACCESS if group_access_level > GroupMember::NO_ACCESS
current_user.project_authorizations.create!(project: @project, current_user.project_authorizations.safe_find_or_create_by!(
access_level: group_access_level) project: @project,
access_level: group_access_level)
end end
if Feature.enabled?(:specialized_project_authorization_workers) if Feature.enabled?(:specialized_project_authorization_workers)
......
---
title: Add different string encoding method in rack middleware
merge_request: 49044
author:
type: fixed
---
title: Fix container_registry url for relative urls
merge_request: 48661
author:
type: fixed
---
title: Resolve Members page 500 error after Invitation sent via API
merge_request: 48937
author:
type: fixed
---
title: Fix MR rendering issue when user is tool admin and not project member
merge_request: 49258
author:
type: fixed
---
title: Fix error 500s creating projects concurrently
merge_request: 48571
author:
type: fixed
---
title: Update Rake check and docs to require Ruby 2.7
merge_request: 48552
author:
type: changed
...@@ -121,6 +121,7 @@ class ObjectStoreSettings ...@@ -121,6 +121,7 @@ class ObjectStoreSettings
if section['enabled'] && target_config['bucket'].blank? if section['enabled'] && target_config['bucket'].blank?
missing_bucket_for(store_type) missing_bucket_for(store_type)
next
end end
# Map bucket (external name) -> remote_directory (internal representation) # Map bucket (external name) -> remote_directory (internal representation)
......
...@@ -97,7 +97,7 @@ Example response: ...@@ -97,7 +97,7 @@ Example response:
{ {
"id": 1, "id": 1,
"invite_email": "member@example.org", "invite_email": "member@example.org",
"invited_at": "2020-10-22T14:13:35Z", "created_at": "2020-10-22T14:13:35Z",
"access_level": 30, "access_level": 30,
"expires_at": "2020-11-22T14:13:35Z", "expires_at": "2020-11-22T14:13:35Z",
"user_name": "Raymond Smith", "user_name": "Raymond Smith",
......
...@@ -224,12 +224,6 @@ make ...@@ -224,12 +224,6 @@ make
sudo make install sudo make install
``` ```
Then install the Bundler gem (a version below 2.x):
```shell
sudo gem install bundler --no-document --version '< 2'
```
## 3. Go ## 3. Go
In GitLab 8.0 and later, GitLab has several daemons written in Go. To install In GitLab 8.0 and later, GitLab has several daemons written in Go. To install
......
...@@ -45,7 +45,13 @@ Please consider using a virtual machine to run GitLab. ...@@ -45,7 +45,13 @@ Please consider using a virtual machine to run GitLab.
### Ruby versions ### Ruby versions
GitLab requires Ruby (MRI) 2.6. Beginning in GitLab 12.2, we no longer support Ruby 2.5 and lower. From GitLab 13.6:
- Ruby 2.7 and later is required.
From GitLab 12.2:
- Ruby 2.6 and later is required.
You must use the standard MRI implementation of Ruby. You must use the standard MRI implementation of Ruby.
We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab
......
...@@ -312,6 +312,8 @@ installation-specific upgrade instructions, based on how you installed GitLab: ...@@ -312,6 +312,8 @@ installation-specific upgrade instructions, based on how you installed GitLab:
### 13.6.0 ### 13.6.0
Ruby 2.7.2 is required. GitLab will not start with Ruby 2.6.6 or older versions.
The required Git version is Git v2.29 or higher. The required Git version is Git v2.29 or higher.
### 13.3.0 ### 13.3.0
......
...@@ -61,8 +61,8 @@ sudo service gitlab stop ...@@ -61,8 +61,8 @@ sudo service gitlab stop
### 3. Update Ruby ### 3. Update Ruby
NOTE: Beginning in GitLab 12.2, we only support Ruby 2.6 or higher, and dropped NOTE: Beginning in GitLab 13.6, we only support Ruby 2.7 or higher, and dropped
support for Ruby 2.5. Be sure to upgrade if necessary. support for Ruby 2.6. Be sure to upgrade if necessary.
You can check which version you are running with `ruby -v`. You can check which version you are running with `ruby -v`.
...@@ -70,21 +70,15 @@ Download Ruby and compile it: ...@@ -70,21 +70,15 @@ Download Ruby and compile it:
```shell ```shell
mkdir /tmp/ruby && cd /tmp/ruby mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.6.tar.gz curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.2.tar.gz
echo '2d78048e293817f38d4ede4ebc7873013e97bb0b ruby-2.6.6.tar.gz' | shasum -c - && tar xzf ruby-2.6.6.tar.gz echo 'cb9731a17487e0ad84037490a6baf8bfa31a09e8 ruby-2.7.2.tar.gz' | shasum -c - && tar xzf ruby-2.7.2.tar.gz
cd ruby-2.6.6 cd ruby-2.7.2
./configure --disable-install-rdoc ./configure --disable-install-rdoc
make make
sudo make install sudo make install
``` ```
Install Bundler:
```shell
sudo gem install bundler --no-document --version '< 2'
```
### 4. Update Node.js ### 4. Update Node.js
NOTE: To check the minimum required Node.js version, see [Node.js versions](../install/requirements.md#nodejs-versions). NOTE: To check the minimum required Node.js version, see [Node.js versions](../install/requirements.md#nodejs-versions).
......
...@@ -188,15 +188,12 @@ To set your current status: ...@@ -188,15 +188,12 @@ To set your current status:
1. Set the desired emoji and/or status message. 1. Set the desired emoji and/or status message.
1. Click **Set status**. Alternatively, you can click **Remove status** to remove your user status entirely. 1. Click **Set status**. Alternatively, you can click **Remove status** to remove your user status entirely.
![Busy status indicator](img/busy_status_indicator_v13_6.png)
or or
1. Click your avatar. 1. Click your avatar.
1. Select **Profile**. 1. Select **Profile**.
1. Click **Edit profile** (pencil icon). 1. Click **Edit profile** (pencil icon).
1. Enter your status message in the **Your status** text field. 1. Enter your status message in the **Your status** text field.
1. Alternatively, select the **Busy** checkbox ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259649) in GitLab 13.6}.
1. Click **Add status emoji** (smiley face), and select the desired emoji. 1. Click **Add status emoji** (smiley face), and select the desired emoji.
1. Click **Update profile settings**. 1. Click **Update profile settings**.
...@@ -204,6 +201,44 @@ You can also set your current status [using the API](../../api/users.md#user-sta ...@@ -204,6 +201,44 @@ You can also set your current status [using the API](../../api/users.md#user-sta
If you previously selected the "Busy" checkbox, remember to deselect it when you become available again. If you previously selected the "Busy" checkbox, remember to deselect it when you become available again.
## Busy status indicator
> - Introduced in GitLab 13.6.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-busy-status-feature).
To indicate to others that you are busy, you can set an indicator
![Busy status indicator](img/busy_status_indicator_v13_6.png)
To set the busy status indicator, either:
- Set it directly:
1. Click your avatar.
1. Click **Set status**, or **Edit status** if you have already set a status.
1. Select the **Busy** checkbox
- Set it on your profile:
1. Click your avatar.
1. Select **Profile**.
1. Click **Edit profile** (**{pencil}**).
1. Select the **Busy** checkbox
### Enable busy status feature
The busy status feature is deployed behind a feature flag and is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) can enable it for your instance from the [rails console](../../administration/feature_flags.md#start-the-gitlab-rails-console).
To enable it:
```ruby
Feature.enable(:set_user_availability_status)
```
## Commit email ## Commit email
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/21598) in GitLab 11.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/21598) in GitLab 11.4.
......
...@@ -4,7 +4,7 @@ module API ...@@ -4,7 +4,7 @@ module API
module Entities module Entities
class Invitation < Grape::Entity class Invitation < Grape::Entity
expose :access_level expose :access_level
expose :requested_at expose :created_at
expose :expires_at expose :expires_at
expose :invite_email expose :invite_email
expose :invite_token expose :invite_token
......
...@@ -93,7 +93,8 @@ module Gitlab ...@@ -93,7 +93,8 @@ module Gitlab
# We try to encode the string from ASCII-8BIT to UTF8. If we failed to do # We try to encode the string from ASCII-8BIT to UTF8. If we failed to do
# so for certain characters in the string, those chars are probably incomplete # so for certain characters in the string, those chars are probably incomplete
# multibyte characters. # multibyte characters.
string.encode(Encoding::UTF_8).match?(NULL_BYTE_REGEX) string.dup.force_encoding(Encoding::UTF_8).match?(NULL_BYTE_REGEX)
rescue ArgumentError, Encoding::UndefinedConversionError rescue ArgumentError, Encoding::UndefinedConversionError
# If we're here, we caught a malformed string. Return true # If we're here, we caught a malformed string. Return true
true true
......
...@@ -7,7 +7,7 @@ module SystemCheck ...@@ -7,7 +7,7 @@ module SystemCheck
set_check_pass -> { "yes (#{self.current_version})" } set_check_pass -> { "yes (#{self.current_version})" }
def self.required_version def self.required_version
@required_version ||= Gitlab::VersionInfo.new(2, 5, 3) @required_version ||= Gitlab::VersionInfo.new(2, 7, 2)
end end
def self.current_version def self.current_version
......
...@@ -88,6 +88,7 @@ RSpec.describe ObjectStoreSettings do ...@@ -88,6 +88,7 @@ RSpec.describe ObjectStoreSettings do
config['object_store']['objects']['pages'].delete('bucket') config['object_store']['objects']['pages'].delete('bucket')
expect { subject }.not_to raise_error expect { subject }.not_to raise_error
expect(settings.pages['object_store']).to eq(nil)
end end
context 'with legacy config' do context 'with legacy config' do
......
...@@ -8,6 +8,10 @@ describe('Utils', () => { ...@@ -8,6 +8,10 @@ describe('Utils', () => {
id: 1, id: 1,
}; };
beforeEach(() => {
window.gon.relative_url_root = null;
});
it('returns the fetch url when no ending is passed', () => { it('returns the fetch url when no ending is passed', () => {
expect(pathGenerator(imageDetails)).toBe('/foo/bar/registry/repository/1/tags?format=json'); expect(pathGenerator(imageDetails)).toBe('/foo/bar/registry/repository/1/tags?format=json');
}); });
...@@ -16,7 +20,7 @@ describe('Utils', () => { ...@@ -16,7 +20,7 @@ describe('Utils', () => {
expect(pathGenerator(imageDetails, '/foo')).toBe('/foo/bar/registry/repository/1/tags/foo'); expect(pathGenerator(imageDetails, '/foo')).toBe('/foo/bar/registry/repository/1/tags/foo');
}); });
it.each` describe.each`
path | name | result path | name | result
${'foo/foo'} | ${''} | ${'/foo/foo/registry/repository/1/tags?format=json'} ${'foo/foo'} | ${''} | ${'/foo/foo/registry/repository/1/tags?format=json'}
${'foo/foo/foo'} | ${'foo'} | ${'/foo/foo/registry/repository/1/tags?format=json'} ${'foo/foo/foo'} | ${'foo'} | ${'/foo/foo/registry/repository/1/tags?format=json'}
...@@ -26,8 +30,15 @@ describe('Utils', () => { ...@@ -26,8 +30,15 @@ describe('Utils', () => {
${'foo/foo/baz/foo/bar'} | ${'foo/bar'} | ${'/foo/foo/baz/registry/repository/1/tags?format=json'} ${'foo/foo/baz/foo/bar'} | ${'foo/bar'} | ${'/foo/foo/baz/registry/repository/1/tags?format=json'}
${'baz/foo/foo'} | ${'foo'} | ${'/baz/foo/registry/repository/1/tags?format=json'} ${'baz/foo/foo'} | ${'foo'} | ${'/baz/foo/registry/repository/1/tags?format=json'}
${'baz/foo/bar'} | ${'foo'} | ${'/baz/foo/bar/registry/repository/1/tags?format=json'} ${'baz/foo/bar'} | ${'foo'} | ${'/baz/foo/bar/registry/repository/1/tags?format=json'}
`('returns the correct path when path is $path and name is $name', ({ name, path, result }) => { `('when path is $path and name is $name', ({ name, path, result }) => {
expect(pathGenerator({ id: 1, name, path })).toBe(result); it('returns the correct value', () => {
expect(pathGenerator({ id: 1, name, path })).toBe(result);
});
it('produces a correct relative url', () => {
window.gon.relative_url_root = '/gitlab';
expect(pathGenerator({ id: 1, name, path })).toBe(`/gitlab${result}`);
});
}); });
it('returns the url unchanged when imageDetails have no name', () => { it('returns the url unchanged when imageDetails have no name', () => {
......
...@@ -260,6 +260,20 @@ describe('mrWidgetOptions', () => { ...@@ -260,6 +260,20 @@ describe('mrWidgetOptions', () => {
}); });
}); });
}); });
describe('formattedHumanAccess', () => {
it('when user is a tool admin but not a member of project', () => {
vm.mr.humanAccess = null;
expect(vm.formattedHumanAccess).toEqual('');
});
it('when user a member of the project', () => {
vm.mr.humanAccess = 'Owner';
expect(vm.formattedHumanAccess).toEqual('owner');
});
});
}); });
describe('methods', () => { describe('methods', () => {
......
...@@ -33,6 +33,16 @@ RSpec.describe MembersHelper do ...@@ -33,6 +33,16 @@ RSpec.describe MembersHelper do
expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to remove this orphaned member from the #{group.name} group and any subresources?" expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to remove this orphaned member from the #{group.name} group and any subresources?"
end end
end end
context 'a pending member invitation with no user associated' do
before do
project_member_invite.update_columns(invite_email: "#{SecureRandom.hex}@example.com", invite_token: 'some-token', user_id: nil)
end
it 'does not error when there is an invitation for the requestor' do
expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?"
end
end
end end
describe '#remove_member_title' do describe '#remove_member_title' do
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
require "rack/test" require "rack/test"
...@@ -104,6 +103,12 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do ...@@ -104,6 +103,12 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
expect(subject.call(env)).not_to eq error_400 expect(subject.call(env)).not_to eq error_400
end end
it 'does not reject correct encoded password with special characters' do
env = env_for.merge(auth_env("username", "RçKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U", nil))
expect(subject.call(env)).not_to eq error_400
end
end end
context 'in params' do context 'in params' do
......
...@@ -58,7 +58,7 @@ RSpec.describe API::Invitations do ...@@ -58,7 +58,7 @@ RSpec.describe API::Invitations do
it 'does not transform the requester into a proper member' do it 'does not transform the requester into a proper member' do
expect do expect do
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email, access_level: Member::MAINTAINER } params: { email: access_requester.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
end.not_to change { source.members.count } end.not_to change { source.members.count }
...@@ -71,7 +71,7 @@ RSpec.describe API::Invitations do ...@@ -71,7 +71,7 @@ RSpec.describe API::Invitations do
params: { email: email, access_level: Member::DEVELOPER } params: { email: email, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
end.to change { source.requesters.count }.by(1) end.to change { source.members.invite.count }.by(1)
end end
it 'invites a list of new email addresses' do it 'invites a list of new email addresses' do
...@@ -82,7 +82,7 @@ RSpec.describe API::Invitations do ...@@ -82,7 +82,7 @@ RSpec.describe API::Invitations do
params: { email: email_list, access_level: Member::DEVELOPER } params: { email: email_list, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
end.to change { source.requesters.count }.by(2) end.to change { source.members.invite.count }.by(2)
end end
end end
...@@ -140,7 +140,7 @@ RSpec.describe API::Invitations do ...@@ -140,7 +140,7 @@ RSpec.describe API::Invitations do
it 'invites a member' do it 'invites a member' do
expect do expect do
subject subject
end.to change { source.requesters.count }.by(1) end.to change { source.members.invite.count }.by(1)
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
end end
......
...@@ -280,6 +280,20 @@ RSpec.describe 'Git HTTP requests' do ...@@ -280,6 +280,20 @@ RSpec.describe 'Git HTTP requests' do
project.add_developer(user) project.add_developer(user)
end end
context 'when user is using credentials with special characters' do
context 'with password with special characters' do
before do
user.update!(password: 'RKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U')
end
it 'allows clones' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
context 'but the repo is disabled' do context 'but the repo is disabled' do
let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) } let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) }
......
...@@ -63,4 +63,15 @@ RSpec.describe Members::InviteService do ...@@ -63,4 +63,15 @@ RSpec.describe Members::InviteService do
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}") expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}")
end end
it 'does not add a member with an access_request' do
requested_member = create(:project_member, :access_request, project: project)
params = { email: requested_member.user.email,
access_level: Gitlab::Access::GUEST }
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message][requested_member.user.email]).to eq("Member cannot be invited because they already requested to join #{project.name}")
end
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