From d26f81239a33b80694783ee35f0da0e2ed082c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 18 Apr 2016 18:53:32 +0200 Subject: Add request access for groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../groups/group_members_controller_spec.rb | 198 +++++++++++++++- .../projects/project_members_controller_spec.rb | 249 +++++++++++++++++++-- .../members/owner_manages_access_requests_spec.rb | 52 +++++ .../groups/members/user_requests_access_spec.rb | 54 +++++ .../members/master_manages_access_requests_spec.rb | 51 +++++ .../projects/members/user_requests_access_spec.rb | 54 +++++ spec/helpers/members_helper_spec.rb | 139 ++++++++++++ spec/helpers/projects_helper_spec.rb | 29 ++- spec/mailers/notify_spec.rb | 100 ++++++++- spec/models/concerns/access_requestable_spec.rb | 41 ++++ spec/models/group_spec.rb | 59 ++++- spec/models/member_spec.rb | 89 ++++++++ spec/models/members/group_member_spec.rb | 22 +- spec/models/members/project_member_spec.rb | 22 ++ spec/models/project_spec.rb | 11 + spec/models/project_team_spec.rb | 150 ++++++++----- 16 files changed, 1230 insertions(+), 90 deletions(-) create mode 100644 spec/features/groups/members/owner_manages_access_requests_spec.rb create mode 100644 spec/features/groups/members/user_requests_access_spec.rb create mode 100644 spec/features/projects/members/master_manages_access_requests_spec.rb create mode 100644 spec/features/projects/members/user_requests_access_spec.rb create mode 100644 spec/helpers/members_helper_spec.rb create mode 100644 spec/models/concerns/access_requestable_spec.rb (limited to 'spec') diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index a5986598715..aea809f890b 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -4,17 +4,211 @@ describe Groups::GroupMembersController do let(:user) { create(:user) } let(:group) { create(:group) } - context "index" do + describe '#index' do before do group.add_owner(user) stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end it 'renders index with group members' do - get :index, group_id: group.path + get :index, group_id: group expect(response.status).to eq(200) expect(response).to render_template(:index) end end + + describe '#destroy' do + let(:group) { create(:group, :public) } + + context 'when member is not found' do + it 'returns 403' do + delete :destroy, group_id: group, + id: 42 + + expect(response.status).to eq(403) + end + end + + context 'when member is found' do + let(:user) { create(:user) } + let(:group_user) { create(:user) } + let(:member) do + group.add_developer(group_user) + group.group_members.find_by(user_id: group_user.id) + end + + context 'when user does not have enough rights' do + before do + group.add_developer(user) + sign_in(user) + end + + it 'returns 403' do + delete :destroy, group_id: group, + id: member + + expect(response.status).to eq(403) + expect(group.users).to include group_user + end + end + + context 'when user has enough rights' do + before do + group.add_owner(user) + sign_in(user) + end + + it '[HTML] removes user from members' do + delete :destroy, group_id: group, + id: member + + expect(response).to set_flash.to 'User was successfully removed from group.' + expect(response).to redirect_to(group_group_members_path(group)) + expect(group.users).not_to include group_user + end + + it '[JS] removes user from members' do + xhr :delete, :destroy, group_id: group, + id: member + + expect(response).to be_success + expect(group.users).not_to include group_user + end + end + end + end + + describe '#leave' do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + + context 'when member is not found' do + before { sign_in(user) } + + it 'returns 403' do + delete :leave, group_id: group + + expect(response.status).to eq(403) + end + end + + context 'when member is found' do + context 'and is not an owner' do + before do + group.add_developer(user) + sign_in(user) + end + + it 'removes user from members' do + delete :leave, group_id: group + + expect(response).to set_flash.to "You left #{group.name} group." + expect(response).to redirect_to(dashboard_groups_path) + expect(group.users).not_to include user + end + end + + context 'and is an owner' do + before do + group.add_owner(user) + sign_in(user) + end + + it 'cannot removes himself from the group' do + delete :leave, group_id: group + + expect(response).to redirect_to(dashboard_groups_path) + expect(response).to set_flash[:alert].to "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group." + expect(group.users).to include user + end + end + + context 'and is a requester' do + before do + group.request_access(user) + sign_in(user) + end + + it 'removes user from members' do + delete :leave, group_id: group + + expect(response).to set_flash.to 'You withdrawn your access request to the group.' + expect(response).to redirect_to(dashboard_groups_path) + expect(group.group_members.request).to be_empty + expect(group.users).not_to include user + end + end + end + end + + describe '#request_access' do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'creates a new GroupMember that is not a team member' do + post :request_access, group_id: group + + expect(response).to set_flash.to 'Your request for access has been queued for review.' + expect(response).to redirect_to(group_path(group)) + expect(group.group_members.request.find_by(created_by_id: user.id).created_by).to eq user + expect(group.users).not_to include user + end + end + + describe '#approve' do + let(:group) { create(:group, :public) } + + context 'when member is not found' do + it 'returns 403' do + post :approve, group_id: group, + id: 42 + + expect(response.status).to eq(403) + end + end + + context 'when member is found' do + let(:user) { create(:user) } + let(:group_requester) { create(:user) } + let(:member) do + group.request_access(group_requester) + group.group_members.request.find_by(created_by_id: group_requester.id) + end + + context 'when user does not have enough rights' do + before do + group.add_developer(user) + sign_in(user) + end + + it 'returns 403' do + post :approve, group_id: group, + id: member + + expect(response.status).to eq(403) + expect(group.users).not_to include group_requester + end + end + + context 'when user has enough rights' do + before do + group.add_owner(user) + sign_in(user) + end + + it 'adds user to members' do + post :approve, group_id: group, + id: member + + expect(response).to redirect_to(group_group_members_path(group)) + expect(group.users).to include group_requester + end + end + end + end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 750fbecdd07..2ea09f43f26 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -1,22 +1,22 @@ require('spec_helper') describe Projects::ProjectMembersController do - let(:project) { create(:project) } - let(:another_project) { create(:project, :private) } - let(:user) { create(:user) } - let(:member) { create(:user) } - - before do - project.team << [user, :master] - another_project.team << [member, :guest] - sign_in(user) - end - describe '#apply_import' do + let(:project) { create(:project) } + let(:another_project) { create(:project, :private) } + let(:user) { create(:user) } + let(:member) { create(:user) } + + before do + project.team << [user, :master] + another_project.team << [member, :guest] + sign_in(user) + end + shared_context 'import applied' do before do - post(:apply_import, namespace_id: project.namespace.to_param, - project_id: project.to_param, + post(:apply_import, namespace_id: project.namespace, + project_id: project, source_project_id: another_project.id) end end @@ -48,18 +48,231 @@ describe Projects::ProjectMembersController do end describe '#index' do - let(:project) { create(:project, :private) } - context 'when user is member' do - let(:member) { create(:user) } - before do + project = create(:project, :private) + member = create(:user) project.team << [member, :guest] sign_in(member) - get :index, namespace_id: project.namespace.to_param, project_id: project.to_param + + get :index, namespace_id: project.namespace, project_id: project end it { expect(response.status).to eq(200) } end end + + describe '#destroy' do + let(:project) { create(:project, :public) } + + context 'when member is not found' do + it 'returns 404' do + delete :destroy, namespace_id: project.namespace, + project_id: project, + id: 42 + + expect(response.status).to eq(404) + end + end + + context 'when member is found' do + let(:user) { create(:user) } + let(:team_user) { create(:user) } + let(:member) do + project.team << [team_user, :developer] + project.project_members.find_by(user_id: team_user.id) + end + + context 'when user does not have enough rights' do + before do + project.team << [user, :developer] + sign_in(user) + end + + it 'returns 404' do + delete :destroy, namespace_id: project.namespace, + project_id: project, + id: member + + expect(response.status).to eq(404) + expect(project.users).to include team_user + end + end + + context 'when user has enough rights' do + before do + project.team << [user, :master] + sign_in(user) + end + + it '[HTML] removes user from members' do + delete :destroy, namespace_id: project.namespace, + project_id: project, + id: member + + expect(response).to redirect_to( + namespace_project_project_members_path(project.namespace, project) + ) + expect(project.users).not_to include team_user + end + + it '[JS] removes user from members' do + xhr :delete, :destroy, namespace_id: project.namespace, + project_id: project, + id: member + + expect(response).to be_success + expect(project.users).not_to include team_user + end + end + end + end + + describe '#leave' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + + context 'when member is not found' do + before { sign_in(user) } + + it 'returns 403' do + delete :leave, namespace_id: project.namespace, + project_id: project + + expect(response.status).to eq(403) + end + end + + context 'when member is found' do + context 'and is not an owner' do + before do + project.team << [user, :developer] + sign_in(user) + end + + it 'removes user from members' do + delete :leave, namespace_id: project.namespace, + project_id: project + + expect(response).to set_flash.to 'You left the project.' + expect(response).to redirect_to(dashboard_projects_path) + expect(project.users).not_to include user + end + end + + context 'and is an owner' do + before do + project.update(namespace_id: user.namespace_id) + project.team << [user, :master, user] + sign_in(user) + end + + it 'cannot removes himself from the project' do + delete :leave, namespace_id: project.namespace, + project_id: project + + expect(response).to redirect_to( + namespace_project_project_members_path(project.namespace, project) + ) + expect(response).to set_flash[:alert].to 'You can not leave your own project. Transfer or delete the project.' + expect(project.users).to include user + end + end + + context 'and is a requester' do + before do + project.request_access(user) + sign_in(user) + end + + it 'removes user from members' do + delete :leave, namespace_id: project.namespace, + project_id: project + + expect(response).to set_flash.to 'You withdrawn your access request to the project.' + expect(response).to redirect_to(dashboard_projects_path) + expect(project.project_members.request).to be_empty + expect(project.users).not_to include user + end + end + end + end + + describe '#request_access' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'creates a new ProjectMember that is not a team member' do + post :request_access, namespace_id: project.namespace, + project_id: project + + expect(response).to set_flash.to 'Your request for access has been queued for review.' + expect(response).to redirect_to( + namespace_project_path(project.namespace, project) + ) + expect(project.project_members.request.find_by(created_by_id: user.id).created_by).to eq user + expect(project.users).not_to include user + end + end + + describe '#approve' do + let(:project) { create(:project, :public) } + + context 'when member is not found' do + it 'returns 404' do + post :approve, namespace_id: project.namespace, + project_id: project, + id: 42 + + expect(response.status).to eq(404) + end + end + + context 'when member is found' do + let(:user) { create(:user) } + let(:team_requester) { create(:user) } + let(:member) do + project.request_access(team_requester) + project.project_members.request.find_by(created_by_id: team_requester.id) + end + + context 'when user does not have enough rights' do + before do + project.team << [user, :developer] + sign_in(user) + end + + it 'returns 404' do + post :approve, namespace_id: project.namespace, + project_id: project, + id: member + + expect(response.status).to eq(404) + expect(project.users).not_to include team_requester + end + end + + context 'when user has enough rights' do + before do + project.team << [user, :master] + sign_in(user) + end + + it 'adds user to members' do + post :approve, namespace_id: project.namespace, + project_id: project, + id: member + + expect(response).to redirect_to( + namespace_project_project_members_path(project.namespace, project) + ) + expect(project.users).to include team_requester + end + end + end + end end diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb new file mode 100644 index 00000000000..d5b5e0e35ea --- /dev/null +++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +feature 'Groups > Members > Owner manages access requests', feature: true do + let(:user) { create(:user) } + let(:owner) { create(:user) } + let(:group) { create(:group, :public) } + + background do + group.request_access(user) + group.add_owner(owner) + login_as(owner) + end + + scenario 'owner can see access requests' do + visit group_group_members_path(group) + + expect_visible_access_request(group, user) + end + + scenario 'master can grant access' do + visit group_group_members_path(group) + + expect_visible_access_request(group, user) + + perform_enqueued_jobs do + click_on 'Grant access' + end + + expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email] + expect(ActionMailer::Base.deliveries.last.subject).to match /Access to #{group.name} group was granted/ + end + + scenario 'master can deny access' do + visit group_group_members_path(group) + + expect_visible_access_request(group, user) + + perform_enqueued_jobs do + click_on 'Deny access' + end + + expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email] + expect(ActionMailer::Base.deliveries.last.subject).to match /Access to #{group.name} group was denied/ + end + + + def expect_visible_access_request(group, user) + expect(group.access_requested?(user)).to be_truthy + expect(page).to have_content "#{group.name} access requests (1)" + expect(page).to have_content user.name + end +end diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb new file mode 100644 index 00000000000..9b8492807fa --- /dev/null +++ b/spec/features/groups/members/user_requests_access_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +feature 'Groups > Members > User requests access', feature: true do + let(:user) { create(:user) } + let(:owner) { create(:user) } + let(:group) { create(:group, :public) } + + background do + group.add_owner(owner) + login_as(user) + end + + scenario 'user can request access to a group' do + visit group_path(group) + + perform_enqueued_jobs do + click_link 'Request Access' + end + + expect(ActionMailer::Base.deliveries.last.to).to eq [owner.notification_email] + expect(ActionMailer::Base.deliveries.last.subject).to match /Request to join #{group.name} group/ + + expect(group.access_requested?(user)).to be_truthy + expect(page).to have_content 'Your request for access has been queued for review.' + expect(page).to have_content 'Withdraw Request' + end + + scenario 'user is not listed in the group members page' do + visit group_path(group) + + click_link 'Request Access' + + expect(group.access_requested?(user)).to be_truthy + + click_link 'Members' + + visit group_group_members_path(group) + page.within('.content') do + expect(page).not_to have_content(user.name) + end + end + + scenario 'user can withdraw its request for access' do + visit group_path(group) + click_link 'Request Access' + + expect(group.access_requested?(user)).to be_truthy + + click_link 'Withdraw Request' + + expect(group.access_requested?(user)).to be_falsey + expect(page).to have_content 'You withdrawn your access request to the group.' + end +end diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb new file mode 100644 index 00000000000..1b5490ba97f --- /dev/null +++ b/spec/features/projects/members/master_manages_access_requests_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +feature 'Projects > Members > Master manages access requests', feature: true do + let(:user) { create(:user) } + let(:master) { create(:user) } + let(:project) { create(:project, :public) } + + background do + project.request_access(user) + project.team << [master, :master] + login_as(master) + end + + scenario 'master can see access requests' do + visit namespace_project_project_members_path(project.namespace, project) + + expect_visible_access_request(project, user) + end + + scenario 'master can grant access' do + visit namespace_project_project_members_path(project.namespace, project) + + expect_visible_access_request(project, user) + + perform_enqueued_jobs do + click_on 'Grant access' + end + + expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email] + expect(ActionMailer::Base.deliveries.last.subject).to match /Access to #{project.name_with_namespace} project was granted/ + end + + scenario 'master can deny access' do + visit namespace_project_project_members_path(project.namespace, project) + + expect_visible_access_request(project, user) + + perform_enqueued_jobs do + click_on 'Deny access' + end + + expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email] + expect(ActionMailer::Base.deliveries.last.subject).to match /Access to #{project.name_with_namespace} project was denied/ + end + + def expect_visible_access_request(project, user) + expect(project.access_requested?(user)).to be_truthy + expect(page).to have_content "#{project.name} access requests (1)" + expect(page).to have_content user.name + end +end diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb new file mode 100644 index 00000000000..58a7ec1880d --- /dev/null +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +feature 'Projects > Members > User requests access', feature: true do + let(:user) { create(:user) } + let(:master) { create(:user) } + let(:project) { create(:project, :public) } + + background do + project.team << [master, :master] + login_as(user) + end + + scenario 'user can request access to a project' do + visit namespace_project_path(project.namespace, project) + + perform_enqueued_jobs do + click_link 'Request Access' + end + + expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email] + expect(ActionMailer::Base.deliveries.last.subject).to match /Request to join #{project.name_with_namespace} project/ + + expect(project.access_requested?(user)).to be_truthy + expect(page).to have_content 'Your request for access has been queued for review.' + expect(page).to have_content 'Withdraw Request' + end + + scenario 'user is not listed in the project members page' do + visit namespace_project_path(project.namespace, project) + + click_link 'Request Access' + + expect(project.access_requested?(user)).to be_truthy + + click_link 'Members' + + visit namespace_project_project_members_path(project.namespace, project) + page.within('.content') do + expect(page).not_to have_content(user.name) + end + end + + scenario 'user can withdraw its request for access' do + visit namespace_project_path(project.namespace, project) + click_link 'Request Access' + + expect(project.access_requested?(user)).to be_truthy + + click_link 'Withdraw Request' + + expect(project.access_requested?(user)).to be_falsey + expect(page).to have_content 'You withdrawn your access request to the project.' + end +end diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb new file mode 100644 index 00000000000..f1782146241 --- /dev/null +++ b/spec/helpers/members_helper_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +describe MembersHelper do + describe '#member_class' do + let(:project_member) { build(:project_member) } + let(:group_member) { build(:group_member) } + + it { expect(member_class(project_member)).to eq ProjectMember } + it { expect(member_class(group_member)).to eq GroupMember } + end + + describe '#members_association' do + let(:project) { build_stubbed(:project) } + let(:group) { build_stubbed(:group) } + + it { expect(members_association(project)).to eq :project_members } + it { expect(members_association(group)).to eq :group_members } + end + + describe '#action_member_permission' do + let(:project_member) { build(:project_member) } + let(:group_member) { build(:group_member) } + + it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member } + it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member } + end + + describe '#can_see_entity_roles?' do + let(:project) { create(:project) } + let(:group) { create(:group) } + let(:user) { build(:user) } + let(:admin) { build(:user, :admin) } + let(:project_member) { create(:project_member, project: project) } + let(:group_member) { create(:group_member, group: group) } + + it { expect(can_see_entity_roles?(nil, project)).to be_falsy } + it { expect(can_see_entity_roles?(nil, group)).to be_falsy } + it { expect(can_see_entity_roles?(admin, project)).to be_truthy } + it { expect(can_see_entity_roles?(admin, group)).to be_truthy } + it { expect(can_see_entity_roles?(project_member.user, project)).to be_truthy } + it { expect(can_see_entity_roles?(group_member.user, group)).to be_truthy } + end + + describe '#member_path' do + let(:project_member) { create(:project_member) } + let(:group_member) { create(:group_member) } + + it { expect(member_path(project_member)).to eq namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) } + it { expect(member_path(group_member)).to eq group_group_member_path(group_member.source, group_member) } + it { expect { member_path(double(:member, source: 'foo')) }.to raise_error ArgumentError, 'Unknown object class' } + end + + describe '#resend_invite_member_path' do + let(:project_member) { create(:project_member) } + let(:group_member) { create(:group_member) } + + it { expect(resend_invite_member_path(project_member)).to eq resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) } + it { expect(resend_invite_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) } + it { expect { resend_invite_member_path(double(:member, source: 'foo')) }.to raise_error ArgumentError, 'Unknown object class' } + end + + describe '#request_access_path' do + let(:project) { build_stubbed(:project) } + let(:group) { build_stubbed(:group) } + + it { expect(request_access_path(project)).to eq request_access_namespace_project_project_members_path(project.namespace, project) } + it { expect(request_access_path(group)).to eq request_access_group_group_members_path(group) } + it { expect { request_access_path(double(:member, source: 'foo')) }.to raise_error ArgumentError, 'Unknown object class' } + end + + describe '#approve_request_member_path' do + let(:project_member) { create(:project_member) } + let(:group_member) { create(:group_member) } + + it { expect(approve_request_member_path(project_member)).to eq approve_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) } + it { expect(approve_request_member_path(group_member)).to eq approve_group_group_member_path(group_member.source, group_member) } + it { expect { approve_request_member_path(double(:member, source: 'foo')) }.to raise_error ArgumentError, 'Unknown object class' } + end + + describe '#leave_path' do + let(:project) { build_stubbed(:project) } + let(:group) { build_stubbed(:group) } + + it { expect(leave_path(project)).to eq leave_namespace_project_project_members_path(project.namespace, project) } + it { expect(leave_path(group)).to eq leave_group_group_members_path(group) } + it { expect { leave_path(double(:member, source: 'foo')) }.to raise_error ArgumentError, 'Unknown object class' } + end + + describe '#withdraw_request_message' do + let(:project) { build_stubbed(:project) } + let(:group) { build_stubbed(:group) } + + it { expect(withdraw_request_message(project)).to eq "Are you sure you want to withdraw your access request for the \"#{project.name_with_namespace}\" project?" } + it { expect(withdraw_request_message(group)).to eq "Are you sure you want to withdraw your access request for the \"#{group.name}\" group?" } + end + + describe '#remove_member_message' do + let(:requester) { build(:user) } + let(:project) { create(:project) } + let(:project_member) { build(:project_member, project: project) } + let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } } + let(:project_member_request) { project.request_access(requester) } + let(:group) { create(:group) } + let(:group_member) { build(:group_member, group: group) } + let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } } + let(:group_member_request) { group.request_access(requester) } + + it { expect(remove_member_message(project_member)).to eq "You are going to remove #{project_member.user.name} from the #{project.name_with_namespace} project. Are you sure?" } + it { expect(remove_member_message(project_member_invite)).to eq "You are going to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project. Are you sure?" } + it { expect(remove_member_message(project_member_request)).to eq "You are going to deny #{requester.name}'s request to join the #{project.name_with_namespace} project. Are you sure?" } + it { expect(remove_member_message(group_member)).to eq "You are going to remove #{group_member.user.name} from the #{group.name} group. Are you sure?" } + it { expect(remove_member_message(group_member_invite)).to eq "You are going to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group. Are you sure?" } + it { expect(remove_member_message(group_member_request)).to eq "You are going to deny #{requester.name}'s request to join the #{group.name} group. Are you sure?" } + end + + describe '#remove_member_title' do + let(:requester) { build(:user) } + let(:project) { create(:project) } + let(:project_member) { build(:project_member, project: project) } + let(:project_member_request) { project.request_access(requester) } + let(:group) { create(:group) } + let(:group_member) { build(:group_member, group: group) } + let(:group_member_request) { group.request_access(requester) } + + it { expect(remove_member_title(project_member)).to eq 'Remove user' } + it { expect(remove_member_title(project_member_request)).to eq 'Deny access request' } + it { expect(remove_member_title(group_member)).to eq 'Remove user' } + it { expect(remove_member_title(group_member_request)).to eq 'Deny access request' } + end + + describe '#leave_confirmation_message' do + let(:project) { build_stubbed(:project) } + let(:group) { build_stubbed(:group) } + let(:user) { build_stubbed(:user) } + + it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave \"#{project.name_with_namespace}\" project?" } + it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave \"#{group.name}\" group?" } + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index ac5af8740dc..fa81c28849e 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1,6 +1,25 @@ require 'spec_helper' describe ProjectsHelper do + describe '#max_access_level' do + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:reporter) { create(:user) } + let(:group) { create(:group) } + let(:project) { build_stubbed(:empty_project, namespace: group) } + + before do + group.add_master(master) + group.add_owner(owner) + group.add_reporter(reporter) + end + + it { expect(max_access_level(project, master)).to eq 'Master' } + it { expect(max_access_level(project, owner)).to eq 'Owner' } + it { expect(max_access_level(project, reporter)).to eq 'Reporter' } + it { expect(max_access_level(project, build_stubbed(:user))).to be_nil } + end + describe "#project_status_css_class" do it "returns appropriate class" do expect(project_status_css_class("started")).to eq("active") @@ -45,16 +64,6 @@ describe ProjectsHelper do end end - describe 'user_max_access_in_project' do - let(:project) { create(:project) } - let(:user) { create(:user) } - before do - project.team.add_user(user, Gitlab::Access::MASTER) - end - - it { expect(helper.user_max_access_in_project(user.id, project)).to eq('Master') } - end - describe "readme_cache_key" do let(:project) { create(:project) } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 818825b1477..2d86038030e 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -400,6 +400,54 @@ describe Notify do end end + describe 'project access requested' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:project_member) do + project.request_access(user) + project.project_members.find_by(created_by_id: user.id) + end + subject { Notify.project_access_requested_email(project_member.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + + it 'has the correct subject' do + is_expected.to have_subject /Request to join #{project.name_with_namespace} project/ + end + + it 'contains name of project' do + is_expected.to have_body_text /#{project.name}/ + end + + it 'contains new user role' do + is_expected.to have_body_text /#{project_member.human_access}/ + end + end + + describe 'project access denied' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:project_member) do + project.request_access(user) + project.project_members.find_by(created_by_id: user.id) + end + subject { Notify.project_access_denied_email(project.id, user.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + + it 'has the correct subject' do + is_expected.to have_subject /Access to #{project.name_with_namespace} project was denied/ + end + + it 'contains name of project' do + is_expected.to have_body_text /#{project.name}/ + end + end + describe 'project access changed' do let(:project) { create(:project) } let(:user) { create(:user) } @@ -411,7 +459,7 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do - is_expected.to have_subject /Access to project was granted/ + is_expected.to have_subject /Access to #{project.name_with_namespace} project was granted/ end it 'contains name of project' do @@ -535,6 +583,54 @@ describe Notify do end end + describe 'group access requested' do + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:group_member) do + group.request_access(user) + group.group_members.find_by(created_by_id: user.id) + end + subject { Notify.group_access_requested_email(group_member.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + + it 'has the correct subject' do + is_expected.to have_subject /Request to join #{group.name} group/ + end + + it 'contains name of group' do + is_expected.to have_body_text /#{group.name}/ + end + + it 'contains new user role' do + is_expected.to have_body_text /#{group_member.human_access}/ + end + end + + describe 'group access denied' do + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:group_member) do + group.request_access(user) + group.group_members.find_by(created_by_id: user.id) + end + subject { Notify.group_access_denied_email(group.id, user.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + + it 'has the correct subject' do + is_expected.to have_subject /Access to #{group.name} group was denied/ + end + + it 'contains name of group' do + is_expected.to have_body_text /#{group.name}/ + end + end + describe 'group access changed' do let(:group) { create(:group) } let(:user) { create(:user) } @@ -547,7 +643,7 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do - is_expected.to have_subject /Access to group was granted/ + is_expected.to have_subject /Access to #{group.name} group was granted/ end it 'contains name of project' do diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb new file mode 100644 index 00000000000..2dfed1eb4c4 --- /dev/null +++ b/spec/models/concerns/access_requestable_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe AccessRequestable do + describe 'Group' do + describe '#request_access' do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + + it { expect(group.request_access(user)).to be_a(GroupMember) } + it { expect(group.request_access(user).user).to be_nil } + it { expect(group.request_access(user).created_by).to eq(user) } + end + + describe '#access_requested?' do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + + before { group.request_access(user) } + + it { expect(group.access_requested?(user)).to be_truthy } + end + end + + describe 'Project' do + describe '#request_access' do + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + + it { expect(project.request_access(user)).to be_a(ProjectMember) } + end + + describe '#access_requested?' do + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + + before { project.request_access(user) } + + it { expect(project.access_requested?(user)).to be_truthy } + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 6fa16be7f04..52f9d57bc0a 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -5,7 +5,22 @@ describe Group, models: true do describe 'associations' do it { is_expected.to have_many :projects } - it { is_expected.to have_many :group_members } + it { is_expected.to have_many(:group_members).dependent(:destroy) } + it { is_expected.to have_many(:users).through(:group_members) } + it { is_expected.to have_many(:project_group_links).dependent(:destroy) } + it { is_expected.to have_many(:shared_projects).through(:project_group_links) } + it { is_expected.to have_many(:notification_settings).dependent(:destroy) } + + describe '#group_members' do + let(:user) { create(:user) } + let(:group) { create(:group) } + + before { group.request_access(user) } + + it 'does not includes membership requests' do + expect(user.group_members).to be_empty + end + end end describe 'modules' do @@ -131,4 +146,46 @@ describe Group, models: true do expect(described_class.search(group.path.upcase)).to eq([group]) end end + + describe '#has_owner?' do + before { @members = setup_group_members(group) } + + it { expect(group.has_owner?(@members[:owner])).to be_truthy } + it { expect(group.has_owner?(@members[:master])).to be_falsey } + it { expect(group.has_owner?(@members[:developer])).to be_falsey } + it { expect(group.has_owner?(@members[:reporter])).to be_falsey } + it { expect(group.has_owner?(@members[:guest])).to be_falsey } + it { expect(group.has_owner?(@members[:requester])).to be_falsey } + end + + describe '#has_master?' do + before { @members = setup_group_members(group) } + + it { expect(group.has_master?(@members[:owner])).to be_falsey } + it { expect(group.has_master?(@members[:master])).to be_truthy } + it { expect(group.has_master?(@members[:developer])).to be_falsey } + it { expect(group.has_master?(@members[:reporter])).to be_falsey } + it { expect(group.has_master?(@members[:guest])).to be_falsey } + it { expect(group.has_master?(@members[:requester])).to be_falsey } + end + + def setup_group_members(group) + members = { + owner: create(:user), + master: create(:user), + developer: create(:user), + reporter: create(:user), + guest: create(:user), + requester: create(:user) + } + + group.add_user(members[:owner], GroupMember::OWNER) + group.add_user(members[:master], GroupMember::MASTER) + group.add_user(members[:developer], GroupMember::DEVELOPER) + group.add_user(members[:reporter], GroupMember::REPORTER) + group.add_user(members[:guest], GroupMember::GUEST) + group.request_access(members[:requester]) + + members + end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 6e51730eecd..a3d525d8d56 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -55,6 +55,47 @@ describe Member, models: true do end end + describe 'Scopes' do + before do + project = create(:project) + @invited_member = build(:project_member, user: nil).tap { |m| m.generate_invite_token! } + @accepted_invite_member = build(:project_member, user: nil).tap { |m| m.generate_invite_token! && m.accept_invite!(build(:user)) } + + requested_user = create(:user).tap { |u| project.request_access(u) } + @requested_member = project.project_members.find_by(created_by_id: requested_user.id) + accepted_request_user = create(:user).tap { |u| project.request_access(u) } + @accepted_request_member = project.project_members.find_by(created_by_id: accepted_request_user.id).tap { |m| m.accept_request } + end + + describe '#invite' do + it { expect(described_class.invite).to include @invited_member } + it { expect(described_class.invite).not_to include @accepted_invite_member } + it { expect(described_class.invite).not_to include @requested_member } + it { expect(described_class.invite).not_to include @accepted_request_member } + end + + describe '#request' do + it { expect(described_class.request).not_to include @invited_member } + it { expect(described_class.request).not_to include @accepted_invite_member } + it { expect(described_class.request).to include @requested_member } + it { expect(described_class.request).not_to include @accepted_request_member } + end + + describe '#non_request' do + it { expect(described_class.non_request).to include @invited_member } + it { expect(described_class.non_request).to include @accepted_invite_member } + it { expect(described_class.non_request).not_to include @requested_member } + it { expect(described_class.non_request).to include @accepted_request_member } + end + + describe '#non_pending' do + it { expect(described_class.non_pending).not_to include @invited_member } + it { expect(described_class.non_pending).to include @accepted_invite_member } + it { expect(described_class.non_pending).not_to include @requested_member } + it { expect(described_class.non_pending).to include @accepted_request_member } + end + end + describe "Delegate methods" do it { is_expected.to respond_to(:user_name) } it { is_expected.to respond_to(:user_email) } @@ -97,6 +138,54 @@ describe Member, models: true do end end + describe '#accept_request' do + let(:user) { create(:user) } + let(:member) { create(:project_member, requested_at: Time.now.utc, user: nil, created_by: user) } + + it 'returns true' do + expect(member.accept_request).to be_truthy + end + + it 'sets the user' do + member.accept_request + + expect(member.user).to eq(user) + end + + it 'clears requested_at' do + member.accept_request + + expect(member.requested_at).to be_nil + end + + it 'calls #after_accept_request' do + expect(member).to receive(:after_accept_request) + + member.accept_request + end + end + + describe '#decline_request' do + let(:user) { create(:user) } + let(:member) { create(:project_member, requested_at: Time.now.utc, user: nil, created_by: user) } + + it 'returns true' do + expect(member.decline_request).to be_truthy + end + + it 'destroys the member' do + member.decline_request + + expect(member).to be_destroyed + end + + it 'calls #after_decline_request' do + expect(member).to receive(:after_decline_request) + + member.decline_request + end + end + describe "#accept_invite!" do let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } let(:user) { create(:user) } diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 5424c9b9cba..c3070d4cb78 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' describe GroupMember, models: true do - context 'notification' do + describe 'notifications' do describe "#after_create" do it "should send email to user" do membership = build(:group_member) @@ -50,5 +50,25 @@ describe GroupMember, models: true do @group_member.update_attribute(:access_level, GroupMember::OWNER) end end + + describe 'after accept_request' do + let(:member) { create(:group_member, user: nil, created_by: build_stubbed(:user), requested_at: Time.now) } + + it "calls #accept_group_access_request" do + expect_any_instance_of(NotificationService).to receive(:new_group_member) + + member.accept_request + end + end + + describe 'after decline_request' do + let(:member) { create(:group_member, user: nil, created_by: build_stubbed(:user), requested_at: Time.now) } + + it "calls #decline_group_access_request" do + expect_any_instance_of(NotificationService).to receive(:decline_group_access_request) + + member.decline_request + end + end end end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 9f13874b532..99b3c77c6cd 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -135,4 +135,26 @@ describe ProjectMember, models: true do it { expect(@project_1.users).to be_empty } it { expect(@project_2.users).to be_empty } end + + describe 'notifications' do + describe 'after accept_request' do + let(:member) { create(:project_member, user: nil, created_by: build_stubbed(:user), requested_at: Time.now) } + + it 'calls #accept_project_access_request' do + expect_any_instance_of(NotificationService).to receive(:new_project_member) + + member.accept_request + end + end + + describe 'after decline_request' do + let(:member) { create(:project_member, user: nil, created_by: build_stubbed(:user), requested_at: Time.now) } + + it 'calls #decline_project_access_request' do + expect_any_instance_of(NotificationService).to receive(:decline_project_access_request) + + member.decline_request + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index de8815f5a38..d5a4b73affd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -29,6 +29,17 @@ describe Project, models: true do it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } it { is_expected.to have_many(:todos).dependent(:destroy) } + + describe '#project_members' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before { project.request_access(user) } + + it 'does not includes membership requests' do + expect(user.project_members).to be_empty + end + end end describe 'modules' do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 8bebd6a9447..36b1f439955 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -73,69 +73,107 @@ describe ProjectTeam, models: true do end end - describe :max_invited_level do - let(:group) { create(:group) } - let(:project) { create(:empty_project) } - - before do - project.project_group_links.create( - group: group, - group_access: Gitlab::Access::DEVELOPER - ) - - group.add_user(master, Gitlab::Access::MASTER) - group.add_user(reporter, Gitlab::Access::REPORTER) + describe '#find_member' do + context 'personal project' do + let(:project) { create(:empty_project) } + let(:requester) { create(:user) } + + before do + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + project.request_access(requester) + end + + it { expect(project.team.find_member(master.id)).to be_a(ProjectMember) } + it { expect(project.team.find_member(reporter.id)).to be_a(ProjectMember) } + it { expect(project.team.find_member(guest.id)).to be_a(ProjectMember) } + it { expect(project.team.find_member(nonmember.id)).to be_nil } + it { expect(project.team.find_member(requester.id)).to be_nil } end - it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) } - it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) } - it { expect(project.team.max_invited_level(nonmember.id)).to be_nil } - end - - describe :max_member_access do - let(:group) { create(:group) } - let(:project) { create(:empty_project) } - - before do - project.project_group_links.create( - group: group, - group_access: Gitlab::Access::DEVELOPER - ) - - group.add_user(master, Gitlab::Access::MASTER) - group.add_user(reporter, Gitlab::Access::REPORTER) - end - - it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) } - it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - - it "does not have an access" do - project.namespace.update(share_with_group_lock: true) - expect(project.team.max_member_access(master.id)).to be_nil - expect(project.team.max_member_access(reporter.id)).to be_nil + context 'group project' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, group: group) } + let(:requester) { create(:user) } + + before do + group.add_master(master) + group.add_reporter(reporter) + group.add_guest(guest) + group.request_access(requester) + end + + it { expect(project.team.find_member(master.id)).to be_a(GroupMember) } + it { expect(project.team.find_member(reporter.id)).to be_a(GroupMember) } + it { expect(project.team.find_member(guest.id)).to be_a(GroupMember) } + it { expect(project.team.find_member(nonmember.id)).to be_nil } + it { expect(project.team.find_member(requester.id)).to be_nil } end end - describe "#human_max_access" do - it 'returns Master role' do - user = create(:user) - group = create(:group) - group.add_master(user) - - project = build_stubbed(:empty_project, namespace: group) - - expect(project.team.human_max_access(user.id)).to eq 'Master' + describe '#max_member_access' do + let(:requester) { create(:user) } + + context 'personal project' do + let(:project) { create(:empty_project) } + + context 'when project is not shared with group' do + before do + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + project.request_access(requester) + end + + it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) } + it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } + it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) } + it { expect(project.team.max_member_access(nonmember.id)).to be_nil } + it { expect(project.team.max_member_access(requester.id)).to be_nil } + end + + context 'when project is shared with group' do + before do + group = create(:group) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER) + + group.add_master(master) + group.add_reporter(reporter) + end + + it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) } + it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } + it { expect(project.team.max_member_access(nonmember.id)).to be_nil } + it { expect(project.team.max_member_access(requester.id)).to be_nil } + + context 'but share_with_group_lock is true' do + before { project.namespace.update(share_with_group_lock: true) } + + it { expect(project.team.max_member_access(master.id)).to be_nil } + it { expect(project.team.max_member_access(reporter.id)).to be_nil } + end + end end - it 'returns Owner role' do - user = create(:user) - group = create(:group) - group.add_owner(user) - - project = build_stubbed(:empty_project, namespace: group) - - expect(project.team.human_max_access(user.id)).to eq 'Owner' + context 'group project' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, group: group) } + + before do + group.add_master(master) + group.add_reporter(reporter) + group.add_guest(guest) + group.request_access(requester) + end + + it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) } + it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } + it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) } + it { expect(project.team.max_member_access(nonmember.id)).to be_nil } + it { expect(project.team.max_member_access(requester.id)).to be_nil } end end end -- cgit v1.2.3