spec/models/group_spec.rb (3,433 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Group, feature_category: :groups_and_projects do
include ReloadHelpers
include StubGitlabCalls
include AdminModeHelper
using RSpec::Parameterized::TableSyntax
let_it_be(:organization) { create(:organization) }
let!(:group) { create(:group) }
let(:developer_access) { Gitlab::Access::DEVELOPER_PROJECT_ACCESS }
let(:maintainer_access) { Gitlab::Access::MAINTAINER_PROJECT_ACCESS }
let(:owner_access) { Gitlab::Access::OWNER_PROJECT_ACCESS }
let(:admin_access) { Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS }
let(:no_one_access) { Gitlab::Access::NO_ONE_PROJECT_ACCESS }
describe 'associations' do
it { is_expected.to have_many :projects }
it { is_expected.to have_many(:all_group_members).dependent(:destroy) }
it { is_expected.to have_many(:all_owner_members) }
it { is_expected.to have_many(:group_members).dependent(:destroy) }
it { is_expected.to have_many(:non_invite_group_members).class_name('GroupMember') }
it { is_expected.to have_many(:request_group_members).class_name('GroupMember').inverse_of(:group) }
it { is_expected.to have_many(:namespace_members) }
it { is_expected.to have_many(:users).through(:group_members) }
it { is_expected.to have_many(:owners).through(:all_owner_members) }
it { is_expected.to have_many(:requesters).dependent(:destroy) }
it { is_expected.to have_many(:namespace_requesters) }
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:namespace_members_and_requesters) }
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) }
it { is_expected.to have_many(:labels).class_name('GroupLabel') }
it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') }
it { is_expected.to have_many(:uploads) }
it { is_expected.to have_one(:chat_team) }
it { is_expected.to have_one(:deletion_schedule) }
it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
it { is_expected.to have_many(:badges).class_name('GroupBadge') }
it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') }
it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') }
it { is_expected.to have_many(:container_repositories) }
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:group_deploy_keys) }
it { is_expected.to have_many(:integrations) }
it { is_expected.to have_one(:dependency_proxy_setting) }
it { is_expected.to have_one(:dependency_proxy_image_ttl_policy) }
it { is_expected.to have_many(:dependency_proxy_blobs) }
it { is_expected.to have_many(:dependency_proxy_manifests) }
it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::GroupDistribution').dependent(:destroy) }
it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) }
it { is_expected.to have_many(:import_export_uploads).dependent(:destroy) }
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
it do
is_expected.to have_many(:bulk_import_entities).class_name('BulkImports::Entity')
.with_foreign_key(:namespace_id).inverse_of(:group)
end
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
it { is_expected.to have_many(:crm_organizations).class_name('CustomerRelations::Organization') }
it { is_expected.to have_many(:crm_targets).class_name('Group::CrmSettings').inverse_of(:source_group) }
it { is_expected.to have_many(:protected_branches).inverse_of(:group).with_foreign_key(:namespace_id) }
it { is_expected.to have_one(:crm_settings) }
it { is_expected.to have_one(:group_feature) }
it { is_expected.to have_one(:harbor_integration) }
describe '#non_invite_group_members' do
let_it_be(:group) { create(:group) }
let_it_be(:non_requested_member) { create(:group_member, group: group) }
let_it_be(:non_invited_member) { create(:group_member, group: group) }
let_it_be(:non_minimal_access_member) { create(:group_member, group: group) }
before do
create(:group_member, :access_request, group: group)
create(:group_member, :invited, group: group)
create(:group_member, :minimal_access, group: group)
end
it 'includes the correct members' do
expect(group.non_invite_group_members).to contain_exactly(non_invited_member, non_requested_member, non_minimal_access_member)
end
end
describe '#request_group_members' do
let_it_be(:group) { create(:group) }
let_it_be(:requested_member) { create(:group_member, :access_request, group: group) }
before do
create(:group_member, group: group) # regular member
create(:group_member, :invited, group: group)
create(:group_member, :minimal_access, group: group)
end
it 'includes the correct members' do
expect(group.request_group_members).to contain_exactly(requested_member)
end
end
describe '#namespace_members' do
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
group.request_access(requester)
group.add_developer(developer)
end
it 'includes the correct users' do
expect(group.namespace_members).to include Member.find_by(user: developer)
expect(group.namespace_members).not_to include Member.find_by(user: requester)
end
it 'is equivelent to #group_members' do
expect(group.namespace_members).to eq group.group_members
end
it_behaves_like 'query without source filters' do
subject { group.namespace_members }
end
end
describe '#namespace_requesters' do
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
group.request_access(requester)
group.add_developer(developer)
end
it 'includes the correct users' do
expect(group.namespace_requesters).to include Member.find_by(user: requester)
expect(group.namespace_requesters).not_to include Member.find_by(user: developer)
end
it 'is equivalent to #requesters' do
expect(group.namespace_requesters).to eq group.requesters
end
it_behaves_like 'query without source filters' do
subject { group.namespace_requesters }
end
end
describe '#namespace_members_and_requesters' do
let_it_be_with_reload(:group) { create(:group) }
let_it_be(:requester) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:invited_member) { create(:group_member, :invited, :owner, group: group) }
before do
group.request_access(requester)
group.add_developer(developer)
end
it 'includes the correct users' do
expect(group.namespace_members_and_requesters).to include(
Member.find_by(user: requester),
Member.find_by(user: developer),
Member.find(invited_member.id)
)
end
it 'is equivalent to #members_and_requesters' do
expect(group.namespace_members_and_requesters).to match_array group.members_and_requesters
end
it_behaves_like 'query without source filters' do
subject { group.namespace_members_and_requesters }
end
end
shared_examples 'polymorphic membership relationship' do
it do
expect(membership.attributes).to include(
'source_type' => 'Namespace',
'source_id' => group.id
)
end
end
shared_examples 'member_namespace membership relationship' do
it do
expect(membership.attributes).to include(
'member_namespace_id' => group.id
)
end
end
describe '#namespace_members setters' do
let(:user) { create(:user) }
let(:membership) { group.namespace_members.create!(user: user, access_level: Gitlab::Access::DEVELOPER) }
it { expect(membership).to be_instance_of(GroupMember) }
it { expect(membership.user).to eq user }
it { expect(membership.group).to eq group }
it { expect(membership.requested_at).to be_nil }
it_behaves_like 'polymorphic membership relationship'
it_behaves_like 'member_namespace membership relationship'
end
describe '#namespace_requesters setters' do
let(:requested_at) { Time.current }
let(:user) { create(:user) }
let(:membership) do
group.namespace_requesters.create!(user: user, requested_at: requested_at, access_level: Gitlab::Access::DEVELOPER)
end
it { expect(membership).to be_instance_of(GroupMember) }
it { expect(membership.user).to eq user }
it { expect(membership.group).to eq group }
it { expect(membership.requested_at).to eq requested_at }
it_behaves_like 'polymorphic membership relationship'
it_behaves_like 'member_namespace membership relationship'
end
describe '#namespace_members_and_requesters setters' do
let(:requested_at) { Time.current }
let(:user) { create(:user) }
let(:membership) do
group.namespace_members_and_requesters.create!(
user: user, requested_at: requested_at, access_level: Gitlab::Access::DEVELOPER
)
end
it { expect(membership).to be_instance_of(GroupMember) }
it { expect(membership.user).to eq user }
it { expect(membership.group).to eq group }
it { expect(membership.requested_at).to eq requested_at }
it_behaves_like 'polymorphic membership relationship'
it_behaves_like 'member_namespace membership relationship'
end
describe '#members & #requesters' do
let_it_be(:requester) { create(:user) }
let_it_be(:developer) { create(:user) }
before do
group.request_access(requester)
group.add_developer(developer)
end
it_behaves_like 'members and requesters associations' do
let(:namespace) { group }
end
end
end
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Referable) }
end
describe 'validations' do
let_it_be(:private_organization) { create(:organization, :private) }
let_it_be(:public_organization) { create(:organization, :public) }
it { is_expected.to validate_presence_of :name }
it { is_expected.not_to allow_value('colon:in:path').for(:path) } # This is to validate that a specially crafted name cannot bypass a pattern match. See !72555
it { is_expected.to allow_value('group test_4').for(:name) }
it { is_expected.not_to allow_value('test/../foo').for(:name) }
it { is_expected.not_to allow_value('<script>alert("Attack!")</script>').for(:name) }
it { is_expected.to validate_presence_of :path }
it { is_expected.not_to validate_presence_of :owner }
it { is_expected.to validate_presence_of :two_factor_grace_period }
it { is_expected.to validate_numericality_of(:two_factor_grace_period).is_greater_than_or_equal_to(0) }
context 'validating the parent of a group' do
context 'when the group has no parent' do
it 'allows a group to have no parent associated with it' do
group = build(:group)
expect(group).to be_valid
end
end
context 'when the group has a parent' do
it 'does not allow a group to have a namespace as its parent' do
group = build(:group, parent: build(:namespace))
expect(group).not_to be_valid
expect(group.errors[:parent_id].first).to eq('user namespace cannot be the parent of another namespace')
end
it 'allows a group to have another group as its parent' do
group = build(:group, parent: build(:group))
expect(group).to be_valid
end
it 'does not allow a subgroup to have the same name as an existing subgroup' do
sub_group1 = create(:group, parent: group, name: "SG", path: 'api')
sub_group2 = described_class.new(parent: group, name: "SG", path: 'api2', organization: organization)
expect(sub_group1).to be_valid
expect(sub_group2).not_to be_valid
expect(sub_group2.errors.full_messages.to_sentence).to eq('Name has already been taken')
end
end
end
describe 'path validation' do
it 'rejects paths reserved on the root namespace when the group has no parent' do
group = build(:group, path: 'api')
expect(group).not_to be_valid
end
it 'allows root paths when the group has a parent' do
group = build(:group, path: 'api', parent: create(:group))
expect(group).to be_valid
end
it 'rejects any wildcard paths when not a top level group' do
group = build(:group, path: 'tree', parent: create(:group))
expect(group).not_to be_valid
end
it 'rejects paths already assigned to any pages unique domain' do
# Simulate the existing domain being in use
create(:project_setting, pages_unique_domain: 'existing-domain')
group = build(:group, path: 'existing-domain')
expect(group).not_to be_valid
expect(group.errors.full_messages.to_sentence).to eq('Group URL has already been taken')
end
end
describe '#notification_settings' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:sub_group) { create(:group, parent_id: group.id) }
before do
group.add_developer(user)
sub_group.add_maintainer(user)
end
it 'also gets notification settings from parent groups' do
expect(sub_group.notification_settings.size).to eq(2)
expect(sub_group.notification_settings).to include(group.notification_settings.first)
end
context 'when sub group is deleted' do
it 'does not delete parent notification settings' do
expect do
sub_group.destroy!
end.to change { NotificationSetting.count }.by(-1)
end
end
end
describe '#notification_email_for' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let(:group_notification_email) { 'user+group@example.com' }
let(:subgroup_notification_email) { 'user+subgroup@example.com' }
before do
create(:email, :confirmed, user: user, email: group_notification_email)
create(:email, :confirmed, user: user, email: subgroup_notification_email)
end
subject { subgroup.notification_email_for(user) }
context 'when both group notification emails are set' do
it 'returns subgroup notification email' do
create(:notification_setting, user: user, source: group, notification_email: group_notification_email)
create(:notification_setting, user: user, source: subgroup, notification_email: subgroup_notification_email)
is_expected.to eq(subgroup_notification_email)
end
end
context 'when subgroup notification email is blank' do
it 'returns parent group notification email' do
create(:notification_setting, user: user, source: group, notification_email: group_notification_email)
create(:notification_setting, user: user, source: subgroup, notification_email: '')
is_expected.to eq(group_notification_email)
end
end
context 'when only the parent group notification email is set' do
it 'returns parent group notification email' do
create(:notification_setting, user: user, source: group, notification_email: group_notification_email)
is_expected.to eq(group_notification_email)
end
end
end
describe '#visibility_level_allowed_by_parent' do
let(:parent) { create(:group, :internal) }
let(:sub_group) { build(:group, parent_id: parent.id) }
context 'without a parent' do
it 'is valid' do
sub_group.parent_id = nil
expect(sub_group).to be_valid
end
end
context 'with a parent' do
context 'when visibility of sub group is greater than the parent' do
it 'is invalid' do
sub_group.visibility_level = Gitlab::VisibilityLevel::PUBLIC
expect(sub_group).to be_invalid
end
end
context 'when visibility of sub group is lower or equal to the parent' do
[Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE].each do |level|
it 'is valid' do
sub_group.visibility_level = level
expect(sub_group).to be_valid
end
end
end
end
end
describe '#visibility_level_allowed_by_projects' do
let!(:internal_group) { create(:group, :internal) }
let!(:internal_project) { create(:project, :internal, group: internal_group) }
context 'when group has a lower visibility' do
it 'is invalid' do
internal_group.visibility_level = Gitlab::VisibilityLevel::PRIVATE
expect(internal_group).to be_invalid
expect(internal_group.errors[:visibility_level]).to include('private is not allowed since this group contains projects with higher visibility.')
end
it 'is valid if higher visibility project is currently undergoing deletion' do
internal_project.update_attribute(:pending_delete, true)
internal_group.visibility_level = Gitlab::VisibilityLevel::PRIVATE
expect(internal_group).to be_valid
end
it 'is valid if higher visibility project is pending deletion via marked_for_deletion_at' do
internal_project.update_attribute(:marked_for_deletion_at, Time.current)
internal_group.visibility_level = Gitlab::VisibilityLevel::PRIVATE
expect(internal_group).to be_valid
end
end
context 'when group has a higher visibility' do
it 'is valid' do
internal_group.visibility_level = Gitlab::VisibilityLevel::PUBLIC
expect(internal_group).to be_valid
end
end
end
describe '#visibility_level_allowed_by_sub_groups' do
let!(:internal_group) { create(:group, :internal) }
let!(:internal_sub_group) { create(:group, :internal, parent: internal_group) }
context 'when parent group has a lower visibility' do
it 'is invalid' do
internal_group.visibility_level = Gitlab::VisibilityLevel::PRIVATE
expect(internal_group).to be_invalid
expect(internal_group.errors[:visibility_level]).to include('private is not allowed since there are sub-groups with higher visibility.')
end
end
context 'when parent group has a higher visibility' do
it 'is valid' do
internal_group.visibility_level = Gitlab::VisibilityLevel::PUBLIC
expect(internal_group).to be_valid
end
end
end
describe '#two_factor_authentication_allowed' do
let_it_be_with_reload(:group) { create(:group) }
context 'for a parent group' do
it 'is valid' do
group.require_two_factor_authentication = true
expect(group).to be_valid
end
end
context 'for a child group' do
let(:sub_group) { create(:group, parent: group) }
it 'is valid when parent group allows' do
sub_group.require_two_factor_authentication = true
expect(sub_group).to be_valid
end
it 'is invalid when parent group blocks' do
group.namespace_settings.update!(allow_mfa_for_subgroups: false)
sub_group.require_two_factor_authentication = true
expect(sub_group).to be_invalid
expect(sub_group.errors[:require_two_factor_authentication]).to include('is forbidden by a top-level group')
end
end
end
describe '#visibility_level_allowed_by_organization?' do
let(:group) { build(:group) }
subject(:visibility_level_allowed_by_organization) { group.visibility_level_allowed_by_organization? }
context 'without an organization' do
before do
group.organization = nil
end
it 'return true but the record is invalid' do
expect(group.visibility_level_allowed_by_organization?).to eq(true)
expect(group.valid?).to eq(false)
end
end
context 'with different visibilities' do
where(:organization, :group_visibility, :visibility_level_allowed_by_organization) do
lazy { private_organization } | Gitlab::VisibilityLevel::PRIVATE | true
lazy { private_organization } | Gitlab::VisibilityLevel::INTERNAL | false
lazy { private_organization } | Gitlab::VisibilityLevel::PUBLIC | false
lazy { public_organization } | Gitlab::VisibilityLevel::PRIVATE | true
lazy { public_organization } | Gitlab::VisibilityLevel::INTERNAL | true
lazy { public_organization } | Gitlab::VisibilityLevel::PUBLIC | true
end
with_them do
let(:group) { build(:group, organization: organization, visibility_level: group_visibility) }
it { is_expected.to eq(visibility_level_allowed_by_organization) }
end
end
end
describe '#visibility_level_allowed_by_organization' do
it 'validates visibility level with visibility changes' do
group = create(:group)
group.visibility_level = Gitlab::VisibilityLevel::PRIVATE
expect(group).to receive(:visibility_level_allowed_by_organization)
group.valid?
end
it 'validates visibility level for new records' do
group = build(:group)
expect(group).to receive(:visibility_level_allowed_by_organization)
group.valid?
end
context 'with different organization and group visibilities' do
where(:organization, :group_visibility, :valid?) do
lazy { private_organization } | :private | true
lazy { private_organization } | :internal | false
lazy { private_organization } | :public | false
lazy { public_organization } | :private | true
lazy { public_organization } | :internal | true
lazy { public_organization } | :public | true
end
with_them do
let(:group) { build(:group, group_visibility, organization: organization) }
let(:error) { "#{group_visibility} is not allowed since the organization has a private visibility." }
it 'returns the visibility error' do
expect(group.valid?).to eq(valid?)
expect(group.errors[:visibility_level]).to include(error) unless valid?
end
end
end
end
end
it_behaves_like 'a BulkUsersByEmailLoad model'
it_behaves_like 'ensures runners_token is prefixed' do
subject(:record) { create(:group, :allow_runner_registration_token) }
end
context 'after initialized' do
it 'has a group_feature' do
expect(described_class.new.group_feature).to be_present
end
end
context 'on create' do
let!(:root) { create(:group) }
let!(:parent) { create(:group, parent: root) }
let(:group) { build(:group, parent: parent) }
subject { group.save! }
it 'locks self and ancestors', :lock_recorder do
expect { subject }.to lock_rows(
root => 'FOR SHARE',
parent => 'FOR SHARE',
group => 'FOR NO KEY UPDATE'
)
end
end
context 'when creating a new project' do
let_it_be(:group) { create(:group) }
it 'automatically creates the groups feature for the group' do
expect(group.group_feature).to be_an_instance_of(Groups::FeatureSetting)
expect(group.group_feature).to be_persisted
end
end
context 'traversal_ids on create' do
context 'default traversal_ids' do
let(:group) { build(:group) }
before do
group.save!
end
it { expect(group.traversal_ids).to eq [group.id] }
end
context 'has a parent' do
let(:parent) { create(:group) }
let(:group) { build(:group, parent: parent) }
before do
group.save!
end
it { expect(parent.traversal_ids).to eq [parent.id] }
it { expect(group.traversal_ids).to eq [parent.id, group.id] }
end
context 'has a parent update before save' do
let(:parent) { create(:group) }
let(:group) { build(:group, parent: parent) }
let!(:new_grandparent) { create(:group) }
before do
parent.update!(parent: new_grandparent)
group.save!
end
it 'avoid traversal_ids race condition' do
expect(parent.traversal_ids).to eq [new_grandparent.id, parent.id]
expect(group.traversal_ids).to eq [new_grandparent.id, parent.id, group.id]
end
end
end
context 'traversal_ids on update' do
context 'parent is updated' do
let(:new_parent) { create(:group) }
subject { group.update!(parent: new_parent, name: 'new name') }
it_behaves_like 'update on column', :traversal_ids
end
context 'parent is not updated' do
subject { group.update!(name: 'new name') }
it_behaves_like 'no update on column', :traversal_ids
end
end
context 'traversal_ids on ancestral update' do
context 'update multiple ancestors before save' do
let(:parent) { create(:group) }
let(:group) { create(:group, parent: parent) }
let!(:new_grandparent) { create(:group) }
let!(:new_parent) { create(:group) }
before do
group.parent = new_parent
new_parent.update!(parent: new_grandparent)
group.save!
end
it 'avoids traversal_ids race condition' do
expect(parent.traversal_ids).to eq [parent.id]
expect(group.traversal_ids).to eq [new_grandparent.id, new_parent.id, group.id]
expect(new_grandparent.traversal_ids).to eq [new_grandparent.id]
expect(new_parent.traversal_ids).to eq [new_grandparent.id, new_parent.id]
end
end
context 'assign a new parent' do
let!(:group) { create(:group, parent: old_parent) }
let(:recorded_queries) { ActiveRecord::QueryRecorder.new }
subject do
recorded_queries.record do
group.update!(parent: new_parent)
end
end
context 'within the same hierarchy' do
let!(:root) { create(:group) }
let!(:old_parent) { create(:group, parent: root) }
let!(:new_parent) { create(:group, parent: root) }
it 'updates traversal_ids' do
subject
expect(group.traversal_ids).to eq [root.id, new_parent.id, group.id]
end
it_behaves_like 'hierarchy with traversal_ids'
it 'locks root ancestor', :lock_recorder do
expect { subject }.to lock_rows(root => 'FOR NO KEY UPDATE')
end
end
context 'to another hierarchy' do
let!(:old_parent) { create(:group) }
let!(:new_parent) { create(:group) }
let!(:group) { create(:group, parent: old_parent) }
it 'updates traversal_ids' do
subject
expect(group.traversal_ids).to eq [new_parent.id, group.id]
end
it 'locks both root ancestors', :lock_recorder do
expect { subject }.to lock_rows(
old_parent => 'FOR NO KEY UPDATE',
new_parent => 'FOR NO KEY UPDATE'
)
end
context 'old hierarchy' do
let(:root) { old_parent.root_ancestor }
it_behaves_like 'hierarchy with traversal_ids'
end
context 'new hierarchy' do
let(:root) { new_parent.root_ancestor }
it_behaves_like 'hierarchy with traversal_ids'
end
end
context 'from being a root ancestor' do
let!(:old_parent) { nil }
let!(:new_parent) { create(:group) }
it 'updates traversal_ids' do
subject
expect(group.traversal_ids).to eq [new_parent.id, group.id]
end
it 'locks rows', :lock_recorder do
expect { subject }.to lock_rows(
group => 'FOR NO KEY UPDATE',
new_parent => 'FOR NO KEY UPDATE'
)
end
it_behaves_like 'hierarchy with traversal_ids' do
let(:root) { new_parent }
end
end
context 'to being a root ancestor' do
let!(:old_parent) { create(:group) }
let!(:new_parent) { nil }
it 'updates traversal_ids' do
subject
expect(group.traversal_ids).to eq [group.id]
end
it 'locks rows', :lock_recorder do
expect { subject }.to lock_rows(
group => 'FOR NO KEY UPDATE',
old_parent => 'FOR NO KEY UPDATE'
)
end
it_behaves_like 'hierarchy with traversal_ids' do
let(:root) { group }
before do
subject
end
end
end
end
context 'assigning a new grandparent' do
let!(:old_grandparent) { create(:group) }
let!(:new_grandparent) { create(:group) }
let!(:parent_group) { create(:group, parent: old_grandparent) }
let!(:group) { create(:group, parent: parent_group) }
before do
parent_group.update!(parent: new_grandparent)
reload_models(parent_group, group)
end
it 'updates traversal_ids for all descendants' do
expect(parent_group.traversal_ids).to eq [new_grandparent.id, parent_group.id]
expect(group.traversal_ids).to eq [new_grandparent.id, parent_group.id, group.id]
end
end
end
context 'traversal queries' do
let_it_be(:group, reload: true) { create(:group, :nested) }
it_behaves_like 'namespace traversal'
describe '#self_and_descendants' do
it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' }
end
describe '#self_and_descendant_ids' do
it { expect(group.self_and_descendant_ids.to_sql).to include 'traversal_ids @>' }
end
describe '#descendants' do
it { expect(group.descendants.to_sql).to include 'traversal_ids @>' }
end
describe '#self_and_hierarchy' do
it { expect(group.self_and_hierarchy.to_sql).to include 'traversal_ids @>' }
end
describe '#ancestors' do
it { expect(group.ancestors.to_sql).to include "\"namespaces\".\"id\" = #{group.parent_id}" }
it 'hierarchy order' do
expect(group.ancestors(hierarchy_order: :asc).to_sql).to include 'ORDER BY "depth" ASC'
end
end
describe '#ancestors_upto' do
it { expect(group.ancestors_upto.to_sql).to include "WITH ORDINALITY" }
end
describe '.shortest_traversal_ids_prefixes' do
subject { filter.shortest_traversal_ids_prefixes }
context 'for many top-level namespaces' do
let!(:top_level_groups) { create_list(:group, 4) }
context 'when querying all groups' do
let(:filter) { described_class.id_in(top_level_groups) }
it "returns all traversal_ids" do
is_expected.to contain_exactly(
*top_level_groups.map { |group| [group.id] }
)
end
end
context 'when querying selected groups' do
let(:filter) { described_class.id_in(top_level_groups.first) }
it "returns only a selected traversal_ids" do
is_expected.to contain_exactly([top_level_groups.first.id])
end
end
end
context 'for namespace hierarchy' do
let!(:group_a) { create(:group) }
let!(:group_a_sub_1) { create(:group, parent: group_a) }
let!(:group_a_sub_2) { create(:group, parent: group_a) }
let!(:group_b) { create(:group) }
let!(:group_b_sub_1) { create(:group, parent: group_b) }
let!(:group_c) { create(:group) }
context 'when querying all groups' do
let(:filter) { described_class.id_in([group_a, group_a_sub_1, group_a_sub_2, group_b, group_b_sub_1, group_c]) }
it 'returns only shortest prefixes of top-level groups' do
is_expected.to contain_exactly(
[group_a.id],
[group_b.id],
[group_c.id]
)
end
end
context 'when sub-group is reparented' do
let(:filter) { described_class.id_in([group_b_sub_1, group_c]) }
before do
group_b_sub_1.update!(parent: group_c)
end
it 'returns a proper shortest prefix of a new group' do
is_expected.to contain_exactly(
[group_c.id]
)
end
end
context 'when querying sub-groups' do
let(:filter) { described_class.id_in([group_a_sub_1, group_b_sub_1, group_c]) }
it 'returns sub-groups as they are shortest prefixes' do
is_expected.to contain_exactly(
[group_a.id, group_a_sub_1.id],
[group_b.id, group_b_sub_1.id],
[group_c.id]
)
end
end
context 'when querying group and sub-group of this group' do
let(:filter) { described_class.id_in([group_a, group_a_sub_1, group_c]) }
it 'returns parent groups as this contains all sub-groups' do
is_expected.to contain_exactly(
[group_a.id],
[group_c.id]
)
end
end
end
end
context 'when project namespace exists in the group' do
let!(:project) { create(:project, group: group) }
let!(:project_namespace) { project.project_namespace }
it 'filters out project namespace' do
expect(group.descendants.find_by_id(project_namespace.id)).to be_nil
end
end
end
describe '.without_integration' do
let(:another_group) { create(:group) }
let(:instance_integration) { build(:jira_integration, :instance) }
before do
create(:jira_integration, :group, group: group)
create(:integrations_slack, :group, group: another_group)
end
it 'returns groups without integration' do
expect(described_class.without_integration(instance_integration)).to contain_exactly(another_group)
end
end
describe '.groups_user_can' do
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:internal_subgroup) { create(:group, :internal, parent: public_group) }
let_it_be(:private_subgroup_1) { create(:group, :private, parent: internal_subgroup) }
let_it_be(:private_subgroup_2) { create(:group, :private, parent: private_subgroup_1) }
let_it_be(:user) { create(:user) }
it 'filters groups based on permissions' do
private_subgroup_2.add_guest(user)
expect(described_class.groups_user_can(public_group.self_and_descendants, user, :read_group)).to contain_exactly(
public_group,
internal_subgroup,
private_subgroup_2
)
end
end
describe '.execute_integrations' do
let(:integration) { create(:integrations_slack, :group, group: group) }
let(:test_data) { { 'foo' => 'bar' } }
before do
allow(group.integrations).to receive(:public_send).and_return([])
allow(group.integrations).to receive(:public_send).with(:push_hooks).and_return([integration])
end
it 'executes integrations with a matching scope' do
expect(integration).to receive(:async_execute).with(test_data)
group.execute_integrations(test_data, :push_hooks)
end
it 'ignores integrations without a matching scope' do
expect(integration).not_to receive(:async_execute).with(test_data)
group.execute_integrations(test_data, :note_hooks)
end
end
describe '#notification_group' do
it 'is expected to reference itself' do
group = build(:group)
expect(group.notification_group).to eq(group)
end
end
describe '.public_or_visible_to_user' do
let!(:private_group) { create(:group, :private) }
let!(:private_subgroup) { create(:group, :private, parent: private_group) }
let!(:internal_group) { create(:group, :internal) }
subject { described_class.public_or_visible_to_user(user) }
context 'when user is nil' do
let!(:user) { nil }
it { is_expected.to match_array([group]) }
end
context 'when user' do
let!(:user) { create(:user) }
context 'when user does not have access to any private group' do
it { is_expected.to match_array([internal_group, group]) }
end
context 'when user is a member of private group' do
before do
private_group.add_member(user, Gitlab::Access::DEVELOPER)
end
it { is_expected.to contain_exactly(private_group, private_subgroup, internal_group, group) }
end
context 'when user is a member of private subgroup' do
let!(:private_subgroup) { create(:group, :private, parent: private_group) }
before do
private_subgroup.add_member(user, Gitlab::Access::DEVELOPER)
end
it { is_expected.to match_array([private_subgroup, internal_group, group]) }
end
end
end
describe '.sort_by_attribute' do
before do
group.destroy!
end
let!(:group_1) { create(:group, id: 10, name: 'Y group') }
let!(:group_2) { create(:group, id: 11, name: 'J group', created_at: 2.days.ago, updated_at: 1.day.ago) }
let!(:group_3) { create(:group, id: 12, name: 'A group') }
let!(:group_4) { create(:group, id: 13, name: 'F group', created_at: 1.day.ago, updated_at: 1.day.ago) }
subject { described_class.with_statistics.with_route.sort_by_attribute(sort) }
context 'when sort by is not provided' do
let(:sort) { nil }
it 'results are not ordered' do
is_expected.to contain_exactly(group_1, group_2, group_3, group_4)
end
end
context 'when sort by name_asc' do
let(:sort) { 'name_asc' }
it { is_expected.to eq([group_3, group_4, group_2, group_1]) }
end
context 'when sort by name_desc' do
let(:sort) { 'name_desc' }
it { is_expected.to eq([group_1, group_2, group_4, group_3]) }
end
context 'when sort by path_asc' do
let(:sort) { 'path_asc' }
it { is_expected.to eq([group_1, group_2, group_3, group_4].sort_by(&:path)) }
end
context 'when sort by path_desc' do
let(:sort) { 'path_desc' }
it { is_expected.to eq([group_1, group_2, group_3, group_4].sort_by(&:path).reverse) }
end
context 'when sort by created_desc' do
let(:sort) { 'created_desc' }
it { is_expected.to eq([group_3, group_1, group_4, group_2]) }
end
context 'when sort by created_asc' do
let(:sort) { 'created_asc' }
it { is_expected.to eq([group_2, group_4, group_1, group_3]) }
end
context 'when sort by storage_size_desc' do
let!(:project_1) do
create(:project,
namespace: group_1,
statistics: build(
:project_statistics,
namespace: group_1,
repository_size: 2178370,
storage_size: 1278370,
wiki_size: 505,
lfs_objects_size: 202,
build_artifacts_size: 303,
pipeline_artifacts_size: 707,
packages_size: 404,
snippets_size: 605,
uploads_size: 808
)
)
end
let!(:project_2) do
create(:project,
namespace: group_2,
statistics: build(
:project_statistics,
namespace: group_2,
repository_size: 3178370,
storage_size: 3178370,
wiki_size: 505,
lfs_objects_size: 202,
build_artifacts_size: 303,
pipeline_artifacts_size: 707,
packages_size: 404,
snippets_size: 605,
uploads_size: 808
)
)
end
let!(:project_3) do
create(:project,
namespace: group_3,
statistics: build(
:project_statistics,
namespace: group_3,
repository_size: 1278370,
storage_size: 1178370,
wiki_size: 505,
lfs_objects_size: 202,
build_artifacts_size: 303,
pipeline_artifacts_size: 707,
packages_size: 404,
snippets_size: 605,
uploads_size: 808
)
)
end
let!(:project_4) do
create(:project,
namespace: group_4,
statistics: build(
:project_statistics,
namespace: group_4,
repository_size: 2178370,
storage_size: 2278370,
wiki_size: 505,
lfs_objects_size: 202,
build_artifacts_size: 303,
pipeline_artifacts_size: 707,
packages_size: 404,
snippets_size: 605,
uploads_size: 808
)
)
end
let(:sort) { 'storage_size_desc' }
it { is_expected.to eq([group_2, group_4, group_1, group_3]) }
end
end
describe 'scopes' do
let_it_be(:private_group) { create(:group, :private) }
let_it_be(:internal_group) { create(:group, :internal) }
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
describe 'public_only' do
subject { described_class.public_only.to_a }
it { is_expected.to eq([group]) }
end
describe 'public_and_internal_only' do
subject { described_class.public_and_internal_only.to_a }
it { is_expected.to match_array([group, internal_group]) }
end
describe 'non_public_only' do
subject { described_class.non_public_only.to_a }
it { is_expected.to match_array([private_group, internal_group]) }
end
describe 'private_only' do
subject { described_class.private_only.to_a }
it { is_expected.to match_array([private_group]) }
end
describe 'with_non_archived_projects' do
let_it_be(:project) { create(:project, group: private_group, archived: false) }
subject { described_class.with_non_archived_projects }
it 'loads the records of non archived projects' do
associations = subject.map { |group| group.association(:non_archived_projects) }
expect(associations).to all(be_loaded)
end
end
describe 'with_non_invite_group_members' do
let_it_be(:group_member) { create(:group_member, member_namespace: private_group, requested_at: nil, invite_token: nil, access_level: Gitlab::Access::DEVELOPER) }
subject { described_class.with_non_invite_group_members }
it 'loads the records of non invite group members' do
associations = subject.map { |group| group.association(:non_invite_group_members) }
expect(associations).to all(be_loaded)
end
end
describe '.with_request_group_members' do
let_it_be(:group_member) { create(:group_member, :access_request, member_namespace: private_group) }
subject(:with_request_group_members) { described_class.with_request_group_members }
it 'loads the records of non invite group members' do
associations = with_request_group_members.map { |group| group.association(:request_group_members) }
expect(associations).to all(be_loaded)
end
end
describe 'for_authorized_group_members' do
let_it_be(:group_member1) { create(:group_member, source: private_group, user_id: user1.id, access_level: Gitlab::Access::OWNER) }
it do
result = described_class.for_authorized_group_members([user1.id, user2.id])
expect(result).to match_array([private_group])
end
end
describe 'for_authorized_project_members' do
let_it_be(:project) { create(:project, group: internal_group) }
let_it_be(:project_member1) { create(:project_member, source: project, user_id: user1.id, access_level: Gitlab::Access::DEVELOPER) }
it do
result = described_class.for_authorized_project_members([user1.id, user2.id])
expect(result).to match_array([internal_group])
end
end
describe '.aimed_for_deletion' do
let!(:date) { 10.days.ago }
subject(:relation) { described_class.aimed_for_deletion(date) }
it 'only includes groups that are marked for deletion on or before the specified date' do
group_not_marked_for_deletion = create(:group)
group_marked_for_deletion_after_specified_date = create(
:group_with_deletion_schedule,
marked_for_deletion_on: date + 2.days
)
group_marked_for_deletion_before_specified_date = create(
:group_with_deletion_schedule,
marked_for_deletion_on: date - 2.days
)
group_marked_for_deletion_on_specified_date = create(
:group_with_deletion_schedule,
marked_for_deletion_on: date
)
expect(relation).to include(
group_marked_for_deletion_before_specified_date,
group_marked_for_deletion_on_specified_date
)
expect(relation).not_to include(
group_marked_for_deletion_after_specified_date,
group_not_marked_for_deletion
)
end
end
describe '.by_marked_for_deletion_on' do
let_it_be(:group_marked_for_deletion) { create(:group_with_deletion_schedule, marked_for_deletion_on: Date.parse('2024-01-01')) }
let_it_be(:group_not_marked_for_deletion) { create(:group) }
context 'when marked_for_deletion_on is present' do
it 'returns groups marked for deletion on the specified date' do
expect(described_class.by_marked_for_deletion_on(Date.parse('2024-01-01'))).to contain_exactly(group_marked_for_deletion)
end
end
context 'when marked_for_deletion_on is not present' do
it 'does not return any groups marked for deletion' do
expect(described_class.by_marked_for_deletion_on(nil)).to be_empty
end
end
end
describe '.with_project_creation_levels' do
let_it_be(:group_1) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
let_it_be(:group_2) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_PROJECT_ACCESS) }
let_it_be(:group_3) { create(:group, project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) }
let_it_be(:group_4) { create(:group, project_creation_level: Gitlab::Access::OWNER_PROJECT_ACCESS) }
let_it_be(:group_5) { create(:group, project_creation_level: nil) }
it 'returns groups with the specified project creation levels' do
result = described_class.with_project_creation_levels([
Gitlab::Access::NO_ONE_PROJECT_ACCESS,
Gitlab::Access::MAINTAINER_PROJECT_ACCESS
])
expect(result).to include(group_1, group_3)
expect(result).not_to include(group_2, group_4, group_5)
end
end
describe '.excluding_restricted_visibility_levels_for_user' do
let_it_be(:admin_user) { create(:admin) }
let(:private_vis) { Gitlab::VisibilityLevel::PRIVATE }
let(:internal_vis) { Gitlab::VisibilityLevel::INTERNAL }
let(:public_vis) { Gitlab::VisibilityLevel::PUBLIC }
subject { described_class.excluding_restricted_visibility_levels_for_user(user1) }
context 'with table syntax' do
where(:restricted_visibility_levels, :expected_groups) do
nil | lazy { [private_group, internal_group, group] }
[] | lazy { [private_group, internal_group, group] }
[private_vis] | lazy { [internal_group, group] }
[internal_vis] | lazy { [private_group, internal_group, group] }
[public_vis] | lazy { [private_group, internal_group, group] }
[private_vis, internal_vis] | lazy { [group] }
[private_vis, public_vis] | lazy { [internal_group, group] }
[internal_vis, public_vis] | lazy { [private_group, internal_group, group] }
[private_vis, internal_vis, public_vis] | lazy { [] }
end
with_them do
before do
stub_application_setting(restricted_visibility_levels: restricted_visibility_levels)
end
it { is_expected.to match_array(expected_groups) }
context 'with admin mode enabled', :enable_admin_mode do
subject { described_class.excluding_restricted_visibility_levels_for_user(admin_user) }
it { is_expected.to match_array([private_group, internal_group, group]) }
end
end
end
end
describe '.project_creation_allowed' do
let_it_be(:group_1) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
let_it_be(:group_2) { create(:group, project_creation_level: Gitlab::Access::DEVELOPER_PROJECT_ACCESS) }
let_it_be(:group_3) { create(:group, project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) }
let_it_be(:group_4) { create(:group, project_creation_level: Gitlab::Access::OWNER_PROJECT_ACCESS) }
let_it_be(:group_5) { create(:group, project_creation_level: Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS) }
let_it_be(:group_6) { create(:group, project_creation_level: nil) } # `nil` inherits `default_project_creation`
let_it_be(:all_groups) { described_class.id_in([group_1, group_2, group_3, group_4, group_5, group_6]) }
where(:admin_user?, :admin_mode, :default_project_creation, :expected_groups) do
false | false | Gitlab::Access::NO_ONE_PROJECT_ACCESS | lazy { [group_2, group_3, group_4] }
false | false | Gitlab::Access::OWNER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_6] }
false | false | Gitlab::Access::MAINTAINER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_6] }
false | false | Gitlab::Access::DEVELOPER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_6] }
false | false | Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS | lazy { [group_2, group_3, group_4] }
true | false | Gitlab::Access::NO_ONE_PROJECT_ACCESS | lazy { [group_2, group_3, group_4] }
true | false | Gitlab::Access::OWNER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_6] }
true | false | Gitlab::Access::MAINTAINER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_6] }
true | false | Gitlab::Access::DEVELOPER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_6] }
true | false | Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS | lazy { [group_2, group_3, group_4] }
true | true | Gitlab::Access::NO_ONE_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_5] }
true | true | Gitlab::Access::OWNER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_5, group_6] }
true | true | Gitlab::Access::MAINTAINER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_5, group_6] }
true | true | Gitlab::Access::DEVELOPER_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_5, group_6] }
true | true | Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS | lazy { [group_2, group_3, group_4, group_5, group_6] }
end
with_them do
let(:user) { admin_user? ? create(:admin) : create(:user) }
before do
enable_admin_mode!(user) if admin_mode
stub_application_setting(default_project_creation: default_project_creation)
end
it 'returns expected groups' do
expect(described_class).to receive(:excluding_restricted_visibility_levels_for_user).and_call_original
result = all_groups.project_creation_allowed(user)
expect(result).to match_array(expected_groups)
end
end
end
describe 'by_ids_or_paths' do
let(:group_path) { 'group_path' }
let!(:group) { create(:group, path: group_path) }
let(:group_id) { group.id }
it 'returns matching records based on paths' do
expect(described_class.by_ids_or_paths(nil, [group_path])).to match_array([group])
expect(described_class.by_ids_or_paths(nil, [group_path.upcase])).to match_array([group])
end
it 'returns matching records based on ids' do
expect(described_class.by_ids_or_paths([group_id], nil)).to match_array([group])
expect(described_class.by_ids_or_paths([group_id], [])).to match_array([group])
end
it 'returns matching records based on both paths and ids' do
new_group = create(:group)
expect(described_class.by_ids_or_paths([new_group.id], [group_path])).to match_array([group, new_group])
end
it 'returns matching records based on full_paths' do
new_group = create(:group, parent: group)
expect(described_class.by_ids_or_paths(nil, [new_group.full_path])).to match_array([new_group])
expect(described_class.by_ids_or_paths(nil, [new_group.full_path.upcase])).to match_array([new_group])
end
end
describe 'by_visibility_level' do
let_it_be(:group1) { create(:group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let_it_be(:group2) { create(:group, visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
let_it_be(:group3) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
context 'when visibility is present' do
it 'returns groups with the specified visibility level' do
expect(described_class.by_visibility_level(Gitlab::VisibilityLevel::PUBLIC)).to contain_exactly(group, group1)
expect(described_class.by_visibility_level(Gitlab::VisibilityLevel::PRIVATE)).to contain_exactly(private_group, group2)
expect(described_class.by_visibility_level(Gitlab::VisibilityLevel::INTERNAL)).to contain_exactly(internal_group, group3)
end
end
context 'when visibility is not present' do
it 'returns all groups' do
expect(described_class.by_visibility_level(nil)).to include(group1, group2, group3)
end
end
end
describe 'excluding_groups' do
let!(:another_group) { create(:group) }
subject { described_class.excluding_groups(excluded_groups) }
context 'when passing a single group' do
let(:excluded_groups) { group }
it 'does not return excluded group' do
expect(subject).not_to include(group)
end
end
context 'when passing an array with groups' do
let(:excluded_groups) { [group, another_group] }
it 'does not return excluded groups' do
expect(subject).not_to include(group, another_group)
end
end
end
describe 'accessible_to_user' do
subject { described_class.accessible_to_user(user) }
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:unaccessible_group) { create(:group, :private) }
let_it_be(:unaccessible_subgroup) { create(:group, :private, parent: unaccessible_group) }
let_it_be(:accessible_group) { create(:group, :private) }
let_it_be(:accessible_subgroup) { create(:group, :private, parent: accessible_group) }
context 'when user is nil' do
let(:user) { nil }
it { is_expected.to match_array([group, public_group]) }
end
context 'when user is present' do
let(:user) { create(:user) }
it { is_expected.to match_array([group, internal_group, public_group]) }
context 'when user has access to accessible group' do
before do
accessible_group.add_developer(user)
end
it { is_expected.to match_array([group, internal_group, public_group, accessible_group, accessible_subgroup]) }
end
end
end
describe '.sorted_by_similarity_desc' do
# Exact match to the search term
let_it_be(:first) { create(:group, path: 'similar-b', name: 'similar-b') }
# Not similar at all
let_it_be(:last) { create(:group, path: 'different-path-a', name: 'different-name-a') }
# The two middle terms have the same distance from the search term
let_it_be(:middle) { create(:group, path: 'similar-a', name: 'similar-a') }
let_it_be(:middle_two) { create(:group, path: 'similar-c', name: 'similar-c') }
let(:search_term) { 'similar-b' }
subject(:ids) do
described_class.where(id: [middle_two.id, middle.id, last.id, first.id])
.sorted_by_similarity_desc(search_term)
.pluck(:id)
end
it 'sorts groups based on path, name, and description similarity, ties broken by ID' do
expect(ids).to eq([first.id, middle.id, middle_two.id, last.id])
end
end
describe '.in_organization' do
let_it_be(:org1) { create(:organization) }
let_it_be(:org2) { create(:organization) }
let_it_be(:groups) { create_pair(:group, organization: org1) }
before do
create(:group, organization: org2)
end
subject { described_class.in_organization(org1) }
it { is_expected.to match_array(groups) }
end
describe '.by_min_access_level' do
let_it_be(:user) { create(:user) }
let_it_be(:group1) { create(:group) }
let_it_be(:group2) { create(:group) }
let(:owner_access_level) { Gitlab::Access::OWNER }
let(:developer_access_level) { Gitlab::Access::DEVELOPER }
before do
create(:group_member, user: user, group: group1, access_level: owner_access_level)
create(:group_member, user: user, group: group2, access_level: developer_access_level)
end
it 'returns groups where the user has the specified access level' do
result = described_class.by_min_access_level(user, owner_access_level)
expect(result).to contain_exactly(group1)
end
it 'returns groups if the user has greater or equal specified access level' do
result = described_class.by_min_access_level(user, developer_access_level)
expect(result).to contain_exactly(group1, group2)
end
end
describe 'descendants_with_shared_with_groups' do
subject { described_class.descendants_with_shared_with_groups(parent_group) }
let_it_be(:grand_parent_group) { create(:group, :public) }
let_it_be(:parent_group) { create(:group, :public, parent: grand_parent_group) }
let_it_be(:subgroup) { create(:group, :public, parent: parent_group) }
let_it_be(:subsubgroup) { create(:group, :public, parent: subgroup) }
let_it_be(:shared_to_group) { create(:group, :public) }
let_it_be(:shared_to_sub_group) { create(:group, :public) }
context 'when parent group is nil' do
let(:parent_group) { nil }
it { is_expected.to match_array([]) }
end
context 'when parent group is present and there are shared groups' do
before do
parent_group.shared_with_groups << shared_to_group
subgroup.shared_with_groups << shared_to_sub_group
end
it { is_expected.to match_array([subgroup, subsubgroup, shared_to_group]) }
end
context 'when parent group is present and there are no shared groups' do
it { is_expected.to match_array([subgroup, subsubgroup]) }
end
end
describe '.active' do
let_it_be(:active_group) { create(:group) }
let_it_be(:archived_group) { create(:group, namespace_settings: create(:namespace_settings, archived: true)) }
let_it_be(:marked_for_deletion_group) { create(:group_with_deletion_schedule) }
subject { described_class.active }
it { is_expected.to include(active_group) }
it { is_expected.not_to include(marked_for_deletion_group) }
it { is_expected.not_to include(archived_group) }
end
describe '.inactive' do
let_it_be(:active_group) { create(:group) }
let_it_be(:archived_group) { create(:group, namespace_settings: create(:namespace_settings, archived: true)) }
let_it_be(:marked_for_deletion_group) { create(:group_with_deletion_schedule) }
subject { described_class.inactive }
it { is_expected.to include(archived_group) }
it { is_expected.to include(marked_for_deletion_group) }
it { is_expected.not_to include(active_group) }
end
end
describe '.project_creation_levels_for_user' do
where(:admin_user?, :admin_mode, :default_project_creation, :expected_levels) do
false | false | no_one_access | lazy { [developer_access, maintainer_access, owner_access] }
false | false | admin_access | lazy { [developer_access, maintainer_access, owner_access] }
false | false | maintainer_access | lazy { [developer_access, maintainer_access, owner_access, nil] }
false | false | owner_access | lazy { [developer_access, maintainer_access, owner_access, nil] }
false | false | developer_access | lazy { [developer_access, maintainer_access, owner_access, nil] }
true | false | no_one_access | lazy { [developer_access, maintainer_access, owner_access] }
true | false | admin_access | lazy { [developer_access, maintainer_access, owner_access] }
true | false | maintainer_access | lazy { [developer_access, maintainer_access, owner_access, nil] }
true | false | owner_access | lazy { [developer_access, maintainer_access, owner_access, nil] }
true | false | developer_access | lazy { [developer_access, maintainer_access, owner_access, nil] }
true | true | no_one_access | lazy { [developer_access, maintainer_access, owner_access, admin_access] }
true | true | admin_access | lazy { [developer_access, maintainer_access, owner_access, admin_access, nil] }
true | true | maintainer_access | lazy { [developer_access, maintainer_access, owner_access, admin_access, nil] }
true | true | owner_access | lazy { [developer_access, maintainer_access, owner_access, admin_access, nil] }
true | true | developer_access | lazy { [developer_access, maintainer_access, owner_access, admin_access, nil] }
end
with_them do
let(:user) { admin_user? ? create(:admin) : create(:user) }
before do
stub_application_setting(default_project_creation: default_project_creation)
enable_admin_mode!(user) if admin_mode
end
it 'returns correct project creation levels' do
expect(described_class.project_creation_levels_for_user(user)).to match_array(expected_levels)
end
end
end
describe '.prevent_project_creation?' do
where(:admin_user?, :admin_mode, :project_creation_setting, :expected_result) do
false | false | lazy { no_one_access } | true
false | false | lazy { admin_access } | true
false | false | lazy { owner_access } | false
false | false | lazy { maintainer_access } | false
false | false | lazy { developer_access } | false
true | false | lazy { no_one_access } | true
true | false | lazy { admin_access } | true
true | false | lazy { owner_access } | false
true | false | lazy { maintainer_access } | false
true | false | lazy { developer_access } | false
true | true | lazy { no_one_access } | true
true | true | lazy { admin_access } | false
true | true | lazy { owner_access } | false
true | true | lazy { maintainer_access } | false
true | true | lazy { developer_access } | false
end
with_them do
let(:user) { admin_user? ? create(:admin) : create(:user) }
before do
enable_admin_mode!(user) if admin_mode
end
subject { described_class.prevent_project_creation?(user, project_creation_setting) }
it { is_expected.to be(expected_result) }
end
end
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(group.to_reference).to eq "@#{group.name}"
end
end
describe '#users' do
let(:group_users) { group.users.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/436662") }
let(:group_owners) { group.owners.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/436662") }
it { expect(group_users).to eq(group_owners) }
end
describe '#human_name' do
it { expect(group.human_name).to eq(group.name) }
end
describe '#to_human_reference' do
let_it_be(:new_group) { create(:group) }
it { expect(group.to_human_reference).to be_nil }
it { expect(group.to_human_reference(new_group)).to eq(group.full_name) }
end
describe '#add_user' do
let(:user) { create(:user) }
it 'adds the user' do
expect_next_instance_of(GroupMember) do |member|
expect(member).to receive(:refresh_member_authorized_projects).and_call_original
end
group.add_member(user, GroupMember::MAINTAINER)
expect(group.group_members.maintainers.map(&:user)).to include(user)
end
end
describe '#add_users' do
let(:user) { create(:user) }
before do
group.add_members([user.id], GroupMember::GUEST)
end
it "updates the group permission" do
expect(group.group_members.guests.map(&:user)).to include(user)
group.add_members([user.id], GroupMember::DEVELOPER)
expect(group.group_members.developers.map(&:user)).to include(user)
expect(group.group_members.guests.map(&:user)).not_to include(user)
end
end
it_behaves_like Avatarable do
let(:model) { create(:group, :with_avatar) }
end
describe '.search' do
it 'returns groups with a matching name' do
expect(described_class.search(group.name)).to eq([group])
end
it 'returns groups with a partially matching name' do
expect(described_class.search(group.name[0..2])).to eq([group])
end
it 'returns groups with a matching name regardless of the casing' do
expect(described_class.search(group.name.upcase)).to eq([group])
end
it 'returns groups with a matching path' do
expect(described_class.search(group.path)).to eq([group])
end
it 'returns groups with a partially matching path' do
expect(described_class.search(group.path[0..2])).to eq([group])
end
it 'returns groups with a matching path regardless of the casing' do
expect(described_class.search(group.path.upcase)).to eq([group])
end
end
describe '#has_owner?' do
before do
@members = setup_group_members(group)
create(:group_member, :invited, :owner, group: group)
end
it { expect(group.has_owner?(@members[:owner])).to be_truthy }
it { expect(group.has_owner?(@members[:maintainer])).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 }
it { expect(group.has_owner?(nil)).to be_falsey }
end
describe '#has_maintainer?' do
before do
@members = setup_group_members(group)
create(:group_member, :invited, :maintainer, group: group)
end
it { expect(group.has_maintainer?(@members[:owner])).to be_falsey }
it { expect(group.has_maintainer?(@members[:maintainer])).to be_truthy }
it { expect(group.has_maintainer?(@members[:developer])).to be_falsey }
it { expect(group.has_maintainer?(@members[:reporter])).to be_falsey }
it { expect(group.has_maintainer?(@members[:guest])).to be_falsey }
it { expect(group.has_maintainer?(@members[:requester])).to be_falsey }
it { expect(group.has_maintainer?(nil)).to be_falsey }
end
describe '#last_owner?' do
before do
@members = setup_group_members(group)
end
it { expect(group.last_owner?(@members[:owner])).to be_truthy }
context 'there is also a project_bot owner' do
before do
group.add_member(create(:user, :project_bot), GroupMember::OWNER)
end
it { expect(group.last_owner?(@members[:owner])).to be_truthy }
end
context 'with two owners' do
before do
create(:group_member, :owner, group: group)
end
it { expect(group.last_owner?(@members[:owner])).to be_falsy }
end
context 'with owners from a parent' do
context 'when top-level group' do
it { expect(group.last_owner?(@members[:owner])).to be_truthy }
context 'with group sharing' do
let!(:subgroup) { create(:group, parent: group) }
before do
create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
create(:group_member, :owner, group: subgroup)
end
it { expect(group.last_owner?(@members[:owner])).to be_truthy }
end
end
context 'when subgroup' do
let!(:subgroup) { create(:group, parent: group) }
it { expect(subgroup.last_owner?(@members[:owner])).to be_truthy }
context 'with two owners' do
before do
create(:group_member, :owner, group: group)
end
it { expect(subgroup.last_owner?(@members[:owner])).to be_falsey }
end
end
end
end
context 'when analyzing blocked owners' do
let_it_be(:blocked_user) { create(:user, :blocked) }
describe '#blocked_owners' do
let_it_be(:user) { create(:user) }
before do
group.add_member(blocked_user, GroupMember::OWNER)
group.add_member(user, GroupMember::OWNER)
end
it 'has only blocked owners' do
expect(group.blocked_owners.map(&:user)).to match([blocked_user])
end
end
end
describe '#member_owners_excluding_project_bots' do
let_it_be(:user) { create(:user) }
let!(:member_owner) do
group.add_member(user, GroupMember::OWNER)
end
before do
# Add an invite to the group, which should be filtered out
create(:group_member, :invited, source: group)
end
it 'returns the member-owners' do
expect(group.member_owners_excluding_project_bots).to contain_exactly(member_owner)
end
it 'preloads user and source' do
owner = group.member_owners_excluding_project_bots.first
expect(owner.association(:user).loaded?).to be_truthy
expect(owner.association(:source).loaded?).to be_truthy
end
context 'there is also a project_bot owner' do
before do
group.add_member(create(:user, :project_bot), GroupMember::OWNER)
end
it 'returns only the human member-owners' do
expect(group.member_owners_excluding_project_bots).to contain_exactly(member_owner)
end
end
context 'when there is user that does not exist' do
before do
# Simulate that the user is deleted but the LFK worker has not
# started yet.
deleted_user = create(:user)
create(:group_member, source: group, user: deleted_user)
deleted_user.destroy!
end
it 'returns the member-owners' do
expect(group.member_owners_excluding_project_bots).to contain_exactly(member_owner)
end
end
context 'with owners from a parent' do
context 'when top-level group' do
context 'with group sharing' do
let!(:subgroup) { create(:group, parent: group) }
before do
create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
subgroup.add_member(user, GroupMember::OWNER)
end
it 'returns only direct member-owners' do
expect(group.member_owners_excluding_project_bots).to contain_exactly(member_owner)
end
context 'when there is an invite in the linked group' do
before do
create(:group_member, :invited, source: subgroup)
end
it 'returns only direct member-owners' do
expect(group.member_owners_excluding_project_bots).to contain_exactly(member_owner)
end
end
end
end
context 'when subgroup' do
let!(:subgroup) { create(:group, parent: group) }
let_it_be(:user_2) { create(:user) }
let!(:member_owner_2) do
subgroup.add_member(user_2, GroupMember::OWNER)
end
it 'returns member-owners including parents' do
expect(subgroup.member_owners_excluding_project_bots).to contain_exactly(member_owner, member_owner_2)
end
context 'with group sharing' do
let_it_be(:invited_group) { create(:group) }
let!(:invited_group_owner) { invited_group.add_member(user, GroupMember::OWNER) }
before do
create(:group_group_link, :owner, shared_group: subgroup, shared_with_group: invited_group)
end
it 'returns member-owners including parents, and member-owners of the invited group' do
expect(subgroup.member_owners_excluding_project_bots).to contain_exactly(member_owner, member_owner_2, invited_group_owner)
end
context 'when there is an invite in the linked group' do
before do
# Add an invite to this group, which should be filtered out
create(:group_member, :invited, source: invited_group)
end
it 'returns member-owners including parents, and member-owners of the invited group' do
expect(subgroup.member_owners_excluding_project_bots).to contain_exactly(member_owner, member_owner_2, invited_group_owner)
end
end
end
end
end
context 'when there are no owners' do
let_it_be(:empty_group) { create(:group) }
it 'returns an empty result' do
expect(empty_group.member_owners_excluding_project_bots).to be_empty
end
end
context 'when user is blocked' do
let(:blocked_user) { create(:user, :blocked) }
let!(:blocked_member) do
group.add_member(blocked_user, GroupMember::OWNER)
end
context 'and it is a direct member' do
it 'does include blocked user' do
expect(group.member_owners_excluding_project_bots).to include(blocked_member)
end
end
context 'and it is a member of a parent' do
let!(:subgroup) { create(:group, parent: group) }
it 'does include blocked user' do
expect(subgroup.member_owners_excluding_project_bots).to include(blocked_member)
end
end
end
end
describe '#lfs_enabled?' do
context 'LFS enabled globally' do
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
it 'returns true when nothing is set' do
expect(group.lfs_enabled?).to be_truthy
end
it 'returns false when set to false' do
group.update_attribute(:lfs_enabled, false)
expect(group.lfs_enabled?).to be_falsey
end
it 'returns true when set to true' do
group.update_attribute(:lfs_enabled, true)
expect(group.lfs_enabled?).to be_truthy
end
end
context 'LFS disabled globally' do
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
end
it 'returns false when nothing is set' do
expect(group.lfs_enabled?).to be_falsey
end
it 'returns false when set to false' do
group.update_attribute(:lfs_enabled, false)
expect(group.lfs_enabled?).to be_falsey
end
it 'returns false when set to true' do
group.update_attribute(:lfs_enabled, true)
expect(group.lfs_enabled?).to be_falsey
end
end
end
describe '#owners' do
let(:owner) { create(:user) }
let(:developer) { create(:user) }
it 'returns the owners of a Group' do
members = setup_group_members(group)
expect(
group.owners.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/436662")
).to eq([members[:owner]])
end
end
describe '#owned_by?' do
let!(:invited_group_member) { create(:group_member, :owner, :invited, group: group) }
before do
@members = setup_group_members(group)
end
it 'returns true for owner' do
expect(group.owned_by?(@members[:owner])).to eq(true)
end
it 'returns false for developer' do
expect(group.owned_by?(@members[:developer])).to eq(false)
end
it 'returns false when nil is passed' do
expect(invited_group_member.user).to eq(nil)
expect(group.owned_by?(invited_group_member.user)).to eq(false)
end
end
def setup_group_members(group)
members = {
owner: create(:user),
maintainer: create(:user),
developer: create(:user),
reporter: create(:user),
guest: create(:user),
requester: create(:user)
}
group.add_member(members[:owner], GroupMember::OWNER)
group.add_member(members[:maintainer], GroupMember::MAINTAINER)
group.add_member(members[:developer], GroupMember::DEVELOPER)
group.add_member(members[:reporter], GroupMember::REPORTER)
group.add_member(members[:guest], GroupMember::GUEST)
group.request_access(members[:requester])
members
end
describe 'nested group' do
subject { build(:group, :nested) }
it { is_expected.to be_valid }
it { expect(subject.parent).to be_kind_of(described_class) }
end
describe '#has_user?' do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:invited_group_member) { create(:group_member, :owner, :invited, group: group) }
subject { group.has_user?(user) }
context 'when the user is a member' do
before_all do
group.add_developer(user)
end
it { is_expected.to be_truthy }
it { expect(group.has_user?(user2)).to be_falsey }
it 'returns false for subgroup' do
expect(subgroup.has_user?(user)).to be_falsey
end
end
context 'when the user is a member with minimal access' do
before_all do
group.add_member(user, GroupMember::MINIMAL_ACCESS)
end
it { is_expected.to be_falsey }
end
context 'when the user has requested membership' do
before_all do
create(:group_member, :developer, :access_request, user: user, source: group)
end
it 'returns false' do
expect(subject).to be_falsey
end
end
context 'when the user is an invited member' do
it 'returns false when nil is passed' do
expect(invited_group_member.user).to eq(nil)
expect(group.has_user?(invited_group_member.user)).to be_falsey
end
end
end
describe '#member?' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
before_all do
group.add_developer(user)
end
subject { group.member?(user) }
context 'when user is a developer' do
it 'returns true' do
expect(group.member?(user)).to be_truthy
end
it 'returns false with maintainer as min_access_level param' do
expect(group.member?(user, Gitlab::Access::MAINTAINER)).to be_falsey
end
end
it 'returns true for a user in parent group' do
subgroup = create(:group, parent: group)
expect(subgroup.member?(user)).to be_truthy
end
context 'in shared group' do
let(:shared_group) { create(:group) }
let(:member_shared) { create(:user) }
before do
create(:group_group_link, shared_group: group, shared_with_group: shared_group)
shared_group.add_developer(member_shared)
end
it 'return true for shared group member' do
expect(group.member?(member_shared)).to be_truthy
end
it 'returns false with maintainer as min_access_level param' do
expect(group.member?(member_shared, Gitlab::Access::MAINTAINER)).to be_falsey
end
end
end
describe '#max_member_access_for_user' do
let_it_be(:group_user) { create(:user) }
context 'with user in the group' do
before do
group.add_owner(group_user)
end
it 'returns correct access level' do
expect(group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::OWNER)
end
context 'when user is not active' do
let_it_be(:group_user) { create(:user, :deactivated) }
it 'returns NO_ACCESS' do
expect(group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
end
context 'when user is nil' do
it 'returns NO_ACCESS' do
expect(group.max_member_access_for_user(nil)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'evaluating admin access level' do
let_it_be(:admin) { create(:admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it 'returns OWNER by default' do
expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER)
end
end
context 'when admin mode is disabled' do
it 'returns NO_ACCESS' do
expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS)
end
end
it 'returns NO_ACCESS when only concrete membership should be considered' do
expect(group.max_member_access_for_user(admin, only_concrete_membership: true))
.to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'when organization owner' do
let_it_be(:group) { create(:group) }
let_it_be(:org_owner) do
create(:organization_owner, organization: organization).user
end
it 'returns OWNER by default' do
expect(group.max_member_access_for_user(org_owner)).to eq(Gitlab::Access::OWNER)
end
context 'when organization owner is also an admin' do
before do
org_owner.update!(admin: true)
end
context 'when admin mode is enabled', :enable_admin_mode do
it 'returns OWNER by default' do
expect(group.max_member_access_for_user(org_owner)).to eq(Gitlab::Access::OWNER)
end
end
context 'when admin mode is disabled' do
it 'returns NO_ACCESS by default' do
expect(group.max_member_access_for_user(org_owner)).to eq(Gitlab::Access::NO_ACCESS)
end
end
end
context 'when only concrete members' do
it 'returns NO_ACCESS' do
expect(group.max_member_access_for_user(org_owner, only_concrete_membership: true))
.to eq(Gitlab::Access::NO_ACCESS)
end
end
end
context 'group shared with another group' do
let_it_be(:parent_group_user) { create(:user) }
let_it_be(:child_group_user) { create(:user) }
let_it_be(:group_parent) { create(:group, :private) }
let_it_be(:group) { create(:group, :private, parent: group_parent) }
let_it_be(:group_child) { create(:group, :private, parent: group) }
let_it_be(:shared_group_parent) { create(:group, :private) }
let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
before do
group_parent.add_owner(parent_group_user)
group.add_owner(group_user)
group_child.add_owner(child_group_user)
create(:group_group_link, { shared_with_group: group,
shared_group: shared_group,
group_access: GroupMember::DEVELOPER })
end
context 'with user in the group' do
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::DEVELOPER)
expect(shared_group_child.max_member_access_for_user(group_user)).to eq(Gitlab::Access::DEVELOPER)
end
context 'with lower group access level than max access level for share' do
let(:user) { create(:user) }
it 'returns correct access level' do
group.add_reporter(user)
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
end
end
end
context 'with user in the parent group' do
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'with user in the child group' do
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'unrelated project owner' do
let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
let!(:group) { create(:group, id: common_id) }
let!(:unrelated_project) { create(:project, id: common_id) }
let(:user) { unrelated_project.first_owner }
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'user without accepted access request' do
let!(:user) { create(:user) }
before do
create(:group_member, :developer, :access_request, user: user, group: group)
end
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
end
context 'multiple groups shared with group' do
let(:user) { create(:user) }
let(:group) { create(:group, :private) }
let(:shared_group_parent) { create(:group, :private) }
let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
before do
group.add_owner(user)
create(:group_group_link, { shared_with_group: group,
shared_group: shared_group,
group_access: GroupMember::DEVELOPER })
create(:group_group_link, { shared_with_group: group,
shared_group: shared_group_parent,
group_access: GroupMember::MAINTAINER })
end
it 'returns correct access level' do
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER)
end
end
end
describe '#direct_members' do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:maintainer) { group.parent.add_member(create(:user), GroupMember::MAINTAINER) }
let_it_be(:developer) { group.add_member(create(:user), GroupMember::DEVELOPER) }
it 'does not return members of the parent' do
expect(group.direct_members).not_to include(maintainer)
end
it 'returns the direct member of the group' do
expect(group.direct_members).to include(developer)
end
context 'group sharing' do
let!(:shared_group) { create(:group) }
before do
create(:group_group_link, shared_group: shared_group, shared_with_group: group)
end
it 'does not return members of the shared_with group' do
expect(shared_group.direct_members).not_to(
include(developer))
end
end
end
shared_examples_for 'members_with_parents' do
let!(:group) { create(:group, :nested) }
let!(:maintainer) { group.parent.add_member(create(:user), GroupMember::MAINTAINER) }
let!(:developer) { group.add_member(create(:user), GroupMember::DEVELOPER) }
let!(:pending_maintainer) { create(:group_member, :awaiting, :maintainer, group: group.parent) }
let!(:pending_developer) { create(:group_member, :awaiting, :developer, group: group) }
let!(:inactive_developer) { group.add_member(create(:user, :deactivated), GroupMember::DEVELOPER) }
it 'returns parents active members' do
expect(group.members_with_parents).to include(developer)
expect(group.members_with_parents).to include(maintainer)
expect(group.members_with_parents).not_to include(pending_developer)
expect(group.members_with_parents).not_to include(pending_maintainer)
expect(group.members_with_parents).not_to include(inactive_developer)
end
context 'group sharing' do
let!(:shared_group) { create(:group) }
before do
create(:group_group_link, shared_group: shared_group, shared_with_group: group)
end
it 'returns shared with group active members' do
expect(shared_group.members_with_parents).to(
include(developer))
expect(shared_group.members_with_parents).not_to(
include(pending_developer))
end
end
context 'when only_active_users is false' do
subject { group.members_with_parents(only_active_users: false) }
it 'returns parents all members' do
expect(subject).to include(developer)
expect(subject).to include(maintainer)
expect(subject).not_to include(pending_developer)
expect(subject).not_to include(pending_maintainer)
expect(subject).to include(inactive_developer)
end
end
end
describe '#members_with_parents' do
it_behaves_like 'members_with_parents'
end
describe '#authorizable_members_with_parents' do
let(:group) { create(:group) }
it_behaves_like 'members_with_parents'
context 'members with associated user but also having invite_token' do
let!(:member) { create(:group_member, :developer, :invited, user: create(:user), group: group) }
it 'includes such members in the result' do
expect(group.authorizable_members_with_parents).to include(member)
end
end
context 'invited members' do
let!(:member) { create(:group_member, :developer, :invited, group: group) }
it 'does not include such members in the result' do
expect(group.authorizable_members_with_parents).not_to include(member)
end
end
context 'members from group shares' do
let(:shared_group) { group }
let(:shared_with_group) { create(:group) }
before do
create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_group)
end
context 'an invited member that is part of the shared_with_group' do
let!(:member) { create(:group_member, :developer, :invited, group: shared_with_group) }
it 'does not include such members in the result' do
expect(shared_group.authorizable_members_with_parents).not_to(
include(member))
end
end
end
end
describe '#members_from_self_and_ancestors_with_effective_access_level' do
let!(:group_parent) { create(:group, :private) }
let!(:group) { create(:group, :private, parent: group_parent) }
let!(:group_child) { create(:group, :private, parent: group) }
let!(:user) { create(:user) }
let(:parent_group_access_level) { Gitlab::Access::REPORTER }
let(:group_access_level) { Gitlab::Access::DEVELOPER }
let(:child_group_access_level) { Gitlab::Access::MAINTAINER }
before do
create(:group_member, user: user, group: group_parent, access_level: parent_group_access_level)
create(:group_member, user: user, group: group, access_level: group_access_level)
create(:group_member, :minimal_access, user: create(:user), source: group)
create(:group_member, user: user, group: group_child, access_level: child_group_access_level)
end
it 'returns effective access level for user' do
expect(group_parent.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
contain_exactly(
hash_including('user_id' => user.id, 'access_level' => parent_group_access_level)
)
)
expect(group.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
contain_exactly(
hash_including('user_id' => user.id, 'access_level' => group_access_level)
)
)
expect(group_child.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
contain_exactly(
hash_including('user_id' => user.id, 'access_level' => child_group_access_level)
)
)
end
end
context 'members-related methods' do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:maintainer) { group.parent.add_member(create(:user), GroupMember::MAINTAINER) }
let_it_be(:developer) { group.add_member(create(:user), GroupMember::DEVELOPER) }
let_it_be(:other_developer) { group.add_member(create(:user), GroupMember::DEVELOPER) }
describe '#hierarchy_members' do
it 'returns parents members' do
expect(group.hierarchy_members).to include(developer)
expect(group.hierarchy_members).to include(maintainer)
end
it 'returns descendant members' do
expect(group.hierarchy_members).to include(other_developer)
end
end
describe '#hierarchy_members_with_inactive' do
let_it_be(:maintainer_blocked) { group.parent.add_member(create(:user, :blocked), GroupMember::MAINTAINER) }
it 'returns parents members' do
expect(group.hierarchy_members_with_inactive).to include(developer)
expect(group.hierarchy_members_with_inactive).to include(maintainer)
expect(group.hierarchy_members_with_inactive).to include(maintainer_blocked)
end
it 'returns descendant members' do
expect(group.hierarchy_members_with_inactive).to include(other_developer)
end
end
describe '#descendant_project_members_with_inactive' do
let_it_be(:ancestor_group_project) { create(:project, group: group) }
let_it_be(:ancestor_group_project_member) { ancestor_group_project.add_maintainer(create(:user)) }
let_it_be(:project) { create(:project, group: sub_group) }
let_it_be(:project_member) { project.add_maintainer(create(:user)) }
let_it_be(:blocked_project_member) { project.add_maintainer(create(:user, :blocked)) }
it 'returns members of descendant projects' do
expect(sub_group.descendant_project_members_with_inactive).to contain_exactly(project_member, blocked_project_member)
end
end
end
describe '#users_with_descendants' do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
deep_nested_group.add_maintainer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(deep_nested_group.users_with_descendants).to contain_exactly(user_a)
end
end
describe '#refresh_members_authorized_projects' do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:parent_group_user) { create(:user) }
let_it_be(:group_user) { create(:user) }
before do
group.parent.add_maintainer(parent_group_user)
group.add_developer(group_user)
end
context 'users for which authorizations refresh is executed' do
it 'processes authorizations refresh for all members of the group' do
expect(UserProjectAccessChangedService).to receive(:new).with(contain_exactly(group_user.id, parent_group_user.id)).and_call_original
group.refresh_members_authorized_projects
end
context 'when explicitly specified to run only for direct members' do
it 'processes authorizations refresh only for direct members of the group' do
expect(UserProjectAccessChangedService).to receive(:new).with(contain_exactly(group_user.id)).and_call_original
group.refresh_members_authorized_projects(direct_members_only: true)
end
end
end
end
describe '#users_ids_of_direct_members' do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:parent_group_user) { create(:user) }
let_it_be(:group_user) { create(:user) }
before do
group.parent.add_maintainer(parent_group_user)
group.add_developer(group_user)
end
it 'does not return user ids of the members of the parent' do
expect(group.users_ids_of_direct_members).not_to include(parent_group_user.id)
end
it 'returns the user ids of the direct member of the group' do
expect(group.users_ids_of_direct_members).to include(group_user.id)
end
context 'group sharing' do
let!(:shared_group) { create(:group) }
before do
create(:group_group_link, shared_group: shared_group, shared_with_group: group)
end
it 'does not return the user ids of members of the shared_with group' do
expect(shared_group.users_ids_of_direct_members).not_to(
include(group_user.id))
end
end
end
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
maintainer = create(:user)
developer = create(:user)
group.add_member(maintainer, GroupMember::MAINTAINER)
group.add_member(developer, GroupMember::DEVELOPER)
expect(group.user_ids_for_project_authorizations)
.to include(maintainer.id, developer.id)
end
context 'group sharing' do
let_it_be(:group) { create(:group) }
let_it_be(:group_user) { create(:user) }
let_it_be(:shared_group) { create(:group) }
before do
group.add_developer(group_user)
create(:group_group_link, shared_group: shared_group, shared_with_group: group)
end
it 'returns the user IDs for shared with group members' do
expect(shared_group.user_ids_for_project_authorizations).to(
include(group_user.id))
end
end
context 'distinct user ids' do
let_it_be(:subgroup) { create(:group, :nested) }
let_it_be(:user) { create(:user) }
let_it_be(:shared_with_group) { create(:group) }
let_it_be(:other_subgroup_user) { create(:user) }
before do
create(:group_group_link, shared_group: subgroup, shared_with_group: shared_with_group)
subgroup.add_maintainer(other_subgroup_user)
# `user` is added as a direct member of the parent group, the subgroup
# and another group shared with the subgroup.
subgroup.parent.add_maintainer(user)
subgroup.add_developer(user)
shared_with_group.add_guest(user)
end
it 'returns only distinct user ids of users for which to refresh authorizations' do
expect(subgroup.user_ids_for_project_authorizations).to(
contain_exactly(user.id, other_subgroup_user.id))
end
end
end
describe '#self_and_hierarchy_intersecting_with_user_groups' do
let_it_be(:user) { create(:user) }
let(:subject) { group.self_and_hierarchy_intersecting_with_user_groups(user) }
it 'makes a call to GroupsFinder' do
expect(GroupsFinder).to receive_message_chain(:new, :execute, :unscope)
subject
end
context 'when the group is private' do
let_it_be(:group) { create(:group, :private) }
context 'when the user is not a member of the group' do
it 'is an empty array' do
expect(subject).to eq([])
end
end
context 'when the user is a member of the group' do
before do
group.add_developer(user)
end
it 'is equal to the group' do
expect(subject).to match_array([group])
end
end
context 'when the group has a sub group' do
let_it_be(:subgroup) { create(:group, :private, parent: group) }
context 'when the user is not a member of the subgroup' do
it 'is an empty array' do
expect(subject).to eq([])
end
end
context 'when the user is a member of the subgroup' do
before do
subgroup.add_developer(user)
end
it 'is equal to the group and subgroup' do
expect(subject).to match_array([group, subgroup])
end
context 'when the group has an ancestor' do
let_it_be(:ancestor) { create(:group, :private) }
before do
group.parent = ancestor
group.save!
end
it 'is equal to the ancestor, group and subgroup' do
expect(subject).to match_array([ancestor, group, subgroup])
end
end
end
end
end
context 'when the group is public' do
let_it_be(:group) { create(:group, :public) }
it 'is equal to the public group regardless of membership' do
expect(subject).to match_array([group])
end
end
end
describe '#update_two_factor_requirement_for_members' do
let_it_be_with_reload(:user) { create(:user) }
context 'group membership' do
it 'enables two_factor_requirement for group members' do
group.add_member(user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: true)
group.update_two_factor_requirement_for_members
expect(user.reload.require_two_factor_authentication_from_group).to be_truthy
end
it 'disables two_factor_requirement for group members' do
user.update!(require_two_factor_authentication_from_group: true)
group.add_member(user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: false)
group.update_two_factor_requirement_for_members
expect(user.reload.require_two_factor_authentication_from_group).to be_falsey
end
end
context 'sub groups and projects' do
context 'expanded group members' do
let(:indirect_user) { create(:user) }
context 'two_factor_requirement is enabled' do
context 'two_factor_requirement is also enabled for ancestor group' do
it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group)
subgroup.add_member(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: true)
group.update_two_factor_requirement_for_members
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
end
context 'two_factor_requirement is disabled for ancestor group' do
it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group, require_two_factor_authentication: true)
subgroup.add_member(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: false)
group.update_two_factor_requirement_for_members
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
it 'enable two_factor_requirement for ancestor group member' do
ancestor_group = create(:group)
ancestor_group.add_member(indirect_user, GroupMember::OWNER)
group.update!(parent: ancestor_group)
group.update!(require_two_factor_authentication: true)
group.update_two_factor_requirement_for_members
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
end
end
context 'two_factor_requirement is disabled' do
context 'two_factor_requirement is enabled for ancestor group' do
it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group)
subgroup.add_member(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: true)
group.update_two_factor_requirement_for_members
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
end
context 'two_factor_requirement is also disabled for ancestor group' do
it 'disables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group)
subgroup.add_member(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: false)
group.update_two_factor_requirement_for_members
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey
end
it 'disables two_factor_requirement for ancestor group member' do
ancestor_group = create(:group, require_two_factor_authentication: false)
indirect_user.update!(require_two_factor_authentication_from_group: true)
ancestor_group.add_member(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: false)
group.update_two_factor_requirement_for_members
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey
end
end
end
end
context 'project members' do
it 'does not enable two_factor_requirement for child project member' do
project = create(:project, group: group)
project.add_maintainer(user)
group.update!(require_two_factor_authentication: true)
group.update_two_factor_requirement_for_members
expect(user.reload.require_two_factor_authentication_from_group).to be_falsey
end
it 'does not enable two_factor_requirement for subgroup child project member' do
subgroup = create(:group, :nested, parent: group)
project = create(:project, group: subgroup)
project.add_maintainer(user)
group.update!(require_two_factor_authentication: true)
group.update_two_factor_requirement_for_members
expect(user.reload.require_two_factor_authentication_from_group).to be_falsey
end
end
end
end
describe '#update_two_factor_requirement' do
it 'enqueues a job when require_two_factor_authentication is changed' do
expect(Groups::UpdateTwoFactorRequirementForMembersWorker).to receive(:perform_async).with(group.id)
group.update!(require_two_factor_authentication: true)
end
it 'enqueues a job when two_factor_grace_period is changed' do
expect(Groups::UpdateTwoFactorRequirementForMembersWorker).to receive(:perform_async).with(group.id)
group.update!(two_factor_grace_period: 23)
end
it 'does not enqueue a job when other attributes are changed' do
expect(Groups::UpdateTwoFactorRequirementForMembersWorker).not_to receive(:perform_async).with(group.id)
group.update!(description: 'foobar')
end
end
describe '#path_changed_hook' do
let(:system_hook_service) { SystemHooksService.new }
context 'for a new group' do
let(:group) { build(:group) }
before do
expect(group).to receive(:system_hook_service).and_return(system_hook_service)
end
it 'does not trigger system hook' do
expect(system_hook_service).to receive(:execute_hooks_for).with(group, :create)
group.save!
end
end
context 'for an existing group' do
let(:group) { create(:group, path: 'old-path') }
context 'when the path is changed' do
let(:new_path) { 'very-new-path' }
it 'triggers the rename system hook' do
expect(group).to receive(:system_hook_service).and_return(system_hook_service)
expect(system_hook_service).to receive(:execute_hooks_for).with(group, :rename)
group.update!(path: new_path)
end
end
context 'when the path is not changed' do
it 'does not trigger system hook' do
expect(group).not_to receive(:system_hook_service)
group.update!(name: 'new name')
end
end
context 'when the path is changed to existing pages unique domain' do
let(:new_path) { 'existing-domain' }
it 'rejects path' do
# Simulate the existing domain being in use
create(:project_setting, pages_unique_domain: 'existing-domain')
expect(group.update(path: new_path)).to be_falsey
expect(group.errors.full_messages.to_sentence).to eq('Group URL has already been taken')
end
end
end
end
describe '#highest_group_member' do
let(:nested_group) { create(:group, parent: group) }
let(:nested_group_2) { create(:group, parent: nested_group) }
let(:user) { create(:user) }
subject(:highest_group_member) { nested_group_2.highest_group_member(user) }
context 'when the user is not a member of any group in the hierarchy' do
it { is_expected.to be_nil }
end
context 'when access request to group is pending' do
before do
create(:group_member, requested_at: Time.current.utc, source: nested_group, user: user)
end
it { is_expected.to be_nil }
end
context 'when the user is only a member of one group in the hierarchy' do
before do
nested_group.add_developer(user)
end
it 'returns that group member' do
expect(highest_group_member.access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
context 'when the user is a member of several groups in the hierarchy' do
before do
group.add_owner(user)
nested_group.add_developer(user)
nested_group_2.add_maintainer(user)
end
it 'returns the group member with the highest access level' do
expect(highest_group_member.access_level).to eq(Gitlab::Access::OWNER)
end
end
end
describe '#bots' do
subject { group.bots.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/436662") }
let_it_be(:group) { create(:group) }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:user) { create(:user) }
before_all do
[project_bot, user].each do |member|
group.add_maintainer(member)
end
end
it { is_expected.to contain_exactly(project_bot) }
it { is_expected.not_to include(user) }
end
describe '#related_group_ids' do
let(:nested_group) { create(:group, parent: group) }
let(:shared_with_group) { create(:group, parent: group) }
before do
create(:group_group_link, shared_group: nested_group, shared_with_group: shared_with_group)
end
subject(:related_group_ids) { nested_group.related_group_ids }
it 'returns id' do
expect(related_group_ids).to include(nested_group.id)
end
it 'returns ancestor id' do
expect(related_group_ids).to include(group.id)
end
it 'returns shared with group id' do
expect(related_group_ids).to include(shared_with_group.id)
end
context 'with more than one ancestor group' do
let(:ancestor_group) { create(:group) }
before do
group.update!(parent: ancestor_group)
end
it 'returns all ancestor group ids' do
expect(related_group_ids).to(
include(group.id, ancestor_group.id))
end
end
context 'with more than one shared with group' do
let(:another_shared_with_group) { create(:group, parent: group) }
before do
create(:group_group_link, shared_group: nested_group, shared_with_group: another_shared_with_group)
end
it 'returns all shared with group ids' do
expect(related_group_ids).to(
include(shared_with_group.id, another_shared_with_group.id))
end
end
end
context 'with uploads' do
it_behaves_like 'model with uploads', true do
let(:model_object) { create(:group, :with_avatar) }
let(:upload_attribute) { :avatar }
let(:uploader_class) { AttachmentUploader }
end
end
describe '#first_auto_devops_config' do
let(:group) { create(:group) }
subject(:fetch_config) { group.first_auto_devops_config }
where(:instance_value, :group_value, :config) do
# Instance level enabled
true | nil | { status: true, scope: :instance }
true | true | { status: true, scope: :group }
true | false | { status: false, scope: :group }
# Instance level disabled
false | nil | { status: false, scope: :instance }
false | true | { status: true, scope: :group }
false | false | { status: false, scope: :group }
end
with_them do
before do
stub_application_setting(auto_devops_enabled: instance_value)
group.update_attribute(:auto_devops_enabled, group_value)
end
it { is_expected.to eq(config) }
end
context 'with parent groups' do
let(:parent) { create(:group) }
where(:instance_value, :parent_value, :group_value, :config) do
# Instance level enabled
true | nil | nil | { status: true, scope: :instance }
true | nil | true | { status: true, scope: :group }
true | nil | false | { status: false, scope: :group }
true | true | nil | { status: true, scope: :group }
true | true | true | { status: true, scope: :group }
true | true | false | { status: false, scope: :group }
true | false | nil | { status: false, scope: :group }
true | false | true | { status: true, scope: :group }
true | false | false | { status: false, scope: :group }
# Instance level disable
false | nil | nil | { status: false, scope: :instance }
false | nil | true | { status: true, scope: :group }
false | nil | false | { status: false, scope: :group }
false | true | nil | { status: true, scope: :group }
false | true | true | { status: true, scope: :group }
false | true | false | { status: false, scope: :group }
false | false | nil | { status: false, scope: :group }
false | false | true | { status: true, scope: :group }
false | false | false | { status: false, scope: :group }
end
with_them do
def define_cache_expectations(cache_key)
if group_value.nil?
expect(Rails.cache).to receive(:fetch).with(start_with(cache_key), expires_in: 1.day)
else
expect(Rails.cache).not_to receive(:fetch).with(start_with(cache_key), expires_in: 1.day)
end
end
before do
stub_application_setting(auto_devops_enabled: instance_value)
group.update!(
auto_devops_enabled: group_value,
parent: parent
)
parent.update!(auto_devops_enabled: parent_value)
group.reload # Reload so we get the populated traversal IDs
end
it { is_expected.to eq(config) }
it 'caches the parent config when group auto_devops_enabled is nil' do
cache_key = "namespaces:{#{group.traversal_ids.first}}:first_auto_devops_config:#{group.id}"
define_cache_expectations(cache_key)
fetch_config
end
end
context 'cache expiration' do
before do
group.update!(parent: parent)
reload_models(parent)
end
it 'clears both self and descendant cache when the parent value is updated' do
expect(Rails.cache).to receive(:delete_multi)
.with(
match_array(
[
start_with("namespaces:{#{parent.traversal_ids.first}}:first_auto_devops_config:#{parent.id}"),
start_with("namespaces:{#{parent.traversal_ids.first}}:first_auto_devops_config:#{group.id}")
])
)
parent.update!(auto_devops_enabled: true)
end
it 'only clears self cache when there are no dependents' do
expect(Rails.cache).to receive(:delete_multi)
.with([start_with("namespaces:{#{group.traversal_ids.first}}:first_auto_devops_config:#{group.id}")])
group.update!(auto_devops_enabled: true)
end
end
end
end
describe '#auto_devops_enabled?' do
subject { group.auto_devops_enabled? }
context 'when auto devops is explicitly enabled on group' do
let(:group) { create(:group, :auto_devops_enabled) }
it { is_expected.to be_truthy }
end
context 'when auto devops is explicitly disabled on group' do
let(:group) { create(:group, :auto_devops_disabled) }
it { is_expected.to be_falsy }
end
context 'when auto devops is implicitly enabled or disabled' do
before do
stub_application_setting(auto_devops_enabled: false)
group.update!(parent: parent_group)
end
context 'when auto devops is enabled on root group' do
let(:root_group) { create(:group, :auto_devops_enabled) }
let(:subgroup) { create(:group, parent: root_group) }
let(:parent_group) { create(:group, parent: subgroup) }
it { is_expected.to be_truthy }
end
context 'when auto devops is disabled on root group' do
let(:root_group) { create(:group, :auto_devops_disabled) }
let(:subgroup) { create(:group, parent: root_group) }
let(:parent_group) { create(:group, parent: subgroup) }
it { is_expected.to be_falsy }
end
context 'when auto devops is disabled on parent group and enabled on root group' do
let(:root_group) { create(:group, :auto_devops_enabled) }
let(:parent_group) { create(:group, :auto_devops_disabled, parent: root_group) }
it { is_expected.to be_falsy }
end
end
end
describe 'project_creation_level' do
it 'outputs the default one if it is nil' do
group = create(:group, project_creation_level: nil)
expect(group.project_creation_level).to eq(Gitlab::CurrentSettings.default_project_creation)
end
end
describe 'subgroup_creation_level' do
it 'defaults to maintainers' do
expect(group.subgroup_creation_level)
.to eq(Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
end
end
describe '#access_request_approvers_to_be_notified' do
let_it_be(:group) { create(:group, :public) }
it 'returns a maximum of ten owners of the group in recent_sign_in descending order' do
limit = 2
stub_const("Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT", limit)
users = create_list(:user, limit + 1, :with_sign_ins)
active_owners = users.map do |user|
create(:group_member, :owner, group: group, user: user)
end
active_owners_in_recent_sign_in_desc_order = group.members_and_requesters
.id_in(active_owners)
.order_recent_sign_in.limit(limit)
expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order)
end
it 'returns active, non_invited, non_requested owners of the group' do
owner = create(:group_member, :owner, source: group)
create(:group_member, :maintainer, group: group)
create(:group_member, :owner, :invited, group: group)
create(:group_member, :owner, :access_request, group: group)
create(:group_member, :owner, :blocked, group: group)
expect(group.access_request_approvers_to_be_notified.to_a).to eq([owner])
end
end
describe '.preset_root_ancestor_for' do
let_it_be(:rootgroup, reload: true) { create(:group) }
let_it_be(:subgroup, reload: true) { create(:group, parent: rootgroup) }
let_it_be(:subgroup2, reload: true) { create(:group, parent: subgroup) }
it 'does noting for single group' do
expect(subgroup).not_to receive(:self_and_ancestors)
described_class.preset_root_ancestor_for([subgroup])
end
it 'sets the same root_ancestor for multiple groups' do
expect(subgroup).not_to receive(:self_and_ancestors)
expect(subgroup2).not_to receive(:self_and_ancestors)
described_class.preset_root_ancestor_for([rootgroup, subgroup, subgroup2])
expect(subgroup.root_ancestor).to eq(rootgroup)
expect(subgroup2.root_ancestor).to eq(rootgroup)
end
end
describe "#default_branch_name" do
context "when group.namespace_settings does not have a default branch name" do
it "returns nil" do
expect(group.default_branch_name).to be_nil
end
end
context "when group.namespace_settings has a default branch name" do
let(:example_branch_name) { "example_branch_name" }
before do
allow(group.namespace_settings)
.to receive(:default_branch_name)
.and_return(example_branch_name)
end
it "returns the default branch name" do
expect(group.default_branch_name).to eq(example_branch_name)
end
end
end
describe "#access_level_roles" do
let(:group) { create(:group) }
it "returns the correct roles" do
expect(group.access_level_roles).to eq(
{
'Guest' => 10,
'Planner' => 15,
'Reporter' => 20,
'Developer' => 30,
'Maintainer' => 40,
'Owner' => 50
}
)
end
end
describe '#membership_locked?' do
it 'returns false' do
expect(build(:group)).not_to be_membership_locked
end
end
describe '#first_owner' do
context 'the group has owners' do
it 'is the first owner' do
user_1 = create(:user)
user_2 = create(:user)
group.add_owner(user_2)
group.add_owner(user_1)
# The senior-most user (not member) who is an OWNER in the group
# is always treated as the first owner
expect(group.first_owner)
.to eq(user_1)
.and be_a(User)
end
end
context 'the group has a parent' do
let(:parent) { build(:group) }
before do
group.parent = parent
parent.add_owner(create(:user))
end
it 'is the first owner of the parent' do
expect(group.first_owner)
.to eq(parent.first_owner)
.and be_a(User)
end
end
context 'we fallback to group.owner' do
before do
group.owner = build(:user)
end
it 'is the group.owner' do
expect(group.first_owner)
.to eq(group.owner)
.and be_a(User)
end
end
end
describe '#parent_allows_two_factor_authentication?' do
it 'returns true for top-level group' do
expect(group.parent_allows_two_factor_authentication?).to eq(true)
end
context 'for subgroup' do
let(:subgroup) { create(:group, parent: group) }
it 'returns true if parent group allows two factor authentication for its descendants' do
expect(subgroup.parent_allows_two_factor_authentication?).to eq(true)
end
it 'returns true if parent group allows two factor authentication for its descendants' do
group.namespace_settings.update!(allow_mfa_for_subgroups: false)
expect(subgroup.parent_allows_two_factor_authentication?).to eq(false)
end
end
end
describe 'has_project_with_service_desk_enabled?' do
let_it_be_with_refind(:group) { create(:group, :private) }
subject { group.has_project_with_service_desk_enabled? }
before do
allow(::ServiceDesk).to receive(:supported?).and_return(true)
end
context 'when service desk is enabled' do
context 'for top level group' do
let_it_be(:project) { create(:project, group: group, service_desk_enabled: true) }
it { is_expected.to eq(true) }
context 'when service desk is not supported' do
before do
allow(::ServiceDesk).to receive(:supported?).and_return(false)
end
it { is_expected.to eq(false) }
end
end
context 'for subgroup project' do
let_it_be(:subgroup) { create(:group, :private, parent: group) }
let_it_be(:project) { create(:project, group: subgroup, service_desk_enabled: true) }
it { is_expected.to eq(true) }
end
end
context 'when none of group child projects has service desk enabled' do
let_it_be(:project) { create(:project, group: group, service_desk_enabled: false) }
before do
project.update!(service_desk_enabled: false)
end
it { is_expected.to eq(false) }
end
end
describe 'with Debian Distributions' do
subject { create(:group) }
it_behaves_like 'model with Debian distributions'
end
describe '.ids_with_disabled_email' do
let_it_be(:parent_1) { create(:group) }
let_it_be(:child_1) { create(:group, parent: parent_1) }
let_it_be(:parent_2) { create(:group) }
let_it_be(:child_2) { create(:group, parent: parent_2) }
let_it_be(:other_group) { create(:group) }
shared_examples 'returns namespaces with disabled email' do
subject(:group_ids_where_email_is_disabled) { described_class.ids_with_disabled_email([child_1, child_2, other_group]) }
it "when a group's parent has disabled emails" do
parent_1.update_attribute(:emails_enabled, false)
is_expected.to eq(Set.new([child_1.id]))
end
it "when a group itself has disabled emails" do
child_2.update_attribute(:emails_enabled, false)
is_expected.to eq(Set.new([child_2.id]))
end
end
it_behaves_like 'returns namespaces with disabled email'
end
describe '.timelogs' do
let(:project) { create(:project, namespace: group) }
let(:issue) { create(:issue, project: project) }
let(:other_project) { create(:project, namespace: create(:group)) }
let(:other_issue) { create(:issue, project: other_project) }
let!(:timelog1) { create(:timelog, issue: issue) }
let!(:timelog2) { create(:timelog, issue: other_issue) }
let!(:timelog3) { create(:timelog, issue: issue) }
it 'returns timelogs belonging to the group' do
expect(group.timelogs).to contain_exactly(timelog1, timelog3)
end
end
describe '.crm_organizations' do
it 'returns crm_organizations belonging to the group' do
crm_organization1 = create(:crm_organization, group: group)
create(:crm_organization)
crm_organization3 = create(:crm_organization, group: group)
expect(group.crm_organizations).to contain_exactly(crm_organization1, crm_organization3)
end
end
describe '.contacts' do
it 'returns contacts belonging to the group' do
contact1 = create(:contact, group: group)
create(:contact)
contact3 = create(:contact, group: group)
expect(group.contacts).to contain_exactly(contact1, contact3)
end
end
describe '#to_ability_name' do
it 'returns group' do
group = build(:group)
expect(group.to_ability_name).to eq('group')
end
end
context 'with export' do
let(:group) { create(:group) }
let(:export_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
let(:export) { create(:import_export_upload, group: group, export_file: export_file) }
it '#export_file_exists? returns true' do
expect(group.export_file_exists?(export.user)).to be true
end
it '#export_archive_exists? returns true' do
expect(group.export_archive_exists?(export.user)).to be true
end
end
describe '#import_export_upload_by_user' do
let(:group) { create(:group) }
let(:user) { create(:user) }
let!(:import_export_upload) { create(:import_export_upload, group: group, user: user) }
it 'returns the import_export_upload' do
expect(group.import_export_upload_by_user(user)).to eq import_export_upload
end
context 'when import_export_upload does not exist for user' do
let(:import_export_upload) { create(:import_export_upload, group: group) }
it 'returns nil' do
expect(group.import_export_upload_by_user(user)).to be_nil
end
end
end
describe '#open_issues_count', :aggregate_failures do
let(:group) { build(:group) }
it 'provides the issue count' do
expect(group.open_issues_count).to eq 0
end
it 'invokes the count service with current_user' do
user = build(:user)
count_service = instance_double(Groups::OpenIssuesCountService)
expect(Groups::OpenIssuesCountService).to receive(:new).with(group, user).and_return(count_service)
expect(count_service).to receive(:count)
group.open_issues_count(user)
end
it 'invokes the count service with no current_user' do
count_service = instance_double(Groups::OpenIssuesCountService)
expect(Groups::OpenIssuesCountService).to receive(:new).with(group, nil).and_return(count_service)
expect(count_service).to receive(:count)
group.open_issues_count
end
end
describe '#open_merge_requests_count', :aggregate_failures do
let(:group) { build(:group) }
it 'provides the merge request count' do
expect(group.open_merge_requests_count).to eq 0
end
it 'invokes the count service with current_user' do
user = build(:user)
count_service = instance_double(Groups::MergeRequestsCountService)
expect(Groups::MergeRequestsCountService).to receive(:new).with(group, user).and_return(count_service)
expect(count_service).to receive(:count)
group.open_merge_requests_count(user)
end
it 'invokes the count service with no current_user' do
count_service = instance_double(Groups::MergeRequestsCountService)
expect(Groups::MergeRequestsCountService).to receive(:new).with(group, nil).and_return(count_service)
expect(count_service).to receive(:count)
group.open_merge_requests_count
end
end
describe '#dependency_proxy_image_prefix' do
let_it_be(:group) { build_stubbed(:group, path: 'GroupWithUPPERcaseLetters') }
it 'converts uppercase letters to lowercase' do
expect(group.dependency_proxy_image_prefix).to end_with("/groupwithuppercaseletters#{DependencyProxy::URL_SUFFIX}")
end
it 'removes the protocol' do
expect(group.dependency_proxy_image_prefix).not_to include('http')
end
it 'does not include /groups' do
expect(group.dependency_proxy_image_prefix).not_to include('/groups')
end
end
describe '#dependency_proxy_image_ttl_policy' do
subject(:ttl_policy) { group.dependency_proxy_image_ttl_policy }
it 'builds a new policy if one does not exist', :aggregate_failures do
expect(ttl_policy.ttl).to eq(90)
expect(ttl_policy.enabled).to eq(false)
expect(ttl_policy.created_at).to be_nil
expect(ttl_policy.updated_at).to be_nil
end
context 'with existing policy' do
before do
group.dependency_proxy_image_ttl_policy.update!(ttl: 30, enabled: true)
end
it 'returns the policy if it already exists', :aggregate_failures do
expect(ttl_policy.ttl).to eq(30)
expect(ttl_policy.enabled).to eq(true)
expect(ttl_policy.created_at).not_to be_nil
expect(ttl_policy.updated_at).not_to be_nil
end
end
end
describe '#dependency_proxy_setting' do
subject(:setting) { group.dependency_proxy_setting }
it 'builds a new policy if one does not exist', :aggregate_failures do
expect(setting.enabled).to eq(true)
expect(setting).not_to be_persisted
end
context 'with existing policy' do
before do
group.dependency_proxy_setting.update!(enabled: false)
end
it 'returns the policy if it already exists', :aggregate_failures do
expect(setting.enabled).to eq(false)
expect(setting).to be_persisted
end
end
end
describe '#crm_enabled?' do
it 'returns true where no crm_settings exist' do
expect(group.crm_enabled?).to be_truthy
end
it 'returns false where crm_settings.state is disabled' do
create(:crm_settings, enabled: false, group: group)
expect(group.crm_enabled?).to be_falsey
end
it 'returns true where crm_settings.state is enabled' do
create(:crm_settings, enabled: true, group: group)
expect(group.crm_enabled?).to be_truthy
end
it 'returns true where crm_settings.state is enabled for subgroup' do
subgroup = create(:group, parent: group)
expect(subgroup.crm_enabled?).to be_truthy
end
end
describe '.get_ids_by_ids_or_paths' do
let(:group_path) { 'group_path' }
let!(:group) { create(:group, path: group_path) }
let(:group_id) { group.id }
it 'returns ids matching records based on paths' do
expect(described_class.get_ids_by_ids_or_paths(nil, [group_path])).to match_array([group_id])
end
it 'returns ids matching records based on ids' do
expect(described_class.get_ids_by_ids_or_paths([group_id], nil)).to match_array([group_id])
end
it 'returns ids matching records based on both paths and ids' do
new_group_id = create(:group).id
expect(described_class.get_ids_by_ids_or_paths([new_group_id], [group_path])).to match_array([group_id, new_group_id])
end
end
describe '.descendant_groups_counts' do
let_it_be(:parent) { create(:group) }
let_it_be(:group) { create(:group, parent: parent) }
let_it_be(:project) { create(:project, namespace: parent) }
subject(:descendant_groups_counts) { described_class.id_in(parent).descendant_groups_counts }
it 'return a hash of group id and descendant groups count without projects' do
expect(descendant_groups_counts).to eq({ parent.id => 1 })
end
end
describe '.projects_counts' do
let_it_be(:parent) { create(:group) }
let_it_be(:group) { create(:group, parent: parent) }
let_it_be(:project) { create(:project, namespace: parent) }
let_it_be(:archived_project) { create(:project, :archived, namespace: parent) }
subject(:projects_counts) { described_class.id_in(parent).projects_counts }
it 'return a hash of group id and projects count without counting archived projects' do
expect(projects_counts).to eq({ parent.id => 1 })
end
end
describe '.group_members_counts' do
let_it_be(:parent) { create(:group) }
let_it_be(:group) { create(:group, parent: parent) }
before_all do
create(:group_member, group: parent)
create(:group_member, group: parent, requested_at: Time.current)
create(:group_member, group: group)
end
subject(:group_members_counts) { described_class.id_in(parent).group_members_counts }
it 'return a hash of group id and approved direct group members' do
expect(group_members_counts).to eq({ parent.id => 1 })
end
end
describe '#shared_with_group_links_visible_to_user' do
let_it_be(:admin) { create :admin }
let_it_be(:normal_user) { create :user }
let_it_be(:user_with_access) { create :user }
let_it_be(:user_with_parent_access) { create :user }
let_it_be(:user_without_access) { create :user }
let_it_be(:shared_group) { create :group }
let_it_be(:parent_group) { create :group, :private }
let_it_be(:shared_with_private_group) { create :group, :private, parent: parent_group }
let_it_be(:shared_with_internal_group) { create :group, :internal }
let_it_be(:shared_with_public_group) { create :group, :public }
let_it_be(:private_group_group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_private_group) }
let_it_be(:internal_group_group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_internal_group) }
let_it_be(:public_group_group_link) { create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_public_group) }
before do
shared_with_private_group.add_developer(user_with_access)
parent_group.add_developer(user_with_parent_access)
end
context 'when user is admin', :enable_admin_mode do
it 'returns all existing shared group links' do
expect(shared_group.shared_with_group_links_visible_to_user(admin)).to contain_exactly(private_group_group_link, internal_group_group_link, public_group_group_link)
end
end
context 'when user is nil' do
it 'returns only link of public shared group' do
expect(shared_group.shared_with_group_links_visible_to_user(nil)).to contain_exactly(public_group_group_link)
end
end
context 'when user has no access to private shared group' do
it 'returns links of internal and public shared groups' do
expect(shared_group.shared_with_group_links_visible_to_user(normal_user)).to contain_exactly(internal_group_group_link, public_group_group_link)
end
end
context 'when user is member of private shared group' do
it 'returns links of private, internal and public shared groups' do
expect(shared_group.shared_with_group_links_visible_to_user(user_with_access)).to contain_exactly(private_group_group_link, internal_group_group_link, public_group_group_link)
end
end
context 'when user is inherited member of private shared group' do
it 'returns links of private, internal and public shared groups' do
expect(shared_group.shared_with_group_links_visible_to_user(user_with_parent_access)).to contain_exactly(private_group_group_link, internal_group_group_link, public_group_group_link)
end
end
end
describe '#enforced_runner_token_expiration_interval and #effective_runner_token_expiration_interval' do
shared_examples 'no enforced expiration interval' do
it { expect(subject.enforced_runner_token_expiration_interval).to be_nil }
end
shared_examples 'enforced expiration interval' do |enforced_interval:|
it { expect(subject.enforced_runner_token_expiration_interval).to eq(enforced_interval) }
end
shared_examples 'no effective expiration interval' do
it { expect(subject.effective_runner_token_expiration_interval).to be_nil }
end
shared_examples 'effective expiration interval' do |effective_interval:|
it { expect(subject.effective_runner_token_expiration_interval).to eq(effective_interval) }
end
context 'when there is no interval in group settings' do
let_it_be(:group) { create(:group) }
subject { group }
it_behaves_like 'no enforced expiration interval'
it_behaves_like 'no effective expiration interval'
end
context 'when there is a group interval' do
let(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 3.days.to_i) }
subject { create(:group, namespace_settings: group_settings) }
it_behaves_like 'no enforced expiration interval'
it_behaves_like 'effective expiration interval', effective_interval: 3.days
end
# runner_token_expiration_interval should not affect the expiration interval, only
# group_runner_token_expiration_interval should.
context 'when there is a site-wide enforced shared interval' do
before do
stub_application_setting(runner_token_expiration_interval: 5.days.to_i)
end
let_it_be(:group) { create(:group) }
subject { group }
it_behaves_like 'no enforced expiration interval'
it_behaves_like 'no effective expiration interval'
end
context 'when there is a site-wide enforced group interval' do
before do
stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
end
let_it_be(:group) { create(:group) }
subject { group }
it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
it_behaves_like 'effective expiration interval', effective_interval: 5.days
end
# project_runner_token_expiration_interval should not affect the expiration interval, only
# group_runner_token_expiration_interval should.
context 'when there is a site-wide enforced project interval' do
before do
stub_application_setting(project_runner_token_expiration_interval: 5.days.to_i)
end
let_it_be(:group) { create(:group) }
subject { group }
it_behaves_like 'no enforced expiration interval'
it_behaves_like 'no effective expiration interval'
end
# runner_token_expiration_interval should not affect the expiration interval, only
# subgroup_runner_token_expiration_interval should.
context 'when there is a grandparent group enforced group interval' do
let_it_be(:grandparent_group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:grandparent_group) { create(:group, namespace_settings: grandparent_group_settings) }
let_it_be(:parent_group) { create(:group, parent: grandparent_group) }
let_it_be(:subgroup) { create(:group, parent: parent_group) }
subject { subgroup }
it_behaves_like 'no enforced expiration interval'
it_behaves_like 'no effective expiration interval'
end
context 'when there is a grandparent group enforced subgroup interval' do
let_it_be(:grandparent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:grandparent_group) { create(:group, namespace_settings: grandparent_group_settings) }
let_it_be(:parent_group) { create(:group, parent: grandparent_group) }
let_it_be(:subgroup) { create(:group, parent: parent_group) }
subject { subgroup }
it_behaves_like 'enforced expiration interval', enforced_interval: 4.days
it_behaves_like 'effective expiration interval', effective_interval: 4.days
end
# project_runner_token_expiration_interval should not affect the expiration interval, only
# subgroup_runner_token_expiration_interval should.
context 'when there is a grandparent group enforced project interval' do
let_it_be(:grandparent_group_settings) { create(:namespace_settings, project_runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:grandparent_group) { create(:group, namespace_settings: grandparent_group_settings) }
let_it_be(:parent_group) { create(:group, parent: grandparent_group) }
let_it_be(:subgroup) { create(:group, parent: parent_group) }
subject { subgroup }
it_behaves_like 'no enforced expiration interval'
it_behaves_like 'no effective expiration interval'
end
context 'when there is a parent group enforced interval overridden by group interval' do
let_it_be(:parent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 5.days.to_i) }
let_it_be(:parent_group) { create(:group, namespace_settings: parent_group_settings) }
let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:subgroup_with_settings) { create(:group, parent: parent_group, namespace_settings: group_settings) }
subject { subgroup_with_settings }
it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
it_behaves_like 'effective expiration interval', effective_interval: 4.days
it 'has human-readable expiration intervals' do
expect(subject.enforced_runner_token_expiration_interval_human_readable).to eq('5d')
expect(subject.effective_runner_token_expiration_interval_human_readable).to eq('4d')
end
end
context 'when site-wide enforced interval overrides group interval' do
before do
stub_application_setting(group_runner_token_expiration_interval: 3.days.to_i)
end
let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:group_with_settings) { create(:group, namespace_settings: group_settings) }
subject { group_with_settings }
it_behaves_like 'enforced expiration interval', enforced_interval: 3.days
it_behaves_like 'effective expiration interval', effective_interval: 3.days
end
context 'when group interval overrides site-wide enforced interval' do
before do
stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
end
let_it_be(:group_settings) { create(:namespace_settings, runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:group_with_settings) { create(:group, namespace_settings: group_settings) }
subject { group_with_settings }
it_behaves_like 'enforced expiration interval', enforced_interval: 5.days
it_behaves_like 'effective expiration interval', effective_interval: 4.days
end
context 'when site-wide enforced interval overrides parent group enforced interval' do
before do
stub_application_setting(group_runner_token_expiration_interval: 3.days.to_i)
end
let_it_be(:parent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:parent_group) { create(:group, namespace_settings: parent_group_settings) }
let_it_be(:subgroup) { create(:group, parent: parent_group) }
subject { subgroup }
it_behaves_like 'enforced expiration interval', enforced_interval: 3.days
it_behaves_like 'effective expiration interval', effective_interval: 3.days
end
context 'when parent group enforced interval overrides site-wide enforced interval' do
before do
stub_application_setting(group_runner_token_expiration_interval: 5.days.to_i)
end
let_it_be(:parent_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:parent_group) { create(:group, namespace_settings: parent_group_settings) }
let_it_be(:subgroup) { create(:group, parent: parent_group) }
subject { subgroup }
it_behaves_like 'enforced expiration interval', enforced_interval: 4.days
it_behaves_like 'effective expiration interval', effective_interval: 4.days
end
# Unrelated groups should not affect the expiration interval.
context 'when there is an enforced group interval in an unrelated group' do
let_it_be(:unrelated_group_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:unrelated_group) { create(:group, namespace_settings: unrelated_group_settings) }
let_it_be(:group) { create(:group) }
subject { group }
it_behaves_like 'no enforced expiration interval'
it_behaves_like 'no effective expiration interval'
end
# Subgroups should not affect the parent group expiration interval.
context 'when there is an enforced group interval in a subgroup' do
let_it_be(:subgroup_settings) { create(:namespace_settings, subgroup_runner_token_expiration_interval: 4.days.to_i) }
let_it_be(:subgroup) { create(:group, parent: group, namespace_settings: subgroup_settings) }
let_it_be(:group) { create(:group) }
subject { group }
it_behaves_like 'no enforced expiration interval'
it_behaves_like 'no effective expiration interval'
end
end
describe '#work_items_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :work_items }
let(:feature_flag_method) { :work_items_feature_flag_enabled? }
end
end
describe '#work_items_beta_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :work_items_beta }
let(:feature_flag_method) { :work_items_beta_feature_flag_enabled? }
end
end
describe '#work_items_alpha_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :work_items_alpha }
let(:feature_flag_method) { :work_items_alpha_feature_flag_enabled? }
end
end
describe '#work_item_status_feature_available?' do
subject { group.work_item_status_feature_available? }
it { is_expected.to be false }
end
describe '#continue_indented_text_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :continue_indented_text }
let(:feature_flag_method) { :continue_indented_text_feature_flag_enabled? }
end
end
describe '#glql_integration_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :glql_integration }
let(:feature_flag_method) { :glql_integration_feature_flag_enabled? }
end
end
describe '#glql_load_on_click_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :glql_load_on_click }
let(:feature_flag_method) { :glql_load_on_click_feature_flag_enabled? }
end
end
describe '#supports_lock_on_merge?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :enforce_locked_labels_on_merge }
let(:feature_flag_method) { :supports_lock_on_merge? }
end
end
describe 'group shares' do
let!(:sub_group) { create(:group, parent: group) }
let!(:sub_sub_group) { create(:group, parent: sub_group) }
let!(:shared_group_1) { create(:group) }
let!(:shared_group_2) { create(:group) }
let!(:shared_group_3) { create(:group) }
before do
group.shared_with_groups << shared_group_1
sub_group.shared_with_groups << shared_group_2
sub_sub_group.shared_with_groups << shared_group_3
end
describe '#shared_with_groups_of_ancestors' do
where(:subject_group, :result) do
ref(:group) | []
ref(:sub_group) | lazy { [shared_group_1].map(&:id) }
ref(:sub_sub_group) | lazy { [shared_group_1, shared_group_2].map(&:id) }
end
with_them do
it 'returns correct group shares' do
expect(subject_group.shared_with_groups_of_ancestors.ids).to match_array(result)
end
end
end
describe '#shared_with_groups_of_ancestors_and_self' do
where(:subject_group, :result) do
ref(:group) | lazy { [shared_group_1].map(&:id) }
ref(:sub_group) | lazy { [shared_group_1, shared_group_2].map(&:id) }
ref(:sub_sub_group) | lazy { [shared_group_1, shared_group_2, shared_group_3].map(&:id) }
end
with_them do
it 'returns correct group shares' do
expect(subject_group.shared_with_groups_of_ancestors_and_self.ids).to match_array(result)
end
end
end
end
describe '#packages_policy_subject' do
it 'returns wrapper' do
expect(group.packages_policy_subject).to be_a(Packages::Policies::Group)
expect(group.packages_policy_subject.group).to eq(group)
end
end
describe '#gitlab_deploy_token' do
subject(:gitlab_deploy_token) { group.gitlab_deploy_token }
context 'when there is a gitlab deploy token associated' do
let!(:deploy_token) { create(:deploy_token, :group, :gitlab_deploy_token, groups: [group]) }
it { is_expected.to eq(deploy_token) }
end
context 'when there is no a gitlab deploy token associated' do
it { is_expected.to be_nil }
end
context 'when there is a gitlab deploy token associated but is has been revoked' do
let!(:deploy_token) { create(:deploy_token, :group, :gitlab_deploy_token, :revoked, groups: [group]) }
it { is_expected.to be_nil }
end
context 'when there is a gitlab deploy token associated but it is expired' do
let!(:deploy_token) { create(:deploy_token, :group, :gitlab_deploy_token, :expired, groups: [group]) }
it { is_expected.to be_nil }
end
context 'when there is a deploy token associated with a different name' do
let!(:deploy_token) { create(:deploy_token, :group, groups: [group]) }
it { is_expected.to be_nil }
end
context 'when there is a gitlab deploy token associated to a different group' do
let!(:deploy_token) { create(:deploy_token, :group, :gitlab_deploy_token, groups: [create(:group)]) }
it { is_expected.to be_nil }
end
end
describe '#usage_quotas_enabled?', feature_category: :consumables_cost_management do
where(root_group: [true, false])
with_them do
before do
allow(group).to receive(:root?).and_return(root_group)
end
it 'returns the expected result' do
expect(group.usage_quotas_enabled?).to be root_group
end
end
end
describe '#readme_project' do
it 'returns groups project containing metadata' do
readme_project = create(:project, path: Group::README_PROJECT_PATH, namespace: group)
create(:project, namespace: group)
expect(group.readme_project).to eq(readme_project)
end
end
describe '#group_readme' do
it 'returns readme from group readme project' do
create(:project, :repository, path: Group::README_PROJECT_PATH, namespace: group)
expect(group.group_readme.name).to eq('README.md')
expect(group.group_readme.data).to include('testme')
end
it 'returns nil if no readme project is present' do
create(:project, :repository, namespace: group)
expect(group.group_readme).to be_nil
end
end
describe '#hook_attrs' do
it 'returns the hook attributes' do
expect(group.hook_attrs).to eq({
group_name: group.name,
group_path: group.path,
group_id: group.id,
full_path: group.full_path
})
end
end
describe '#crm_group' do
let!(:crm_group) { create(:group) }
let!(:root_group) { create(:group) }
let!(:parent_group) { create(:group, parent: root_group) }
let!(:child_group) { create(:group, parent: parent_group) }
context 'when the group has a source_group_id' do
let!(:crm_settings) { create(:crm_settings, group: child_group, source_group: crm_group) }
it 'returns the source_group' do
expect(child_group.crm_group).to eq(crm_group)
end
end
context 'when the group does not have a source_group_id but is a root group' do
it 'returns the root group' do
expect(root_group.crm_group).to eq(root_group)
end
end
context 'when the group has no source_group_id and is not a root group' do
context 'when a parent group has a source_group_id' do
let!(:crm_settings) { create(:crm_settings, group: parent_group, source_group: crm_group) }
it 'traverses up the hierarchy and returns the first group with a source_group_id' do
expect(child_group.crm_group).to eq(crm_group)
end
end
it 'returns the root group if no groups in the hierarchy have a source_group_id' do
expect(child_group.crm_group).to eq(root_group)
end
end
end
describe '#has_issues_with_contacts?' do
context 'when group has no issues with contacts' do
it 'returns false' do
expect(group.has_issues_with_contacts?).to be_falsey
end
end
context 'when group has issues with contacts' do
let!(:issue) { create(:issue, project: create(:project, group: group)) }
let!(:contact) { create(:contact, group: group) }
let!(:issue_contact) { create(:issue_customer_relations_contact, issue: issue, contact: contact) }
it 'returns true' do
expect(group.has_issues_with_contacts?).to be_truthy
end
end
context 'when a subgroup has issues with contacts' do
let!(:subgroup) { create(:group, parent: group) }
let!(:issue) { create(:issue, project: create(:project, group: subgroup)) }
let!(:contact) { create(:contact, group: group) }
let!(:issue_contact) { create(:issue_customer_relations_contact, issue: issue, contact: contact) }
it 'returns true' do
expect(group.has_issues_with_contacts?).to be_truthy
end
end
end
describe '#cluster_agents' do
let_it_be(:other_group) { create(:group) }
let_it_be(:other_project) { create(:project, namespace: other_group) }
let_it_be(:root_group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: root_group) }
let_it_be(:project_in_group) { create(:project, namespace: root_group) }
let_it_be(:project_in_subgroup) { create(:project, namespace: subgroup) }
let_it_be(:cluster_agent_for_other_project) { create(:cluster_agent, project: other_project) }
let_it_be(:cluster_agent_for_project) { create(:cluster_agent, project: project_in_group) }
let_it_be(:cluster_agent_for_project_in_subgroup) { create(:cluster_agent, project: project_in_subgroup) }
subject { root_group.cluster_agents }
it { is_expected.to contain_exactly(cluster_agent_for_project, cluster_agent_for_project_in_subgroup) }
end
describe '#active?' do
let_it_be(:active_group) { create(:group) }
let_it_be(:inactive_group) { create(:group_with_deletion_schedule) }
context 'when group is active' do
specify { expect(active_group.active?).to be(true) }
end
context 'when group is inactive' do
specify { expect(inactive_group.active?).to be(false) }
end
context 'when ancestor is active' do
let_it_be(:group_with_active_ancestor) { create(:group, parent: active_group) }
specify { expect(group_with_active_ancestor.active?).to be(true) }
end
context 'when ancestor is inactive' do
let_it_be(:group_with_inactive_ancestor) { create(:group, parent: inactive_group) }
specify { expect(group_with_inactive_ancestor.active?).to be(false) }
end
end
end