Skip to content

Commit 84736de

Browse files
oakbanialiabbasrizvi
authored andcommitted
Params validation for API methods (#119)
1 parent 90e172d commit 84736de

File tree

4 files changed

+160
-17
lines changed

4 files changed

+160
-17
lines changed

optimizely/helpers/validator.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2017, Optimizely
1+
# Copyright 2016-2018, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -13,6 +13,7 @@
1313

1414
import json
1515
import jsonschema
16+
from six import string_types
1617

1718
from optimizely.user_profile import UserProfile
1819
from . import constants
@@ -151,3 +152,18 @@ def is_user_profile_valid(user_profile):
151152
return False
152153

153154
return True
155+
156+
157+
def is_non_empty_string(input_id_key):
158+
""" Determine if provided input_id_key is a non-empty string or not.
159+
160+
Args:
161+
input_id_key: Variable which needs to be validated.
162+
163+
Returns:
164+
Boolean depending upon whether input is valid or not.
165+
"""
166+
if input_id_key and isinstance(input_id_key, string_types):
167+
return True
168+
169+
return False

optimizely/optimizely.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def _send_impression_event(self, experiment, variation, user_id, attributes):
179179
self.event_dispatcher.dispatch_event(impression_event)
180180
except:
181181
self.logger.exception('Unable to dispatch impression event!')
182+
182183
self.notification_center.send_notifications(enums.NotificationTypes.ACTIVATE,
183184
experiment, user_id, attributes, variation, impression_event)
184185

@@ -262,6 +263,14 @@ def activate(self, experiment_key, user_id, attributes=None):
262263
self.logger.error(enums.Errors.INVALID_DATAFILE.format('activate'))
263264
return None
264265

266+
if not validator.is_non_empty_string(experiment_key):
267+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
268+
return None
269+
270+
if not validator.is_non_empty_string(user_id):
271+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
272+
return None
273+
265274
variation_key = self.get_variation(experiment_key, user_id, attributes)
266275

267276
if not variation_key:
@@ -291,6 +300,14 @@ def track(self, event_key, user_id, attributes=None, event_tags=None):
291300
self.logger.error(enums.Errors.INVALID_DATAFILE.format('track'))
292301
return
293302

303+
if not validator.is_non_empty_string(event_key):
304+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('event_key'))
305+
return
306+
307+
if not validator.is_non_empty_string(user_id):
308+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
309+
return
310+
294311
if not self._validate_user_inputs(attributes, event_tags):
295312
return
296313

@@ -339,6 +356,14 @@ def get_variation(self, experiment_key, user_id, attributes=None):
339356
self.logger.error(enums.Errors.INVALID_DATAFILE.format('get_variation'))
340357
return None
341358

359+
if not validator.is_non_empty_string(experiment_key):
360+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
361+
return None
362+
363+
if not validator.is_non_empty_string(user_id):
364+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
365+
return None
366+
342367
experiment = self.config.get_experiment_from_key(experiment_key)
343368

344369
if not experiment:
@@ -373,12 +398,12 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
373398
self.logger.error(enums.Errors.INVALID_DATAFILE.format('is_feature_enabled'))
374399
return False
375400

376-
if feature_key is None:
377-
self.logger.error(enums.Errors.NONE_FEATURE_KEY_PARAMETER)
401+
if not validator.is_non_empty_string(feature_key):
402+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('feature_key'))
378403
return False
379404

380-
if user_id is None:
381-
self.logger.error(enums.Errors.NONE_USER_ID_PARAMETER)
405+
if not validator.is_non_empty_string(user_id):
406+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
382407
return False
383408

384409
feature = self.config.get_feature_from_key(feature_key)
@@ -417,6 +442,10 @@ def get_enabled_features(self, user_id, attributes=None):
417442
self.logger.error(enums.Errors.INVALID_DATAFILE.format('get_enabled_features'))
418443
return enabled_features
419444

445+
if not validator.is_non_empty_string(user_id):
446+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
447+
return enabled_features
448+
420449
for feature in self.config.feature_key_map.values():
421450
if self.is_feature_enabled(feature.key, user_id, attributes):
422451
enabled_features.append(feature.key)

tests/helpers_tests/test_validator.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2017, Optimizely
1+
# Copyright 2016-2018, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -130,6 +130,22 @@ def test_is_user_profile_valid__returns_false(self):
130130
'experiment_bucket_map': {'1234': {'variation_id': '5678'},
131131
'1235': {'some_key': 'some_value'}}}))
132132

133+
def test_is_non_empty_string(self):
134+
""" Test that the method returns True only for a non-empty string. """
135+
136+
self.assertFalse(validator.is_non_empty_string(None))
137+
self.assertFalse(validator.is_non_empty_string([]))
138+
self.assertFalse(validator.is_non_empty_string({}))
139+
self.assertFalse(validator.is_non_empty_string(0))
140+
self.assertFalse(validator.is_non_empty_string(99))
141+
self.assertFalse(validator.is_non_empty_string(1.2))
142+
self.assertFalse(validator.is_non_empty_string(True))
143+
self.assertFalse(validator.is_non_empty_string(False))
144+
self.assertFalse(validator.is_non_empty_string(''))
145+
146+
self.assertTrue(validator.is_non_empty_string('0'))
147+
self.assertTrue(validator.is_non_empty_string('test_user'))
148+
133149

134150
class DatafileValidationTests(base.BaseTest):
135151

tests/test_optimizely.py

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,30 @@ def test_track__invalid_object(self):
11431143

11441144
mock_client_logging.error.assert_called_once_with('Datafile has invalid format. Failing "track".')
11451145

1146+
def test_track__invalid_experiment_key(self):
1147+
""" Test that None is returned and expected log messages are logged during track \
1148+
when exp_key is in invalid format. """
1149+
1150+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \
1151+
mock.patch('optimizely.helpers.validator.is_non_empty_string', return_value=False) as mock_validator:
1152+
self.assertIsNone(self.optimizely.track(99, 'test_user'))
1153+
1154+
mock_validator.assert_any_call(99)
1155+
1156+
mock_client_logging.error.assert_called_once_with('Provided "event_key" is in an invalid format.')
1157+
1158+
def test_track__invalid_user_id(self):
1159+
""" Test that None is returned and expected log messages are logged during track \
1160+
when user_id is in invalid format. """
1161+
1162+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \
1163+
mock.patch('optimizely.helpers.validator.is_non_empty_string', side_effect=[True, False]) as mock_validator:
1164+
self.assertIsNone(self.optimizely.track('test_event', 99))
1165+
1166+
mock_validator.assert_any_call(99)
1167+
1168+
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
1169+
11461170
def test_get_variation__invalid_object(self):
11471171
""" Test that get_variation logs error if Optimizely object is not created correctly. """
11481172

@@ -1153,7 +1177,7 @@ def test_get_variation__invalid_object(self):
11531177

11541178
mock_client_logging.error.assert_called_once_with('Datafile has invalid format. Failing "get_variation".')
11551179

1156-
def test_get_variation_invalid_experiment_key(self):
1180+
def test_get_variation_unknown_experiment_key(self):
11571181
""" Test that get_variation retuns None when invalid experiment key is given. """
11581182
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
11591183
self.optimizely.get_variation('aabbccdd', 'test_user', None)
@@ -1162,33 +1186,37 @@ def test_get_variation_invalid_experiment_key(self):
11621186
'Experiment key "aabbccdd" is invalid. Not activating user "test_user".'
11631187
)
11641188

1165-
def test_is_feature_enabled__returns_false_for_none_feature_key(self):
1166-
""" Test that is_feature_enabled returns false if the provided feature key is None. """
1189+
def test_is_feature_enabled__returns_false_for_invalid_feature_key(self):
1190+
""" Test that is_feature_enabled returns false if the provided feature key is invalid. """
11671191

11681192
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
11691193

1170-
with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
1194+
with mock.patch.object(opt_obj, 'logger') as mock_client_logging,\
1195+
mock.patch('optimizely.helpers.validator.is_non_empty_string', return_value=False) as mock_validator:
11711196
self.assertFalse(opt_obj.is_feature_enabled(None, 'test_user'))
11721197

1173-
mock_client_logging.error.assert_called_once_with(enums.Errors.NONE_FEATURE_KEY_PARAMETER)
1198+
mock_validator.assert_any_call(None)
1199+
mock_client_logging.error.assert_called_with('Provided "feature_key" is in an invalid format.')
11741200

1175-
def test_is_feature_enabled__returns_false_for_none_user_id(self):
1176-
""" Test that is_feature_enabled returns false if the provided user ID is None. """
1201+
def test_is_feature_enabled__returns_false_for_invalid_user_id(self):
1202+
""" Test that is_feature_enabled returns false if the provided user ID is invalid. """
11771203

11781204
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
11791205

1180-
with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
1181-
self.assertFalse(opt_obj.is_feature_enabled('feature_key', None))
1206+
with mock.patch.object(opt_obj, 'logger') as mock_client_logging,\
1207+
mock.patch('optimizely.helpers.validator.is_non_empty_string', side_effect=[True, False]) as mock_validator:
1208+
self.assertFalse(opt_obj.is_feature_enabled('feature_key', 1.2))
11821209

1183-
mock_client_logging.error.assert_called_once_with(enums.Errors.NONE_USER_ID_PARAMETER)
1210+
mock_validator.assert_any_call(1.2)
1211+
mock_client_logging.error.assert_called_with('Provided "user_id" is in an invalid format.')
11841212

11851213
def test_is_feature_enabled__returns_false_for_invalid_feature(self):
11861214
""" Test that the feature is not enabled for the user if the provided feature key is invalid. """
11871215

11881216
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
11891217

11901218
with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature') as mock_decision, \
1191-
mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event:
1219+
mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event:
11921220
self.assertFalse(opt_obj.is_feature_enabled('invalid_feature', 'user1'))
11931221

11941222
self.assertFalse(mock_decision.called)
@@ -1462,6 +1490,14 @@ def side_effect(*args, **kwargs):
14621490
mock_is_feature_enabled.assert_any_call('test_feature_in_group', 'user_1', None)
14631491
mock_is_feature_enabled.assert_any_call('test_feature_in_experiment_and_rollout', 'user_1', None)
14641492

1493+
def test_get_enabled_features_invalid_user_id(self):
1494+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \
1495+
mock.patch('optimizely.helpers.validator.is_non_empty_string', return_value=False) as mock_validator:
1496+
self.optimizely.get_enabled_features(1.2)
1497+
1498+
mock_validator.assert_any_call(1.2)
1499+
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
1500+
14651501
def test_get_enabled_features__invalid_object(self):
14661502
""" Test that get_enabled_features returns empty list if Optimizely object is not valid. """
14671503

@@ -2003,6 +2039,52 @@ def test_get_variation__invalid_attributes(self):
20032039

20042040
mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.')
20052041

2042+
def test_get_variation__invalid_experiment_key(self):
2043+
""" Test that None is returned and expected log messages are logged during get_variation \
2044+
when exp_key is in invalid format. """
2045+
2046+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging,\
2047+
mock.patch('optimizely.helpers.validator.is_non_empty_string', return_value=False) as mock_validator:
2048+
self.assertIsNone(self.optimizely.get_variation(99, 'test_user'))
2049+
2050+
mock_validator.assert_any_call(99)
2051+
mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.')
2052+
2053+
def test_get_variation__invalid_user_id(self):
2054+
""" Test that None is returned and expected log messages are logged during get_variation \
2055+
when user_id is in invalid format. """
2056+
2057+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging,\
2058+
mock.patch('optimizely.helpers.validator.is_non_empty_string', side_effect=[True, False]) as mock_validator:
2059+
self.assertIsNone(self.optimizely.get_variation('test_experiment', 99))
2060+
2061+
mock_validator.assert_any_call(99)
2062+
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
2063+
2064+
def test_activate__invalid_experiment_key(self):
2065+
""" Test that None is returned and expected log messages are logged during activate \
2066+
when exp_key is in invalid format. """
2067+
2068+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging,\
2069+
mock.patch('optimizely.helpers.validator.is_non_empty_string', return_value=False) as mock_validator:
2070+
self.assertIsNone(self.optimizely.activate(99, 'test_user'))
2071+
2072+
mock_validator.assert_any_call(99)
2073+
2074+
mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.')
2075+
2076+
def test_activate__invalid_user_id(self):
2077+
""" Test that None is returned and expected log messages are logged during activate \
2078+
when user_id is in invalid format. """
2079+
2080+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging,\
2081+
mock.patch('optimizely.helpers.validator.is_non_empty_string', side_effect=[True, False]) as mock_validator:
2082+
self.assertIsNone(self.optimizely.activate('test_experiment', 99))
2083+
2084+
mock_validator.assert_any_call(99)
2085+
2086+
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
2087+
20062088
def test_activate__invalid_attributes(self):
20072089
""" Test that expected log messages are logged during activate when attributes are in invalid format. """
20082090
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:

0 commit comments

Comments
 (0)