# frozen_string_literal: true require 'spec_helper' RSpec.describe Feature, stub_feature_flags: false do before do # reset Flipper AR-engine Feature.reset skip_feature_flags_yaml_validation end describe '.get' do let(:feature) { double(:feature) } let(:key) { 'my_feature' } it 'returns the Flipper feature' do expect_any_instance_of(Flipper::DSL).to receive(:feature).with(key) .and_return(feature) expect(described_class.get(key)).to be(feature) end end describe '.persisted_names' do it 'returns the names of the persisted features' do Feature.enable('foo') expect(described_class.persisted_names).to contain_exactly('foo') end it 'returns an empty Array when no features are presisted' do expect(described_class.persisted_names).to be_empty end it 'caches the feature names when request store is active', :request_store, :use_clean_rails_memory_store_caching do Feature.enable('foo') expect(Gitlab::ProcessMemoryCache.cache_backend) .to receive(:fetch) .once .with('flipper/v1/features', expires_in: 1.minute) .and_call_original 2.times do expect(described_class.persisted_names).to contain_exactly('foo') end end it 'fetches all flags once in a single query', :request_store do Feature.enable('foo1') Feature.enable('foo2') queries = ActiveRecord::QueryRecorder.new(skip_cached: false) do expect(described_class.persisted_names).to contain_exactly('foo1', 'foo2') RequestStore.clear! expect(described_class.persisted_names).to contain_exactly('foo1', 'foo2') end expect(queries.count).to eq(1) end end describe '.persisted_name?' do context 'when the feature is persisted' do it 'returns true when feature name is a string' do Feature.enable('foo') expect(described_class.persisted_name?('foo')).to eq(true) end it 'returns true when feature name is a symbol' do Feature.enable('foo') expect(described_class.persisted_name?(:foo)).to eq(true) end end context 'when the feature is not persisted' do it 'returns false when feature name is a string' do expect(described_class.persisted_name?('foo')).to eq(false) end it 'returns false when feature name is a symbol' do expect(described_class.persisted_name?(:bar)).to eq(false) end end end describe '.all' do let(:features) { Set.new } it 'returns the Flipper features as an array' do expect_any_instance_of(Flipper::DSL).to receive(:features) .and_return(features) expect(described_class.all).to eq(features.to_a) end end describe '.flipper' do context 'when request store is inactive' do it 'memoizes the Flipper instance' do expect(Flipper).to receive(:new).once.and_call_original 2.times do described_class.send(:flipper) end end end context 'when request store is active', :request_store do it 'memoizes the Flipper instance' do expect(Flipper).to receive(:new).once.and_call_original described_class.send(:flipper) described_class.instance_variable_set(:@flipper, nil) described_class.send(:flipper) end end end describe '.enabled?' do it 'returns false for undefined feature' do 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 end it 'returns false for existing disabled feature in the database' do described_class.disable(:disabled_feature_flag) expect(described_class.enabled?(:disabled_feature_flag)).to be_falsey end it 'returns true for existing enabled feature in the database' do described_class.enable(:enabled_feature_flag) expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy end it { expect(described_class.send(:l1_cache_backend)).to eq(Gitlab::ProcessMemoryCache.cache_backend) } it { expect(described_class.send(:l2_cache_backend)).to eq(Rails.cache) } 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" expect(described_class.send(:l2_cache_backend)) .to receive(:fetch) .once .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) .and_call_original 2.times do expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy end end it 'returns the default value when the database does not exist' 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) 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) 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 end.not_to exceed_query_limit(0) end it 'caches the status in L2 cache after 2 minutes' do Timecop.travel 2.minutes 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 end.not_to exceed_query_limit(0) end end it 'fetches the status after an hour' do Timecop.travel 61.minutes 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 end.not_to exceed_query_limit(1) end end end context 'with an individual actor' do let(:actor) { stub_feature_flag_gate('CustomActor:5') } let(:another_actor) { stub_feature_flag_gate('CustomActor:10') } before do described_class.enable(:enabled_feature_flag, actor) end it 'returns true when same actor is informed' do expect(described_class.enabled?(:enabled_feature_flag, actor)).to be_truthy end it 'returns false when different actor is informed' do expect(described_class.enabled?(:enabled_feature_flag, another_actor)).to be_falsey end it 'returns false when no actor is informed' do expect(described_class.enabled?(:enabled_feature_flag)).to be_falsey end end context 'with invalid actor' do let(:actor) { double('invalid actor') } context 'when is dev_or_test_env' do it 'does raise exception' do expect { described_class.enabled?(:enabled_feature_flag, actor) } .to raise_error /needs to include `FeatureGate` or implement `flipper_id`/ end end end context 'validates usage of feature flag with YAML definition' do let(:definition) do Feature::Definition.new('development/my_feature_flag.yml', name: 'my_feature_flag', type: 'development', default_enabled: false ).tap(&:validate!) end before do stub_env('LAZILY_CREATE_FEATURE_FLAG', '0') allow(Feature::Definition).to receive(:valid_usage!).and_call_original allow(Feature::Definition).to receive(:definitions) do { definition.key => definition } end end it 'when usage is correct' do expect { described_class.enabled?(:my_feature_flag) }.not_to raise_error end it 'when invalid type is used' do expect { described_class.enabled?(:my_feature_flag, type: :licensed) } .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/) end end end describe '.disable?' do it 'returns true for undefined feature' do 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 end it 'returns true for existing disabled feature in the database' do 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 described_class.enable(:enabled_feature_flag) expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey end end shared_examples_for 'logging' do let(:expected_action) { } let(:expected_extra) { } it 'logs the event' do expect(Feature.logger).to receive(:info).with(key: key, action: expected_action, **expected_extra) subject end end describe '.enable' do subject { described_class.enable(key, thing) } let(:key) { :awesome_feature } let(:thing) { true } it_behaves_like 'logging' do let(:expected_action) { :enable } let(:expected_extra) { { "extra.thing" => "true" } } end context 'when thing is an actor' do let(:thing) { create(:project) } it_behaves_like 'logging' do let(:expected_action) { :enable } let(:expected_extra) { { "extra.thing" => "#{thing.flipper_id}" } } end end end describe '.disable' do subject { described_class.disable(key, thing) } let(:key) { :awesome_feature } let(:thing) { false } it_behaves_like 'logging' do let(:expected_action) { :disable } let(:expected_extra) { { "extra.thing" => "false" } } end context 'when thing is an actor' do let(:thing) { create(:project) } it_behaves_like 'logging' do let(:expected_action) { :disable } let(:expected_extra) { { "extra.thing" => "#{thing.flipper_id}" } } end end end describe '.enable_percentage_of_time' do subject { described_class.enable_percentage_of_time(key, percentage) } let(:key) { :awesome_feature } let(:percentage) { 50 } it_behaves_like 'logging' do let(:expected_action) { :enable_percentage_of_time } let(:expected_extra) { { "extra.percentage" => "#{percentage}" } } end end describe '.disable_percentage_of_time' do subject { described_class.disable_percentage_of_time(key) } let(:key) { :awesome_feature } it_behaves_like 'logging' do let(:expected_action) { :disable_percentage_of_time } let(:expected_extra) { {} } end end describe '.enable_percentage_of_actors' do subject { described_class.enable_percentage_of_actors(key, percentage) } let(:key) { :awesome_feature } let(:percentage) { 50 } it_behaves_like 'logging' do let(:expected_action) { :enable_percentage_of_actors } let(:expected_extra) { { "extra.percentage" => "#{percentage}" } } end end describe '.disable_percentage_of_actors' do subject { described_class.disable_percentage_of_actors(key) } let(:key) { :awesome_feature } it_behaves_like 'logging' do let(:expected_action) { :disable_percentage_of_actors } let(:expected_extra) { {} } end end describe '.remove' do subject { described_class.remove(key) } let(:key) { :awesome_feature } before do described_class.enable(key) end it_behaves_like 'logging' do let(:expected_action) { :remove } let(:expected_extra) { {} } end context 'for a non-persisted feature' do it 'returns nil' do expect(described_class.remove(:non_persisted_feature_flag)).to be_nil end end context 'for a persisted feature' do it 'returns true' do described_class.enable(:persisted_feature_flag) expect(described_class.remove(:persisted_feature_flag)).to be_truthy end end end describe Feature::Target do describe '#targets' do let(:project) { create(:project) } let(:group) { create(:group) } let(:user_name) { project.owner.username } subject { described_class.new(user: user_name, project: project.full_path, group: group.full_path) } it 'returns all found targets' do expect(subject.targets).to be_an(Array) expect(subject.targets).to eq([project.owner, project, group]) end end end end