diff --git a/ee/app/models/namespaces/free_user_cap.rb b/ee/app/models/namespaces/free_user_cap.rb index 4e6625c397a1b3e9147218251a6b8aa7a818a0b3..4d29e575b6ac3c849b80377d1f14f2371e5c56f6 100644 --- a/ee/app/models/namespaces/free_user_cap.rb +++ b/ee/app/models/namespaces/free_user_cap.rb @@ -21,7 +21,7 @@ def enforce_cap? end def feature_enabled? - ::Feature.enabled?(:free_user_cap, root_namespace) + ::Feature.enabled?(:free_user_cap, root_namespace) && !root_namespace.exclude_from_free_user_cap? end def self.trimming_enabled? diff --git a/ee/spec/helpers/ee/invite_members_helper_spec.rb b/ee/spec/helpers/ee/invite_members_helper_spec.rb index 4fe7e978f4a27ac7c72d9cd1b77c9c49b046699a..dc8dde74d5765869ae751dcbb9b86097530aa20a 100644 --- a/ee/spec/helpers/ee/invite_members_helper_spec.rb +++ b/ee/spec/helpers/ee/invite_members_helper_spec.rb @@ -30,7 +30,7 @@ end end - context 'when applying the free user cap is valid' do + context 'when on free plan' do context 'when user namespace' do let!(:user_namespace) do build(:user_namespace, projects: [project], gitlab_subscription: build(:gitlab_subscription, :free)) @@ -59,6 +59,16 @@ 'members_path' => group_usage_quotas_path(project.root_ancestor) })) end + + context 'when exclude_from_free_user_cap set to true' do + before do + group.namespace_settings.update_column(:exclude_from_free_user_cap, true) + end + + it 'does not include users limit notification data' do + expect(helper.common_invite_modal_dataset(project)).not_to have_key(:users_limit_dataset) + end + end end end end diff --git a/ee/spec/models/gitlab_subscription_spec.rb b/ee/spec/models/gitlab_subscription_spec.rb index ee97077bbabda781705645a210a75d92069ea728..7b77e9029fe8f0a87b7c7b1cbf55ab787f5f5e2f 100644 --- a/ee/spec/models/gitlab_subscription_spec.rb +++ b/ee/spec/models/gitlab_subscription_spec.rb @@ -668,6 +668,16 @@ end.to change { namespace_settings.prevent_sharing_groups_outside_hierarchy }.from(true).to(false) end + context 'when exclude_from_free_user_cap set to true' do + before do + namespace.namespace_settings.update_column(:exclude_from_free_user_cap, true) + end + + it 'does not prevent sharing outside hierarchy' do + expect(namespace_settings).not_to be_prevent_sharing_groups_outside_hierarchy + end + end + context 'with :free_user_cap feature flag disabled' do before do stub_feature_flags(free_user_cap: false) diff --git a/ee/spec/models/namespaces/free_user_cap_spec.rb b/ee/spec/models/namespaces/free_user_cap_spec.rb index 54b7e5137a0943834683cbeb07bbbbec97c1a147..5169855763a2e79b5c8d1ce68b6c202915078fd6 100644 --- a/ee/spec/models/namespaces/free_user_cap_spec.rb +++ b/ee/spec/models/namespaces/free_user_cap_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Namespaces::FreeUserCap, :saas do - let_it_be(:namespace) { create(:group_with_plan, plan: :free_plan) } + let_it_be(:namespace, reload: true) { create(:group_with_plan, plan: :free_plan) } let(:should_check_namespace_plan) { true } @@ -77,6 +77,14 @@ it { is_expected.to be false } end + + context 'when exclude_from_free_user_cap set to true' do + before do + namespace.namespace_settings.update_column(:exclude_from_free_user_cap, true) + end + + it { is_expected.to be false } + end end end end @@ -118,6 +126,14 @@ it { is_expected.to be false } end + + context 'when excluded from free user cap' do + before do + namespace.namespace_settings.update_column(:exclude_from_free_user_cap, true) + end + + it { is_expected.to be false } + end end end @@ -138,6 +154,14 @@ end it { is_expected.to be true } + + context 'when excluded from free user cap' do + before do + namespace.namespace_settings.update_column(:exclude_from_free_user_cap, true) + end + + it { is_expected.to be false } + end end end diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 360d087236fefed41ef0201f17992540b09a3e10..b2fd31c63076205904195191f116896c3ef44fb4 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -1021,6 +1021,15 @@ def stub_group_saml_config(enabled) is_expected.to be_disallowed permission end + + context 'when exclude_from_free_user_cap is set to true' do + it 'allows changing the setting' do + group.namespace_settings.update_column(:exclude_from_free_user_cap, true) + create(:gitlab_subscription, :free, namespace: group) + + is_expected.to be_allowed permission + end + end end context 'with paid plans', :saas do diff --git a/ee/spec/workers/namespaces/free_user_cap_worker_spec.rb b/ee/spec/workers/namespaces/free_user_cap_worker_spec.rb index c9defba69eb75817952c03b33e7698de024fc3af..574be45eb4823ec9b8f117d79b79652fb781c930 100644 --- a/ee/spec/workers/namespaces/free_user_cap_worker_spec.rb +++ b/ee/spec/workers/namespaces/free_user_cap_worker_spec.rb @@ -63,7 +63,10 @@ external_pgl_for_g4 = create(:project_group_link, project: p1_for_g4) g5 = create(:group) + g6 = create(:group_with_plan, plan: :free_plan) + g6.namespace_settings.update_column(:exclude_from_free_user_cap, true) + g7 = create(:namespace_with_plan, plan: :free_plan) p1_for_g7 = create(:project, namespace: g7) @@ -77,38 +80,59 @@ # first run trims 2 namespaces: g2 and g3. g1 already within limit and is skipped described_class.new.perform - expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 1, 2, 0, 0, 0, 0]) - expect_shared_setting_remediated(namespaces: namespaces, remediated_namespaces: [g1, g2, g3]) - expect(ProjectGroupLink.in_project(g2.all_projects)).to match_array([internal_pgl_for_g2]) - expect(GroupGroupLink.in_shared_group(g2.self_and_descendants)).to match_array([internal_ggl_for_g2]) - # second run skips g4 trims g5, g6 + aggregate_failures do + expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 1, 2, 0, 0, 0, 0]) + expect_shared_setting_remediated(namespaces: namespaces, remediated_namespaces: [g1, g2, g3]) + expect(ProjectGroupLink.in_project(g2.all_projects)).to match_array([internal_pgl_for_g2]) + expect(GroupGroupLink.in_shared_group(g2.self_and_descendants)).to match_array([internal_ggl_for_g2]) + end + + # second run skips g4, g6 trims g5, g7 described_class.new.perform - expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 1, 2, 0, 4, 5, 0]) - expect_shared_setting_remediated(namespaces: namespaces, remediated_namespaces: [g1, g2, g3, g5, g6]) - expect(ProjectGroupLink.in_project(g4.all_projects)) - .to match_array([internal_pgl_for_g4, external_pgl_for_g4]) - expect(GroupGroupLink.in_shared_group(g4.self_and_descendants)) - .to match_array([internal_ggl_for_g4, external_ggl_for_g4]) - - # third run trims g7 + + aggregate_failures do + expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 1, 2, 0, 4, 0, 7]) + expect_shared_setting_remediated(namespaces: namespaces, remediated_namespaces: [g1, g2, g3, g5, g7]) + expect(ProjectGroupLink.in_project(g4.all_projects)) + .to match_array([internal_pgl_for_g4, external_pgl_for_g4]) + expect(GroupGroupLink.in_shared_group(g4.self_and_descendants)) + .to match_array([internal_ggl_for_g4, external_ggl_for_g4]) + end + + # third run updates exclusion setting to false and trims g6 + g6.namespace_settings.update_column(:exclude_from_free_user_cap, false) + described_class.new.perform - expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 1, 2, 0, 4, 5, 7]) - expect_shared_setting_remediated(namespaces: namespaces, remediated_namespaces: [g1, g2, g3, g5, g6, g7]) + + aggregate_failures do + expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 1, 2, 0, 4, 5, 7]) + expect_shared_setting_remediated(namespaces: namespaces, remediated_namespaces: [g1, g2, g3, g5, g6, g7]) + end # fourth run finally updates g4, which is downgraded to free g4.gitlab_subscription.update!(hosted_plan: create(:free_plan)) + described_class.new.perform - expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 1, 2, 3, 4, 5, 7]) - expect_shared_setting_remediated(namespaces: namespaces, remediated_namespaces: [g1, g2, g3, g5, g6, g7, g4]) - expect(ProjectGroupLink.in_project(g4.all_projects)).to match_array([internal_pgl_for_g4]) - expect(GroupGroupLink.in_shared_group(g4.self_and_descendants)).to match_array([internal_ggl_for_g4]) + + aggregate_failures do + expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 1, 2, 3, 4, 5, 7]) + expect_shared_setting_remediated(namespaces: namespaces, + remediated_namespaces: [g1, g2, g3, g5, g6, g7, g4]) + expect(ProjectGroupLink.in_project(g4.all_projects)).to match_array([internal_pgl_for_g4]) + expect(GroupGroupLink.in_shared_group(g4.self_and_descendants)).to match_array([internal_ggl_for_g4]) + end # fifth run trims g2 which adds more members create_list(:group_member, 4, :active, source: g2) + described_class.new.perform - expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 5, 2, 3, 4, 5, 7]) - expect_shared_setting_remediated(namespaces: namespaces, remediated_namespaces: [g1, g2, g3, g4, g5, g6, g7]) + + aggregate_failures do + expect(namespaces.map { |ns| Member.in_hierarchy(ns).awaiting.count }).to eq([0, 5, 2, 3, 4, 5, 7]) + expect_shared_setting_remediated(namespaces: namespaces, + remediated_namespaces: [g1, g2, g3, g4, g5, g6, g7]) + end end def expect_shared_setting_remediated(namespaces:, remediated_namespaces:)