2019-10-12 21:52:04 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
describe Feature do
|
2018-11-20 20:47:30 +05:30
|
|
|
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
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
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
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
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
|
|
|
|
|
2019-09-30 21:07:59 +05:30
|
|
|
it 'caches the feature names when request store is active',
|
|
|
|
:request_store, :use_clean_rails_memory_store_caching do
|
2018-03-17 18:26:18 +05:30
|
|
|
Feature::FlipperFeature.create!(key: 'foo')
|
|
|
|
|
|
|
|
expect(Feature::FlipperFeature)
|
|
|
|
.to receive(:feature_names)
|
|
|
|
.once
|
|
|
|
.and_call_original
|
|
|
|
|
2019-09-30 21:07:59 +05:30
|
|
|
expect(Gitlab::ThreadMemoryCache.cache_backend)
|
|
|
|
.to receive(:fetch)
|
|
|
|
.once
|
|
|
|
.with('flipper:persisted_names', expires_in: 1.minute)
|
|
|
|
.and_call_original
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
2.times do
|
|
|
|
expect(described_class.persisted_names).to eq(%w[foo])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.persisted?' do
|
2018-11-18 11:00:15 +05:30
|
|
|
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')
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2018-11-18 11:00:15 +05:30
|
|
|
feature = double(:feature, name: :foo)
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2018-11-18 11:00:15 +05:30
|
|
|
expect(described_class.persisted?(feature)).to eq(true)
|
|
|
|
end
|
2018-03-17 18:26:18 +05:30
|
|
|
end
|
|
|
|
|
2018-11-18 11:00:15 +05:30
|
|
|
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
|
2018-03-17 18:26:18 +05:30
|
|
|
|
2018-11-18 11:00:15 +05:30
|
|
|
it 'returns false when feature name is a symbol' do
|
|
|
|
feature = double(:feature, name: :bar)
|
|
|
|
|
|
|
|
expect(described_class.persisted?(feature)).to eq(false)
|
|
|
|
end
|
2018-03-17 18:26:18 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
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
|
2018-11-08 19:23:39 +05:30
|
|
|
|
|
|
|
describe '.flipper' do
|
2018-12-05 23:21:45 +05:30
|
|
|
before do
|
|
|
|
described_class.instance_variable_set(:@flipper, nil)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when request store is inactive' do
|
2018-11-08 19:23:39 +05:30
|
|
|
it 'memoizes the Flipper instance' do
|
|
|
|
expect(Flipper).to receive(:new).once.and_call_original
|
|
|
|
|
|
|
|
2.times do
|
|
|
|
described_class.flipper
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
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
|
2018-11-08 19:23:39 +05:30
|
|
|
described_class.instance_variable_set(:@flipper, nil)
|
2018-12-05 23:21:45 +05:30
|
|
|
described_class.flipper
|
2018-11-08 19:23:39 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-11-20 20:47:30 +05:30
|
|
|
|
|
|
|
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
|
2019-09-30 21:07:59 +05:30
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-03-09 13:42:32 +05:30
|
|
|
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
|
|
|
|
|
2019-09-30 21:07:59 +05:30
|
|
|
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
|
2018-11-20 20:47:30 +05:30
|
|
|
|
|
|
|
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
|
2019-03-02 22:35:43 +05:30
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
describe '.remove' do
|
|
|
|
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
|
|
|
|
|
2019-03-02 22:35:43 +05:30
|
|
|
describe Feature::Target do
|
|
|
|
describe '#targets' do
|
|
|
|
let(:project) { create(:project) }
|
2019-07-07 11:18:12 +05:30
|
|
|
let(:group) { create(:group) }
|
2019-03-02 22:35:43 +05:30
|
|
|
let(:user_name) { project.owner.username }
|
|
|
|
|
2019-07-07 11:18:12 +05:30
|
|
|
subject { described_class.new(user: user_name, project: project.full_path, group: group.full_path) }
|
2019-03-02 22:35:43 +05:30
|
|
|
|
|
|
|
it 'returns all found targets' do
|
|
|
|
expect(subject.targets).to be_an(Array)
|
2019-07-07 11:18:12 +05:30
|
|
|
expect(subject.targets).to eq([project.owner, project, group])
|
2019-03-02 22:35:43 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2017-09-10 17:25:29 +05:30
|
|
|
end
|