2020-09-03 17:08:18 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
RSpec.describe Operations::FeatureFlags::Strategy do
|
|
|
|
let_it_be(:project) { create(:project) }
|
2020-10-03 02:08:24 -04:00
|
|
|
let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
|
2020-09-03 17:08:18 -04:00
|
|
|
|
|
|
|
describe 'validations' do
|
|
|
|
it do
|
|
|
|
is_expected.to validate_inclusion_of(:name)
|
2020-10-01 20:08:51 -04:00
|
|
|
.in_array(%w[default gradualRolloutUserId flexibleRollout userWithId gitlabUserList])
|
2020-09-03 17:08:18 -04:00
|
|
|
.with_message('strategy name is invalid')
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'parameters' do
|
|
|
|
context 'when the strategy name is invalid' do
|
|
|
|
where(:invalid_name) do
|
|
|
|
[nil, {}, [], 'nothing', 3]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'skips parameters validation' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: invalid_name, parameters: { bad: 'params' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:name]).to eq(['strategy name is invalid'])
|
|
|
|
expect(strategy.errors[:parameters]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the strategy name is gradualRolloutUserId' do
|
|
|
|
where(:invalid_parameters) do
|
|
|
|
[nil, {}, { percentage: '40', groupId: 'mygroup', userIds: '4' }, { percentage: '40' },
|
|
|
|
{ percentage: '40', groupId: 'mygroup', extra: nil }, { groupId: 'mygroup' }]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'must have valid parameters for the strategy' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId', parameters: invalid_parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'allows the parameters in any order' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId',
|
|
|
|
parameters: { percentage: '10', groupId: 'mygroup' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to be_empty
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'percentage' do
|
|
|
|
where(:invalid_value) do
|
2020-10-01 20:08:51 -04:00
|
|
|
[50, 40.0, { key: "value" }, "garbage", "101", "-1", "-10", "1000", "10.0", "5%", "25%",
|
|
|
|
"100hi", "e100", "30m", " ", "\r\n", "\n", "\t", "\n10", "20\n", "\n100", "100\n",
|
|
|
|
"\n ", nil]
|
2020-09-03 17:08:18 -04:00
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId',
|
|
|
|
parameters: { groupId: 'mygroup', percentage: invalid_value })
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq(['percentage must be a string between 0 and 100 inclusive'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
where(:valid_value) do
|
|
|
|
%w[0 1 10 38 100 93]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId',
|
|
|
|
parameters: { groupId: 'mygroup', percentage: valid_value })
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq([])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'groupId' do
|
|
|
|
where(:invalid_value) do
|
|
|
|
[nil, 4, 50.0, {}, 'spaces bad', 'bad$', '%bad', '<bad', 'bad>', '!bad',
|
|
|
|
'.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'must be a string value of up to 32 lowercase characters' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId',
|
|
|
|
parameters: { groupId: invalid_value, percentage: '40' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq(['groupId parameter is invalid'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
where(:valid_value) do
|
|
|
|
["somegroup", "anothergroup", "okay", "g", "a" * 32]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'must be a string value of up to 32 lowercase characters' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId',
|
|
|
|
parameters: { groupId: valid_value, percentage: '40' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq([])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-10-01 20:08:51 -04:00
|
|
|
context 'when the strategy name is flexibleRollout' do
|
|
|
|
valid_parameters = { rollout: '40', groupId: 'mygroup', stickiness: 'DEFAULT' }
|
|
|
|
where(invalid_parameters: [
|
|
|
|
nil,
|
|
|
|
{},
|
|
|
|
*valid_parameters.to_a.combination(1).to_a.map { |p| p.to_h },
|
|
|
|
*valid_parameters.to_a.combination(2).to_a.map { |p| p.to_h },
|
|
|
|
{ **valid_parameters, userIds: '4' },
|
|
|
|
{ **valid_parameters, extra: nil }
|
|
|
|
])
|
|
|
|
with_them do
|
|
|
|
it 'must have valid parameters for the strategy' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: invalid_parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
[
|
|
|
|
[:rollout, '10'],
|
|
|
|
[:stickiness, 'DEFAULT'],
|
|
|
|
[:groupId, 'mygroup']
|
|
|
|
].permutation(3).each do |parameters|
|
|
|
|
it "allows the parameters in the order #{parameters.map { |p| p.first }.join(', ')}" do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: Hash[parameters])
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'rollout' do
|
|
|
|
where(invalid_value: [50, 40.0, { key: "value" }, "garbage", "101", "-1", " ", "-10",
|
|
|
|
"1000", "10.0", "5%", "25%", "100hi", "e100", "30m", "\r\n",
|
|
|
|
"\n", "\t", "\n10", "20\n", "\n100", "100\n", "\n ", nil])
|
|
|
|
with_them do
|
|
|
|
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
|
|
|
|
parameters = { stickiness: 'DEFAULT', groupId: 'mygroup', rollout: invalid_value }
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq([
|
|
|
|
'rollout must be a string between 0 and 100 inclusive'
|
|
|
|
])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
where(valid_value: %w[0 1 10 38 100 93])
|
|
|
|
with_them do
|
|
|
|
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
|
|
|
|
parameters = { stickiness: 'DEFAULT', groupId: 'mygroup', rollout: valid_value }
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq([])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'groupId' do
|
|
|
|
where(invalid_value: [nil, 4, 50.0, {}, 'spaces bad', 'bad$', '%bad', '<bad', 'bad>',
|
|
|
|
'!bad', '.bad', 'Bad', 'bad1', "", " ", "b" * 33, "ba_d", "ba\nd"])
|
|
|
|
with_them do
|
|
|
|
it 'must be a string value of up to 32 lowercase characters' do
|
|
|
|
parameters = { stickiness: 'DEFAULT', groupId: invalid_value, rollout: '40' }
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq(['groupId parameter is invalid'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
where(valid_value: ["somegroup", "anothergroup", "okay", "g", "a" * 32])
|
|
|
|
with_them do
|
|
|
|
it 'must be a string value of up to 32 lowercase characters' do
|
|
|
|
parameters = { stickiness: 'DEFAULT', groupId: valid_value, rollout: '40' }
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq([])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'stickiness' do
|
|
|
|
where(invalid_value: [nil, " ", "default", "DEFAULT\n", "UserId", "USER", "USERID "])
|
|
|
|
with_them do
|
|
|
|
it 'must be a string representing a supported stickiness setting' do
|
|
|
|
parameters = { stickiness: invalid_value, groupId: 'mygroup', rollout: '40' }
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq([
|
|
|
|
'stickiness parameter must be DEFAULT, USERID, SESSIONID, or RANDOM'
|
|
|
|
])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
where(valid_value: %w[DEFAULT USERID SESSIONID RANDOM])
|
|
|
|
with_them do
|
|
|
|
it 'must be a string representing a supported stickiness setting' do
|
|
|
|
parameters = { stickiness: valid_value, groupId: 'mygroup', rollout: '40' }
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq([])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-09-03 17:08:18 -04:00
|
|
|
context 'when the strategy name is userWithId' do
|
|
|
|
where(:invalid_parameters) do
|
|
|
|
[nil, { userIds: 'sam', percentage: '40' }, { userIds: 'sam', some: 'param' }, { percentage: '40' }, {}]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'must have valid parameters for the strategy' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'userWithId', parameters: invalid_parameters)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'userIds' do
|
|
|
|
where(:valid_value) do
|
|
|
|
["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike",
|
|
|
|
"gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0",
|
|
|
|
"$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed",
|
|
|
|
"a" * 256, "a,#{'b' * 256},ccc", "many spaces"]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'is valid with a string of comma separated values' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'userWithId', parameters: { userIds: valid_value })
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
where(:invalid_value) do
|
|
|
|
[1, 2.5, {}, [], nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r",
|
|
|
|
"joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ",
|
|
|
|
" ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'is invalid' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'userWithId', parameters: { userIds: invalid_value })
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to include(
|
|
|
|
'userIds must be a string of unique comma separated values each 256 characters or less'
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the strategy name is default' do
|
|
|
|
where(:invalid_value) do
|
|
|
|
[{ groupId: "hi", percentage: "7" }, "", "nothing", 7, nil, [], 2.5]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'must be empty' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'default',
|
|
|
|
parameters: invalid_value)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'must be empty' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'default',
|
|
|
|
parameters: {})
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the strategy name is gitlabUserList' do
|
|
|
|
where(:invalid_value) do
|
|
|
|
[{ groupId: "default", percentage: "7" }, "", "nothing", 7, nil, [], 2.5, { userIds: 'user1' }]
|
|
|
|
end
|
|
|
|
with_them do
|
|
|
|
it 'must be empty' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gitlabUserList',
|
|
|
|
parameters: invalid_value)
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'must be empty' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gitlabUserList',
|
|
|
|
parameters: {})
|
|
|
|
|
|
|
|
expect(strategy.errors[:parameters]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'associations' do
|
|
|
|
context 'when name is gitlabUserList' do
|
|
|
|
it 'is valid when associated with a user list' do
|
|
|
|
user_list = create(:operations_feature_flag_user_list, project: project)
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gitlabUserList',
|
|
|
|
user_list: user_list,
|
|
|
|
parameters: {})
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to be_empty
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is invalid without a user list' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gitlabUserList',
|
|
|
|
parameters: {})
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to eq(["can't be blank"])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is invalid when associated with a user list from another project' do
|
|
|
|
other_project = create(:project)
|
|
|
|
user_list = create(:operations_feature_flag_user_list, project: other_project)
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gitlabUserList',
|
|
|
|
user_list: user_list,
|
|
|
|
parameters: {})
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to eq(['must belong to the same project'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when name is default' do
|
|
|
|
it 'is invalid when associated with a user list' do
|
|
|
|
user_list = create(:operations_feature_flag_user_list, project: project)
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'default',
|
|
|
|
user_list: user_list,
|
|
|
|
parameters: {})
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to eq(['must be blank'])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is valid without a user list' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'default',
|
|
|
|
parameters: {})
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when name is userWithId' do
|
|
|
|
it 'is invalid when associated with a user list' do
|
|
|
|
user_list = create(:operations_feature_flag_user_list, project: project)
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'userWithId',
|
|
|
|
user_list: user_list,
|
|
|
|
parameters: { userIds: 'user1' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to eq(['must be blank'])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is valid without a user list' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'userWithId',
|
|
|
|
parameters: { userIds: 'user1' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when name is gradualRolloutUserId' do
|
|
|
|
it 'is invalid when associated with a user list' do
|
|
|
|
user_list = create(:operations_feature_flag_user_list, project: project)
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId',
|
|
|
|
user_list: user_list,
|
|
|
|
parameters: { groupId: 'default', percentage: '10' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to eq(['must be blank'])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is valid without a user list' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'gradualRolloutUserId',
|
|
|
|
parameters: { groupId: 'default', percentage: '10' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
2020-10-01 20:08:51 -04:00
|
|
|
|
|
|
|
context 'when name is flexibleRollout' do
|
|
|
|
it 'is invalid when associated with a user list' do
|
|
|
|
user_list = create(:operations_feature_flag_user_list, project: project)
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
user_list: user_list,
|
|
|
|
parameters: { groupId: 'default',
|
|
|
|
rollout: '10',
|
|
|
|
stickiness: 'DEFAULT' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to eq(['must be blank'])
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is valid without a user list' do
|
|
|
|
strategy = described_class.create(feature_flag: feature_flag,
|
|
|
|
name: 'flexibleRollout',
|
|
|
|
parameters: { groupId: 'default',
|
|
|
|
rollout: '10',
|
|
|
|
stickiness: 'DEFAULT' })
|
|
|
|
|
|
|
|
expect(strategy.errors[:user_list]).to be_empty
|
|
|
|
end
|
|
|
|
end
|
2020-09-03 17:08:18 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|