# frozen_string_literal: true require 'spec_helper' describe Feature do before do # We mock all calls to .enabled? to return true in order to force all # specs to run the feature flag gated behavior, but here we need a clean # behavior from the class allow(described_class).to receive(:enabled?).and_call_original 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::FlipperFeature.create!(key: 'foo') expect(described_class.persisted_names).to eq(%w[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::FlipperFeature.create!(key: 'foo') expect(Feature::FlipperFeature) .to receive(:feature_names) .once .and_call_original expect(Gitlab::ThreadMemoryCache.cache_backend) .to receive(:fetch) .once .with('flipper:persisted_names', expires_in: 1.minute) .and_call_original 2.times do expect(described_class.persisted_names).to eq(%w[foo]) end end end describe '.persisted?' do context 'when the feature is persisted' do it 'returns true when feature name is a string' do Feature::FlipperFeature.create!(key: 'foo') feature = double(:feature, name: 'foo') expect(described_class.persisted?(feature)).to eq(true) end it 'returns true when feature name is a symbol' do Feature::FlipperFeature.create!(key: 'foo') feature = double(:feature, name: :foo) expect(described_class.persisted?(feature)).to eq(true) end end context 'when the feature is not persisted' do it 'returns false when feature name is a string' do feature = double(:feature, name: 'foo') expect(described_class.persisted?(feature)).to eq(false) end it 'returns false when feature name is a symbol' do feature = double(:feature, name: :bar) expect(described_class.persisted?(feature)).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 before do described_class.instance_variable_set(:@flipper, nil) end 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.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.flipper described_class.instance_variable_set(:@flipper, nil) described_class.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.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) } it { expect(described_class.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.l2_cache_backend) .to receive(:fetch) .once .with(flipper_key, expires_in: 1.hour) .and_call_original expect(described_class.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 context 'cached feature flag', :request_store do let(:flag) { :some_feature_flag } before do described_class.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.l1_cache_backend).to receive(:fetch).once.and_call_original expect(described_class.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.l1_cache_backend).to receive(:fetch).once.and_call_original expect(described_class.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.l1_cache_backend).to receive(:fetch).once.and_call_original expect(described_class.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 CustomActor = Struct.new(:flipper_id) let(:actor) { CustomActor.new(flipper_id: 'CustomActor:5') } let(:another_actor) { CustomActor.new(flipper_id: '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 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 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