summaryrefslogtreecommitdiff
path: root/spec/lib/feature_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/feature_spec.rb')
-rw-r--r--spec/lib/feature_spec.rb182
1 files changed, 116 insertions, 66 deletions
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 90c0684f8b7..6e32db09426 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Feature, stub_feature_flags: false do
expect_any_instance_of(Flipper::DSL).to receive(:feature).with(key)
.and_return(feature)
- expect(described_class.get(key)).to be(feature)
+ expect(described_class.get(key)).to eq(feature)
end
end
@@ -67,7 +67,7 @@ RSpec.describe Feature, stub_feature_flags: false do
expect(Gitlab::ProcessMemoryCache.cache_backend)
.to receive(:fetch)
.once
- .with('flipper/v1/features', expires_in: 1.minute)
+ .with('flipper/v1/features', { expires_in: 1.minute })
.and_call_original
2.times do
@@ -157,14 +157,65 @@ RSpec.describe Feature, stub_feature_flags: false do
describe '.enabled?' do
before do
allow(Feature).to receive(:log_feature_flag_states?).and_return(false)
+
+ stub_feature_flag_definition(:disabled_feature_flag)
+ stub_feature_flag_definition(:enabled_feature_flag, default_enabled: true)
+ end
+
+ context 'when self-recursive' do
+ before do
+ allow(Feature).to receive(:with_feature).and_wrap_original do |original, name, &block|
+ original.call(name) do |ff|
+ Feature.enabled?(name)
+ block.call(ff)
+ end
+ end
+ end
+
+ it 'returns the default value' do
+ expect(described_class.enabled?(:enabled_feature_flag)).to eq true
+ end
+
+ it 'detects self recursion' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(have_attributes(message: 'self recursion'), { stack: [:enabled_feature_flag] })
+
+ described_class.enabled?(:enabled_feature_flag)
+ end
end
- it 'returns false for undefined feature' do
+ context 'when deeply recursive' do
+ before do
+ allow(Feature).to receive(:with_feature).and_wrap_original do |original, name, &block|
+ original.call(name) do |ff|
+ Feature.enabled?(:"deeper_#{name}", type: :undefined, default_enabled_if_undefined: true)
+ block.call(ff)
+ end
+ end
+ end
+
+ it 'detects deep recursion' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(have_attributes(message: 'deep recursion'), stack: have_attributes(size: be > 10))
+
+ described_class.enabled?(:enabled_feature_flag)
+ end
+ end
+
+ it 'returns false (and tracks / raises exception for dev) for undefined feature' do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
expect(described_class.enabled?(:some_random_feature_flag)).to be_falsey
end
- it 'returns true for undefined feature with default_enabled' do
- expect(described_class.enabled?(:some_random_feature_flag, default_enabled: true)).to be_truthy
+ it 'returns false for undefined feature with default_enabled_if_undefined: false' do
+ expect(described_class.enabled?(:some_random_feature_flag, default_enabled_if_undefined: false)).to be_falsey
+ end
+
+ it 'returns true for undefined feature with default_enabled_if_undefined: true' do
+ expect(described_class.enabled?(:some_random_feature_flag, default_enabled_if_undefined: true)).to be_truthy
end
it 'returns false for existing disabled feature in the database' do
@@ -184,23 +235,23 @@ RSpec.describe Feature, stub_feature_flags: false do
it 'caches the status in L1 and L2 caches',
:request_store, :use_clean_rails_memory_store_caching do
- described_class.enable(:enabled_feature_flag)
- flipper_key = "flipper/v1/feature/enabled_feature_flag"
+ described_class.enable(:disabled_feature_flag)
+ flipper_key = "flipper/v1/feature/disabled_feature_flag"
expect(described_class.send(:l2_cache_backend))
.to receive(:fetch)
.once
- .with(flipper_key, expires_in: 1.hour)
+ .with(flipper_key, { expires_in: 1.hour })
.and_call_original
expect(described_class.send(:l1_cache_backend))
.to receive(:fetch)
.once
- .with(flipper_key, expires_in: 1.minute)
+ .with(flipper_key, { expires_in: 1.minute })
.and_call_original
2.times do
- expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy
+ expect(described_class.enabled?(:disabled_feature_flag)).to be_truthy
end
end
@@ -208,22 +259,14 @@ RSpec.describe Feature, stub_feature_flags: false do
fake_default = double('fake default')
expect(ActiveRecord::Base).to receive(:connection) { raise ActiveRecord::NoDatabaseError, "No database" }
- expect(described_class.enabled?(:a_feature, default_enabled: fake_default)).to eq(fake_default)
+ expect(described_class.enabled?(:a_feature, default_enabled_if_undefined: fake_default)).to eq(fake_default)
end
context 'logging is enabled', :request_store do
before do
allow(Feature).to receive(:log_feature_flag_states?).and_call_original
- definition = Feature::Definition.new("development/enabled_feature_flag.yml",
- name: :enabled_feature_flag,
- type: 'development',
- log_state_changes: true,
- default_enabled: false)
-
- allow(Feature::Definition).to receive(:definitions) do
- { definition.key => definition }
- end
+ stub_feature_flag_definition(:enabled_feature_flag, log_state_changes: true)
described_class.enable(:feature_flag_state_logs)
described_class.enable(:enabled_feature_flag)
@@ -241,18 +284,16 @@ RSpec.describe Feature, stub_feature_flags: false do
end
context 'cached feature flag', :request_store do
- let(:flag) { :some_feature_flag }
-
before do
described_class.send(:flipper).memoize = false
- described_class.enabled?(flag)
+ described_class.enabled?(:disabled_feature_flag)
end
it 'caches the status in L1 cache for the first minute' do
expect do
expect(described_class.send(:l1_cache_backend)).to receive(:fetch).once.and_call_original
expect(described_class.send(:l2_cache_backend)).not_to receive(:fetch)
- expect(described_class.enabled?(flag)).to be_truthy
+ expect(described_class.enabled?(:disabled_feature_flag)).to be_truthy
end.not_to exceed_query_limit(0)
end
@@ -261,7 +302,7 @@ RSpec.describe Feature, stub_feature_flags: false do
expect do
expect(described_class.send(:l1_cache_backend)).to receive(:fetch).once.and_call_original
expect(described_class.send(:l2_cache_backend)).to receive(:fetch).once.and_call_original
- expect(described_class.enabled?(flag)).to be_truthy
+ expect(described_class.enabled?(:disabled_feature_flag)).to be_truthy
end.not_to exceed_query_limit(0)
end
end
@@ -271,7 +312,7 @@ RSpec.describe Feature, stub_feature_flags: false do
expect do
expect(described_class.send(:l1_cache_backend)).to receive(:fetch).once.and_call_original
expect(described_class.send(:l2_cache_backend)).to receive(:fetch).once.and_call_original
- expect(described_class.enabled?(flag)).to be_truthy
+ expect(described_class.enabled?(:disabled_feature_flag)).to be_truthy
end.not_to exceed_query_limit(1)
end
end
@@ -338,21 +379,38 @@ RSpec.describe Feature, stub_feature_flags: false do
.to raise_error(/The `type:` of/)
end
- it 'when invalid default_enabled is used' do
- expect { described_class.enabled?(:my_feature_flag, default_enabled: true) }
- .to raise_error(/The `default_enabled:` of/)
+ context 'when default_enabled: is false in the YAML definition' do
+ it 'reads the default from the YAML definition' do
+ expect(described_class.enabled?(:my_feature_flag)).to eq(default_enabled)
+ end
end
- context 'when `default_enabled: :yaml` is used in code' do
+ context 'when default_enabled: is true in the YAML definition' do
+ let(:default_enabled) { true }
+
it 'reads the default from the YAML definition' do
- expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(false)
+ expect(described_class.enabled?(:my_feature_flag)).to eq(true)
end
- context 'when default_enabled is true in the YAML definition' do
- let(:default_enabled) { true }
+ context 'and feature has been disabled' do
+ before do
+ described_class.disable(:my_feature_flag)
+ end
- it 'reads the default from the YAML definition' do
- expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(true)
+ it 'is not enabled' do
+ expect(described_class.enabled?(:my_feature_flag)).to eq(false)
+ end
+ end
+
+ context 'with a cached value and the YAML definition is changed thereafter' do
+ before do
+ described_class.enabled?(:my_feature_flag)
+ end
+
+ it 'reads new default value' do
+ allow(definition).to receive(:default_enabled).and_return(true)
+
+ expect(described_class.enabled?(:my_feature_flag)).to eq(true)
end
end
@@ -361,7 +419,7 @@ RSpec.describe Feature, stub_feature_flags: false do
context 'when in dev or test environment' do
it 'raises an error for dev' do
- expect { described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml) }
+ expect { described_class.enabled?(:non_existent_flag, type: optional_type) }
.to raise_error(
Feature::InvalidFeatureFlagError,
"The feature flag YAML definition for 'non_existent_flag' does not exist")
@@ -379,9 +437,9 @@ RSpec.describe Feature, stub_feature_flags: false do
end
it 'checks the persisted status and returns false' do
- expect(described_class).to receive(:get).with(:non_existent_flag).and_call_original
+ expect(described_class).to receive(:with_feature).with(:non_existent_flag).and_call_original
- expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false)
+ expect(described_class.enabled?(:non_existent_flag, type: optional_type)).to eq(false)
end
end
@@ -393,7 +451,7 @@ RSpec.describe Feature, stub_feature_flags: false do
it 'returns false without checking the status in the database' do
expect(described_class).not_to receive(:get)
- expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false)
+ expect(described_class.enabled?(:non_existent_flag, type: optional_type)).to eq(false)
end
end
end
@@ -403,21 +461,29 @@ RSpec.describe Feature, stub_feature_flags: false do
end
describe '.disable?' do
- it 'returns true for undefined feature' do
+ it 'returns true (and tracks / raises exception for dev) for undefined feature' do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
expect(described_class.disabled?(:some_random_feature_flag)).to be_truthy
end
- it 'returns false for undefined feature with default_enabled' do
- expect(described_class.disabled?(:some_random_feature_flag, default_enabled: true)).to be_falsey
+ it 'returns true for undefined feature with default_enabled_if_undefined: false' do
+ expect(described_class.disabled?(:some_random_feature_flag, default_enabled_if_undefined: false)).to be_truthy
+ end
+
+ it 'returns false for undefined feature with default_enabled_if_undefined: true' do
+ expect(described_class.disabled?(:some_random_feature_flag, default_enabled_if_undefined: true)).to be_falsey
end
it 'returns true for existing disabled feature in the database' do
+ stub_feature_flag_definition(:disabled_feature_flag)
described_class.disable(:disabled_feature_flag)
expect(described_class.disabled?(:disabled_feature_flag)).to be_truthy
end
it 'returns false for existing enabled feature in the database' do
+ stub_feature_flag_definition(:enabled_feature_flag)
described_class.enable(:enabled_feature_flag)
expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey
@@ -556,14 +622,7 @@ RSpec.describe Feature, stub_feature_flags: false do
let(:log_state_changes) { false }
let(:milestone) { "0.0" }
let(:flag_name) { :some_flag }
- let(:definition) do
- Feature::Definition.new("development/#{flag_name}.yml",
- name: flag_name,
- type: 'development',
- milestone: milestone,
- log_state_changes: log_state_changes,
- default_enabled: false)
- end
+ let(:flag_type) { 'development' }
before do
Feature.enable(:feature_flag_state_logs)
@@ -573,9 +632,10 @@ RSpec.describe Feature, stub_feature_flags: false do
allow(Feature).to receive(:log_feature_flag_states?).with(:feature_flag_state_logs).and_call_original
allow(Feature).to receive(:log_feature_flag_states?).with(:some_flag).and_call_original
- allow(Feature::Definition).to receive(:definitions) do
- { definition.key => definition }
- end
+ stub_feature_flag_definition(flag_name,
+ type: flag_type,
+ milestone: milestone,
+ log_state_changes: log_state_changes)
end
subject { described_class.log_feature_flag_states?(flag_name) }
@@ -583,6 +643,7 @@ RSpec.describe Feature, stub_feature_flags: false do
context 'when flag is feature_flag_state_logs' do
let(:milestone) { "14.6" }
let(:flag_name) { :feature_flag_state_logs }
+ let(:flag_type) { 'ops' }
let(:log_state_changes) { true }
it { is_expected.to be_falsey }
@@ -593,13 +654,7 @@ RSpec.describe Feature, stub_feature_flags: false do
end
context 'when flag is old while log_state_changes is not present ' do
- let(:definition) do
- Feature::Definition.new("development/#{flag_name}.yml",
- name: flag_name,
- type: 'development',
- milestone: milestone,
- default_enabled: false)
- end
+ let(:log_state_changes) { nil }
it { is_expected.to be_falsey }
end
@@ -621,12 +676,7 @@ RSpec.describe Feature, stub_feature_flags: false do
end
context 'when milestone is nil' do
- let(:definition) do
- Feature::Definition.new("development/#{flag_name}.yml",
- name: flag_name,
- type: 'development',
- default_enabled: false)
- end
+ let(:milestone) { nil }
it { is_expected.to be_falsey }
end