Skip to content

Commit e5ec118

Browse files
rashidspmikeproeng37
authored andcommitted
refact(API): Adds missing input validations in all API methods and validates empty user Id. (#144)
1 parent 46d31a6 commit e5ec118

File tree

4 files changed

+140
-58
lines changed

4 files changed

+140
-58
lines changed

optimizely/optimizely.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
13+
from six import string_types
14+
1315
from . import decision_service
1416
from . import entities
1517
from . import event_builder
@@ -200,16 +202,16 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ
200202
- Variable key is invalid.
201203
- Mismatch with type of variable.
202204
"""
203-
if feature_key is None:
204-
self.logger.error(enums.Errors.NONE_FEATURE_KEY_PARAMETER)
205+
if not validator.is_non_empty_string(feature_key):
206+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('feature_key'))
205207
return None
206208

207-
if variable_key is None:
208-
self.logger.error(enums.Errors.NONE_VARIABLE_KEY_PARAMETER)
209+
if not validator.is_non_empty_string(variable_key):
210+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('variable_key'))
209211
return None
210212

211-
if user_id is None:
212-
self.logger.error(enums.Errors.NONE_USER_ID_PARAMETER)
213+
if not isinstance(user_id, string_types):
214+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
213215
return None
214216

215217
if not self._validate_user_inputs(attributes):
@@ -271,7 +273,7 @@ def activate(self, experiment_key, user_id, attributes=None):
271273
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
272274
return None
273275

274-
if not validator.is_non_empty_string(user_id):
276+
if not isinstance(user_id, string_types):
275277
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
276278
return None
277279

@@ -308,7 +310,7 @@ def track(self, event_key, user_id, attributes=None, event_tags=None):
308310
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('event_key'))
309311
return
310312

311-
if not validator.is_non_empty_string(user_id):
313+
if not isinstance(user_id, string_types):
312314
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
313315
return
314316

@@ -364,7 +366,7 @@ def get_variation(self, experiment_key, user_id, attributes=None):
364366
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
365367
return None
366368

367-
if not validator.is_non_empty_string(user_id):
369+
if not isinstance(user_id, string_types):
368370
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
369371
return None
370372

@@ -406,7 +408,7 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
406408
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('feature_key'))
407409
return False
408410

409-
if not validator.is_non_empty_string(user_id):
411+
if not isinstance(user_id, string_types):
410412
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
411413
return False
412414

@@ -449,7 +451,7 @@ def get_enabled_features(self, user_id, attributes=None):
449451
self.logger.error(enums.Errors.INVALID_DATAFILE.format('get_enabled_features'))
450452
return enabled_features
451453

452-
if not validator.is_non_empty_string(user_id):
454+
if not isinstance(user_id, string_types):
453455
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
454456
return enabled_features
455457

@@ -551,6 +553,18 @@ def set_forced_variation(self, experiment_key, user_id, variation_key):
551553
A boolean value that indicates if the set completed successfully.
552554
"""
553555

556+
if not self.is_valid:
557+
self.logger.error(enums.Errors.INVALID_DATAFILE.format('set_forced_variation'))
558+
return False
559+
560+
if not validator.is_non_empty_string(experiment_key):
561+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
562+
return False
563+
564+
if not isinstance(user_id, string_types):
565+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
566+
return False
567+
554568
return self.config.set_forced_variation(experiment_key, user_id, variation_key)
555569

556570
def get_forced_variation(self, experiment_key, user_id):
@@ -564,5 +578,17 @@ def get_forced_variation(self, experiment_key, user_id):
564578
The forced variation key. None if no forced variation key.
565579
"""
566580

581+
if not self.is_valid:
582+
self.logger.error(enums.Errors.INVALID_DATAFILE.format('get_forced_variation'))
583+
return None
584+
585+
if not validator.is_non_empty_string(experiment_key):
586+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
587+
return None
588+
589+
if not isinstance(user_id, string_types):
590+
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
591+
return None
592+
567593
forced_variation = self.config.get_forced_variation(experiment_key, user_id)
568594
return forced_variation.key if forced_variation else None

optimizely/project_config.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,6 @@ def set_forced_variation(self, experiment_key, user_id, variation_key):
490490
Returns:
491491
A boolean value that indicates if the set completed successfully.
492492
"""
493-
if not user_id:
494-
self.logger.debug('User ID is invalid.')
495-
return False
496-
497493
experiment = self.get_experiment_from_key(experiment_key)
498494
if not experiment:
499495
# The invalid experiment key will be logged inside this call.
@@ -551,9 +547,6 @@ def get_forced_variation(self, experiment_key, user_id):
551547
Returns:
552548
The variation which the given user and experiment should be forced into.
553549
"""
554-
if not user_id:
555-
self.logger.debug('User ID is invalid.')
556-
return None
557550

558551
if user_id not in self.forced_variation_map:
559552
self.logger.debug('User "%s" is not in the forced variation map.' % user_id)

tests/test_config.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,13 +1035,6 @@ def test_get_forced_variation_missing_variation_mapped_to_experiment(self):
10351035
'No variation mapped to experiment "test_experiment" in the forced variation map.'
10361036
)
10371037

1038-
# set_forced_variation tests
1039-
def test_set_forced_variation__invalid_user_id(self):
1040-
""" Test invalid user IDs set fail to set a forced variation """
1041-
1042-
self.assertFalse(self.project_config.set_forced_variation('test_experiment', None, 'variation'))
1043-
self.assertFalse(self.project_config.set_forced_variation('test_experiment', '', 'variation'))
1044-
10451038
def test_set_forced_variation__invalid_experiment_key(self):
10461039
""" Test invalid experiment keys set fail to set a forced variation """
10471040

tests/test_optimizely.py

Lines changed: 103 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,12 +1285,8 @@ def test_track__invalid_user_id(self):
12851285
""" Test that None is returned and expected log messages are logged during track \
12861286
when user_id is in invalid format. """
12871287

1288-
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \
1289-
mock.patch('optimizely.helpers.validator.is_non_empty_string', side_effect=[True, False]) as mock_validator:
1288+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
12901289
self.assertIsNone(self.optimizely.track('test_event', 99))
1291-
1292-
mock_validator.assert_any_call(99)
1293-
12941290
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
12951291

12961292
def test_get_variation__invalid_object(self):
@@ -1329,11 +1325,8 @@ def test_is_feature_enabled__returns_false_for_invalid_user_id(self):
13291325

13301326
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
13311327

1332-
with mock.patch.object(opt_obj, 'logger') as mock_client_logging,\
1333-
mock.patch('optimizely.helpers.validator.is_non_empty_string', side_effect=[True, False]) as mock_validator:
1328+
with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
13341329
self.assertFalse(opt_obj.is_feature_enabled('feature_key', 1.2))
1335-
1336-
mock_validator.assert_any_call(1.2)
13371330
mock_client_logging.error.assert_called_with('Provided "user_id" is in an invalid format.')
13381331

13391332
def test_is_feature_enabled__returns_false_for__invalid_attributes(self):
@@ -1628,11 +1621,9 @@ def side_effect(*args, **kwargs):
16281621
mock_is_feature_enabled.assert_any_call('test_feature_in_experiment_and_rollout', 'user_1', None)
16291622

16301623
def test_get_enabled_features_invalid_user_id(self):
1631-
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \
1632-
mock.patch('optimizely.helpers.validator.is_non_empty_string', return_value=False) as mock_validator:
1633-
self.optimizely.get_enabled_features(1.2)
1624+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
1625+
self.assertEqual([], self.optimizely.get_enabled_features(1.2))
16341626

1635-
mock_validator.assert_any_call(1.2)
16361627
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
16371628

16381629
def test_get_enabled_features__invalid_attributes(self):
@@ -1853,22 +1844,22 @@ def test_get_feature_variable__returns_none_if_none_feature_key(self):
18531844
with mock.patch.object(opt_obj, 'logger') as mock_client_logger:
18541845
# Check for booleans
18551846
self.assertIsNone(opt_obj.get_feature_variable_boolean(None, 'variable_key', 'test_user'))
1856-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_FEATURE_KEY_PARAMETER)
1847+
mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.')
18571848
mock_client_logger.reset_mock()
18581849

18591850
# Check for doubles
18601851
self.assertIsNone(opt_obj.get_feature_variable_double(None, 'variable_key', 'test_user'))
1861-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_FEATURE_KEY_PARAMETER)
1852+
mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.')
18621853
mock_client_logger.reset_mock()
18631854

18641855
# Check for integers
18651856
self.assertIsNone(opt_obj.get_feature_variable_integer(None, 'variable_key', 'test_user'))
1866-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_FEATURE_KEY_PARAMETER)
1857+
mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.')
18671858
mock_client_logger.reset_mock()
18681859

18691860
# Check for strings
18701861
self.assertIsNone(opt_obj.get_feature_variable_string(None, 'variable_key', 'test_user'))
1871-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_FEATURE_KEY_PARAMETER)
1862+
mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.')
18721863
mock_client_logger.reset_mock()
18731864

18741865
def test_get_feature_variable__returns_none_if_none_variable_key(self):
@@ -1878,22 +1869,22 @@ def test_get_feature_variable__returns_none_if_none_variable_key(self):
18781869
with mock.patch.object(opt_obj, 'logger') as mock_client_logger:
18791870
# Check for booleans
18801871
self.assertIsNone(opt_obj.get_feature_variable_boolean('feature_key', None, 'test_user'))
1881-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_VARIABLE_KEY_PARAMETER)
1872+
mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.')
18821873
mock_client_logger.reset_mock()
18831874

18841875
# Check for doubles
18851876
self.assertIsNone(opt_obj.get_feature_variable_double('feature_key', None, 'test_user'))
1886-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_VARIABLE_KEY_PARAMETER)
1877+
mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.')
18871878
mock_client_logger.reset_mock()
18881879

18891880
# Check for integers
18901881
self.assertIsNone(opt_obj.get_feature_variable_integer('feature_key', None, 'test_user'))
1891-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_VARIABLE_KEY_PARAMETER)
1882+
mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.')
18921883
mock_client_logger.reset_mock()
18931884

18941885
# Check for strings
18951886
self.assertIsNone(opt_obj.get_feature_variable_string('feature_key', None, 'test-User'))
1896-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_VARIABLE_KEY_PARAMETER)
1887+
mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.')
18971888
mock_client_logger.reset_mock()
18981889

18991890
def test_get_feature_variable__returns_none_if_none_user_id(self):
@@ -1903,22 +1894,22 @@ def test_get_feature_variable__returns_none_if_none_user_id(self):
19031894
with mock.patch.object(opt_obj, 'logger') as mock_client_logger:
19041895
# Check for booleans
19051896
self.assertIsNone(opt_obj.get_feature_variable_boolean('feature_key', 'variable_key', None))
1906-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_USER_ID_PARAMETER)
1897+
mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.')
19071898
mock_client_logger.reset_mock()
19081899

19091900
# Check for doubles
19101901
self.assertIsNone(opt_obj.get_feature_variable_double('feature_key', 'variable_key', None))
1911-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_USER_ID_PARAMETER)
1902+
mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.')
19121903
mock_client_logger.reset_mock()
19131904

19141905
# Check for integers
19151906
self.assertIsNone(opt_obj.get_feature_variable_integer('feature_key', 'variable_key', None))
1916-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_USER_ID_PARAMETER)
1907+
mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.')
19171908
mock_client_logger.reset_mock()
19181909

19191910
# Check for strings
19201911
self.assertIsNone(opt_obj.get_feature_variable_string('feature_key', 'variable_key', None))
1921-
mock_client_logger.error.assert_called_with(enums.Errors.NONE_USER_ID_PARAMETER)
1912+
mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.')
19221913
mock_client_logger.reset_mock()
19231914

19241915
def test_get_feature_variable__invalid_attributes(self):
@@ -2246,11 +2237,8 @@ def test_get_variation__invalid_user_id(self):
22462237
""" Test that None is returned and expected log messages are logged during get_variation \
22472238
when user_id is in invalid format. """
22482239

2249-
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging,\
2250-
mock.patch('optimizely.helpers.validator.is_non_empty_string', side_effect=[True, False]) as mock_validator:
2240+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
22512241
self.assertIsNone(self.optimizely.get_variation('test_experiment', 99))
2252-
2253-
mock_validator.assert_any_call(99)
22542242
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
22552243

22562244
def test_activate__invalid_experiment_key(self):
@@ -2269,14 +2257,35 @@ def test_activate__invalid_user_id(self):
22692257
""" Test that None is returned and expected log messages are logged during activate \
22702258
when user_id is in invalid format. """
22712259

2272-
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging,\
2273-
mock.patch('optimizely.helpers.validator.is_non_empty_string', side_effect=[True, False]) as mock_validator:
2260+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
22742261
self.assertIsNone(self.optimizely.activate('test_experiment', 99))
22752262

2276-
mock_validator.assert_any_call(99)
2277-
22782263
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
22792264

2265+
def test_activate__empty_user_id(self):
2266+
""" Test that expected log messages are logged during activate. """
2267+
2268+
variation_key = 'variation'
2269+
experiment_key = 'test_experiment'
2270+
user_id = ''
2271+
2272+
with mock.patch('optimizely.decision_service.DecisionService.get_variation',
2273+
return_value=self.project_config.get_variation_from_id(
2274+
'test_experiment', '111129')), \
2275+
mock.patch('time.time', return_value=42), \
2276+
mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event'), \
2277+
mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
2278+
self.assertEqual(variation_key, self.optimizely.activate(experiment_key, user_id))
2279+
2280+
mock_client_logging.info.assert_called_once_with(
2281+
'Activating user "" in experiment "test_experiment".'
2282+
)
2283+
debug_message = mock_client_logging.debug.call_args_list[0][0][0]
2284+
self.assertRegexpMatches(
2285+
debug_message,
2286+
'Dispatching impression event to URL https://logx.optimizely.com/v1/events with params'
2287+
)
2288+
22802289
def test_activate__invalid_attributes(self):
22812290
""" Test that expected log messages are logged during activate when attributes are in invalid format. """
22822291
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
@@ -2373,3 +2382,64 @@ def test_get_variation__invalid_attributes__forced_bucketing(self):
23732382
'test_user',
23742383
attributes={'test_attribute': 'test_value_invalid'})
23752384
self.assertEqual('variation', variation_key)
2385+
2386+
def test_set_forced_variation__invalid_object(self):
2387+
""" Test that set_forced_variation logs error if Optimizely object is not created correctly. """
2388+
2389+
opt_obj = optimizely.Optimizely('invalid_datafile')
2390+
2391+
with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
2392+
self.assertFalse(opt_obj.set_forced_variation('test_experiment', 'test_user', 'test_variation'))
2393+
2394+
mock_client_logging.error.assert_called_once_with('Datafile has invalid format. Failing "set_forced_variation".')
2395+
2396+
def test_set_forced_variation__invalid_experiment_key(self):
2397+
""" Test that None is returned and expected log messages are logged during set_forced_variation \
2398+
when exp_key is in invalid format. """
2399+
2400+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \
2401+
mock.patch('optimizely.helpers.validator.is_non_empty_string', return_value=False) as mock_validator:
2402+
self.assertFalse(self.optimizely.set_forced_variation(99, 'test_user', 'variation'))
2403+
2404+
mock_validator.assert_any_call(99)
2405+
2406+
mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.')
2407+
2408+
def test_set_forced_variation__invalid_user_id(self):
2409+
""" Test that None is returned and expected log messages are logged during set_forced_variation \
2410+
when user_id is in invalid format. """
2411+
2412+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
2413+
self.assertFalse(self.optimizely.set_forced_variation('test_experiment', 99, 'variation'))
2414+
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')
2415+
2416+
def test_get_forced_variation__invalid_object(self):
2417+
""" Test that get_forced_variation logs error if Optimizely object is not created correctly. """
2418+
2419+
opt_obj = optimizely.Optimizely('invalid_datafile')
2420+
2421+
with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
2422+
self.assertIsNone(opt_obj.get_forced_variation('test_experiment', 'test_user'))
2423+
2424+
mock_client_logging.error.assert_called_once_with('Datafile has invalid format. Failing "get_forced_variation".')
2425+
2426+
def test_get_forced_variation__invalid_experiment_key(self):
2427+
""" Test that None is returned and expected log messages are logged during get_forced_variation \
2428+
when exp_key is in invalid format. """
2429+
2430+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \
2431+
mock.patch('optimizely.helpers.validator.is_non_empty_string', return_value=False) as mock_validator:
2432+
self.assertIsNone(self.optimizely.get_forced_variation(99, 'test_user'))
2433+
2434+
mock_validator.assert_any_call(99)
2435+
2436+
mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.')
2437+
2438+
def test_get_forced_variation__invalid_user_id(self):
2439+
""" Test that None is returned and expected log messages are logged during get_forced_variation \
2440+
when user_id is in invalid format. """
2441+
2442+
with mock.patch.object(self.optimizely, 'logger') as mock_client_logging:
2443+
self.assertIsNone(self.optimizely.get_forced_variation('test_experiment', 99))
2444+
2445+
mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.')

0 commit comments

Comments
 (0)