Commit 1802d98a authored by Jason Goodman's avatar Jason Goodman Committed by Shinya Maeda

Add validations for operations strategy

Validate name and parameters
parent fa0e92fe
# frozen_string_literal: true
module Operations
module FeatureFlags
class Strategy < ApplicationRecord
STRATEGY_DEFAULT = 'default'
STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId'
STRATEGY_USERWITHID = 'userWithId'
STRATEGIES = {
STRATEGY_DEFAULT => [].freeze,
STRATEGY_GRADUALROLLOUTUSERID => %w[groupId percentage].freeze,
STRATEGY_USERWITHID => ['userIds'].freeze
}.freeze
USERID_MAX_LENGTH = 256
self.table_name = 'operations_strategies'
belongs_to :feature_flag
validates :name,
inclusion: {
in: STRATEGIES.keys,
message: 'strategy name is invalid'
}
validate :parameters_validations, if: -> { errors[:name].blank? }
private
def parameters_validations
validate_parameters_type &&
validate_parameters_keys &&
validate_parameters_values
end
def validate_parameters_type
parameters.is_a?(Hash) || parameters_error('parameters are invalid')
end
def validate_parameters_keys
actual_keys = parameters.keys.sort
expected_keys = STRATEGIES[name].sort
expected_keys == actual_keys || parameters_error('parameters are invalid')
end
def validate_parameters_values
case name
when STRATEGY_GRADUALROLLOUTUSERID
gradual_rollout_user_id_parameters_validation
when STRATEGY_USERWITHID
user_with_id_parameters_validation
end
end
def gradual_rollout_user_id_parameters_validation
percentage = parameters['percentage']
group_id = parameters['groupId']
unless percentage.is_a?(String) && percentage.match(/\A[1-9]?[0-9]\z|\A100\z/)
parameters_error('percentage must be a string between 0 and 100 inclusive')
end
unless group_id.is_a?(String) && group_id.match(/\A[a-z]{1,32}\z/)
parameters_error('groupId parameter is invalid')
end
end
def user_with_id_parameters_validation
user_ids = parameters['userIds']
unless user_ids.is_a?(String) && !user_ids.match(/[\n\r\t]|,,/) && valid_ids?(user_ids.split(","))
parameters_error("userIds must be a string of unique comma separated values each #{USERID_MAX_LENGTH} characters or less")
end
end
def valid_ids?(user_ids)
user_ids.uniq.length == user_ids.length &&
user_ids.all? { |id| valid_id?(id) }
end
def valid_id?(user_id)
user_id.present? &&
user_id.strip == user_id &&
user_id.length <= USERID_MAX_LENGTH
end
def parameters_error(message)
errors.add(:parameters, message)
false
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Operations::FeatureFlags::Strategy do
describe 'validations' do
it do
is_expected.to validate_inclusion_of(:name)
.in_array(%w[default gradualRolloutUserId userWithId])
.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
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
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
[50, 40.0, { key: "value" }, "garbage", "00", "01", "101", "-1", "-10", "0100",
"1000", "10.0", "5%", "25%", "100hi", "e100", "30m", " ", "\r\n", "\n", "\t",
"\n10", "20\n", "\n100", "100\n", "\n ", nil]
end
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
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
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
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
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
feature_flag = create(:operations_feature_flag)
strategy = described_class.create(feature_flag: feature_flag,
name: 'default',
parameters: {})
expect(strategy.errors[:parameters]).to be_empty
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment