gitlab-org--gitlab-foss/spec/models/member_spec.rb
Stan Hu 32ddc3fed6 Allow a member to have an access level equal to parent group
Suppose you have this configuration:

1. Subgroup `hello/world`
2. Subgroup `hello/mergers`.
3. Project `hello/world/my-project` has invited group `hello/world` to
access protected branches.
4. The rule allows the group to merge but no one can push.
5. User `newuser` has Owner access to the parent group `hello`.

Previously, there was no way for the user `newuser` to be added to the
`hello/mergers` group since the validation only allowed a user to be
added at a higher access level.

Since membership in a subgroup confers certain access rights, such as
being able to merge or push code to protected branches, we have to
loosen the validation and allow someone to be added at an equal level
granted by the parent group.

Closes https://gitlab.com/gitlab-org/gitlab-ee/issues/11323
2019-04-30 13:31:51 -07:00

572 lines
19 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
describe Member do
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
describe "Validation" do
subject { described_class.new(access_level: Member::GUEST) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
it_behaves_like 'an object with email-formated attributes', :invite_email do
subject { build(:project_member) }
end
context "when an invite email is provided" do
let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) }
it "doesn't require a user" do
expect(member).to be_valid
end
it "requires a valid invite email" do
member.invite_email = "nope"
expect(member).not_to be_valid
end
it "requires a unique invite email scoped to this source" do
create(:project_member, source: member.source, invite_email: member.invite_email)
expect(member).not_to be_valid
end
it "is valid otherwise" do
expect(member).to be_valid
end
end
context "when an invite email is not provided" do
let(:member) { build(:project_member) }
it "requires a user" do
member.user = nil
expect(member).not_to be_valid
end
it "is valid otherwise" do
expect(member).to be_valid
end
end
context "when a child member inherits its access level" do
let(:user) { create(:user) }
let(:member) { create(:group_member, :developer, user: user) }
let(:child_group) { create(:group, parent: member.group) }
let(:child_member) { build(:group_member, group: child_group, user: user) }
it "requires a higher level" do
child_member.access_level = GroupMember::REPORTER
child_member.validate
expect(child_member).not_to be_valid
end
# Membership in a subgroup confers certain access rights, such as being
# able to merge or push code to protected branches.
it "is valid with an equal level" do
child_member.access_level = GroupMember::DEVELOPER
child_member.validate
expect(child_member).to be_valid
end
it "is valid with a higher level" do
child_member.access_level = GroupMember::MAINTAINER
child_member.validate
expect(child_member).to be_valid
end
end
end
describe 'Scopes & finders' do
before do
project = create(:project, :public, :access_requestable)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@maintainer_user = create(:user).tap { |u| project.add_maintainer(u) }
@maintainer = project.members.find_by(user_id: @maintainer_user.id)
@blocked_user = create(:user).tap do |u|
project.add_maintainer(u)
project.add_developer(u)
u.block!
end
@blocked_maintainer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MAINTAINER)
@blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER)
@invited_member = create(:project_member, :developer,
project: project,
invite_token: '1234',
invite_email: 'toto1@example.com')
accepted_invite_user = build(:user, state: :active)
@accepted_invite_member = create(:project_member, :developer,
project: project,
invite_token: '1234',
invite_email: 'toto2@example.com')
.tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
@requested_member = project.requesters.find_by(user_id: requested_user.id)
accepted_request_user = create(:user).tap { |u| project.request_access(u) }
@accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
end
describe '.access_for_user_ids' do
it 'returns the right access levels' do
users = [@owner_user.id, @maintainer_user.id, @blocked_user.id]
expected = {
@owner_user.id => Gitlab::Access::OWNER,
@maintainer_user.id => Gitlab::Access::MAINTAINER
}
expect(described_class.access_for_user_ids(users)).to eq(expected)
end
end
describe '.invite' do
it { expect(described_class.invite).not_to include @maintainer }
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 '.non_invite' do
it { expect(described_class.non_invite).to include @maintainer }
it { expect(described_class.non_invite).not_to include @invited_member }
it { expect(described_class.non_invite).to include @accepted_invite_member }
it { expect(described_class.non_invite).to include @requested_member }
it { expect(described_class.non_invite).to include @accepted_request_member }
end
describe '.request' do
it { expect(described_class.request).not_to include @maintainer }
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 @maintainer }
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 '.developers' do
subject { described_class.developers.to_a }
it { is_expected.not_to include @owner }
it { is_expected.not_to include @maintainer }
it { is_expected.to include @invited_member }
it { is_expected.to include @accepted_invite_member }
it { is_expected.not_to include @requested_member }
it { is_expected.to include @accepted_request_member }
it { is_expected.not_to include @blocked_maintainer }
it { is_expected.not_to include @blocked_developer }
end
describe '.owners_and_maintainers' do
it { expect(described_class.owners_and_maintainers).to include @owner }
it { expect(described_class.owners_and_maintainers).to include @maintainer }
it { expect(described_class.owners_and_maintainers).not_to include @invited_member }
it { expect(described_class.owners_and_maintainers).not_to include @accepted_invite_member }
it { expect(described_class.owners_and_maintainers).not_to include @requested_member }
it { expect(described_class.owners_and_maintainers).not_to include @accepted_request_member }
it { expect(described_class.owners_and_maintainers).not_to include @blocked_maintainer }
end
describe '.has_access' do
subject { described_class.has_access.to_a }
it { is_expected.to include @owner }
it { is_expected.to include @maintainer }
it { is_expected.to include @invited_member }
it { is_expected.to include @accepted_invite_member }
it { is_expected.not_to include @requested_member }
it { is_expected.to include @accepted_request_member }
it { is_expected.not_to include @blocked_maintainer }
it { is_expected.not_to include @blocked_developer }
end
end
describe "Delegate methods" do
it { is_expected.to respond_to(:user_name) }
it { is_expected.to respond_to(:user_email) }
end
describe '.add_user' do
%w[project group].each do |source_type|
context "when source is a #{source_type}" do
let!(:source) { create(source_type, :public, :access_requestable) }
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
it 'returns a <Source>Member object' do
member = described_class.add_user(source, user, :maintainer)
expect(member).to be_a "#{source_type.classify}Member".constantize
expect(member).to be_persisted
end
it 'sets members.created_by to the given current_user' do
member = described_class.add_user(source, user, :maintainer, current_user: admin)
expect(member.created_by).to eq(admin)
end
it 'sets members.expires_at to the given expires_at' do
member = described_class.add_user(source, user, :maintainer, expires_at: Date.new(2016, 9, 22))
expect(member.expires_at).to eq(Date.new(2016, 9, 22))
end
described_class.access_levels.each do |sym_key, int_access_level|
it "accepts the :#{sym_key} symbol as access level" do
expect(source.users).not_to include(user)
member = described_class.add_user(source, user.id, sym_key)
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
it "accepts the #{int_access_level} integer as access level" do
expect(source.users).not_to include(user)
member = described_class.add_user(source, user.id, int_access_level)
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
end
context 'with no current_user' do
context 'when called with a known user id' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.add_user(source, user.id, :maintainer)
expect(source.users.reload).to include(user)
end
end
context 'when called with an unknown user id' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.add_user(source, 42, :maintainer)
expect(source.users.reload).not_to include(user)
end
end
context 'when called with a user object' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.add_user(source, user, :maintainer)
expect(source.users.reload).to include(user)
end
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'adds the requester as a member' do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
expect { described_class.add_user(source, user, :maintainer) }
.to raise_error(Gitlab::Access::AccessDeniedError)
expect(source.users.reload).not_to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
end
end
context 'when called with a known user email' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.add_user(source, user.email, :maintainer)
expect(source.users.reload).to include(user)
end
end
context 'when called with an unknown user email' do
it 'creates an invited member' do
expect(source.users).not_to include(user)
described_class.add_user(source, 'user@example.com', :maintainer)
expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
end
end
context 'when current_user can update member' do
it 'creates the member' do
expect(source.users).not_to include(user)
described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.users.reload).to include(user)
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'adds the requester as a member' do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.users.reload).to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
end
end
end
context 'when current_user cannot update member' do
it 'does not create the member' do
expect(source.users).not_to include(user)
member = described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.users.reload).not_to include(user)
expect(member).not_to be_persisted
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'does not destroy the requester' do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.users.reload).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
end
end
end
context 'when member already exists' do
before do
source.add_user(user, :developer)
end
context 'with no current_user' do
it 'updates the member' do
expect(source.users).to include(user)
described_class.add_user(source, user, :maintainer)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user can update member' do
it 'updates the member' do
expect(source.users).to include(user)
described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
context 'when current_user cannot update member' do
it 'does not update the member' do
expect(source.users).to include(user)
described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
end
end
end
end
describe '.add_users' do
%w[project group].each do |source_type|
context "when source is a #{source_type}" do
let!(:source) { create(source_type, :public, :access_requestable) }
let!(:admin) { create(:admin) }
let(:user1) { create(:user) }
let(:user2) { create(:user) }
it 'returns a <Source>Member objects' do
members = described_class.add_users(source, [user1, user2], :maintainer)
expect(members).to be_a Array
expect(members.size).to eq(2)
expect(members.first).to be_a "#{source_type.classify}Member".constantize
expect(members.first).to be_persisted
end
it 'returns an empty array' do
members = described_class.add_users(source, [], :maintainer)
expect(members).to be_a Array
expect(members).to be_empty
end
it 'supports differents formats' do
list = ['joe@local.test', admin, user1.id, user2.id.to_s]
members = described_class.add_users(source, list, :maintainer)
expect(members.size).to eq(4)
expect(members.first).to be_invite
end
end
end
end
describe '#accept_request' do
let(:member) { create(:project_member, requested_at: Time.now.utc) }
it { expect(member.accept_request).to be_truthy }
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 '#invite?' do
subject { create(:project_member, invite_email: "user@example.com", user: nil) }
it { is_expected.to be_invite }
end
describe '#request?' do
subject { create(:project_member, requested_at: Time.now.utc) }
it { is_expected.to be_request }
end
describe '#pending?' do
let(:invited_member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:requester) { create(:project_member, requested_at: Time.now.utc) }
it { expect(invited_member).to be_invite }
it { expect(requester).to be_pending }
end
describe "#accept_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:user) { create(:user) }
it "resets the invite token" do
member.accept_invite!(user)
expect(member.invite_token).to be_nil
end
it "sets the invite accepted timestamp" do
member.accept_invite!(user)
expect(member.invite_accepted_at).not_to be_nil
end
it "sets the user" do
member.accept_invite!(user)
expect(member.user).to eq(user)
end
it "calls #after_accept_invite" do
expect(member).to receive(:after_accept_invite)
member.accept_invite!(user)
end
it "refreshes user's authorized projects", :delete do
project = member.source
expect(user.authorized_projects).not_to include(project)
member.accept_invite!(user)
expect(user.authorized_projects.reload).to include(project)
end
end
describe "#decline_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
it "destroys the member" do
member.decline_invite!
expect(member).to be_destroyed
end
it "calls #after_decline_invite" do
expect(member).to receive(:after_decline_invite)
member.decline_invite!
end
end
describe "#generate_invite_token" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
it "sets the invite token" do
expect { member.generate_invite_token }.to change { member.invite_token}
end
end
describe "destroying a record", :delete do
it "refreshes user's authorized projects" do
project = create(:project, :private)
user = create(:user)
member = project.add_reporter(user)
member.destroy
expect(user.authorized_projects).not_to include(project)
end
end
end