diff options
Diffstat (limited to 'spec/lib/feature/definition_spec.rb')
-rw-r--r-- | spec/lib/feature/definition_spec.rb | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/spec/lib/feature/definition_spec.rb b/spec/lib/feature/definition_spec.rb new file mode 100644 index 00000000000..49224cf4279 --- /dev/null +++ b/spec/lib/feature/definition_spec.rb @@ -0,0 +1,209 @@ +# 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 + :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 + expect { definition.valid_usage!(type_in_code: :invalid, default_enabled_in_code: false) } + .to raise_error(/The `type:` of `feature_flag` is not equal to config/) + end + end + + context 'validates default enabled' do + it 'raises exception for different value' do + expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: false) } + .to raise_error(/The `default_enabled:` 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 + expect(File).to receive(:read).with(path) { yaml_content } + + 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 + expect(File).to receive(:read).with(path) { '{}' } + + 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') } + + before do + allow(described_class).to receive(:paths).and_return( + [ + File.join(store1, '**', '*.yml'), + File.join(store2, '**', '*.yml') + ] + ) + end + + it "when there's no feature flags a list of definitions is empty" do + expect(described_class.load_all!).to be_empty + end + + it "when there's a single feature flag it properly loads them" do + write_feature_flag(store1, path, yaml_content) + + expect(described_class.load_all!).to be_one + 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) + + expect { described_class.load_all! } + .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, '{}') + + expect { described_class.load_all! } + .to raise_error(/Feature flag is missing name/) + end + + after do + FileUtils.rm_rf(store1) + FileUtils.rm_rf(store2) + 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 + + 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!) + + described_class.valid_usage!(:feature_flag, type: :development, default_enabled: false) + 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 + described_class.valid_usage!(:unknown_feature_flag, type: :development, default_enabled: false) + 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 + described_class.valid_usage!(:unknown_feature_flag, type: :development, default_enabled: false) + end.not_to raise_error + end + end + + context 'for an unknown type' do + it 'raises exception' do + expect do + described_class.valid_usage!(:unknown_feature_flag, type: :unknown_type, default_enabled: false) + end.to raise_error(/Unknown feature flag type used: `unknown_type`/) + end + end + end + end +end |