Commit 80bf781a authored by Imre Farkas's avatar Imre Farkas

Merge branch '257886-user-admin-approval-approve-user-via-admin-ui' into 'master'

User admin approval - Approve users pending approval via admin UI

See merge request gitlab-org/gitlab!44877
parents 1ba856f5 f13736a4
......@@ -6,6 +6,7 @@ class Admin::UsersController < Admin::ApplicationController
before_action :user, except: [:index, :new, :create]
before_action :check_impersonation_availability, only: :impersonate
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
before_action :check_admin_approval_feature_available!, only: [:approve]
feature_category :users
......@@ -62,6 +63,16 @@ class Admin::UsersController < Admin::ApplicationController
end
end
def approve
result = Users::ApproveService.new(current_user).execute(user)
if result[:status] == :success
redirect_back_or_admin_user(notice: _("Successfully approved"))
else
redirect_back_or_admin_user(alert: result[:message])
end
end
def activate
return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user must be unblocked to be activated")) if user.blocked?
......@@ -82,7 +93,7 @@ class Admin::UsersController < Admin::ApplicationController
def block
result = Users::BlockService.new(current_user).execute(user)
if result[:status] = :success
if result[:status] == :success
redirect_back_or_admin_user(notice: _("Successfully blocked"))
else
redirect_back_or_admin_user(alert: _("Error occurred. User was not blocked"))
......@@ -287,6 +298,10 @@ class Admin::UsersController < Admin::ApplicationController
def log_impersonation_event
Gitlab::AppLogger.info(_("User %{current_user_username} has started impersonating %{username}") % { current_user_username: current_user.username, username: user.username })
end
def check_admin_approval_feature_available!
access_denied! unless Feature.enabled?(:admin_approval_for_new_user_signups)
end
end
Admin::UsersController.prepend_if_ee('EE::Admin::UsersController')
......@@ -84,7 +84,7 @@ module UsersHelper
def user_badges_in_admin_section(user)
[].tap do |badges|
badges << { text: s_('AdminUsers|Blocked'), variant: 'danger' } if user.blocked?
badges << blocked_user_badge(user) if user.blocked?
badges << { text: s_('AdminUsers|Admin'), variant: 'success' } if user.admin?
badges << { text: s_('AdminUsers|External'), variant: 'secondary' } if user.external?
badges << { text: s_("AdminUsers|It's you!"), variant: nil } if current_user == user
......@@ -106,8 +106,19 @@ module UsersHelper
end
end
def can_force_email_confirmation?(user)
!user.confirmed?
end
private
def blocked_user_badge(user)
pending_approval_badge = { text: s_('AdminUsers|Pending approval'), variant: 'info' }
return pending_approval_badge if user.blocked_pending_approval?
{ text: s_('AdminUsers|Blocked'), variant: 'danger' }
end
def get_profile_tabs
tabs = []
......
......@@ -120,6 +120,7 @@ module ApplicationSettingImplementation
repository_checks_enabled: true,
repository_storages_weighted: { default: 100 },
repository_storages: ['default'],
require_admin_approval_after_user_signup: false,
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
rsa_key_restriction: 0,
......
......@@ -293,6 +293,7 @@ class User < ApplicationRecord
transition active: :blocked
transition deactivated: :blocked
transition ldap_blocked: :blocked
transition blocked_pending_approval: :blocked
end
event :ldap_block do
......@@ -338,7 +339,8 @@ class User < ApplicationRecord
# Scopes
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked, :blocked_pending_approval) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :blocked_pending_approval, -> { with_states(:blocked_pending_approval) }
scope :external, -> { where(external: true) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :active, -> { with_state(:active).non_internal }
......@@ -538,6 +540,8 @@ class User < ApplicationRecord
admins
when 'blocked'
blocked
when 'blocked_pending_approval'
blocked_pending_approval
when 'two_factor_disabled'
without_two_factor
when 'two_factor_enabled'
......
......@@ -98,6 +98,7 @@ class GlobalPolicy < BasePolicy
rule { admin }.policy do
enable :read_custom_attribute
enable :update_custom_attribute
enable :approve_user
end
# We can't use `read_statistics` because the user may have different permissions for different projects
......
# frozen_string_literal: true
module Users
class ApproveService < BaseService
def initialize(current_user)
@current_user = current_user
end
def execute(user)
return error(_('You are not allowed to approve a user')) unless allowed?
return error(_('The user you are trying to approve is not pending an approval')) unless approval_required?(user)
if user.activate
# Resends confirmation email if the user isn't confirmed yet.
# Please see Devise's implementation of `resend_confirmation_instructions` for detail.
user.resend_confirmation_instructions
user.accept_pending_invitations! if user.active_for_authentication?
success
else
error(user.errors.full_messages.uniq.join('. '))
end
end
private
attr_reader :current_user
def allowed?
can?(current_user, :approve_user)
end
def approval_required?(user)
user.blocked_pending_approval?
end
end
end
.card.border-info
.card-header.gl-bg-blue-500.gl-text-white
= s_('AdminUsers|This user has requested access')
.card-body
= render partial: 'admin/users/user_approve_effects'
%br
= link_to s_('AdminUsers|Approve user'), approve_admin_user_path(user), method: :put, class: "btn gl-button btn-info", data: { confirm: s_('AdminUsers|Are you sure?') }
.card.border-warning
.card-header.bg-warning.text-white
= s_('AdminUsers|Block this user')
.card-body
= render partial: 'admin/users/user_block_effects'
%br
%button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'block',
content: s_('AdminUsers|You can always unblock their account, their data will remain intact.'),
url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Block user')
%h3.page-title
= @user.name
- if @user.blocked?
%span.cred (Blocked)
- if @user.blocked_pending_approval?
%span.cred
= s_('AdminUsers|(Pending approval)')
- elsif @user.blocked?
%span.cred
= s_('AdminUsers|(Blocked)')
- if @user.internal?
%span.cred (Internal)
%span.cred
= s_('AdminUsers|(Internal)')
- if @user.admin
%span.cred (Admin)
%span.cred
= s_('AdminUsers|(Admin)')
- if @user.deactivated?
%span.cred (Deactivated)
%span.cred
= s_('AdminUsers|(Deactivated)')
= render_if_exists 'admin/users/audtior_user_badge'
.float-right
......
......@@ -35,15 +35,22 @@
%span.small
= s_('AdminUsers|Cannot unblock LDAP blocked users')
- elsif user.blocked?
- if user.blocked_pending_approval?
= link_to s_('AdminUsers|Approve'), approve_admin_user_path(user), method: :put
%button.btn.btn-default-tertiary{ data: { 'gl-modal-action': 'block',
url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Block')
- else
= link_to _('Unblock'), unblock_admin_user_path(user), method: :put
- else
%button.btn.gl-button.btn-default-tertiary{ data: { 'gl-modal-action': 'block',
%button.btn.btn-default-tertiary{ data: { 'gl-modal-action': 'block',
url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Block')
- if user.can_be_deactivated?
%li
%button.btn.gl-button.btn-default-tertiary{ data: { 'gl-modal-action': 'deactivate',
%button.btn.btn-default-tertiary{ data: { 'gl-modal-action': 'deactivate',
url: deactivate_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Deactivate')
......@@ -57,13 +64,13 @@
%li.divider
- if user.can_be_removed?
%li
%button.delete-user-button.btn.gl-button.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete',
%button.delete-user-button.btn.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Delete user')
%li
%button.delete-user-button.btn.gl-button.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
%button.delete-user-button.btn.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
......
%p
= s_('AdminUsers|Approved users can:')
%ul
%li
= s_('AdminUsers|Log in')
%li
= s_('AdminUsers|Access Git repositories')
%li
= s_('AdminUsers|Access the API')
%li
= s_('AdminUsers|Be added to groups and projects')
......@@ -30,6 +30,11 @@
= link_to admin_users_path(filter: "blocked") do
= s_('AdminUsers|Blocked')
%small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
- if Feature.enabled?(:admin_approval_for_new_user_signups)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'blocked_pending_approval')} filter-blocked-pending-approval" }) do
= link_to admin_users_path(filter: "blocked_pending_approval") do
= s_('AdminUsers|Pending approval')
%small.badge.badge-pill= limited_counter_with_delimiter(User.blocked_pending_approval)
= nav_link(html_options: { class: active_when(params[:filter] == 'deactivated') }) do
= link_to admin_users_path(filter: "deactivated") do
= s_('AdminUsers|Deactivated')
......
......@@ -137,7 +137,7 @@
.col-md-6
- unless @user == current_user
- unless @user.confirmed?
- if can_force_email_confirmation?(@user)
.card.border-info
.card-header.bg-info.text-white
Confirm user
......@@ -173,8 +173,12 @@
= s_('AdminUsers|Deactivate user')
- if @user.blocked?
- if @user.blocked_pending_approval?
= render 'admin/users/approve_user', user: @user
= render 'admin/users/block_user', user: @user
- else
.card.border-info
.card-header.bg-info.text-white
.card-header.gl-bg-blue-500.gl-text-white
This user is blocked
.card-body
%p A blocked user cannot:
......@@ -182,19 +186,10 @@
%li Log in
%li Access Git repositories
%br
= link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
= link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: s_('AdminUsers|Are you sure?') }
- elsif !@user.internal?
.card.border-warning
.card-header.bg-warning.text-white
Block this user
.card-body
= render partial: 'admin/users/user_block_effects'
%br
%button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'block',
content: 'You can always unblock their account, their data will remain intact.',
url: block_admin_user_path(@user),
username: sanitize_name(@user.name) } }
= s_('AdminUsers|Block user')
= render 'admin/users/block_user', user: @user
- if @user.access_locked?
.card.border-info
.card-header.bg-info.text-white
......
......@@ -17,6 +17,7 @@ namespace :admin do
put :activate
put :unlock
put :confirm
put :approve
post :impersonate
patch :disable_two_factor
delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
......
......@@ -2042,6 +2042,21 @@ msgstr ""
msgid "AdminStatistics|Snippets"
msgstr ""
msgid "AdminUsers|(Admin)"
msgstr ""
msgid "AdminUsers|(Blocked)"
msgstr ""
msgid "AdminUsers|(Deactivated)"
msgstr ""
msgid "AdminUsers|(Internal)"
msgstr ""
msgid "AdminUsers|(Pending approval)"
msgstr ""
msgid "AdminUsers|2FA Disabled"
msgstr ""
......@@ -2051,6 +2066,12 @@ msgstr ""
msgid "AdminUsers|Access"
msgstr ""
msgid "AdminUsers|Access Git repositories"
msgstr ""
msgid "AdminUsers|Access the API"
msgstr ""
msgid "AdminUsers|Active"
msgstr ""
......@@ -2063,12 +2084,30 @@ msgstr ""
msgid "AdminUsers|Admins"
msgstr ""
msgid "AdminUsers|Approve"
msgstr ""
msgid "AdminUsers|Approve user"
msgstr ""
msgid "AdminUsers|Approved users can:"
msgstr ""
msgid "AdminUsers|Are you sure?"
msgstr ""
msgid "AdminUsers|Automatically marked as default internal user"
msgstr ""
msgid "AdminUsers|Be added to groups and projects"
msgstr ""
msgid "AdminUsers|Block"
msgstr ""
msgid "AdminUsers|Block this user"
msgstr ""
msgid "AdminUsers|Block user"
msgstr ""
......@@ -2123,6 +2162,9 @@ msgstr ""
msgid "AdminUsers|It's you!"
msgstr ""
msgid "AdminUsers|Log in"
msgstr ""
msgid "AdminUsers|New user"
msgstr ""
......@@ -2132,6 +2174,9 @@ msgstr ""
msgid "AdminUsers|Owned groups will be left"
msgstr ""
msgid "AdminUsers|Pending approval"
msgstr ""
msgid "AdminUsers|Personal projects will be left"
msgstr ""
......@@ -2177,6 +2222,9 @@ msgstr ""
msgid "AdminUsers|The user will not receive any notifications"
msgstr ""
msgid "AdminUsers|This user has requested access"
msgstr ""
msgid "AdminUsers|To confirm, type %{projectName}"
msgstr ""
......@@ -2201,6 +2249,9 @@ msgstr ""
msgid "AdminUsers|You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd}, it cannot be undone or recovered."
msgstr ""
msgid "AdminUsers|You can always unblock their account, their data will remain intact."
msgstr ""
msgid "AdminUsers|You cannot remove your own admin rights."
msgstr ""
......@@ -25262,6 +25313,9 @@ msgstr ""
msgid "Successfully activated"
msgstr ""
msgid "Successfully approved"
msgstr ""
msgid "Successfully blocked"
msgstr ""
......@@ -26163,6 +26217,9 @@ msgstr ""
msgid "The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames will be imported into GitLab. You can change this by populating the table below."
msgstr ""
msgid "The user you are trying to approve is not pending an approval"
msgstr ""
msgid "The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated"
msgstr ""
......@@ -29713,6 +29770,9 @@ msgstr ""
msgid "You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}."
msgstr ""
msgid "You are not allowed to approve a user"
msgstr ""
msgid "You are not allowed to push into this branch. Create another branch or open a merge request."
msgstr ""
......
......@@ -102,6 +102,58 @@ RSpec.describe Admin::UsersController do
end
end
describe 'PUT #approve' do
let(:user) { create(:user, :blocked_pending_approval) }
subject { put :approve, params: { id: user.username } }
context 'when feature is disabled' do
before do
stub_feature_flags(admin_approval_for_new_user_signups: false)
end
it 'responds with access denied' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when feature is enabled' do
before do
stub_feature_flags(admin_approval_for_new_user_signups: true)
end
context 'when successful' do
it 'activates the user' do
subject
user.reload
expect(user).to be_active
expect(flash[:notice]).to eq('Successfully approved')
end
end
context 'when unsuccessful' do
let(:user) { create(:user, :blocked) }
it 'displays the error' do
subject
expect(flash[:alert]).to eq('The user you are trying to approve is not pending an approval')
end
it 'does not activate the user' do
subject
user.reload
expect(user).not_to be_active
end
end
end
end
describe 'PUT #activate' do
shared_examples 'a request that activates the user' do
it 'activates the user' do
......
......@@ -62,6 +62,43 @@ RSpec.describe "Admin::Users" do
end
end
describe 'tabs' do
it 'has multiple tabs to filter users' do
expect(page).to have_link('Active', href: admin_users_path)
expect(page).to have_link('Admins', href: admin_users_path(filter: 'admins'))
expect(page).to have_link('2FA Enabled', href: admin_users_path(filter: 'two_factor_enabled'))
expect(page).to have_link('2FA Disabled', href: admin_users_path(filter: 'two_factor_disabled'))
expect(page).to have_link('External', href: admin_users_path(filter: 'external'))
expect(page).to have_link('Blocked', href: admin_users_path(filter: 'blocked'))
expect(page).to have_link('Deactivated', href: admin_users_path(filter: 'deactivated'))
expect(page).to have_link('Without projects', href: admin_users_path(filter: 'wop'))
end
context '`Pending approval` tab' do
context 'feature is enabled' do
before do
stub_feature_flags(admin_approval_for_new_user_signups: true)
visit admin_users_path
end
it 'shows the `Pending approval` tab' do
expect(page).to have_link('Pending approval', href: admin_users_path(filter: 'blocked_pending_approval'))
end
end
context 'feature is disabled' do
before do
stub_feature_flags(admin_approval_for_new_user_signups: false)
visit admin_users_path
end
it 'does not show the `Pending approval` tab' do
expect(page).not_to have_link('Pending approval', href: admin_users_path(filter: 'blocked_pending_approval'))
end
end
end
end
describe 'search and sort' do
before do
create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago)
......@@ -160,6 +197,27 @@ RSpec.describe "Admin::Users" do
expect(page).to have_content(user.email)
end
end
describe 'Pending approval filter' do
it 'counts users who are pending approval' do
create_list(:user, 2, :blocked_pending_approval)
visit admin_users_path
page.within('.filter-blocked-pending-approval small') do
expect(page).to have_content('2')
end
end
it 'filters by users who are pending approval' do
user = create(:user, :blocked_pending_approval)
visit admin_users_path
click_link 'Pending approval'
expect(page).to have_content(user.email)
end
end
end
describe "GET /admin/users/new" do
......@@ -301,6 +359,23 @@ RSpec.describe "Admin::Users" do
expect(page).to have_button('Delete user and contributions')
end
context 'user pending approval' do
it 'shows user info' do
user = create(:user, :blocked_pending_approval)
visit admin_users_path
click_link 'Pending approval'
click_link user.name
expect(page).to have_content(user.name)
expect(page).to have_content('Pending approval')
expect(page).to have_link('Approve user')
expect(page).to have_button('Block user')
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
end
end
describe 'Impersonation' do
let(:another_user) { create(:user) }
......
......@@ -126,6 +126,16 @@ RSpec.describe UsersHelper do
end
end
context 'with a pending approval user' do
it 'returns the pending approval badge' do
blocked_pending_approval_user = create(:user, :blocked_pending_approval)
badges = helper.user_badges_in_admin_section(blocked_pending_approval_user)
expect(filter_ee_badges(badges)).to eq([text: 'Pending approval', variant: 'info'])
end
end
context 'with an admin user' do
it "returns the admin badge" do
admin_user = create(:admin)
......@@ -179,6 +189,20 @@ RSpec.describe UsersHelper do
end
end
describe '#can_force_email_confirmation?' do
subject { helper.can_force_email_confirmation?(user) }
context 'for a user that is already confirmed' do
it { is_expected.to eq(false) }
end
context 'for a user that is not confirmed' do
let(:user) { create(:user, :unconfirmed) }
it { is_expected.to eq(true) }
end
end
describe '#work_information' do
subject { helper.work_information(user) }
......
......@@ -705,22 +705,31 @@ RSpec.describe User do
end
describe "scopes" do
context 'blocked users' do
let_it_be(:active_user) { create(:user) }
let_it_be(:blocked_user) { create(:user, :blocked) }
let_it_be(:ldap_blocked_user) { create(:omniauth_user, :ldap_blocked) }
let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval) }
describe '.blocked' do
subject { described_class.blocked }
it 'returns only blocked users' do
active_user = create(:user)
blocked_user = create(:user, :blocked)
blocked_pending_approval_user = create(:user, :blocked_pending_approval)
ldap_blocked_user = create(:omniauth_user, :ldap_blocked)
expect(subject).to include(
blocked_user,
blocked_pending_approval_user,
ldap_blocked_user
)
expect(subject).not_to include(active_user)
expect(subject).not_to include(active_user, blocked_pending_approval_user)
end
end
describe '.blocked_pending_approval' do
subject { described_class.blocked_pending_approval }
it 'returns only pending approval users' do
expect(subject).to contain_exactly(blocked_pending_approval_user)
end
end
end
......@@ -1752,6 +1761,12 @@ RSpec.describe User do
expect(described_class.filter_items('blocked')).to include user
end
it 'filters by blocked pending approval' do
expect(described_class).to receive(:blocked_pending_approval).and_return([user])
expect(described_class.filter_items('blocked_pending_approval')).to include user
end
it 'filters by deactivated' do
expect(described_class).to receive(:deactivated).and_return([user])
......
......@@ -130,6 +130,24 @@ RSpec.describe GlobalPolicy do
end
end
describe 'approving users' do
context 'regular user' do
it { is_expected.not_to be_allowed(:approve_user) }
end
context 'admin' do
let(:current_user) { create(:admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:approve_user) }
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:approve_user) }
end
end
end
describe 'using project statistics filters' do
context 'regular user' do
it { is_expected.not_to be_allowed(:use_project_statistics_filters) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::ApproveService do
let_it_be(:current_user) { create(:admin) }
let(:user) { create(:user, :blocked_pending_approval) }
subject(:execute) { described_class.new(current_user).execute(user) }
describe '#execute' do
context 'failures' do
context 'when the executor user is not allowed to approve users' do
let(:current_user) { create(:user) }
it 'returns error result' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to match(/You are not allowed to approve a user/)
end
end
context 'when user is not in pending approval state' do
let(:user) { create(:user, state: 'active') }
it 'returns error result' do
expect(subject[:status]).to eq(:error)
expect(subject[:message])
.to match(/The user you are trying to approve is not pending an approval/)
end
end
context 'when user cannot be activated' do
let(:user) do
build(:user, state: 'blocked_pending_approval', email: 'invalid email')
end
it 'returns error result' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to match(/Email is invalid/)
end
it 'does not change the state of the user' do
expect { subject }.not_to change { user.state }
end
end
end
context 'success' do
it 'activates the user' do
expect(subject[:status]).to eq(:success)
expect(user.reload).to be_active
end
context 'email confirmation status' do
context 'user is unconfirmed' do
let(:user) { create(:user, :blocked_pending_approval, :unconfirmed) }
it 'sends confirmation instructions' do
expect { subject }
.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
end
end
context 'user is confirmed' do
it 'does not send a confirmation email' do
expect { subject }
.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
end
end
end
context 'pending invitiations' do
let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) }
let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) }
context 'user is unconfirmed' do
let(:user) { create(:user, :blocked_pending_approval, :unconfirmed) }
it 'does not accept pending invites of the user' do
expect(subject[:status]).to eq(:success)
group_member_invite.reload
project_member_invite.reload
expect(group_member_invite).to be_invite
expect(project_member_invite).to be_invite
end
end
context 'user is confirmed' do
it 'accepts pending invites of the user' do
expect(subject[:status]).to eq(:success)
group_member_invite.reload
project_member_invite.reload
expect(group_member_invite).not_to be_invite
expect(project_member_invite).not_to be_invite
expect(group_member_invite.user).to eq(user)
expect(project_member_invite.user).to eq(user)
end
end
end
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