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