3160 lines
110 KiB
Ruby
3160 lines
110 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe Group do
|
|
include ReloadHelpers
|
|
include StubGitlabCalls
|
|
|
|
let!(:group) { create(:group) }
|
|
|
|
describe 'associations' do
|
|
it { is_expected.to have_many :projects }
|
|
it { is_expected.to have_many(:group_members).dependent(:destroy) }
|
|
it { is_expected.to have_many(:users).through(:group_members) }
|
|
it { is_expected.to have_many(:owners).through(:group_members) }
|
|
it { is_expected.to have_many(:requesters).dependent(:destroy) }
|
|
it { is_expected.to have_many(: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_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(:bulk_import_exports).class_name('BulkImports::Export') }
|
|
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
|
|
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
|
|
it { is_expected.to have_one(:crm_settings) }
|
|
|
|
describe '#members & #requesters' do
|
|
let(:requester) { create(:user) }
|
|
let(: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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
end
|
|
|
|
context 'traversal_ids on create' do
|
|
context 'default traversal_ids' do
|
|
let(:group) { build(:group) }
|
|
|
|
before do
|
|
group.save!
|
|
group.reload
|
|
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!
|
|
reload_models(parent, group)
|
|
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!
|
|
reload_models(parent, group)
|
|
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!
|
|
reload_models(parent, group, new_grandparent, new_parent)
|
|
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
|
|
|
|
before do
|
|
subject
|
|
reload_models(old_parent, new_parent, group)
|
|
end
|
|
|
|
context 'within the same hierarchy' do
|
|
let!(:root) { create(:group).reload }
|
|
let!(:old_parent) { create(:group, parent: root) }
|
|
let!(:new_parent) { create(:group, parent: root) }
|
|
|
|
it 'updates traversal_ids' do
|
|
expect(group.traversal_ids).to eq [root.id, new_parent.id, group.id]
|
|
end
|
|
|
|
it_behaves_like 'hierarchy with traversal_ids'
|
|
it_behaves_like 'locked row' do
|
|
let(:row) { root }
|
|
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
|
|
expect(group.traversal_ids).to eq [new_parent.id, group.id]
|
|
end
|
|
|
|
it_behaves_like 'locked rows' do
|
|
let(:rows) { [old_parent, new_parent] }
|
|
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
|
|
expect(group.traversal_ids).to eq [new_parent.id, group.id]
|
|
end
|
|
|
|
it_behaves_like 'locked rows' do
|
|
let(:rows) { [group, new_parent] }
|
|
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
|
|
expect(group.traversal_ids).to eq [group.id]
|
|
end
|
|
|
|
it_behaves_like 'locked rows' do
|
|
let(:rows) { [old_parent, group] }
|
|
end
|
|
|
|
it_behaves_like 'hierarchy with traversal_ids' do
|
|
let(:root) { group }
|
|
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)
|
|
end
|
|
|
|
it 'updates traversal_ids for all descendants' do
|
|
expect(parent_group.reload.traversal_ids).to eq [new_grandparent.id, parent_group.id]
|
|
expect(group.reload.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) }
|
|
|
|
context 'recursive' do
|
|
before do
|
|
stub_feature_flags(use_traversal_ids: false)
|
|
end
|
|
|
|
it_behaves_like 'namespace traversal'
|
|
|
|
describe '#self_and_descendants' do
|
|
it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' }
|
|
end
|
|
|
|
describe '#self_and_descendant_ids' do
|
|
it { expect(group.self_and_descendant_ids.to_sql).not_to include 'traversal_ids @>' }
|
|
end
|
|
|
|
describe '#descendants' do
|
|
it { expect(group.descendants.to_sql).not_to include 'traversal_ids @>' }
|
|
end
|
|
|
|
describe '#self_and_hierarchy' do
|
|
it { expect(group.self_and_hierarchy.to_sql).not_to include 'traversal_ids @>' }
|
|
end
|
|
|
|
describe '#ancestors' do
|
|
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
|
|
end
|
|
|
|
describe '#ancestors_upto' do
|
|
it { expect(group.ancestors_upto.to_sql).not_to include "WITH ORDINALITY" }
|
|
end
|
|
end
|
|
|
|
context 'linear' do
|
|
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
|
|
|
|
context 'ancestor linear queries feature flag disabled' do
|
|
before do
|
|
stub_feature_flags(use_traversal_ids_for_ancestors: false)
|
|
end
|
|
|
|
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
|
|
end
|
|
end
|
|
|
|
describe '#ancestors_upto' do
|
|
it { expect(group.ancestors_upto.to_sql).to include "WITH ORDINALITY" }
|
|
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
|
|
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(Group.without_integration(instance_integration)).to contain_exactly(another_group)
|
|
end
|
|
end
|
|
|
|
describe '.public_or_visible_to_user' do
|
|
let!(:private_group) { create(:group, :private) }
|
|
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_user(user, Gitlab::Access::DEVELOPER)
|
|
end
|
|
|
|
it { is_expected.to match_array([private_group, 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_user(user, Gitlab::Access::DEVELOPER)
|
|
end
|
|
|
|
it { is_expected.to match_array([private_subgroup, internal_group, group]) }
|
|
end
|
|
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_onboarding_progress' do
|
|
subject { described_class.with_onboarding_progress }
|
|
|
|
it 'joins onboarding_progress' do
|
|
create(:onboarding_progress, namespace: group)
|
|
|
|
expect(subject).to eq([group])
|
|
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 '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])
|
|
end
|
|
|
|
it 'returns matching records based on ids' do
|
|
expect(described_class.by_ids_or_paths([group_id], nil)).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
|
|
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
|
|
it { expect(group.users).to eq(group.owners) }
|
|
end
|
|
|
|
describe '#human_name' do
|
|
it { expect(group.human_name).to eq(group.name) }
|
|
end
|
|
|
|
describe '#add_user' do
|
|
let(:user) { create(:user) }
|
|
|
|
before do
|
|
group.add_user(user, GroupMember::MAINTAINER)
|
|
end
|
|
|
|
it { expect(group.group_members.maintainers.map(&:user)).to include(user) }
|
|
end
|
|
|
|
describe '#add_users' do
|
|
let(:user) { create(:user) }
|
|
|
|
before do
|
|
group.add_users([user.id], GroupMember::GUEST)
|
|
end
|
|
|
|
it "updates the group permission" do
|
|
expect(group.group_members.guests.map(&:user)).to include(user)
|
|
group.add_users([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
|
|
|
|
context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
|
|
let!(:project) { create(:project, group: group) }
|
|
|
|
before do
|
|
group.add_users([create(:user)], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: project.id)
|
|
end
|
|
|
|
it 'creates a member_task with the correct attributes', :aggregate_failures do
|
|
member = group.group_members.last
|
|
|
|
expect(member.tasks_to_be_done).to match_array([:ci, :code])
|
|
expect(member.member_task.project).to eq(project)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#avatar_type' do
|
|
let(:user) { create(:user) }
|
|
|
|
before do
|
|
group.add_user(user, GroupMember::MAINTAINER)
|
|
end
|
|
|
|
it "is true if avatar is image" do
|
|
group.update_attribute(:avatar, 'uploads/avatar.png')
|
|
expect(group.avatar_type).to be_truthy
|
|
end
|
|
|
|
it "is false if avatar is html page" do
|
|
group.update_attribute(:avatar, 'uploads/avatar.html')
|
|
group.avatar_type
|
|
|
|
expect(group.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
|
|
end
|
|
end
|
|
|
|
describe '#avatar_url' do
|
|
let!(:group) { create(:group, :with_avatar) }
|
|
let(:user) { create(:user) }
|
|
|
|
context 'when avatar file is uploaded' do
|
|
before do
|
|
group.add_maintainer(user)
|
|
end
|
|
|
|
it 'shows correct avatar url' do
|
|
expect(group.avatar_url).to eq(group.avatar.url)
|
|
expect(group.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, group.avatar.url].join)
|
|
end
|
|
end
|
|
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 '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
|
|
before do
|
|
parent_group = create(:group)
|
|
create(:group_member, :owner, group: parent_group)
|
|
group.update!(parent: parent_group)
|
|
end
|
|
|
|
it { expect(group.last_owner?(@members[:owner])).to be_falsy }
|
|
end
|
|
end
|
|
|
|
describe '#member_last_blocked_owner?' do
|
|
let_it_be(:blocked_user) { create(:user, :blocked) }
|
|
|
|
let(:member) { blocked_user.group_members.last }
|
|
|
|
before do
|
|
group.add_user(blocked_user, GroupMember::OWNER)
|
|
end
|
|
|
|
context 'when last_blocked_owner is set' do
|
|
before do
|
|
expect(group).not_to receive(:members_with_parents)
|
|
end
|
|
|
|
it 'returns true' do
|
|
member.last_blocked_owner = true
|
|
|
|
expect(group.member_last_blocked_owner?(member)).to be(true)
|
|
end
|
|
|
|
it 'returns false' do
|
|
member.last_blocked_owner = false
|
|
|
|
expect(group.member_last_blocked_owner?(member)).to be(false)
|
|
end
|
|
end
|
|
|
|
context 'when last_blocked_owner is not set' do
|
|
it { expect(group.member_last_blocked_owner?(member)).to be(true) }
|
|
|
|
context 'with another active owner' do
|
|
before do
|
|
group.add_user(create(:user), GroupMember::OWNER)
|
|
end
|
|
|
|
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
|
|
end
|
|
|
|
context 'with 2 blocked owners' do
|
|
before do
|
|
group.add_user(create(:user, :blocked), GroupMember::OWNER)
|
|
end
|
|
|
|
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
|
|
end
|
|
|
|
context 'with owners from a parent' do
|
|
before do
|
|
parent_group = create(:group)
|
|
create(:group_member, :owner, group: parent_group)
|
|
group.update!(parent: parent_group)
|
|
end
|
|
|
|
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when analyzing blocked owners' do
|
|
let_it_be(:blocked_user) { create(:user, :blocked) }
|
|
|
|
describe '#single_blocked_owner?' do
|
|
context 'when there is only one blocked owner' do
|
|
before do
|
|
group.add_user(blocked_user, GroupMember::OWNER)
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(group.single_blocked_owner?).to eq(true)
|
|
end
|
|
end
|
|
|
|
context 'when there are multiple blocked owners' do
|
|
let_it_be(:blocked_user_2) { create(:user, :blocked) }
|
|
|
|
before do
|
|
group.add_user(blocked_user, GroupMember::OWNER)
|
|
group.add_user(blocked_user_2, GroupMember::OWNER)
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(group.single_blocked_owner?).to eq(false)
|
|
end
|
|
end
|
|
|
|
context 'when there are no blocked owners' do
|
|
it 'returns false' do
|
|
expect(group.single_blocked_owner?).to eq(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#blocked_owners' do
|
|
let_it_be(:user) { create(:user) }
|
|
|
|
before do
|
|
group.add_user(blocked_user, GroupMember::OWNER)
|
|
group.add_user(user, GroupMember::OWNER)
|
|
end
|
|
|
|
it 'has only blocked owners' do
|
|
expect(group.blocked_owners.map(&:user)).to match([blocked_user])
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#single_owner?' do
|
|
let_it_be(:user) { create(:user) }
|
|
|
|
context 'when there is only one owner' do
|
|
before do
|
|
group.add_user(user, GroupMember::OWNER)
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(group.single_owner?).to eq(true)
|
|
end
|
|
end
|
|
|
|
context 'when there are multiple owners' do
|
|
let_it_be(:user_2) { create(:user) }
|
|
|
|
before do
|
|
group.add_user(user, GroupMember::OWNER)
|
|
group.add_user(user_2, GroupMember::OWNER)
|
|
end
|
|
|
|
it 'returns true' do
|
|
expect(group.single_owner?).to eq(false)
|
|
end
|
|
end
|
|
|
|
context 'when there are no owners' do
|
|
it 'returns false' do
|
|
expect(group.single_owner?).to eq(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#member_last_owner?' do
|
|
let_it_be(:user) { create(:user) }
|
|
|
|
let(:member) { group.members.last }
|
|
|
|
before do
|
|
group.add_user(user, GroupMember::OWNER)
|
|
end
|
|
|
|
context 'when last_owner is set' do
|
|
before do
|
|
expect(group).not_to receive(:last_owner?)
|
|
end
|
|
|
|
it 'returns true' do
|
|
member.last_owner = true
|
|
|
|
expect(group.member_last_owner?(member)).to be(true)
|
|
end
|
|
|
|
it 'returns false' do
|
|
member.last_owner = false
|
|
|
|
expect(group.member_last_owner?(member)).to be(false)
|
|
end
|
|
end
|
|
|
|
context 'when last_owner is not set' do
|
|
it 'returns true' do
|
|
expect(group).to receive(:last_owner?).and_call_original
|
|
|
|
expect(group.member_last_owner?(member)).to be(true)
|
|
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
|
|
group.add_owner(owner)
|
|
group.add_developer(developer)
|
|
|
|
expect(group.owners).to eq([owner])
|
|
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_user(members[:owner], GroupMember::OWNER)
|
|
group.add_user(members[:maintainer], GroupMember::MAINTAINER)
|
|
group.add_user(members[:developer], GroupMember::DEVELOPER)
|
|
group.add_user(members[:reporter], GroupMember::REPORTER)
|
|
group.add_user(members[:guest], GroupMember::GUEST)
|
|
group.request_access(members[:requester])
|
|
|
|
members
|
|
end
|
|
|
|
describe '#web_url' do
|
|
it 'returns the canonical URL' do
|
|
expect(group.web_url).to include("groups/#{group.name}")
|
|
end
|
|
|
|
context 'nested group' do
|
|
let(:nested_group) { create(:group, :nested) }
|
|
|
|
it { expect(nested_group.web_url).to include("groups/#{nested_group.full_path}") }
|
|
end
|
|
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 '#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
|
|
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 '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.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_user(create(:user), GroupMember::MAINTAINER) }
|
|
let_it_be(:developer) { group.add_user(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_user(create(:user), GroupMember::MAINTAINER) }
|
|
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
|
|
|
|
it 'returns parents members' do
|
|
expect(group.members_with_parents).to include(developer)
|
|
expect(group.members_with_parents).to include(maintainer)
|
|
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 members' do
|
|
expect(shared_group.members_with_parents).to(
|
|
include(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!(:group) { create(:group, :nested) }
|
|
let!(:sub_group) { create(:group, parent: group) }
|
|
let!(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) }
|
|
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
|
|
let!(:other_developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
|
|
|
|
describe '#direct_and_indirect_members' do
|
|
it 'returns parents members' do
|
|
expect(group.direct_and_indirect_members).to include(developer)
|
|
expect(group.direct_and_indirect_members).to include(maintainer)
|
|
end
|
|
|
|
it 'returns descendant members' do
|
|
expect(group.direct_and_indirect_members).to include(other_developer)
|
|
end
|
|
end
|
|
|
|
describe '#direct_and_indirect_members_with_inactive' do
|
|
let!(:maintainer_blocked) { group.parent.add_user(create(:user, :blocked), GroupMember::MAINTAINER) }
|
|
|
|
it 'returns parents members' do
|
|
expect(group.direct_and_indirect_members_with_inactive).to include(developer)
|
|
expect(group.direct_and_indirect_members_with_inactive).to include(maintainer)
|
|
expect(group.direct_and_indirect_members_with_inactive).to include(maintainer_blocked)
|
|
end
|
|
|
|
it 'returns descendant members' do
|
|
expect(group.direct_and_indirect_members_with_inactive).to include(other_developer)
|
|
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
|
|
|
|
context 'user-related methods' do
|
|
let(:user_a) { create(:user) }
|
|
let(:user_b) { create(:user) }
|
|
let(:user_c) { create(:user) }
|
|
let(:user_d) { create(:user) }
|
|
|
|
let(:group) { create(:group) }
|
|
let(:nested_group) { create(:group, parent: group) }
|
|
let(:deep_nested_group) { create(:group, parent: nested_group) }
|
|
let(:project) { create(:project, namespace: group) }
|
|
|
|
before do
|
|
group.add_developer(user_a)
|
|
group.add_developer(user_c)
|
|
nested_group.add_developer(user_b)
|
|
deep_nested_group.add_developer(user_a)
|
|
project.add_developer(user_d)
|
|
end
|
|
|
|
describe '#direct_and_indirect_users' do
|
|
it 'returns member users on every nest level without duplication' do
|
|
expect(group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c, user_d)
|
|
expect(nested_group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c)
|
|
expect(deep_nested_group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c)
|
|
end
|
|
|
|
it 'does not return members of projects belonging to ancestor groups' do
|
|
expect(nested_group.direct_and_indirect_users).not_to include(user_d)
|
|
end
|
|
end
|
|
|
|
describe '#direct_and_indirect_users_with_inactive' do
|
|
let(:user_blocked_1) { create(:user, :blocked) }
|
|
let(:user_blocked_2) { create(:user, :blocked) }
|
|
let(:user_blocked_3) { create(:user, :blocked) }
|
|
let(:project_in_group) { create(:project, namespace: nested_group) }
|
|
|
|
before do
|
|
group.add_developer(user_blocked_1)
|
|
nested_group.add_developer(user_blocked_1)
|
|
deep_nested_group.add_developer(user_blocked_2)
|
|
project_in_group.add_developer(user_blocked_3)
|
|
end
|
|
|
|
it 'returns member users on every nest level without duplication' do
|
|
expect(group.direct_and_indirect_users_with_inactive).to contain_exactly(user_a, user_b, user_c, user_d, user_blocked_1, user_blocked_2, user_blocked_3)
|
|
expect(nested_group.direct_and_indirect_users_with_inactive).to contain_exactly(user_a, user_b, user_c, user_blocked_1, user_blocked_2, user_blocked_3)
|
|
expect(deep_nested_group.direct_and_indirect_users_with_inactive).to contain_exactly(user_a, user_b, user_c, user_blocked_1, user_blocked_2)
|
|
end
|
|
|
|
it 'returns members of projects belonging to group' do
|
|
expect(nested_group.direct_and_indirect_users_with_inactive).to include(user_blocked_3)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#project_users_with_descendants' do
|
|
let(:user_a) { create(:user) }
|
|
let(:user_b) { create(:user) }
|
|
let(:user_c) { create(:user) }
|
|
|
|
let(:group) { create(:group) }
|
|
let(:nested_group) { create(:group, parent: group) }
|
|
let(:deep_nested_group) { create(:group, parent: nested_group) }
|
|
let(:project_a) { create(:project, namespace: group) }
|
|
let(:project_b) { create(:project, namespace: nested_group) }
|
|
let(:project_c) { create(:project, namespace: deep_nested_group) }
|
|
|
|
it 'returns members of all projects in group and subgroups' do
|
|
project_a.add_developer(user_a)
|
|
project_b.add_developer(user_b)
|
|
project_c.add_developer(user_c)
|
|
|
|
expect(group.project_users_with_descendants).to contain_exactly(user_a, user_b, user_c)
|
|
expect(nested_group.project_users_with_descendants).to contain_exactly(user_b, user_c)
|
|
expect(deep_nested_group.project_users_with_descendants).to contain_exactly(user_c)
|
|
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_user(maintainer, GroupMember::MAINTAINER)
|
|
group.add_user(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 '#update_two_factor_requirement' do
|
|
let(:user) { create(:user) }
|
|
|
|
context 'group membership' do
|
|
before do
|
|
group.add_user(user, GroupMember::OWNER)
|
|
end
|
|
|
|
it 'is called when require_two_factor_authentication is changed' do
|
|
expect_any_instance_of(User).to receive(:update_two_factor_requirement)
|
|
|
|
group.update!(require_two_factor_authentication: true)
|
|
end
|
|
|
|
it 'is called when two_factor_grace_period is changed' do
|
|
expect_any_instance_of(User).to receive(:update_two_factor_requirement)
|
|
|
|
group.update!(two_factor_grace_period: 23)
|
|
end
|
|
|
|
it 'is not called when other attributes are changed' do
|
|
expect_any_instance_of(User).not_to receive(:update_two_factor_requirement)
|
|
|
|
group.update!(description: 'foobar')
|
|
end
|
|
|
|
it 'calls #update_two_factor_requirement on each group member' do
|
|
other_user = create(:user)
|
|
group.add_user(other_user, GroupMember::OWNER)
|
|
|
|
calls = 0
|
|
allow_any_instance_of(User).to receive(:update_two_factor_requirement) do
|
|
calls += 1
|
|
end
|
|
|
|
group.update!(require_two_factor_authentication: true, two_factor_grace_period: 23)
|
|
|
|
expect(calls).to eq 2
|
|
end
|
|
end
|
|
|
|
context 'sub groups and projects' do
|
|
it 'enables two_factor_requirement for group member' do
|
|
group.add_user(user, GroupMember::OWNER)
|
|
|
|
group.update!(require_two_factor_authentication: true)
|
|
|
|
expect(user.reload.require_two_factor_authentication_from_group).to be_truthy
|
|
end
|
|
|
|
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_user(indirect_user, GroupMember::OWNER)
|
|
|
|
group.update!(require_two_factor_authentication: true)
|
|
|
|
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_user(indirect_user, GroupMember::OWNER)
|
|
|
|
group.update!(require_two_factor_authentication: false)
|
|
|
|
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_user(indirect_user, GroupMember::OWNER)
|
|
group.update!(parent: ancestor_group)
|
|
|
|
group.update!(require_two_factor_authentication: true)
|
|
|
|
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_user(indirect_user, GroupMember::OWNER)
|
|
|
|
group.update!(require_two_factor_authentication: true)
|
|
|
|
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_user(indirect_user, GroupMember::OWNER)
|
|
|
|
group.update!(require_two_factor_authentication: false)
|
|
|
|
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_user(indirect_user, GroupMember::OWNER)
|
|
|
|
group.update!(require_two_factor_authentication: false)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
expect(user.reload.require_two_factor_authentication_from_group).to be_falsey
|
|
end
|
|
end
|
|
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
|
|
end
|
|
end
|
|
|
|
describe '#ci_variables_for' do
|
|
let(:project) { create(:project, group: group) }
|
|
let(:environment_scope) { '*' }
|
|
|
|
let!(:ci_variable) do
|
|
create(:ci_group_variable, value: 'secret', group: group, environment_scope: environment_scope)
|
|
end
|
|
|
|
let!(:protected_variable) do
|
|
create(:ci_group_variable, :protected, value: 'protected', group: group)
|
|
end
|
|
|
|
subject { group.ci_variables_for('ref', project) }
|
|
|
|
it 'memoizes the result by ref and environment', :request_store do
|
|
scoped_variable = create(:ci_group_variable, value: 'secret', group: group, environment_scope: 'scoped')
|
|
|
|
expect(project).to receive(:protected_for?).with('ref').once.and_return(true)
|
|
expect(project).to receive(:protected_for?).with('other').twice.and_return(false)
|
|
|
|
2.times do
|
|
expect(group.ci_variables_for('ref', project, environment: 'production')).to contain_exactly(ci_variable, protected_variable)
|
|
expect(group.ci_variables_for('other', project)).to contain_exactly(ci_variable)
|
|
expect(group.ci_variables_for('other', project, environment: 'scoped')).to contain_exactly(ci_variable, scoped_variable)
|
|
end
|
|
end
|
|
|
|
shared_examples 'ref is protected' do
|
|
it 'contains all the variables' do
|
|
is_expected.to contain_exactly(ci_variable, protected_variable)
|
|
end
|
|
end
|
|
|
|
context 'when the ref is not protected' do
|
|
before do
|
|
stub_application_setting(
|
|
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
|
|
end
|
|
|
|
it 'contains only the CI variables' do
|
|
is_expected.to contain_exactly(ci_variable)
|
|
end
|
|
end
|
|
|
|
context 'when the ref is a protected branch' do
|
|
before do
|
|
allow(project).to receive(:protected_for?).with('ref').and_return(true)
|
|
end
|
|
|
|
it_behaves_like 'ref is protected'
|
|
end
|
|
|
|
context 'when the ref is a protected tag' do
|
|
before do
|
|
allow(project).to receive(:protected_for?).with('ref').and_return(true)
|
|
end
|
|
|
|
it_behaves_like 'ref is protected'
|
|
end
|
|
|
|
context 'when environment name is specified' do
|
|
let(:environment) { 'review/name' }
|
|
|
|
subject do
|
|
group.ci_variables_for('ref', project, environment: environment)
|
|
end
|
|
|
|
context 'when environment scope is exactly matched' do
|
|
let(:environment_scope) { 'review/name' }
|
|
|
|
it { is_expected.to contain_exactly(ci_variable) }
|
|
end
|
|
|
|
context 'when environment scope is matched by wildcard' do
|
|
let(:environment_scope) { 'review/*' }
|
|
|
|
it { is_expected.to contain_exactly(ci_variable) }
|
|
end
|
|
|
|
context 'when environment scope does not match' do
|
|
let(:environment_scope) { 'review/*/special' }
|
|
|
|
it { is_expected.not_to contain_exactly(ci_variable) }
|
|
end
|
|
|
|
context 'when environment scope has _' do
|
|
let(:environment_scope) { '*_*' }
|
|
|
|
it 'does not treat it as wildcard' do
|
|
is_expected.not_to contain_exactly(ci_variable)
|
|
end
|
|
|
|
context 'when environment name contains underscore' do
|
|
let(:environment) { 'foo_bar/test' }
|
|
let(:environment_scope) { 'foo_bar/*' }
|
|
|
|
it 'matches literally for _' do
|
|
is_expected.to contain_exactly(ci_variable)
|
|
end
|
|
end
|
|
end
|
|
|
|
# The environment name and scope cannot have % at the moment,
|
|
# but we're considering relaxing it and we should also make sure
|
|
# it doesn't break in case some data sneaked in somehow as we're
|
|
# not checking this integrity in database level.
|
|
context 'when environment scope has %' do
|
|
it 'does not treat it as wildcard' do
|
|
ci_variable.update_attribute(:environment_scope, '*%*')
|
|
|
|
is_expected.not_to contain_exactly(ci_variable)
|
|
end
|
|
|
|
context 'when environment name contains a percent' do
|
|
let(:environment) { 'foo%bar/test' }
|
|
|
|
it 'matches literally for %' do
|
|
ci_variable.update_attribute(:environment_scope, 'foo%bar/*')
|
|
|
|
is_expected.to contain_exactly(ci_variable)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when variables with the same name have different environment scopes' do
|
|
let!(:partially_matched_variable) do
|
|
create(:ci_group_variable,
|
|
key: ci_variable.key,
|
|
value: 'partial',
|
|
environment_scope: 'review/*',
|
|
group: group)
|
|
end
|
|
|
|
let!(:perfectly_matched_variable) do
|
|
create(:ci_group_variable,
|
|
key: ci_variable.key,
|
|
value: 'prefect',
|
|
environment_scope: 'review/name',
|
|
group: group)
|
|
end
|
|
|
|
it 'puts variables matching environment scope more in the end' do
|
|
is_expected.to eq(
|
|
[ci_variable,
|
|
partially_matched_variable,
|
|
perfectly_matched_variable])
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when group has children' do
|
|
let(:group_child) { create(:group, parent: group) }
|
|
let(:group_child_2) { create(:group, parent: group_child) }
|
|
let(:group_child_3) { create(:group, parent: group_child_2) }
|
|
let(:variable_child) { create(:ci_group_variable, group: group_child) }
|
|
let(:variable_child_2) { create(:ci_group_variable, group: group_child_2) }
|
|
let(:variable_child_3) { create(:ci_group_variable, group: group_child_3) }
|
|
|
|
before do
|
|
allow(project).to receive(:protected_for?).with('ref').and_return(true)
|
|
end
|
|
|
|
context 'traversal queries' do
|
|
shared_examples 'correct ancestor order' do
|
|
it 'returns all variables belong to the group and parent groups' do
|
|
expected_array1 = [protected_variable, ci_variable]
|
|
expected_array2 = [variable_child, variable_child_2, variable_child_3]
|
|
got_array = group_child_3.ci_variables_for('ref', project).to_a
|
|
|
|
expect(got_array.shift(2)).to contain_exactly(*expected_array1)
|
|
expect(got_array).to eq(expected_array2)
|
|
end
|
|
end
|
|
|
|
context 'recursive' do
|
|
before do
|
|
stub_feature_flags(use_traversal_ids: false)
|
|
end
|
|
|
|
include_examples 'correct ancestor order'
|
|
end
|
|
|
|
context 'linear' do
|
|
before do
|
|
stub_feature_flags(use_traversal_ids: true)
|
|
|
|
group_child_3.reload # make sure traversal_ids are reloaded
|
|
end
|
|
|
|
include_examples 'correct ancestor order'
|
|
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 'returns nil' do
|
|
expect(highest_group_member).to be_nil
|
|
end
|
|
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 }
|
|
|
|
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
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
let(:group) { create(:group) }
|
|
|
|
subject { 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
|
|
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
|
|
before do
|
|
stub_application_setting(auto_devops_enabled: instance_value)
|
|
parent = create(:group, auto_devops_enabled: parent_value)
|
|
|
|
group.update!(
|
|
auto_devops_enabled: group_value,
|
|
parent: parent
|
|
)
|
|
end
|
|
|
|
it { is_expected.to eq(config) }
|
|
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 '.groups_including_descendants_by' do
|
|
let_it_be(:parent_group1) { create(:group) }
|
|
let_it_be(:parent_group2) { create(:group) }
|
|
let_it_be(:extra_group) { create(:group) }
|
|
let_it_be(:child_group1) { create(:group, parent: parent_group1) }
|
|
let_it_be(:child_group2) { create(:group, parent: parent_group1) }
|
|
let_it_be(:child_group3) { create(:group, parent: parent_group2) }
|
|
|
|
subject { described_class.groups_including_descendants_by([parent_group2.id, parent_group1.id]) }
|
|
|
|
shared_examples 'returns the expected groups for a group and its descendants' do
|
|
specify { is_expected.to contain_exactly(parent_group1, parent_group2, child_group1, child_group2, child_group3) }
|
|
end
|
|
|
|
it_behaves_like 'returns the expected groups for a group and its descendants'
|
|
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 '#update_shared_runners_setting!' do
|
|
context 'enabled' do
|
|
subject { group.update_shared_runners_setting!('enabled') }
|
|
|
|
context 'group that its ancestors have shared runners disabled' do
|
|
let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled) }
|
|
let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, parent: parent) }
|
|
let_it_be(:project, reload: true) { create(:project, shared_runners_enabled: false, group: group) }
|
|
|
|
it 'raises exception' do
|
|
expect { subject }
|
|
.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Shared runners enabled cannot be enabled because parent group has shared Runners disabled')
|
|
end
|
|
|
|
it 'does not enable shared runners' do
|
|
expect do
|
|
subject rescue nil
|
|
|
|
parent.reload
|
|
group.reload
|
|
project.reload
|
|
end.to not_change { parent.shared_runners_enabled }
|
|
.and not_change { group.shared_runners_enabled }
|
|
.and not_change { project.shared_runners_enabled }
|
|
end
|
|
end
|
|
|
|
context 'root group with shared runners disabled' do
|
|
let_it_be(:group) { create(:group, :shared_runners_disabled) }
|
|
let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
|
|
let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
|
|
|
|
it 'enables shared Runners only for itself' do
|
|
expect { subject_and_reload(group, sub_group, project) }
|
|
.to change { group.shared_runners_enabled }.from(false).to(true)
|
|
.and not_change { sub_group.shared_runners_enabled }
|
|
.and not_change { project.shared_runners_enabled }
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'disabled_and_unoverridable' do
|
|
let_it_be(:group) { create(:group) }
|
|
let_it_be(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) }
|
|
let_it_be(:sub_group_2) { create(:group, parent: group) }
|
|
let_it_be(:project) { create(:project, group: group, shared_runners_enabled: true) }
|
|
let_it_be(:project_2) { create(:project, group: sub_group_2, shared_runners_enabled: true) }
|
|
|
|
subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_AND_UNOVERRIDABLE) }
|
|
|
|
it 'disables shared Runners for all descendant groups and projects' do
|
|
expect { subject_and_reload(group, sub_group, sub_group_2, project, project_2) }
|
|
.to change { group.shared_runners_enabled }.from(true).to(false)
|
|
.and not_change { group.allow_descendants_override_disabled_shared_runners }
|
|
.and not_change { sub_group.shared_runners_enabled }
|
|
.and change { sub_group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
|
|
.and change { sub_group_2.shared_runners_enabled }.from(true).to(false)
|
|
.and not_change { sub_group_2.allow_descendants_override_disabled_shared_runners }
|
|
.and change { project.shared_runners_enabled }.from(true).to(false)
|
|
.and change { project_2.shared_runners_enabled }.from(true).to(false)
|
|
end
|
|
|
|
context 'with override on self' do
|
|
let_it_be(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
|
|
|
it 'disables it' do
|
|
expect { subject_and_reload(group) }
|
|
.to not_change { group.shared_runners_enabled }
|
|
.and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'disabled_with_override' do
|
|
subject { group.update_shared_runners_setting!(Namespace::SR_DISABLED_WITH_OVERRIDE) }
|
|
|
|
context 'top level group' do
|
|
let_it_be(:group) { create(:group, :shared_runners_disabled) }
|
|
let_it_be(:sub_group) { create(:group, :shared_runners_disabled, parent: group) }
|
|
let_it_be(:project) { create(:project, shared_runners_enabled: false, group: sub_group) }
|
|
|
|
it 'enables allow descendants to override only for itself' do
|
|
expect { subject_and_reload(group, sub_group, project) }
|
|
.to change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
.and not_change { group.shared_runners_enabled }
|
|
.and not_change { sub_group.allow_descendants_override_disabled_shared_runners }
|
|
.and not_change { sub_group.shared_runners_enabled }
|
|
.and not_change { project.shared_runners_enabled }
|
|
end
|
|
end
|
|
|
|
context 'group that its ancestors have shared Runners disabled but allows to override' do
|
|
let_it_be(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
|
let_it_be(:group) { create(:group, :shared_runners_disabled, parent: parent) }
|
|
let_it_be(:project) { create(:project, shared_runners_enabled: false, group: group) }
|
|
|
|
it 'enables allow descendants to override' do
|
|
expect { subject_and_reload(parent, group, project) }
|
|
.to not_change { parent.allow_descendants_override_disabled_shared_runners }
|
|
.and not_change { parent.shared_runners_enabled }
|
|
.and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
.and not_change { group.shared_runners_enabled }
|
|
.and not_change { project.shared_runners_enabled }
|
|
end
|
|
end
|
|
|
|
context 'when parent does not allow' do
|
|
let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false ) }
|
|
let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
|
|
|
|
it 'raises exception' do
|
|
expect { subject }
|
|
.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Allow descendants override disabled shared runners cannot be enabled because parent group does not allow it')
|
|
end
|
|
|
|
it 'does not allow descendants to override' do
|
|
expect do
|
|
subject rescue nil
|
|
|
|
parent.reload
|
|
group.reload
|
|
end.to not_change { parent.allow_descendants_override_disabled_shared_runners }
|
|
.and not_change { parent.shared_runners_enabled }
|
|
.and not_change { group.allow_descendants_override_disabled_shared_runners }
|
|
.and not_change { group.shared_runners_enabled }
|
|
end
|
|
end
|
|
|
|
context 'top level group that has shared Runners enabled' do
|
|
let_it_be(:group) { create(:group, shared_runners_enabled: true) }
|
|
let_it_be(:sub_group) { create(:group, shared_runners_enabled: true, parent: group) }
|
|
let_it_be(:project) { create(:project, shared_runners_enabled: true, group: sub_group) }
|
|
|
|
it 'enables allow descendants to override & disables shared runners everywhere' do
|
|
expect { subject_and_reload(group, sub_group, project) }
|
|
.to change { group.shared_runners_enabled }.from(true).to(false)
|
|
.and change { group.allow_descendants_override_disabled_shared_runners }.from(false).to(true)
|
|
.and change { sub_group.shared_runners_enabled }.from(true).to(false)
|
|
.and change { project.shared_runners_enabled }.from(true).to(false)
|
|
end
|
|
end
|
|
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 '#membership_locked?' do
|
|
it 'returns false' do
|
|
expect(build(:group)).not_to be_membership_locked
|
|
end
|
|
end
|
|
|
|
describe '#first_owner' do
|
|
let(:group) { build(:group) }
|
|
|
|
context 'the group has owners' do
|
|
before do
|
|
group.add_owner(create(:user))
|
|
group.add_owner(create(:user))
|
|
end
|
|
|
|
it 'is the first owner' do
|
|
expect(group.first_owner)
|
|
.to eq(group.owners.first)
|
|
.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(:group) { create(:group, :private) }
|
|
|
|
subject { group.has_project_with_service_desk_enabled? }
|
|
|
|
before do
|
|
allow(Gitlab::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(Gitlab::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, emails_disabled: true) }
|
|
let_it_be(:child_1) { create(:group, parent: parent_1) }
|
|
|
|
let_it_be(:parent_2) { create(:group, emails_disabled: false) }
|
|
let_it_be(:child_2) { create(:group, parent: parent_2) }
|
|
|
|
let_it_be(:other_group) { create(:group, emails_disabled: false) }
|
|
|
|
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 { is_expected.to eq(Set.new([child_1.id])) }
|
|
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 '.organizations' do
|
|
it 'returns organizations belonging to the group' do
|
|
organization1 = create(:organization, group: group)
|
|
create(:organization)
|
|
organization3 = create(:organization, group: group)
|
|
|
|
expect(group.organizations).to contain_exactly(organization1, 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
|
|
|
|
describe '#activity_path' do
|
|
it 'returns the group activity_path' do
|
|
expected_path = "/groups/#{group.name}/-/activity"
|
|
|
|
expect(group.activity_path).to eq(expected_path)
|
|
end
|
|
end
|
|
|
|
context 'with export' do
|
|
let(:group) { create(:group, :with_export) }
|
|
|
|
it '#export_file_exists? returns true' do
|
|
expect(group.export_file_exists?).to be true
|
|
end
|
|
|
|
it '#export_archive_exists? returns true' do
|
|
expect(group.export_archive_exists?).to be true
|
|
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 false where no crm_settings exist' do
|
|
expect(group.crm_enabled?).to be_falsey
|
|
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
|
|
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 '#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 '#runners_token' do
|
|
let_it_be(:group) { create(:group) }
|
|
|
|
subject { group }
|
|
|
|
it_behaves_like 'it has a prefixable runners_token', :groups_runners_token_prefix
|
|
end
|
|
end
|