Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement get_feature_variable and create unit tests #190

Merged
merged 7 commits into from
Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,32 @@ def get_enabled_features(user_id, attributes = nil)
enabled_features
end

# Get the value of the specified variable in the feature flag.
#
# @param feature_flag_key - String key of feature flag the variable belongs to
# @param variable_key - String key of variable for which we are getting the value
# @param user_id - String user ID
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
#
# @return [*] the type-casted variable value.
# @return [nil] if the feature flag or variable are not found.

def get_feature_variable(feature_flag_key, variable_key, user_id, attributes = nil)
unless is_valid
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable').message)
return nil
end
variable_value = get_feature_variable_for_type(
feature_flag_key,
variable_key,
nil,
user_id,
attributes
)

variable_value
end

# Get the String value of the specified variable in the feature flag.
#
# @param feature_flag_key - String key of feature flag the variable belongs to
Expand Down Expand Up @@ -556,6 +582,9 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
return nil if variable.nil?

feature_enabled = false

# If variable_type is nil, set it equal to variable['type']
variable_type ||= variable['type']
# Returns nil if type differs
if variable['type'] != variable_type
@logger.log(Logger::WARN,
Expand Down
9 changes: 9 additions & 0 deletions lib/optimizely/helpers/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ def inputs_valid?(variables, logger = NoOpLogger.new, level = Logger::ERROR)
variables.delete :user_id
end

if variables.include? :variable_type
# Empty variable_type is a valid user ID.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh? This can you explain this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If variables[:variable_type] is anything other than a String or null value, it is invalid. Otherwise, it is valid, and so we remove variable_type from variables. That way, it does not go through the value.is_a?(String) && !value.empty? check on line 145.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Thanks

unless variables[:variable_type].is_a?(String) || !variables[:variable_type]
is_valid = false
logger.log(level, "#{Constants::INPUT_VARIABLES['VARIABLE_TYPE']} is invalid")
end
variables.delete :variable_type
end

variables.each do |key, value|
next if value.is_a?(String) && !value.empty?

Expand Down
179 changes: 179 additions & 0 deletions spec/project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,185 @@ class InvalidErrorHandler; end
end
end

describe '#get_feature_variable' do
user_id = 'test_user'
user_attributes = {}

it 'should return nil when called with invalid project config' do
logger = double('logger')
allow(logger).to receive(:log)
allow(Optimizely::SimpleLogger).to receive(:new) { logger }
invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
expect(invalid_project.get_feature_variable('string_single_variable_feature', 'string_variable', user_id, user_attributes))
.to eq(nil)
expect(logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_feature_variable'.")
end

it 'should return nil and log an error when Config Manager returns nil config' do
allow(project_instance.config_manager).to receive(:config).and_return(nil)
expect(project_instance.get_feature_variable('string_single_variable_feature', 'string_variable', user_id, user_attributes)).to eq(nil)
expect(spy_logger).to have_received(:log).once.with(
Logger::ERROR,
"Optimizely instance is not valid. Failing 'get_feature_variable'."
)
end

describe 'when the feature flag is enabled for the user' do
describe 'and a variable usage instance is not found' do
it 'should return the default variable value!!!' do
variation_to_return = project_instance.config_manager.config.rollout_id_map['166661']['experiments'][0]['variations'][0]
decision_to_return = {
'experiment' => nil,
'variation' => variation_to_return
}
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)

expect(project_instance.get_feature_variable('string_single_variable_feature', 'string_variable', user_id, user_attributes))
.to eq('wingardium leviosa')
expect(spy_logger).to have_received(:log).once
.with(
Logger::DEBUG,
"Variable 'string_variable' is not used in variation '177775'. Returning the default variable value 'wingardium leviosa'."
)
end
end

describe 'and a variable usage instance is found' do
it 'should return the string variable value for the variation for the user is bucketed into' do
experiment_to_return = project_instance.config_manager.config.experiment_key_map['test_experiment_with_feature_rollout']
variation_to_return = experiment_to_return['variations'][0]
decision_to_return = {
'experiment' => experiment_to_return,
'variation' => variation_to_return
}
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)

expect(project_instance.get_feature_variable('string_single_variable_feature', 'string_variable', user_id, user_attributes))
.to eq('cta_1')

expect(spy_logger).to have_received(:log).once
expect(spy_logger).to have_received(:log).once
.with(
Logger::INFO,
"Got variable value 'cta_1' for variable 'string_variable' of feature flag 'string_single_variable_feature'."
)
end

it 'should return the boolean variable value for the variation for the user is bucketed into' do
boolean_feature = project_instance.config_manager.config.feature_flag_key_map['boolean_single_variable_feature']
rollout = project_instance.config_manager.config.rollout_id_map[boolean_feature['rolloutId']]
variation_to_return = rollout['experiments'][0]['variations'][0]
decision_to_return = {
'experiment' => nil,
'variation' => variation_to_return
}
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)

expect(project_instance.get_feature_variable('boolean_single_variable_feature', 'boolean_variable', user_id, user_attributes))
.to eq(true)

expect(spy_logger).to have_received(:log).once
expect(spy_logger).to have_received(:log).once
.with(
Logger::INFO,
"Got variable value 'true' for variable 'boolean_variable' of feature flag 'boolean_single_variable_feature'."
)
end

it 'should return the double variable value for the variation for the user is bucketed into' do
double_feature = project_instance.config_manager.config.feature_flag_key_map['double_single_variable_feature']
experiment_to_return = project_instance.config_manager.config.experiment_id_map[double_feature['experimentIds'][0]]
variation_to_return = experiment_to_return['variations'][0]
decision_to_return = {
'experiment' => experiment_to_return,
'variation' => variation_to_return
}

allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)

expect(project_instance.get_feature_variable('double_single_variable_feature', 'double_variable', user_id, user_attributes))
.to eq(42.42)

expect(spy_logger).to have_received(:log).once
expect(spy_logger).to have_received(:log).once
.with(
Logger::INFO,
"Got variable value '42.42' for variable 'double_variable' of feature flag 'double_single_variable_feature'."
)
end

it 'should return the integer variable value for the variation for the user is bucketed into' do
integer_feature = project_instance.config_manager.config.feature_flag_key_map['integer_single_variable_feature']
experiment_to_return = project_instance.config_manager.config.experiment_id_map[integer_feature['experimentIds'][0]]
variation_to_return = experiment_to_return['variations'][0]
decision_to_return = {
'experiment' => experiment_to_return,
'variation' => variation_to_return
}

allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)

expect(project_instance.get_feature_variable('integer_single_variable_feature', 'integer_variable', user_id, user_attributes))
.to eq(42)

expect(spy_logger).to have_received(:log).once
expect(spy_logger).to have_received(:log).once
.with(
Logger::INFO,
"Got variable value '42' for variable 'integer_variable' of feature flag 'integer_single_variable_feature'."
)
end
end
end

describe 'when the feature flag is not enabled for the user' do
it 'should return the default variable value' do
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(nil)

expect(project_instance.get_feature_variable('string_single_variable_feature', 'string_variable', user_id, user_attributes))
.to eq('wingardium leviosa')
expect(spy_logger).to have_received(:log).once
expect(spy_logger).to have_received(:log).once
.with(
Logger::INFO,
"User 'test_user' was not bucketed into any variation for feature flag 'string_single_variable_feature'. Returning the default variable value 'wingardium leviosa'."
)
end
end

describe 'when the specified feature flag is invalid' do
it 'should log an error message and return nil' do
expect(project_instance.get_feature_variable('totally_invalid_feature_key', 'string_variable', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).twice
expect(spy_logger).to have_received(:log).once
.with(
Logger::ERROR,
"Feature flag key 'totally_invalid_feature_key' is not in datafile."
)
expect(spy_logger).to have_received(:log).once
.with(
Logger::INFO,
"No feature flag was found for key 'totally_invalid_feature_key'."
)
end
end

describe 'when the specified feature variable is invalid' do
it 'should log an error message and return nil' do
expect(project_instance.get_feature_variable('string_single_variable_feature', 'invalid_string_variable', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).once
expect(spy_logger).to have_received(:log).once
.with(
Logger::ERROR,
"No feature variable was found for key 'invalid_string_variable' in feature flag 'string_single_variable_feature'."
)
end
end
end

describe '#get_feature_variable_for_type with empty params' do
user_id = 'test_user'
user_attributes = {}
Expand Down