Commit 27b28c1a authored by Doug Stull's avatar Doug Stull

Merge branch '356700-follow-up-from-add-alert-for-free-plans-at-limit-as-a-preview' into 'master'

Create class to encapsulate free user cap logic

See merge request gitlab-org/gitlab!83682
parents 5a6c3635 5ae4e946
......@@ -8,11 +8,11 @@ module EE
def common_invite_modal_dataset(source)
dataset = super
if source.root_ancestor.apply_free_user_cap? && !source.root_ancestor.user_namespace?
if !source.root_ancestor.user_namespace? && ::Namespaces::FreeUserCap.new(source.root_ancestor).enforce_cap?
dataset.merge({
new_trial_registration_path: new_trial_path,
purchase_path: group_billings_path(source.root_ancestor),
free_users_limit: ::Plan::FREE_USER_LIMIT,
free_users_limit: ::Namespaces::FreeUserCap::FREE_USER_LIMIT,
members_count: source.root_ancestor.free_plan_members_count
})
else
......
......@@ -12,7 +12,7 @@ module EE
return false if user_dismissed_for_group(PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT, namespace, 14.days.ago)
return false unless Ability.allowed?(current_user, :owner_access, namespace)
namespace.preview_free_user_cap_over?
::Namespaces::PreviewFreeUserCap.new(namespace).over_limit?
end
end
end
......
......@@ -458,34 +458,12 @@ module EE
::Feature.enabled?(:saas_user_caps, root_ancestor, default_enabled: :yaml)
end
def apply_free_user_cap?
return false unless ::Gitlab.com?
return false unless has_free_or_no_subscription?
::Feature.enabled?(:free_user_cap, root_ancestor, default_enabled: :yaml)
end
def apply_user_cap?
user_cap_available? || apply_free_user_cap?
end
def free_user_cap_reached?
return false unless apply_free_user_cap?
free_plan_at_user_limit?
end
def preview_free_user_cap_over?
return false unless apply_preview_free_user_cap?
members_count = root_ancestor.free_plan_members_count
return false unless members_count
members_count > ::Plan::FREE_USER_LIMIT
user_cap_available? || free_user_cap.enforce_cap?
end
def user_limit_reached?(use_cache: false)
free_user_cap_reached?
free_user_cap.reached_limit?
end
def free_plan_user_ids
......@@ -496,18 +474,8 @@ module EE
private
def apply_preview_free_user_cap?
return false unless ::Gitlab::CurrentSettings.should_check_namespace_plan?
return false unless ::Feature.enabled?(:preview_free_user_cap, root_ancestor, default_enabled: :yaml)
has_free_or_no_subscription?
end
def free_plan_at_user_limit?
members_count = root_ancestor.free_plan_members_count
return false unless members_count
::Plan::FREE_USER_LIMIT <= members_count
def free_user_cap
@free_user_cap ||= ::Namespaces::FreeUserCap.new(self)
end
# Members belonging directly to Projects within user/project namespaces
......
......@@ -16,8 +16,6 @@ module EE
PREMIUM_TRIAL = 'premium_trial'
OPEN_SOURCE = 'opensource'
FREE_USER_LIMIT = 5
EE_DEFAULT_PLANS = (const_get(:DEFAULT_PLANS, false) + [FREE]).freeze
PAID_HOSTED_PLANS = [BRONZE, SILVER, PREMIUM, GOLD, ULTIMATE, ULTIMATE_TRIAL, PREMIUM_TRIAL, OPEN_SOURCE].freeze
EE_ALL_PLANS = (EE_DEFAULT_PLANS + PAID_HOSTED_PLANS).freeze
......
# frozen_string_literal: true
module Namespaces
class FreeUserCap
FREE_USER_LIMIT = 5
def initialize(root_namespace)
@root_namespace = root_namespace.root_ancestor # just in case the true root isn't passed
end
def reached_limit?
return false unless enforce_cap?
users_count >= FREE_USER_LIMIT
end
def enforce_cap?
return false unless enforceable_subscription?
feature_enabled?
end
private
attr_reader :root_namespace
def users_count
root_namespace.free_plan_members_count || 0
end
def enforceable_subscription?
::Gitlab::CurrentSettings.should_check_namespace_plan? && root_namespace.has_free_or_no_subscription?
end
def feature_enabled?
::Feature.enabled?(:free_user_cap, root_namespace, default_enabled: :yaml)
end
end
end
# frozen_string_literal: true
# to be removed upon rollout finishing for https://gitlab.com/gitlab-org/gitlab/-/issues/356561
module Namespaces
class PreviewFreeUserCap < FreeUserCap
def over_limit?
return false unless enforce_cap?
users_count > FREE_USER_LIMIT
end
private
def feature_enabled?
::Feature.enabled?(:preview_free_user_cap, root_namespace, default_enabled: :yaml)
end
end
end
......@@ -10,13 +10,13 @@
feature_id: Users::GroupCalloutsHelper::PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT,
group_id: source.root_ancestor.id,
testid: 'user-over-limit-free-plan-alert' },
title: _('From June 22, 2022 (GitLab 15.1), free personal namespaces and top-level groups will be limited to %{free_limit} members') % { free_limit: ::Plan::FREE_USER_LIMIT },
title: _('From June 22, 2022 (GitLab 15.1), free personal namespaces and top-level groups will be limited to %{free_limit} members') % { free_limit: ::Namespaces::FreeUserCap::FREE_USER_LIMIT },
close_button_data: { track_action: 'dismiss_banner',
track_label: 'user_limit_banner',
testid: 'user-over-limit-free-plan-dismiss' } do
.gl-alert-body
= _('Your %{doc_link_start}namespace%{doc_link_end}, %{strong_start}%{namespace_name}%{strong_end} has more than %{free_limit} members. From June 22, 2022, it will be limited to %{free_limit}, and the remaining members will get a %{link_start}status of Over limit%{link_end} and lose access to the namespace. You can go to the Usage Quotas page to manage which %{free_limit} members will remain in your namespace. To get more members, an owner can start a trial or upgrade to a paid tier.').html_safe % { namespace_name: source.root_ancestor.name,
free_limit: ::Plan::FREE_USER_LIMIT,
free_limit: ::Namespaces::FreeUserCap::FREE_USER_LIMIT,
doc_link_start: '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/index', anchor: 'namespaces') },
doc_link_end: '</a>'.html_safe,
strong_start: "<strong>".html_safe,
......
......@@ -9,16 +9,20 @@ RSpec.describe EE::InviteMembersHelper do
let(:notification_attributes) do
{
free_users_limit: 5,
members_count: 0,
new_trial_registration_path: '/-/trials/new',
purchase_path: "/groups/#{project.root_ancestor.path}/-/billings"
free_users_limit: ::Namespaces::FreeUserCap::FREE_USER_LIMIT,
members_count: project.root_ancestor.free_plan_members_count,
new_trial_registration_path: new_trial_path,
purchase_path: group_billings_path(project.root_ancestor)
}
end
before do
stub_ee_application_setting(should_check_namespace_plan: true)
end
context 'when applying the free user cap is not valid' do
let!(:group) do
build(:group, projects: [project], gitlab_subscription: build(:gitlab_subscription, :default))
create(:group_with_plan, projects: [project], plan: :default_plan)
end
it 'does not include users limit notification data' do
......@@ -39,7 +43,7 @@ RSpec.describe EE::InviteMembersHelper do
context 'when group namespace' do
let!(:group) do
build(:group, projects: [project], gitlab_subscription: build(:gitlab_subscription, :free))
create(:group_with_plan, projects: [project], plan: :free_plan)
end
it 'includes users limit notification data' do
......
......@@ -16,7 +16,9 @@ RSpec.describe EE::Users::GroupCalloutsHelper do
subject { helper.show_user_over_limit_free_plan_alert?(group) }
before do
allow(group).to receive(:preview_free_user_cap_over?).and_return(preview_free_user_cap_over?)
allow_next_instance_of(::Namespaces::PreviewFreeUserCap) do |preview_free_user_cap|
allow(preview_free_user_cap).to receive(:over_limit?).and_return(preview_free_user_cap_over?)
end
end
context 'when it is a group namespace' do
......
......@@ -1320,24 +1320,6 @@ RSpec.describe Group do
end
end
describe '#free_user_cap_reached?' do
subject(:free_user_cap_reached_for_group?) { group.free_user_cap_reached? }
context 'when this group has no root ancestor' do
it_behaves_like 'returning the right value for free_user_cap_reached?' do
let_it_be(:group) { create(:group) }
let(:root_group) { group }
end
end
context 'when this group has a root ancestor' do
it_behaves_like 'returning the right value for free_user_cap_reached?' do
let_it_be(:group) { create(:group) }
let_it_be(:root_group) { create(:group, children: [group]) }
end
end
end
describe '#users_count' do
subject { group.users_count }
......@@ -2162,7 +2144,7 @@ RSpec.describe Group do
end
describe '#user_limit_reached?' do
where(:user_cap_reached, :free_user_cap_reached, :result) do
where(:user_cap_reached, :reached_free_limit, :result) do
false | false | false
false | true | true
true | false | true
......@@ -2174,7 +2156,8 @@ RSpec.describe Group do
with_them do
before do
allow(group).to receive(:user_cap_reached?).and_return(user_cap_reached)
allow(group).to receive(:free_user_cap_reached?).and_return(free_user_cap_reached)
free_user_cap = instance_double(Namespaces::FreeUserCap, reached_limit?: reached_free_limit)
allow(group).to receive(:free_user_cap).and_return(free_user_cap)
end
it { is_expected.to eq(result) }
......
......@@ -967,97 +967,8 @@ RSpec.describe Namespace do
end
end
describe '#free_user_cap_reached?' do
subject(:free_user_cap_reached_for_group?) { group.free_user_cap_reached? }
it_behaves_like 'returning the right value for free_user_cap_reached?' do
let_it_be(:group) { create(:user).namespace }
let(:root_group) { group }
end
end
describe '#preview_free_user_cap_over?', :saas do
let_it_be(:namespace) { create(:group_with_plan, plan: :free_plan) }
let(:should_check_namespace_plan) { true }
before do
stub_ee_application_setting(should_check_namespace_plan: should_check_namespace_plan)
end
subject(:preview_free_user_cap_over?) { namespace.preview_free_user_cap_over? }
context 'when :preview_free_user_cap is disabled' do
before do
stub_feature_flags(preview_free_user_cap: false)
end
it { is_expected.to be false }
end
context 'when :preview_free_user_cap is enabled' do
before do
stub_feature_flags(preview_free_user_cap: true)
end
it { is_expected.to be false }
context 'when the member counts should be compared for that root ancestor' do
before do
allow(namespace).to receive(:free_plan_members_count).and_return(free_plan_members_count)
end
context 'when under the number of free users limit' do
let(:free_plan_members_count) { 3 }
it { is_expected.to be false }
end
context 'when at the same number as the free users limit' do
let(:free_plan_members_count) { ::Plan::FREE_USER_LIMIT }
it { is_expected.to be false }
end
context 'when over the number of free users limit' do
let(:free_plan_members_count) { 6 }
it { is_expected.to be true }
context 'when the namespace is not a group' do
let_it_be(:namespace) do
namespace = create(:user).namespace
create(:gitlab_subscription, hosted_plan: create(:free_plan), namespace: namespace)
namespace
end
it { is_expected.to be true }
end
context 'when it is a non free plan' do
let_it_be(:namespace) { create(:group_with_plan, plan: :ultimate_plan) }
it { is_expected.to be false }
end
context 'when no plan exists' do
let_it_be(:namespace) { create(:group) }
it { is_expected.to be true }
end
context 'when should check namespace plan is false' do
let(:should_check_namespace_plan) { false }
it { is_expected.to be false }
end
end
end
end
end
describe '#user_limit_reached?' do
where(:free_user_cap_reached) do
where(:reached_free_limit) do
[
true,
false
......@@ -1070,10 +981,11 @@ RSpec.describe Namespace do
with_them do
before do
allow(namespace).to receive(:free_user_cap_reached?).and_return(free_user_cap_reached)
free_user_cap = instance_double(Namespaces::FreeUserCap, reached_limit?: reached_free_limit)
allow(namespace).to receive(:free_user_cap).and_return(free_user_cap)
end
it { is_expected.to eq(free_user_cap_reached) }
it { is_expected.to eq(reached_free_limit) }
end
end
......@@ -1750,63 +1662,10 @@ RSpec.describe Namespace do
end
end
describe '#apply_free_user_cap?', :saas do
let_it_be(:namespace) { create(:group_with_plan, plan: :free_plan) }
let_it_be(:subgroup) { create(:group, parent: namespace) }
subject(:apply_free_user_cap?) { namespace.apply_free_user_cap? }
context 'when not on Gitlab.com' do
before do
allow(::Gitlab).to receive(:com?).and_return(false)
end
it { is_expected.to be false }
end
context 'when :free_user_cap is disabled' do
before do
stub_feature_flags(free_user_cap: false)
end
it { is_expected.to be false }
end
context 'when :free_user_cap is enabled' do
before do
stub_feature_flags(free_user_cap: true)
end
it { is_expected.to be true }
context 'when the namespace is not a group' do
let_it_be(:namespace) do
namespace = create(:user).namespace
create(:gitlab_subscription, hosted_plan: create(:free_plan), namespace: namespace)
namespace
end
it { is_expected.to be true }
end
context 'when it is a non free plan' do
let_it_be(:namespace) { create(:group_with_plan, plan: :ultimate_plan) }
it { is_expected.to be false }
end
context 'when no plan exists' do
let_it_be(:namespace) { create(:group) }
it { is_expected.to be true }
end
end
end
describe '#apply_user_cap?' do
let(:namespace) { build(:namespace) }
where(:user_cap_available, :apply_free_user_cap, :result) do
where(:user_cap_available, :enforce_free_cap, :result) do
false | false | false
false | true | true
true | false | true
......@@ -1817,8 +1676,9 @@ RSpec.describe Namespace do
with_them do
before do
free_user_cap = instance_double(Namespaces::FreeUserCap, enforce_cap?: enforce_free_cap)
allow(namespace).to receive(:user_cap_available?).and_return(user_cap_available)
allow(namespace).to receive(:apply_free_user_cap?).and_return(apply_free_user_cap)
allow(namespace).to receive(:free_user_cap).and_return(free_user_cap)
end
it { is_expected.to eq(result) }
......
......@@ -285,6 +285,11 @@ RSpec.describe Member, type: :model do
group.add_developer(create(:user))
end
before do
allow(group).to receive(:user_cap_available?).and_return(false)
stub_ee_application_setting(should_check_namespace_plan: true)
end
context 'when the :free_user_cap feature flag is disabled' do
before do
stub_feature_flags(free_user_cap: false)
......@@ -341,7 +346,7 @@ RSpec.describe Member, type: :model do
context 'when the free user cap has been reached' do
before do
stub_const('::Plan::FREE_USER_LIMIT', 1)
stub_const('::Namespaces::FreeUserCap::FREE_USER_LIMIT', 1)
end
it 'sets the group member to awaiting' do
......@@ -364,7 +369,7 @@ RSpec.describe Member, type: :model do
context 'when multiple members are added' do
before do
stub_const('::Plan::FREE_USER_LIMIT', 2)
stub_const('::Namespaces::FreeUserCap::FREE_USER_LIMIT', 2)
end
it 'sets members to the correct status' do
......@@ -418,7 +423,7 @@ RSpec.describe Member, type: :model do
end
before do
stub_const('::Plan::FREE_USER_LIMIT', 2)
stub_const('::Namespaces::FreeUserCap::FREE_USER_LIMIT', 2)
end
it 'adds multiple members and correctly shows the state' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::FreeUserCap, :saas do
let_it_be(:namespace) { create(:group_with_plan, plan: :free_plan) }
let(:should_check_namespace_plan) { true }
before do
stub_ee_application_setting(should_check_namespace_plan: should_check_namespace_plan)
end
describe '#reached_limit?' do
let(:free_plan_members_count) { described_class::FREE_USER_LIMIT + 1 }
subject(:reached_limit?) { described_class.new(namespace).reached_limit? }
before do
stub_ee_application_setting(should_check_namespace_plan: should_check_namespace_plan)
allow(namespace).to receive(:free_plan_members_count).and_return(free_plan_members_count)
end
context 'when :free_user_cap is disabled' do
before do
stub_feature_flags(free_user_cap: false)
end
it { is_expected.to be false }
end
context 'when :free_user_cap is enabled' do
before do
stub_feature_flags(free_user_cap: true)
end
context 'when under the number of free users limit' do
let(:free_plan_members_count) { described_class::FREE_USER_LIMIT - 1 }
it { is_expected.to be false }
end
context 'when at the same number as the free users limit' do
let(:free_plan_members_count) { described_class::FREE_USER_LIMIT }
it { is_expected.to be true }
end
context 'when over the number of free users limit' do
context 'when it is a free plan' do
it { is_expected.to be true }
context 'when the namespace is not a group' do
let_it_be(:namespace) do
namespace = create(:user).namespace
create(:gitlab_subscription, hosted_plan: create(:free_plan), namespace: namespace)
namespace
end
it { is_expected.to be true }
end
end
context 'when it is a non free plan' do
let_it_be(:namespace) { create(:group_with_plan, plan: :ultimate_plan) }
it { is_expected.to be false }
end
context 'when no plan exists' do
let_it_be(:namespace) { create(:group) }
it { is_expected.to be true }
end
context 'when should check namespace plan is false' do
let(:should_check_namespace_plan) { false }
it { is_expected.to be false }
end
end
end
end
describe '#over_limit?'
describe '#enforce_cap?' do
subject(:enforce_cap?) { described_class.new(namespace).enforce_cap? }
context 'when :free_user_cap is disabled' do
before do
stub_feature_flags(free_user_cap: false)
end
it { is_expected.to be false }
end
context 'when :free_user_cap is enabled' do
before do
stub_feature_flags(free_user_cap: true)
end
context 'when it is a free plan' do
it { is_expected.to be true }
end
context 'when it is a non free plan' do
let_it_be(:namespace) { create(:group_with_plan, plan: :ultimate_plan) }
it { is_expected.to be false }
end
context 'when no plan exists' do
let_it_be(:namespace) { create(:group) }
it { is_expected.to be true }
end
context 'when should check namespace plan is false' do
let(:should_check_namespace_plan) { false }
it { is_expected.to be false }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::PreviewFreeUserCap, :saas do
let_it_be(:namespace) { create(:group_with_plan, plan: :free_plan) }
let(:should_check_namespace_plan) { true }
before do
stub_ee_application_setting(should_check_namespace_plan: should_check_namespace_plan)
end
describe '#over_limit?' do
let(:free_plan_members_count) { described_class::FREE_USER_LIMIT + 1 }
subject(:over_limit?) { described_class.new(namespace).over_limit? }
before do
stub_ee_application_setting(should_check_namespace_plan: should_check_namespace_plan)
allow(namespace).to receive(:free_plan_members_count).and_return(free_plan_members_count)
end
context 'when :preview_free_user_cap is disabled' do
before do
stub_feature_flags(preview_free_user_cap: false)
end
it { is_expected.to be false }
end
context 'when :preview_free_user_cap is enabled' do
before do
stub_feature_flags(preview_free_user_cap: true)
end
context 'when under the number of free users limit' do
let(:free_plan_members_count) { described_class::FREE_USER_LIMIT - 1 }
it { is_expected.to be false }
end
context 'when at the same number as the free users limit' do
let(:free_plan_members_count) { described_class::FREE_USER_LIMIT }
it { is_expected.to be false }
end
context 'when over the number of free users limit' do
context 'when it is a free plan' do
it { is_expected.to be true }
context 'when the namespace is not a group' do
let_it_be(:namespace) do
namespace = create(:user).namespace
create(:gitlab_subscription, hosted_plan: create(:free_plan), namespace: namespace)
namespace
end
it { is_expected.to be true }
end
end
context 'when it is a non free plan' do
let_it_be(:namespace) { create(:group_with_plan, plan: :ultimate_plan) }
it { is_expected.to be false }
end
context 'when no plan exists' do
let_it_be(:namespace) { create(:group) }
it { is_expected.to be true }
end
context 'when should check namespace plan is false' do
let(:should_check_namespace_plan) { false }
it { is_expected.to be false }
end
end
end
end
end
......@@ -129,7 +129,8 @@ RSpec.describe Members::CreateService do
let_it_be(:over_limit_user) { project_users.last }
before do
stub_const('::Plan::FREE_USER_LIMIT', 3)
stub_const('::Namespaces::FreeUserCap::FREE_USER_LIMIT', 3)
stub_ee_application_setting(should_check_namespace_plan: true)
end
context 'with a group-less project' do
......
......@@ -3,8 +3,9 @@
RSpec.shared_examples_for 'over the free user limit alert' do
before do
stub_ee_application_setting(should_check_namespace_plan: true)
stub_feature_flags(free_user_cap: false)
stub_feature_flags(preview_free_user_cap: true)
stub_const('::Plan::FREE_USER_LIMIT', 1)
stub_const('::Namespaces::FreeUserCap::FREE_USER_LIMIT', 1)
end
it 'shows free user limit warning and honors dismissal', :js do
......
# frozen_string_literal: true
RSpec.shared_examples 'returning the right value for free_user_cap_reached?' do
context 'when free user cap feature is not applied' do
before do
allow(group).to receive(:apply_free_user_cap?).and_return(false)
end
it { is_expected.to be_falsey }
end
context 'when free user cap feature is applied' do
before do
allow(group).to receive(:apply_free_user_cap?).and_return(true)
end
context 'when the :saas_user_caps feature flag is not enabled' do
it { is_expected.to be_falsey }
end
context 'when the :free_user_cap feature flag is enabled' do
before do
stub_feature_flags(free_user_cap: true)
allow(root_group).to receive(:apply_free_user_cap?).and_return(true)
allow(root_group).to receive(:has_free_or_no_subscription?).and_return(free_plan)
end
let(:free_plan) { false }
context 'when no free user cap has been set to that root ancestor' do
it { is_expected.to be_falsey }
end
context 'when a free user cap has been set to that root ancestor' do
let(:free_plan) { true }
before do
allow(root_group).to receive(:free_plan_members_count).and_return(free_plan_members_count)
allow(group).to receive(:root_ancestor).and_return(root_group)
end
context 'when the free cap is higher than the number of billable members' do
let(:free_plan_members_count) { 3 }
it { is_expected.to be_falsey }
end
context 'when the free cap is the same as the number of billable members' do
let(:free_plan_members_count) { ::Plan::FREE_USER_LIMIT }
it { is_expected.to be_truthy }
end
context 'when the free cap is lower than the number of billable members' do
let(:free_plan_members_count) { 6 }
it { is_expected.to be_truthy }
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment