332 lines
12 KiB
Ruby
332 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe QA::Runtime::Feature do
|
|
let(:api_client) { double('QA::Runtime::API::Client') }
|
|
let(:request) { Struct.new(:url).new('http://api') }
|
|
let(:response_post) { Struct.new(:code).new(201) }
|
|
|
|
before do
|
|
allow(described_class).to receive(:api_client).and_return(api_client)
|
|
end
|
|
|
|
where(:feature_flag) do
|
|
['a_flag', :a_flag]
|
|
end
|
|
|
|
with_them do
|
|
shared_examples 'enables a feature flag' do
|
|
it 'enables a feature flag for a scope' do
|
|
allow(described_class).to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "on" }]'))
|
|
|
|
expect(QA::Runtime::API::Request).to receive(:new)
|
|
.with(api_client, "/features/a_flag").and_return(request)
|
|
expect(described_class).to receive(:post)
|
|
.with(request.url, { value: true, scope => actor_name }).and_return(response_post)
|
|
expect(QA::Runtime::API::Request).to receive(:new)
|
|
.with(api_client, "/features").and_return(request)
|
|
expect(QA::Runtime::Logger).to receive(:info).with("Enabling feature: a_flag for scope \"#{scope}: #{actor_name}\"")
|
|
expect(QA::Runtime::Logger).to receive(:info).with("Successfully enabled and verified feature flag: a_flag")
|
|
|
|
described_class.enable(feature_flag, scope => actor)
|
|
end
|
|
end
|
|
|
|
shared_examples 'disables a feature flag' do
|
|
it 'disables a feature flag for a scope' do
|
|
allow(described_class).to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "off" }]'))
|
|
|
|
expect(QA::Runtime::API::Request).to receive(:new)
|
|
.with(api_client, "/features/a_flag").and_return(request)
|
|
expect(described_class).to receive(:post)
|
|
.with(request.url, { value: false, scope => actor_name }).and_return(response_post)
|
|
expect(QA::Runtime::API::Request).to receive(:new)
|
|
.with(api_client, "/features").and_return(request)
|
|
expect(QA::Runtime::Logger).to receive(:info).with("Disabling feature: a_flag for scope \"#{scope}: #{actor_name}\"")
|
|
expect(QA::Runtime::Logger).to receive(:info).with("Successfully disabled and verified feature flag: a_flag")
|
|
|
|
described_class.disable(feature_flag, scope => actor )
|
|
end
|
|
end
|
|
|
|
shared_examples 'checks a feature flag' do
|
|
context 'when the flag is enabled for a scope' do
|
|
it 'returns the feature flag state' do
|
|
expect(QA::Runtime::API::Request)
|
|
.to receive(:new)
|
|
.with(api_client, "/features")
|
|
.and_return(request)
|
|
expect(described_class)
|
|
.to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, %Q([{ "name": "a_flag", "state": "conditional", "gates": #{gates} }])))
|
|
|
|
expect(described_class.enabled?(feature_flag, scope => actor)).to be true
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.enable' do
|
|
it 'enables a feature flag' do
|
|
allow(described_class).to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "on" }]'))
|
|
|
|
expect(QA::Runtime::API::Request).to receive(:new)
|
|
.with(api_client, "/features/a_flag").and_return(request)
|
|
expect(described_class).to receive(:post)
|
|
.with(request.url, { value: true }).and_return(response_post)
|
|
expect(QA::Runtime::API::Request).to receive(:new)
|
|
.with(api_client, "/features").and_return(request)
|
|
|
|
described_class.enable(feature_flag)
|
|
end
|
|
|
|
context 'when a project scope is provided' do
|
|
it_behaves_like 'enables a feature flag' do
|
|
let(:scope) { :project }
|
|
let(:actor_name) { 'group-name/project-name' }
|
|
let(:actor) { Struct.new(:full_path).new(actor_name) }
|
|
end
|
|
end
|
|
|
|
context 'when a group scope is provided' do
|
|
it_behaves_like 'enables a feature flag' do
|
|
let(:scope) { :group }
|
|
let(:actor_name) { 'group-name' }
|
|
let(:actor) { Struct.new(:full_path).new(actor_name) }
|
|
end
|
|
end
|
|
|
|
context 'when a user scope is provided' do
|
|
it_behaves_like 'enables a feature flag' do
|
|
let(:scope) { :user }
|
|
let(:actor_name) { 'user-name' }
|
|
let(:actor) { Struct.new(:username).new(actor_name) }
|
|
end
|
|
end
|
|
|
|
context 'when a feature group scope is provided' do
|
|
it_behaves_like 'enables a feature flag' do
|
|
let(:scope) { :feature_group }
|
|
let(:actor_name) { 'foo' }
|
|
let(:actor) { "foo" }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.disable' do
|
|
it 'disables a feature flag' do
|
|
allow(described_class).to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "off" }]'))
|
|
|
|
expect(QA::Runtime::API::Request).to receive(:new)
|
|
.with(api_client, "/features/a_flag").and_return(request)
|
|
expect(described_class).to receive(:post)
|
|
.with(request.url, { value: false }).and_return(response_post)
|
|
expect(QA::Runtime::API::Request).to receive(:new)
|
|
.with(api_client, "/features").and_return(request)
|
|
|
|
described_class.disable(feature_flag)
|
|
end
|
|
|
|
context 'when a project scope is provided' do
|
|
it_behaves_like 'disables a feature flag' do
|
|
let(:scope) { :project }
|
|
let(:actor_name) { 'group-name/project-name' }
|
|
let(:actor) { Struct.new(:full_path).new(actor_name) }
|
|
end
|
|
end
|
|
|
|
context 'when a group scope is provided' do
|
|
it_behaves_like 'disables a feature flag' do
|
|
let(:scope) { :group }
|
|
let(:actor_name) { 'group-name' }
|
|
let(:actor) { Struct.new(:full_path).new(actor_name) }
|
|
end
|
|
end
|
|
|
|
context 'when a user scope is provided' do
|
|
it_behaves_like 'disables a feature flag' do
|
|
let(:scope) { :user }
|
|
let(:actor_name) { 'user-name' }
|
|
let(:actor) { Struct.new(:username).new(actor_name) }
|
|
end
|
|
end
|
|
|
|
context 'when a feature group scope is provided' do
|
|
it_behaves_like 'disables a feature flag' do
|
|
let(:scope) { :feature_group }
|
|
let(:actor_name) { 'foo' }
|
|
let(:actor) { "foo" }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.enabled?' do
|
|
it 'returns a feature flag state' do
|
|
expect(QA::Runtime::API::Request)
|
|
.to receive(:new)
|
|
.with(api_client, "/features")
|
|
.and_return(request)
|
|
expect(described_class)
|
|
.to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "on" }]'))
|
|
|
|
expect(described_class.enabled?(feature_flag)).to be true
|
|
end
|
|
|
|
it 'raises an error when the scope is unknown' do
|
|
expect(QA::Runtime::API::Request)
|
|
.to receive(:new)
|
|
.with(api_client, "/features")
|
|
.and_return(request)
|
|
expect(described_class)
|
|
.to receive(:get)
|
|
.and_return(
|
|
Struct.new(:code, :body)
|
|
.new(200, %([{ "name": "a_flag", "state": "conditional", "gates": { "key": "groups", "value": ["foo"] } }])))
|
|
|
|
expect { described_class.enabled?(feature_flag, scope: 'foo') }.to raise_error(QA::Runtime::Feature::UnknownScopeError)
|
|
end
|
|
|
|
context 'when a project scope is provided' do
|
|
it_behaves_like 'checks a feature flag' do
|
|
let(:scope) { :project }
|
|
let(:actor_name) { 'group-name/project-name' }
|
|
let(:actor) { Struct.new(:full_path, :id).new(actor_name, 270) }
|
|
let(:gates) { %q([{"key": "actors", "value": ["Project:270"]}]) }
|
|
end
|
|
end
|
|
|
|
context 'when a group scope is provided' do
|
|
it_behaves_like 'checks a feature flag' do
|
|
let(:scope) { :group }
|
|
let(:actor_name) { 'group-name' }
|
|
let(:actor) { Struct.new(:full_path, :id).new(actor_name, 33) }
|
|
let(:gates) { %q([{"key": "actors", "value": ["Group:33"]}]) }
|
|
end
|
|
end
|
|
|
|
context 'when a user scope is provided' do
|
|
it_behaves_like 'checks a feature flag' do
|
|
let(:scope) { :user }
|
|
let(:actor_name) { 'user-name' }
|
|
let(:actor) { Struct.new(:full_path, :id).new(actor_name, 13) }
|
|
let(:gates) { %q([{"key": "actors", "value": ["User:13"]}]) }
|
|
end
|
|
end
|
|
|
|
context 'when a feature group scope is provided' do
|
|
it_behaves_like 'checks a feature flag' do
|
|
let(:scope) { :feature_group }
|
|
let(:actor_name) { 'foo' }
|
|
let(:actor) { "foo" }
|
|
let(:gates) { %q([{"key": "groups", "value": ["foo"]}]) }
|
|
end
|
|
end
|
|
|
|
context 'when a feature flag is not found via the API and there is no definition file' do
|
|
before do
|
|
allow(QA::Runtime::API::Request)
|
|
.to receive(:new)
|
|
.with(api_client, "/features")
|
|
.and_return(request)
|
|
allow(described_class)
|
|
.to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, '[]'))
|
|
allow(Dir).to receive(:glob).and_return([])
|
|
end
|
|
|
|
it 'raises an error' do
|
|
expect { described_class.enabled?(feature_flag) }
|
|
.to raise_error(QA::Runtime::Feature::UnknownFeatureFlagError)
|
|
end
|
|
end
|
|
|
|
context 'with definition files' do
|
|
context 'when no features are found via the API' do
|
|
before do
|
|
allow(QA::Runtime::API::Request)
|
|
.to receive(:new)
|
|
.with(api_client, "/features")
|
|
.and_return(request)
|
|
allow(described_class)
|
|
.to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, '[]'))
|
|
allow(Dir).to receive(:glob).and_return(['file_path'])
|
|
allow(File).to receive(:read).and_return(definition)
|
|
end
|
|
|
|
context 'with a default enabled defintion' do
|
|
let(:definition) { 'default_enabled: true' }
|
|
|
|
it 'returns a default enabled flag' do
|
|
expect(described_class.enabled?(feature_flag)).to be true
|
|
end
|
|
end
|
|
|
|
context 'with a default disabled defintion' do
|
|
let(:definition) { 'default_enabled: false' }
|
|
|
|
it 'returns a default disabled flag' do
|
|
expect(described_class.enabled?(feature_flag)).to be false
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the feature is found via the API' do
|
|
before do
|
|
allow(QA::Runtime::API::Request)
|
|
.to receive(:new)
|
|
.with(api_client, "/features")
|
|
.and_return(request)
|
|
allow(described_class)
|
|
.to receive(:get)
|
|
.and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "on" }]'))
|
|
end
|
|
|
|
it 'returns the value from the API not the definition file' do
|
|
expect(Dir).not_to receive(:glob)
|
|
expect(File).not_to receive(:read)
|
|
|
|
expect(described_class.enabled?(feature_flag)).to be true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.set' do
|
|
let(:scope) { { scope: 'actor' } }
|
|
|
|
it 'raises an error when the flag state is unknown' do
|
|
expect(described_class).not_to receive(:enable)
|
|
expect(described_class).not_to receive(:disable)
|
|
|
|
expect { described_class.set({ foo: 'bar' }, **scope) }.to raise_error(QA::Runtime::Feature::UnknownStateError, 'Unknown feature flag state: bar')
|
|
end
|
|
|
|
it 'enables feature flags' do
|
|
expect(described_class).to receive(:enable).with(:flag1, scope)
|
|
expect(described_class).to receive(:enable).with(:flag2, scope)
|
|
expect(described_class).not_to receive(:disable)
|
|
|
|
described_class.set({ flag1: 'enabled', flag2: 'enable' }, **scope)
|
|
end
|
|
|
|
it 'disables feature flags' do
|
|
expect(described_class).to receive(:disable).with(:flag1, scope)
|
|
expect(described_class).to receive(:disable).with(:flag2, scope)
|
|
expect(described_class).not_to receive(:enable)
|
|
|
|
described_class.set({ flag1: 'disable', flag2: 'disable' }, **scope)
|
|
end
|
|
|
|
it 'enables and disables feature flags' do
|
|
expect(described_class).to receive(:enable).with(:flag1, scope)
|
|
expect(described_class).to receive(:disable).with(:flag2, scope)
|
|
|
|
described_class.set({ flag1: 'enabled', flag2: 'disabled' }, **scope)
|
|
end
|
|
end
|
|
end
|