Commit c653ae39 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'dm-license-features' into 'master'

Simplify specification of EE license/plan features

Closes #2466

See merge request gitlab-org/gitlab-ee!2943
parents 13ef4412 4aa10f53
class License < ActiveRecord::Base class License < ActiveRecord::Base
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
ADMIN_AUDIT_LOG_FEATURE = 'GitLab_AdminAuditLog'.freeze
AUDIT_EVENTS_FEATURE = 'GitLab_AuditEvents'.freeze
AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze
BURNDOWN_CHARTS_FEATURE = 'GitLab_BurndownCharts'.freeze
CONTRIBUTION_ANALYTICS_FEATURE = 'GitLab_ContributionAnalytics'.freeze
CROSS_PROJECT_PIPELINES_FEATURE = 'GitLab_CrossProjectPipelines'.freeze
DB_LOAD_BALANCING_FEATURE = 'GitLab_DbLoadBalancing'.freeze
DEPLOY_BOARD_FEATURE = 'GitLab_DeployBoard'.freeze
ELASTIC_SEARCH_FEATURE = 'GitLab_ElasticSearch'.freeze
EXPORT_ISSUES_FEATURE = 'GitLab_ExportIssues'.freeze
FAST_FORWARD_MERGE_FEATURE = 'GitLab_FastForwardMerge'.freeze
FILE_LOCKS_FEATURE = 'GitLab_FileLocks'.freeze
GEO_FEATURE = 'GitLab_Geo'.freeze
GROUP_WEBHOOKS_FEATURE = 'GitLab_GroupWebhooks'.freeze
ISSUABLE_DEFAULT_TEMPLATES_FEATURE = 'GitLab_IssuableDefaultTemplates'.freeze
ISSUE_BOARD_FOCUS_MODE_FEATURE = 'GitLab_IssueBoardFocusMode'.freeze
ISSUE_BOARD_MILESTONE_FEATURE = 'GitLab_IssueBoardMilestone'.freeze
GROUP_ISSUE_BOARDS_FEATURE = 'GitLab_GroupIssueBoards'.freeze
ISSUE_WEIGHTS_FEATURE = 'GitLab_IssueWeights'.freeze
JENKINS_INTEGRATION_FEATURE = 'GitLab_JenkinsIntegration'.freeze
JIRA_DEV_PANEL_INTEGRATION_FEATURE = 'GitLab_JiraDevelopmentPanelIntegration'.freeze
LDAP_EXTRAS_FEATURE = 'GitLab_LdapExtras'.freeze
MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze
MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze
MERGE_REQUEST_SQUASH_FEATURE = 'GitLab_MergeRequestSquash'.freeze
MULTIPLE_ISSUE_ASSIGNEES_FEATURE = 'GitLab_MultipleIssueAssignees'.freeze
MULTIPLE_ISSUE_BOARDS_FEATURE = 'GitLab_MultipleIssueBoards'.freeze
OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze
PROTECTED_REFS_FOR_USERS_FEATURE = 'GitLab_RefPermissionsForUsers'.freeze
PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze
RELATED_ISSUES_FEATURE = 'GitLab_RelatedIssues'.freeze
REPOSITORY_MIRRORS_FEATURE = 'GitLab_RepositoryMirrors'.freeze
REPOSITORY_SIZE_LIMIT_FEATURE = 'GitLab_RepositorySizeLimit'.freeze
SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze
VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze
FEATURE_CODES = {
admin_audit_log: ADMIN_AUDIT_LOG_FEATURE,
auditor_user: AUDITOR_USER_FEATURE,
db_load_balancing: DB_LOAD_BALANCING_FEATURE,
elastic_search: ELASTIC_SEARCH_FEATURE,
geo: GEO_FEATURE,
ldap_extras: LDAP_EXTRAS_FEATURE,
object_storage: OBJECT_STORAGE_FEATURE,
related_issues: RELATED_ISSUES_FEATURE,
repository_size_limit: REPOSITORY_SIZE_LIMIT_FEATURE,
service_desk: SERVICE_DESK_FEATURE,
variable_environment_scope: VARIABLE_ENVIRONMENT_SCOPE_FEATURE,
# Features that make sense to Namespace:
audit_events: AUDIT_EVENTS_FEATURE,
burndown_charts: BURNDOWN_CHARTS_FEATURE,
contribution_analytics: CONTRIBUTION_ANALYTICS_FEATURE,
cross_project_pipelines: CROSS_PROJECT_PIPELINES_FEATURE,
deploy_board: DEPLOY_BOARD_FEATURE,
export_issues: EXPORT_ISSUES_FEATURE,
fast_forward_merge: FAST_FORWARD_MERGE_FEATURE,
file_locks: FILE_LOCKS_FEATURE,
group_webhooks: GROUP_WEBHOOKS_FEATURE,
issuable_default_templates: ISSUABLE_DEFAULT_TEMPLATES_FEATURE,
issue_board_focus_mode: ISSUE_BOARD_FOCUS_MODE_FEATURE,
issue_board_milestone: ISSUE_BOARD_MILESTONE_FEATURE,
group_issue_boards: GROUP_ISSUE_BOARDS_FEATURE,
issue_weights: ISSUE_WEIGHTS_FEATURE,
jenkins_integration: JENKINS_INTEGRATION_FEATURE,
jira_dev_panel_integration: JIRA_DEV_PANEL_INTEGRATION_FEATURE,
merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE,
merge_request_rebase: MERGE_REQUEST_REBASE_FEATURE,
merge_request_squash: MERGE_REQUEST_SQUASH_FEATURE,
multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE,
multiple_issue_boards: MULTIPLE_ISSUE_BOARDS_FEATURE,
protected_refs_for_users: PROTECTED_REFS_FOR_USERS_FEATURE,
push_rules: PUSH_RULES_FEATURE,
repository_mirrors: REPOSITORY_MIRRORS_FEATURE
}.freeze
STARTER_PLAN = 'starter'.freeze STARTER_PLAN = 'starter'.freeze
PREMIUM_PLAN = 'premium'.freeze PREMIUM_PLAN = 'premium'.freeze
ULTIMATE_PLAN = 'ultimate'.freeze ULTIMATE_PLAN = 'ultimate'.freeze
EARLY_ADOPTER_PLAN = 'early_adopter'.freeze EARLY_ADOPTER_PLAN = 'early_adopter'.freeze
EES_FEATURES = [ EES_FEATURES = %i[
{ AUDIT_EVENTS_FEATURE => 1 }, audit_events
{ BURNDOWN_CHARTS_FEATURE => 1 }, burndown_charts
{ CONTRIBUTION_ANALYTICS_FEATURE => 1 }, contribution_analytics
{ ELASTIC_SEARCH_FEATURE => 1 }, elastic_search
{ EXPORT_ISSUES_FEATURE => 1 }, export_issues
{ FAST_FORWARD_MERGE_FEATURE => 1 }, fast_forward_merge
{ GROUP_WEBHOOKS_FEATURE => 1 }, group_webhooks
{ ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 }, issuable_default_templates
{ ISSUE_BOARD_FOCUS_MODE_FEATURE => 1 }, issue_board_focus_mode
{ ISSUE_BOARD_MILESTONE_FEATURE => 1 }, issue_board_milestone
{ ISSUE_WEIGHTS_FEATURE => 1 }, issue_weights
{ JENKINS_INTEGRATION_FEATURE => 1 }, jenkins_integration
{ LDAP_EXTRAS_FEATURE => 1 }, ldap_extras
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, merge_request_approvers
{ MERGE_REQUEST_REBASE_FEATURE => 1 }, merge_request_rebase
{ MERGE_REQUEST_SQUASH_FEATURE => 1 }, merge_request_squash
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 }, multiple_issue_assignees
{ MULTIPLE_ISSUE_BOARDS_FEATURE => 1 }, multiple_issue_boards
{ PUSH_RULES_FEATURE => 1 }, push_rules
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 }, protected_refs_for_users
{ RELATED_ISSUES_FEATURE => 1 }, related_issues
{ REPOSITORY_MIRRORS_FEATURE => 1 }, repository_mirrors
{ REPOSITORY_SIZE_LIMIT_FEATURE => 1 } repository_size_limit
].freeze ].freeze
EEP_FEATURES = [ EEP_FEATURES = EES_FEATURES + %i[
*EES_FEATURES, admin_audit_log
{ ADMIN_AUDIT_LOG_FEATURE => 1 }, auditor_user
{ AUDITOR_USER_FEATURE => 1 }, cross_project_pipelines
{ CROSS_PROJECT_PIPELINES_FEATURE => 1 }, db_load_balancing
{ DB_LOAD_BALANCING_FEATURE => 1 }, deploy_board
{ DEPLOY_BOARD_FEATURE => 1 }, file_locks
{ FILE_LOCKS_FEATURE => 1 }, geo
{ GEO_FEATURE => 1 }, group_issue_boards
{ OBJECT_STORAGE_FEATURE => 1 }, jira_dev_panel_integration
{ JIRA_DEV_PANEL_INTEGRATION_FEATURE => 1 }, object_storage
{ SERVICE_DESK_FEATURE => 1 }, service_desk
{ VARIABLE_ENVIRONMENT_SCOPE_FEATURE => 1 }, variable_environment_scope
{ GROUP_ISSUE_BOARDS_FEATURE => 1 }
].freeze ].freeze
EEU_FEATURES = [ EEU_FEATURES = EEP_FEATURES
*EEP_FEATURES
# ..
].freeze
# List all features available for early adopters, # List all features available for early adopters,
# i.e. users that started using GitLab.com before # i.e. users that started using GitLab.com before
...@@ -135,40 +55,32 @@ class License < ActiveRecord::Base ...@@ -135,40 +55,32 @@ class License < ActiveRecord::Base
# Obs.: Do not extend from other feature constants. # Obs.: Do not extend from other feature constants.
# Early adopters should not earn new features as they're # Early adopters should not earn new features as they're
# introduced. # introduced.
EARLY_ADOPTER_FEATURES = [ EARLY_ADOPTER_FEATURES = %i[
{ ADMIN_AUDIT_LOG_FEATURE => 1 }, audit_events
{ AUDIT_EVENTS_FEATURE => 1 }, burndown_charts
{ AUDITOR_USER_FEATURE => 1 }, contribution_analytics
{ BURNDOWN_CHARTS_FEATURE => 1 }, cross_project_pipelines
{ CONTRIBUTION_ANALYTICS_FEATURE => 1 }, deploy_board
{ CROSS_PROJECT_PIPELINES_FEATURE => 1 }, export_issues
{ DB_LOAD_BALANCING_FEATURE => 1 }, fast_forward_merge
{ DEPLOY_BOARD_FEATURE => 1 }, file_locks
{ ELASTIC_SEARCH_FEATURE => 1 }, group_webhooks
{ EXPORT_ISSUES_FEATURE => 1 }, issuable_default_templates
{ FAST_FORWARD_MERGE_FEATURE => 1 }, issue_board_focus_mode
{ FILE_LOCKS_FEATURE => 1 }, issue_board_milestone
{ GEO_FEATURE => 1 }, issue_weights
{ GROUP_WEBHOOKS_FEATURE => 1 }, jenkins_integration
{ ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 }, merge_request_approvers
{ ISSUE_BOARD_FOCUS_MODE_FEATURE => 1 }, merge_request_rebase
{ ISSUE_BOARD_MILESTONE_FEATURE => 1 }, merge_request_squash
{ ISSUE_WEIGHTS_FEATURE => 1 }, multiple_issue_assignees
{ JENKINS_INTEGRATION_FEATURE => 1 }, multiple_issue_boards
{ LDAP_EXTRAS_FEATURE => 1 }, protected_refs_for_users
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, push_rules
{ MERGE_REQUEST_REBASE_FEATURE => 1 }, related_issues
{ MERGE_REQUEST_SQUASH_FEATURE => 1 }, repository_mirrors
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 }, service_desk
{ MULTIPLE_ISSUE_BOARDS_FEATURE => 1 }, variable_environment_scope
{ OBJECT_STORAGE_FEATURE => 1 },
{ PROTECTED_REFS_FOR_USERS_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 },
{ REPOSITORY_MIRRORS_FEATURE => 1 },
{ REPOSITORY_SIZE_LIMIT_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 },
{ VARIABLE_ENVIRONMENT_SCOPE_FEATURE => 1 }
].freeze ].freeze
FEATURES_BY_PLAN = { FEATURES_BY_PLAN = {
...@@ -178,6 +90,29 @@ class License < ActiveRecord::Base ...@@ -178,6 +90,29 @@ class License < ActiveRecord::Base
EARLY_ADOPTER_PLAN => EARLY_ADOPTER_FEATURES EARLY_ADOPTER_PLAN => EARLY_ADOPTER_FEATURES
}.freeze }.freeze
# Add on codes that may occur in legacy licenses that don't have a plan yet.
FEATURES_FOR_ADD_ONS = {
'GitLab_Auditor_User' => :auditor_user,
'GitLab_DeployBoard' => :deploy_board,
'GitLab_FileLocks' => :file_locks,
'GitLab_Geo' => :geo,
'GitLab_ServiceDesk' => :service_desk
}.freeze
# Global features that cannot be restricted to only a subset of projects or namespaces.
# Use `License.feature_available?(:feature)` to check if these features are available.
# For all other features, use `project.feature_available?` or `namespace.feature_available?` when possible.
GLOBAL_FEATURES = %i[
admin_audit_log
auditor_user
db_load_balancing
elastic_search
geo
ldap_extras
object_storage
repository_size_limit
].freeze
validate :valid_license validate :valid_license
validate :check_users_limit, if: :new_record?, unless: :validate_with_trueup? validate :check_users_limit, if: :new_record?, unless: :validate_with_trueup?
validate :check_trueup, unless: :persisted?, if: :validate_with_trueup? validate :check_trueup, unless: :persisted?, if: :validate_with_trueup?
...@@ -192,7 +127,7 @@ class License < ActiveRecord::Base ...@@ -192,7 +127,7 @@ class License < ActiveRecord::Base
class << self class << self
def features_for_plan(plan) def features_for_plan(plan)
FEATURES_BY_PLAN.fetch(plan, []).reduce({}, :merge) FEATURES_BY_PLAN.fetch(plan, [])
end end
def current def current
...@@ -209,11 +144,12 @@ class License < ActiveRecord::Base ...@@ -209,11 +144,12 @@ class License < ActiveRecord::Base
RequestStore.delete(:current_license) RequestStore.delete(:current_license)
end end
def plan_includes_feature?(plan, code) def plan_includes_feature?(plan, feature)
features = features_for_plan(plan) if GLOBAL_FEATURES.include?(feature)
feature = FEATURE_CODES.fetch(code) raise ArgumentError, "Use `License.feature_available?` for features that cannot be restricted to only a subset of projects or namespaces"
end
features[feature].to_i > 0 features_for_plan(plan).include?(feature)
end end
def load_license def load_license
...@@ -278,20 +214,23 @@ class License < ActiveRecord::Base ...@@ -278,20 +214,23 @@ class License < ActiveRecord::Base
end end
# New licenses persists only the `plan` (premium, starter, ..). But, old licenses # New licenses persists only the `plan` (premium, starter, ..). But, old licenses
# keep `add_ons`, therefore this method needs to be backward-compatible in that sense. # keep `add_ons`.
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/2019
def add_ons def add_ons
explicit_add_ons = restricted_attr(:add_ons, {}) restricted_attr(:add_ons, {})
plan_features = self.class.features_for_plan(plan) end
def features_from_add_ons
add_ons.map { |name, count| FEATURES_FOR_ADD_ONS[name] if count > 0 }.compact
end
explicit_add_ons.merge(plan_features) def features
@features ||= (self.class.features_for_plan(plan) + features_from_add_ons).to_set
end end
def feature_available?(code) def feature_available?(feature)
return false if trial? && expired? return false if trial? && expired?
feature = FEATURE_CODES.fetch(code) features.include?(feature)
add_ons[feature].to_i > 0
end end
def restricted_user_count def restricted_user_count
......
...@@ -179,14 +179,11 @@ module EE ...@@ -179,14 +179,11 @@ module EE
!public? && shared_runners_enabled? && namespace.shared_runners_minutes_limit_enabled? !public? && shared_runners_enabled? && namespace.shared_runners_minutes_limit_enabled?
end end
# Checks licensed feature availability if `feature` matches any
# key on License::FEATURE_CODES. Otherwise, check feature availability
# through ProjectFeature.
def feature_available?(feature, user = nil) def feature_available?(feature, user = nil)
if License::FEATURE_CODES.key?(feature) if ProjectFeature::FEATURES.include?(feature)
licensed_feature_available?(feature)
else
super super
else
licensed_feature_available?(feature)
end end
end end
......
...@@ -94,6 +94,7 @@ module Gitlab ...@@ -94,6 +94,7 @@ module Gitlab
usage_data[:license_user_count] = license.restricted_user_count usage_data[:license_user_count] = license.restricted_user_count
usage_data[:license_starts_at] = license.starts_at usage_data[:license_starts_at] = license.starts_at
usage_data[:license_expires_at] = license.expires_at usage_data[:license_expires_at] = license.expires_at
usage_data[:license_plan] = license.plan
usage_data[:license_add_ons] = license.add_ons usage_data[:license_add_ons] = license.add_ons
end end
......
...@@ -107,11 +107,11 @@ describe Project do ...@@ -107,11 +107,11 @@ describe Project do
allow(namespace).to receive(:plan) { plan_license } allow(namespace).to receive(:plan) { plan_license }
end end
License::FEATURE_CODES.each do |feature_sym, feature_code| License::EEU_FEATURES.each do |feature_sym|
context feature_sym.to_s do
let(:feature) { feature_sym } let(:feature) { feature_sym }
let(:feature_code) { feature_code }
context feature_sym.to_s do
unless License::GLOBAL_FEATURES.include?(feature_sym)
context "checking #{feature_sym} availability both on Global and Namespace license" do context "checking #{feature_sym} availability both on Global and Namespace license" do
let(:check_namespace_plan) { true } let(:check_namespace_plan) { true }
...@@ -156,8 +156,9 @@ describe Project do ...@@ -156,8 +156,9 @@ describe Project do
end end
end end
end end
end
context "when checking #{feature_code} only for Global license" do context "when checking #{feature_sym} only for Global license" do
let(:check_namespace_plan) { false } let(:check_namespace_plan) { false }
context 'allowed by Global License' do context 'allowed by Global License' do
......
...@@ -9,9 +9,6 @@ module EE ...@@ -9,9 +9,6 @@ module EE
# This enables `geo` and disables `deploy_board` features for a spec. # This enables `geo` and disables `deploy_board` features for a spec.
# Other features are still enabled/disabled as defined in the licence. # Other features are still enabled/disabled as defined in the licence.
def stub_licensed_features(features) def stub_licensed_features(features)
unknown_features = features.keys - License::FEATURE_CODES.keys
raise "Unknown features: #{unknown_features.inspect}" unless unknown_features.empty?
allow(License).to receive(:feature_available?).and_call_original allow(License).to receive(:feature_available?).and_call_original
features.each do |feature, enabled| features.each do |feature, enabled|
......
...@@ -23,6 +23,7 @@ describe Gitlab::UsageData do ...@@ -23,6 +23,7 @@ describe Gitlab::UsageData do
counts counts
historical_max_users historical_max_users
license_add_ons license_add_ons
license_plan
license_expires_at license_expires_at
license_starts_at license_starts_at
license_user_count license_user_count
......
...@@ -214,21 +214,21 @@ describe License do ...@@ -214,21 +214,21 @@ describe License do
describe '.features_for_plan' do describe '.features_for_plan' do
it 'returns features for starter plan' do it 'returns features for starter plan' do
expect(described_class.features_for_plan('starter')) expect(described_class.features_for_plan('starter'))
.to include({ 'GitLab_MultipleIssueAssignees' => 1 }) .to include(:multiple_issue_assignees)
end end
it 'returns features for premium plan' do it 'returns features for premium plan' do
expect(described_class.features_for_plan('premium')) expect(described_class.features_for_plan('premium'))
.to include({ 'GitLab_MultipleIssueAssignees' => 1, 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 }) .to include(:multiple_issue_assignees, :deploy_board, :file_locks)
end end
it 'returns features for early adopter plan' do it 'returns features for early adopter plan' do
expect(described_class.features_for_plan('premium')) expect(described_class.features_for_plan('premium'))
.to include({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 } ) .to include(:deploy_board, :file_locks)
end end
it 'returns empty Hash if no features for given plan' do it 'returns empty array if no features for given plan' do
expect(described_class.features_for_plan('bronze')).to eq({}) expect(described_class.features_for_plan('bronze')).to eq([])
end end
end end
...@@ -264,8 +264,8 @@ describe License do ...@@ -264,8 +264,8 @@ describe License do
let(:plan) { 'premium' } let(:plan) { 'premium' }
let(:feature) { nil } let(:feature) { nil }
it 'raises KeyError' do it 'returns false' do
expect { subject }.to raise_error(KeyError) is_expected.to eq(false)
end end
end end
end end
...@@ -420,73 +420,60 @@ describe License do ...@@ -420,73 +420,60 @@ describe License do
end end
end end
describe '#add_ons' do describe '#features_from_add_ons' do
context 'without add-ons' do context 'without add-ons' do
it 'returns an empty Hash' do it 'returns an empty array' do
license = build_license_with_add_ons({}, plan: 'unknown') license = build_license_with_add_ons({}, plan: 'unknown')
expect(license.add_ons).to eq({}) expect(license.features_from_add_ons).to eq([])
end end
end end
context 'with add-ons' do context 'with add-ons' do
it 'returns all available add-ons' do it 'returns all available add-ons' do
license = build_license_with_add_ons({ License::DEPLOY_BOARD_FEATURE => 1, License::FILE_LOCKS_FEATURE => 2 }) license = build_license_with_add_ons({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 2 })
expect(license.add_ons.keys).to include(License::DEPLOY_BOARD_FEATURE, License::FILE_LOCKS_FEATURE)
end
it 'can return details about a single add-on' do
license = build_license_with_add_ons({ License::DEPLOY_BOARD_FEATURE => 2 })
expect(license.add_ons[License::DEPLOY_BOARD_FEATURE]).to eq(2)
end
end
context 'with extra features mapped by plan' do
it 'returns all available add-ons and extra features' do
license = build_license_with_add_ons({ License::DEPLOY_BOARD_FEATURE => 1 }, plan: License::PREMIUM_PLAN)
eep_features = License::EEP_FEATURES.reduce({}, :merge).keys
expect(license.add_ons.keys).to include(License::DEPLOY_BOARD_FEATURE, *eep_features) expect(license.features_from_add_ons).to match_array([:deploy_board, :file_locks])
end end
end end
end end
describe '#feature_available?' do describe '#feature_available?' do
it 'returns true if add-on exists and have a quantity greater than 0' do it 'returns true if add-on exists and have a quantity greater than 0' do
license = build_license_with_add_ons({ License::DEPLOY_BOARD_FEATURE => 1 }) license = build_license_with_add_ons({ 'GitLab_DeployBoard' => 1 })
expect(license.feature_available?(:deploy_board)).to eq(true) expect(license.feature_available?(:deploy_board)).to eq(true)
end end
it 'returns false if add-on exists but have a quantity of 0' do it 'returns true if the feature is included in the plan do' do
license = build_license_with_add_ons({ License::DEPLOY_BOARD_FEATURE => 0 }) license = build_license_with_add_ons({}, plan: License::PREMIUM_PLAN)
expect(license.feature_available?(:deploy_board)).to eq(false) expect(license.feature_available?(:auditor_user)).to eq(true)
end end
it 'returns false if add-on does not exists' do it 'returns false if add-on exists but have a quantity of 0' do
license = build_license_with_add_ons({}) license = build_license_with_add_ons({ 'GitLab_DeployBoard' => 0 })
expect(license.feature_available?(:deploy_board)).to eq(false) expect(license.feature_available?(:deploy_board)).to eq(false)
end end
it 'raises error if invalid symbol is sent' do it 'returns false if add-on does not exists' do
license = build_license_with_add_ons({}) license = build_license_with_add_ons({})
expect { license.feature_available?(:invalid) }.to raise_error(KeyError) expect(license.feature_available?(:deploy_board)).to eq(false)
expect(license.feature_available?(:auditor_user)).to eq(false)
end end
context 'with an expired trial license' do context 'with an expired trial license' do
let(:license) { create(:license, trial: true, expired: true) }
before(:all) do before(:all) do
described_class.destroy_all described_class.destroy_all
create(:license, trial: true, expired: true)
end end
::License::FEATURE_CODES.keys do |feature_code| ::License::EES_FEATURES.each do |feature|
it "returns false for #{feature_code}" do it "returns false for #{feature}" do
expect(license.feature_available?(feature_code)).to eq(false) expect(license.feature_available?(feature)).to eq(false)
end 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