2020-07-28 23:09:34 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
RSpec.describe Feature::Definition do
|
|
|
|
let(:attributes) do
|
|
|
|
{ name: 'feature_flag',
|
|
|
|
type: 'development',
|
|
|
|
default_enabled: true }
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:path) { File.join('development', 'feature_flag.yml') }
|
|
|
|
let(:definition) { described_class.new(path, attributes) }
|
|
|
|
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
|
|
|
|
|
|
|
|
describe '#key' do
|
|
|
|
subject { definition.key }
|
|
|
|
|
|
|
|
it 'returns a symbol from name' do
|
|
|
|
is_expected.to eq(:feature_flag)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#validate!' do
|
|
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
|
|
|
|
where(:param, :value, :result) do
|
2023-03-04 22:38:38 +05:30
|
|
|
:name | 'colon:separated' | /Feature flag 'colon:separated' is invalid/
|
|
|
|
:name | 'space separated' | /Feature flag 'space separated' is invalid/
|
|
|
|
:name | 'ALL_CAPS' | /Feature flag 'ALL_CAPS' is invalid/
|
2020-07-28 23:09:34 +05:30
|
|
|
:name | nil | /Feature flag is missing name/
|
|
|
|
:path | nil | /Feature flag 'feature_flag' is missing path/
|
|
|
|
:type | nil | /Feature flag 'feature_flag' is missing type/
|
|
|
|
:type | 'invalid' | /Feature flag 'feature_flag' type 'invalid' is invalid/
|
|
|
|
:path | 'development/invalid.yml' | /Feature flag 'feature_flag' has an invalid path/
|
|
|
|
:path | 'invalid/feature_flag.yml' | /Feature flag 'feature_flag' has an invalid type/
|
|
|
|
:default_enabled | nil | /Feature flag 'feature_flag' is missing default_enabled/
|
|
|
|
end
|
|
|
|
|
|
|
|
with_them do
|
|
|
|
let(:params) { attributes.merge(path: path) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
params[param] = value
|
|
|
|
end
|
|
|
|
|
|
|
|
it do
|
|
|
|
expect do
|
|
|
|
described_class.new(
|
|
|
|
params[:path], params.except(:path)
|
|
|
|
).validate!
|
|
|
|
end.to raise_error(result)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#valid_usage!' do
|
|
|
|
context 'validates type' do
|
|
|
|
it 'raises exception for invalid type' do
|
2022-07-16 23:28:13 +05:30
|
|
|
expect { definition.valid_usage!(type_in_code: :invalid) }
|
2020-07-28 23:09:34 +05:30
|
|
|
.to raise_error(/The `type:` of `feature_flag` is not equal to config/)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.paths' do
|
|
|
|
it 'returns at least one path' do
|
|
|
|
expect(described_class.paths).not_to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.load_from_file' do
|
|
|
|
it 'properly loads a definition from file' do
|
2021-02-22 17:27:13 +05:30
|
|
|
expect_file_read(path, content: yaml_content)
|
2020-07-28 23:09:34 +05:30
|
|
|
|
|
|
|
expect(described_class.send(:load_from_file, path).attributes)
|
|
|
|
.to eq(definition.attributes)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'for missing file' do
|
|
|
|
let(:path) { 'missing/feature-flag/file.yml' }
|
|
|
|
|
|
|
|
it 'raises exception' do
|
|
|
|
expect do
|
|
|
|
described_class.send(:load_from_file, path)
|
|
|
|
end.to raise_error(/Invalid definition for/)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'for invalid definition' do
|
|
|
|
it 'raises exception' do
|
2021-02-22 17:27:13 +05:30
|
|
|
expect_file_read(path, content: '{}')
|
2020-07-28 23:09:34 +05:30
|
|
|
|
|
|
|
expect do
|
|
|
|
described_class.send(:load_from_file, path)
|
|
|
|
end.to raise_error(/Feature flag is missing name/)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.load_all!' do
|
|
|
|
let(:store1) { Dir.mktmpdir('path1') }
|
|
|
|
let(:store2) { Dir.mktmpdir('path2') }
|
2021-01-03 14:25:43 +05:30
|
|
|
let(:definitions) { {} }
|
2020-07-28 23:09:34 +05:30
|
|
|
|
|
|
|
before do
|
|
|
|
allow(described_class).to receive(:paths).and_return(
|
|
|
|
[
|
|
|
|
File.join(store1, '**', '*.yml'),
|
|
|
|
File.join(store2, '**', '*.yml')
|
|
|
|
]
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
subject { described_class.send(:load_all!) }
|
|
|
|
|
2023-03-04 22:38:38 +05:30
|
|
|
after do
|
|
|
|
FileUtils.rm_rf(store1)
|
|
|
|
FileUtils.rm_rf(store2)
|
|
|
|
end
|
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
it "when there's no feature flags a list of definitions is empty" do
|
2021-01-03 14:25:43 +05:30
|
|
|
is_expected.to be_empty
|
2020-07-28 23:09:34 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it "when there's a single feature flag it properly loads them" do
|
|
|
|
write_feature_flag(store1, path, yaml_content)
|
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
is_expected.to be_one
|
2020-07-28 23:09:34 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it "when the same feature flag is stored multiple times raises exception" do
|
|
|
|
write_feature_flag(store1, path, yaml_content)
|
|
|
|
write_feature_flag(store2, path, yaml_content)
|
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
expect { subject }
|
2020-07-28 23:09:34 +05:30
|
|
|
.to raise_error(/Feature flag 'feature_flag' is already defined/)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "when one of the YAMLs is invalid it does raise exception" do
|
|
|
|
write_feature_flag(store1, path, '{}')
|
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
expect { subject }
|
2020-07-28 23:09:34 +05:30
|
|
|
.to raise_error(/Feature flag is missing name/)
|
|
|
|
end
|
|
|
|
|
|
|
|
def write_feature_flag(store, path, content)
|
|
|
|
path = File.join(store, path)
|
|
|
|
dir = File.dirname(path)
|
|
|
|
FileUtils.mkdir_p(dir)
|
|
|
|
File.write(path, content)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-01-26 12:08:38 +05:30
|
|
|
describe '.for_upcoming_milestone?' do
|
|
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
|
|
|
|
let(:definition) do
|
2022-07-16 23:28:13 +05:30
|
|
|
described_class.new("development/enabled_feature_flag.yml",
|
|
|
|
name: :enabled_feature_flag,
|
|
|
|
type: 'development',
|
|
|
|
milestone: milestone,
|
|
|
|
default_enabled: false)
|
2022-01-26 12:08:38 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
allow(Gitlab).to receive(:version_info).and_return(Gitlab::VersionInfo.parse(current_milestone))
|
|
|
|
end
|
|
|
|
|
|
|
|
subject { definition.for_upcoming_milestone? }
|
|
|
|
|
|
|
|
where(:ctx, :milestone, :current_milestone, :expected) do
|
|
|
|
'no milestone' | nil | '1.0.0' | false
|
|
|
|
'upcoming milestone - major' | '2.3' | '1.9.999' | true
|
|
|
|
'upcoming milestone - minor' | '2.3' | '2.2.999' | true
|
|
|
|
'current milestone' | '2.3' | '2.3.999' | true
|
|
|
|
'past milestone - major' | '1.9' | '2.3.999' | false
|
|
|
|
'past milestone - minor' | '2.2' | '2.3.999' | false
|
|
|
|
end
|
|
|
|
|
|
|
|
with_them do
|
2022-07-16 23:28:13 +05:30
|
|
|
it { is_expected.to be(expected) }
|
2022-01-26 12:08:38 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
describe '.valid_usage!' do
|
|
|
|
before do
|
|
|
|
allow(described_class).to receive(:definitions) do
|
|
|
|
{ definition.key => definition }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when a known feature flag is used' do
|
|
|
|
it 'validates it usage' do
|
|
|
|
expect(definition).to receive(:valid_usage!)
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
described_class.valid_usage!(:feature_flag, type: :development)
|
2020-07-28 23:09:34 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when an unknown feature flag is used' do
|
|
|
|
context 'for a type that is required to have all feature flags registered' do
|
|
|
|
before do
|
|
|
|
stub_const('Feature::Shared::TYPES', {
|
|
|
|
development: { optional: false }
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'raises exception' do
|
|
|
|
expect do
|
2022-07-16 23:28:13 +05:30
|
|
|
described_class.valid_usage!(:unknown_feature_flag, type: :development)
|
2020-07-28 23:09:34 +05:30
|
|
|
end.to raise_error(/Missing feature definition for `unknown_feature_flag`/)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'for a type that is optional' do
|
|
|
|
before do
|
|
|
|
stub_const('Feature::Shared::TYPES', {
|
|
|
|
development: { optional: true }
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not raise exception' do
|
|
|
|
expect do
|
2022-07-16 23:28:13 +05:30
|
|
|
described_class.valid_usage!(:unknown_feature_flag, type: :development)
|
2020-07-28 23:09:34 +05:30
|
|
|
end.not_to raise_error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'for an unknown type' do
|
|
|
|
it 'raises exception' do
|
|
|
|
expect do
|
2022-07-16 23:28:13 +05:30
|
|
|
described_class.valid_usage!(:unknown_feature_flag, type: :unknown_type)
|
2020-07-28 23:09:34 +05:30
|
|
|
end.to raise_error(/Unknown feature flag type used: `unknown_type`/)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2021-02-22 17:27:13 +05:30
|
|
|
|
2022-01-26 12:08:38 +05:30
|
|
|
describe '.log_states?' do
|
|
|
|
using RSpec::Parameterized::TableSyntax
|
|
|
|
|
|
|
|
let(:definition) do
|
2022-07-16 23:28:13 +05:30
|
|
|
described_class.new("development/enabled_feature_flag.yml",
|
|
|
|
name: :enabled_feature_flag,
|
|
|
|
type: 'development',
|
|
|
|
milestone: milestone,
|
|
|
|
log_state_changes: log_state_change,
|
|
|
|
default_enabled: false)
|
2022-01-26 12:08:38 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
2022-07-16 23:28:13 +05:30
|
|
|
stub_feature_flag_definition(:enabled_feature_flag,
|
|
|
|
milestone: milestone,
|
|
|
|
log_state_changes: log_state_change)
|
2022-01-26 12:08:38 +05:30
|
|
|
|
|
|
|
allow(Gitlab).to receive(:version_info).and_return(Gitlab::VersionInfo.new(10, 0, 0))
|
|
|
|
end
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
subject { described_class.log_states?(key) }
|
2022-01-26 12:08:38 +05:30
|
|
|
|
|
|
|
where(:ctx, :key, :milestone, :log_state_change, :expected) do
|
|
|
|
'When flag does not exist' | :no_flag | "0.0" | true | false
|
|
|
|
'When flag is old, and logging is not forced' | :enabled_feature_flag | "0.0" | false | false
|
|
|
|
'When flag is old, but logging is forced' | :enabled_feature_flag | "0.0" | true | true
|
|
|
|
'When flag is current' | :enabled_feature_flag | "10.0" | true | true
|
|
|
|
'Flag is upcoming' | :enabled_feature_flag | "10.0" | true | true
|
|
|
|
end
|
|
|
|
|
|
|
|
with_them do
|
|
|
|
it { is_expected.to be(expected) }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '.default_enabled?' do
|
2022-07-16 23:28:13 +05:30
|
|
|
subject { described_class.default_enabled?(key, default_enabled_if_undefined: default_value) }
|
2021-02-22 17:27:13 +05:30
|
|
|
|
|
|
|
context 'when feature flag exist' do
|
|
|
|
let(:key) { definition.key }
|
2022-07-16 23:28:13 +05:30
|
|
|
let(:default_value) { nil }
|
2021-02-22 17:27:13 +05:30
|
|
|
|
|
|
|
before do
|
|
|
|
allow(described_class).to receive(:definitions) do
|
|
|
|
{ definition.key => definition }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when default_enabled is true' do
|
|
|
|
it 'returns the value from the definition' do
|
|
|
|
expect(subject).to eq(true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when default_enabled is false' do
|
|
|
|
let(:attributes) do
|
|
|
|
{ name: 'feature_flag',
|
|
|
|
type: 'development',
|
|
|
|
default_enabled: false }
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns the value from the definition' do
|
|
|
|
expect(subject).to eq(false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when feature flag does not exist' do
|
|
|
|
let(:key) { :unknown_feature_flag }
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
context 'when passing default value' do
|
|
|
|
let(:default_value) { false }
|
|
|
|
|
|
|
|
it 'returns default value' do
|
|
|
|
expect(subject).to eq(default_value)
|
2021-02-22 17:27:13 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
context 'when default value is undefined' do
|
|
|
|
let(:default_value) { nil }
|
|
|
|
|
|
|
|
context 'when on dev or test environment' do
|
|
|
|
it 'raises an error' do
|
|
|
|
expect { subject }.to raise_error(
|
|
|
|
Feature::InvalidFeatureFlagError,
|
|
|
|
"The feature flag YAML definition for 'unknown_feature_flag' does not exist")
|
|
|
|
end
|
2021-02-22 17:27:13 +05:30
|
|
|
end
|
|
|
|
|
2022-07-16 23:28:13 +05:30
|
|
|
context 'when on production environment' do
|
|
|
|
before do
|
|
|
|
allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns false' do
|
|
|
|
expect(subject).to eq(false)
|
|
|
|
end
|
2021-02-22 17:27:13 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2020-07-28 23:09:34 +05:30
|
|
|
end
|