diff --git a/optimizely/event/event_processor.py b/optimizely/event/event_processor.py index e7eebc03..ea241031 100644 --- a/optimizely/event/event_processor.py +++ b/optimizely/event/event_processor.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 Optimizely +# Copyright 2019-2021 Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -120,7 +120,7 @@ def __init__( @property def is_running(self): """ Property to check if consumer thread is alive or not. """ - return self.executor.isAlive() if self.executor else False + return self.executor.is_alive() if self.executor else False def _validate_instantiation_props(self, prop, prop_name, default_value): """ Method to determine if instantiation properties like batch_size, flush_interval diff --git a/optimizely/optimizely_config.py b/optimizely/optimizely_config.py index 52887d43..47dae824 100644 --- a/optimizely/optimizely_config.py +++ b/optimizely/optimizely_config.py @@ -1,4 +1,4 @@ -# Copyright 2020, Optimizely +# Copyright 2020-2021, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -17,11 +17,16 @@ class OptimizelyConfig(object): - def __init__(self, revision, experiments_map, features_map, datafile=None): + def __init__(self, revision, experiments_map, features_map, datafile=None, + sdk_key=None, environment_key=None, attributes=None, events=None): self.revision = revision self.experiments_map = experiments_map self.features_map = features_map self._datafile = datafile + self.sdk_key = sdk_key + self.environment_key = environment_key + self.attributes = attributes or [] + self.events = events or [] def get_datafile(self): """ Get the datafile associated with OptimizelyConfig. @@ -31,6 +36,38 @@ def get_datafile(self): """ return self._datafile + def get_sdk_key(self): + """ Get the sdk key associated with OptimizelyConfig. + + Returns: + A string containing sdk key. + """ + return self.sdk_key + + def get_environment_key(self): + """ Get the environemnt key associated with OptimizelyConfig. + + Returns: + A string containing environment key. + """ + return self.environment_key + + def get_attributes(self): + """ Get the attributes associated with OptimizelyConfig + + returns: + A list of attributes. + """ + return self.attributes + + def get_events(self): + """ Get the events associated with OptimizelyConfig + + returns: + A list of events. + """ + return self.events + class OptimizelyExperiment(object): def __init__(self, id, key, variations_map): @@ -63,6 +100,19 @@ def __init__(self, id, key, variable_type, value): self.value = value +class OptimizelyAttribute(object): + def __init__(self, id, key): + self.id = id + self.key = key + + +class OptimizelyEvent(object): + def __init__(self, id, key, experiment_ids): + self.id = id + self.key = key + self.experiment_ids = experiment_ids + + class OptimizelyConfigService(object): """ Class encapsulating methods to be used in creating instance of OptimizelyConfig. """ @@ -82,6 +132,10 @@ def __init__(self, project_config): self.feature_flags = project_config.feature_flags self.groups = project_config.groups self.revision = project_config.revision + self.sdk_key = project_config.sdk_key + self.environment_key = project_config.environment_key + self.attributes = project_config.attributes + self.events = project_config.events self._create_lookup_maps() @@ -98,7 +152,15 @@ def get_config(self): experiments_key_map, experiments_id_map = self._get_experiments_maps() features_map = self._get_features_map(experiments_id_map) - return OptimizelyConfig(self.revision, experiments_key_map, features_map, self._datafile) + return OptimizelyConfig( + self.revision, + experiments_key_map, + features_map, + self._datafile, + self.sdk_key, + self.environment_key, + self.attributes, + self.events) def _create_lookup_maps(self): """ Creates lookup maps to avoid redundant iteration of config objects. """ @@ -233,3 +295,37 @@ def _get_features_map(self, experiments_id_map): features_map[feature['key']] = optly_feature return features_map + + def get_attributes_map(self): + """ Gets attributes map for the project config. + + Returns: + dict -- Attribute key, OptimizelyAttribute map + """ + + attributes_map = {} + + for attribute in self.attributes: + optly_attribute = OptimizelyAttribute( + attribute['id'], attribute['key'] + ) + attributes_map[attribute['key']] = optly_attribute + + return attributes_map + + def get_events_map(self): + """ Gets events map for the project config. + + Returns: + dict -- Event key, OptimizelyEvent map + """ + + events_map = {} + + for event in self.events: + optly_event = OptimizelyEvent( + event['id'], event['key'], event.get('experimentIds', []) + ) + events_map[event['key']] = optly_event + + return events_map diff --git a/optimizely/optimizely_factory.py b/optimizely/optimizely_factory.py index 5b7a879a..d9da72ba 100644 --- a/optimizely/optimizely_factory.py +++ b/optimizely/optimizely_factory.py @@ -113,7 +113,6 @@ def default_instance_with_config_manager(config_manager): def custom_instance(sdk_key, datafile=None, event_dispatcher=None, logger=None, error_handler=None, skip_json_validation=None, user_profile_service=None, config_manager=None, notification_center=None): - """ Returns a new optimizely instance. if max_event_batch_size and max_event_flush_interval are None then default batch_size and flush_interval will be used to setup BatchEventProcessor. diff --git a/optimizely/project_config.py b/optimizely/project_config.py index c0004495..ac97fac6 100644 --- a/optimizely/project_config.py +++ b/optimizely/project_config.py @@ -1,4 +1,4 @@ -# Copyright 2016-2019, Optimizely +# Copyright 2016-2019, 2021, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -52,6 +52,8 @@ def __init__(self, datafile, logger, error_handler): self.account_id = config.get('accountId') self.project_id = config.get('projectId') self.revision = config.get('revision') + self.sdk_key = config.get('sdkKey', None) + self.environment_key = config.get('environmentKey', None) self.groups = config.get('groups', []) self.experiments = config.get('experiments', []) self.events = config.get('events', []) @@ -213,6 +215,24 @@ def get_revision(self): return self.revision + def get_sdk_key(self): + """ Get sdk key from the datafile. + + Returns: + Revision of the sdk key. + """ + + return self.sdk_key + + def get_environment_key(self): + """ Get environment key from the datafile. + + Returns: + Revision of the environment key. + """ + + return self.environment_key + def get_account_id(self): """ Get account ID from the config. diff --git a/tests/base.py b/tests/base.py index 83506c8f..48b89106 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,4 +1,4 @@ -# Copyright 2016-2020, Optimizely +# Copyright 2016-2021, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -22,6 +22,12 @@ def long(a): raise NotImplementedError('Tests should only call `long` if running in PY2') +# Check to verify if TestCase has the attribute assertRasesRegex or assertRaisesRegexp +# This check depends on the version of python with assertRaisesRegexp being used by +# python2.7. Later versions of python are using the non-deprecated assertRaisesRegex. +if not hasattr(unittest.TestCase, 'assertRaisesRegex'): + unittest.TestCase.assertRaisesRegex = getattr(unittest.TestCase, 'assertRaisesRegexp') + class BaseTest(unittest.TestCase): def assertStrictTrue(self, to_assert): diff --git a/tests/test_config.py b/tests/test_config.py index 4bf1f61c..c9051054 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,4 +1,4 @@ -# Copyright 2016-2019, Optimizely +# Copyright 2016-2019, 2021, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -1117,7 +1117,7 @@ def setUp(self): def test_get_experiment_from_key__invalid_key(self): """ Test that exception is raised when provided experiment key is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidExperimentException, enums.Errors.INVALID_EXPERIMENT_KEY, self.project_config.get_experiment_from_key, @@ -1127,14 +1127,14 @@ def test_get_experiment_from_key__invalid_key(self): def test_get_audience__invalid_id(self): """ Test that message is logged when provided audience ID is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidAudienceException, enums.Errors.INVALID_AUDIENCE, self.project_config.get_audience, '42', ) def test_get_variation_from_key__invalid_experiment_key(self): """ Test that exception is raised when provided experiment key is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidExperimentException, enums.Errors.INVALID_EXPERIMENT_KEY, self.project_config.get_variation_from_key, @@ -1145,7 +1145,7 @@ def test_get_variation_from_key__invalid_experiment_key(self): def test_get_variation_from_key__invalid_variation_key(self): """ Test that exception is raised when provided variation key is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidVariationException, enums.Errors.INVALID_VARIATION, self.project_config.get_variation_from_key, @@ -1156,7 +1156,7 @@ def test_get_variation_from_key__invalid_variation_key(self): def test_get_variation_from_id__invalid_experiment_key(self): """ Test that exception is raised when provided experiment key is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidExperimentException, enums.Errors.INVALID_EXPERIMENT_KEY, self.project_config.get_variation_from_id, @@ -1167,7 +1167,7 @@ def test_get_variation_from_id__invalid_experiment_key(self): def test_get_variation_from_id__invalid_variation_id(self): """ Test that exception is raised when provided variation ID is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidVariationException, enums.Errors.INVALID_VARIATION, self.project_config.get_variation_from_key, @@ -1178,7 +1178,7 @@ def test_get_variation_from_id__invalid_variation_id(self): def test_get_event__invalid_key(self): """ Test that exception is raised when provided event key is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidEventException, enums.Errors.INVALID_EVENT_KEY, self.project_config.get_event, @@ -1188,7 +1188,7 @@ def test_get_event__invalid_key(self): def test_get_attribute_id__invalid_key(self): """ Test that exception is raised when provided attribute key is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidAttributeException, enums.Errors.INVALID_ATTRIBUTE, self.project_config.get_attribute_id, @@ -1198,7 +1198,7 @@ def test_get_attribute_id__invalid_key(self): def test_get_group__invalid_id(self): """ Test that exception is raised when provided group ID is invalid. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidGroupException, enums.Errors.INVALID_GROUP_ID, self.project_config.get_group, '42', ) diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 15c93245..272e2f92 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020, Optimizely +# Copyright 2019-2021, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -32,7 +32,7 @@ def test_init__invalid_logger_fails(self): class InvalidLogger(object): pass - with self.assertRaisesRegexp( + with self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Provided "logger" is in an invalid format.', ): config_manager.StaticConfigManager(logger=InvalidLogger()) @@ -43,7 +43,7 @@ def test_init__invalid_error_handler_fails(self): class InvalidErrorHandler(object): pass - with self.assertRaisesRegexp( + with self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Provided "error_handler" is in an invalid format.', ): config_manager.StaticConfigManager(error_handler=InvalidErrorHandler()) @@ -54,7 +54,7 @@ def test_init__invalid_notification_center_fails(self): class InvalidNotificationCenter(object): pass - with self.assertRaisesRegexp( + with self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Provided "notification_center" is in an invalid format.', ): config_manager.StaticConfigManager(notification_center=InvalidNotificationCenter()) @@ -222,7 +222,7 @@ def test_get_config_blocks(self): class PollingConfigManagerTest(base.BaseTest): def test_init__no_sdk_key_no_url__fails(self, _): """ Test that initialization fails if there is no sdk_key or url provided. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Must provide at least one of sdk_key or url.', config_manager.PollingConfigManager, @@ -232,7 +232,7 @@ def test_init__no_sdk_key_no_url__fails(self, _): def test_get_datafile_url__no_sdk_key_no_url_raises(self, _): """ Test that get_datafile_url raises exception if no sdk_key or url is provided. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Must provide at least one of sdk_key or url.', config_manager.PollingConfigManager.get_datafile_url, @@ -244,7 +244,7 @@ def test_get_datafile_url__no_sdk_key_no_url_raises(self, _): def test_get_datafile_url__invalid_url_template_raises(self, _): """ Test that get_datafile_url raises if url_template is invalid. """ # No url_template provided - self.assertRaisesRegexp( + self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Invalid url_template None provided', config_manager.PollingConfigManager.get_datafile_url, @@ -255,7 +255,7 @@ def test_get_datafile_url__invalid_url_template_raises(self, _): # Incorrect url_template provided test_url_template = 'invalid_url_template_without_sdk_key_field_{key}' - self.assertRaisesRegexp( + self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Invalid url_template {} provided'.format(test_url_template), config_manager.PollingConfigManager.get_datafile_url, @@ -298,7 +298,7 @@ def test_set_update_interval(self, _): project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') # Assert that if invalid update_interval is set, then exception is raised. - with self.assertRaisesRegexp( + with self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Invalid update_interval "invalid interval" provided.', ): project_config_manager.set_update_interval('invalid interval') @@ -325,7 +325,7 @@ def test_set_blocking_timeout(self, _): project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') # Assert that if invalid blocking_timeout is set, then exception is raised. - with self.assertRaisesRegexp( + with self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'Invalid blocking timeout "invalid timeout" provided.', ): project_config_manager.set_blocking_timeout('invalid timeout') @@ -484,7 +484,7 @@ def test_is_running(self, _): class AuthDatafilePollingConfigManagerTest(base.BaseTest): def test_init__datafile_access_token_none__fails(self, _): """ Test that initialization fails if datafile_access_token is None. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( optimizely_exceptions.InvalidInputException, 'datafile_access_token cannot be empty or None.', config_manager.AuthDatafilePollingConfigManager, diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 1c21dc6a..23454342 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -26,7 +26,6 @@ from optimizely import optimizely_config from optimizely import project_config from optimizely import version -from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption as DecideOption from optimizely.event.event_factory import EventFactory from optimizely.helpers import enums from . import base @@ -677,24 +676,6 @@ def on_activate(experiment, user_id, attributes, variation, event): self.assertEqual(1, mock_process.call_count) self.assertEqual(True, access_callback[0]) - def test_decide_experiment(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. - Also confirm that no impression event is processed. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), - ): - user_context = opt_obj.create_user_context('test_user') - decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) - self.assertTrue(decision.enabled, "decision should be enabled") - def test_activate__with_attributes__audience_match(self): """ Test that activate calls process with right params and returns expected variation when attributes are provided and audience conditions are met. """ @@ -4633,7 +4614,7 @@ def setUp(self): def test_activate__with_attributes__invalid_attributes(self): """ Test that activate raises exception if attributes are in invalid format. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidAttributeException, enums.Errors.INVALID_ATTRIBUTE_FORMAT, self.optimizely.activate, @@ -4645,7 +4626,7 @@ def test_activate__with_attributes__invalid_attributes(self): def test_track__with_attributes__invalid_attributes(self): """ Test that track raises exception if attributes are in invalid format. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidAttributeException, enums.Errors.INVALID_ATTRIBUTE_FORMAT, self.optimizely.track, @@ -4657,7 +4638,7 @@ def test_track__with_attributes__invalid_attributes(self): def test_track__with_event_tag__invalid_event_tag(self): """ Test that track raises exception if event_tag is in invalid format. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidEventTagException, enums.Errors.INVALID_EVENT_TAG_FORMAT, self.optimizely.track, @@ -4669,7 +4650,7 @@ def test_track__with_event_tag__invalid_event_tag(self): def test_get_variation__with_attributes__invalid_attributes(self): """ Test that get variation raises exception if attributes are in invalid format. """ - self.assertRaisesRegexp( + self.assertRaisesRegex( exceptions.InvalidAttributeException, enums.Errors.INVALID_ATTRIBUTE_FORMAT, self.optimizely.get_variation, diff --git a/tests/test_optimizely_config.py b/tests/test_optimizely_config.py index 94e1fb00..8ff8986b 100644 --- a/tests/test_optimizely_config.py +++ b/tests/test_optimizely_config.py @@ -1,4 +1,4 @@ -# Copyright 2020, Optimizely +# Copyright 2020-2021, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -26,6 +26,10 @@ def setUp(self): self.opt_config_service = optimizely_config.OptimizelyConfigService(self.project_config) self.expected_config = { + 'sdk_key': None, + 'environment_key': None, + 'attributes': [{'key': 'test_attribute', 'id': '111094'}], + 'events': [{'key': 'test_event', 'experimentIds': ['111127'], 'id': '111095'}], 'experiments_map': { 'test_experiment2': { 'variations_map': { @@ -732,3 +736,172 @@ def test__get_datafile(self): actual_datafile = self.actual_config.get_datafile() self.assertEqual(expected_datafile, actual_datafile) + + def test__get_sdk_key(self): + """ Test that get_sdk_key returns the expected value. """ + + config = optimizely_config.OptimizelyConfig( + revision='101', + experiments_map={}, + features_map={}, + sdk_key='testSdkKey', + ) + + expected_value = 'testSdkKey' + + self.assertEqual(expected_value, config.get_sdk_key()) + + def test__get_sdk_key_invalid(self): + """ Negative Test that tests get_sdk_key does not return the expected value. """ + + config = optimizely_config.OptimizelyConfig( + revision='101', + experiments_map={}, + features_map={}, + sdk_key='testSdkKey', + ) + + invalid_value = 123 + + self.assertNotEqual(invalid_value, config.get_sdk_key()) + + def test__get_environment_key(self): + """ Test that get_environment_key returns the expected value. """ + + config = optimizely_config.OptimizelyConfig( + revision='101', + experiments_map={}, + features_map={}, + environment_key='TestEnvironmentKey' + ) + + expected_value = 'TestEnvironmentKey' + + self.assertEqual(expected_value, config.get_environment_key()) + + def test__get_environment_key_invalid(self): + """ Negative Test that tests get_environment_key does not return the expected value. """ + + config = optimizely_config.OptimizelyConfig( + revision='101', + experiments_map={}, + features_map={}, + environment_key='testEnvironmentKey' + ) + + invalid_value = 321 + + self.assertNotEqual(invalid_value, config.get_environment_key()) + + def test__get_attributes(self): + """ Test that the get_attributes returns the expected value. """ + + config = optimizely_config.OptimizelyConfig( + revision='101', + experiments_map={}, + features_map={}, + attributes=[{ + 'id': '123', + 'key': '123' + }, + { + 'id': '234', + 'key': '234' + }] + ) + + expected_value = [{ + 'id': '123', + 'key': '123' + }, + { + 'id': '234', + 'key': '234' + }] + + self.assertEqual(expected_value, config.get_attributes()) + self.assertEqual(len(config.get_attributes()), 2) + + def test__get_events(self): + """ Test that the get_events returns the expected value. """ + + config = optimizely_config.OptimizelyConfig( + revision='101', + experiments_map={}, + features_map={}, + events=[{ + 'id': '123', + 'key': '123', + 'experiment_ids': { + '54321' + } + }, + { + 'id': '234', + 'key': '234', + 'experiment_ids': { + '3211', '54365' + } + }] + ) + + expected_value = [{ + 'id': '123', + 'key': '123', + 'experiment_ids': { + '54321' + } + }, + { + 'id': '234', + 'key': '234', + 'experiment_ids': { + '3211', + '54365' + } + }] + + self.assertEqual(expected_value, config.get_events()) + self.assertEqual(len(config.get_events()), 2) + + def test__get_attributes_map(self): + """ Test to check get_attributes_map returns the correct value """ + + actual_attributes_map = self.opt_config_service.get_attributes_map() + expected_attributes = self.expected_config['attributes'] + + expected_attributes_map = {} + + for expected_attribute in expected_attributes: + optly_attribute = optimizely_config.OptimizelyAttribute( + expected_attribute['id'], expected_attribute['key'] + ) + expected_attributes_map[expected_attribute['key']] = optly_attribute + + for attribute in actual_attributes_map.values(): + self.assertIsInstance(attribute, optimizely_config.OptimizelyAttribute) + + self.assertEqual(len(expected_attributes), len(actual_attributes_map)) + self.assertEqual(self.to_dict(actual_attributes_map), self.to_dict(expected_attributes_map)) + + def test__get_events_map(self): + """ Test to check that get_events_map returns the correct value """ + + actual_events_map = self.opt_config_service.get_events_map() + expected_events = self.expected_config['events'] + + expected_events_map = {} + + for expected_event in expected_events: + optly_event = optimizely_config.OptimizelyEvent( + expected_event['id'], + expected_event['key'], + expected_event['experimentIds'] + ) + expected_events_map[expected_event['key']] = optly_event + + for event in actual_events_map.values(): + self.assertIsInstance(event, optimizely_config.OptimizelyEvent) + + self.assertEqual(len(expected_events), len(actual_events_map)) + self.assertEqual(self.to_dict(actual_events_map), self.to_dict(expected_events_map)) diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 7c979028..fcffc415 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -15,6 +15,7 @@ import mock from optimizely.decision.optimizely_decision import OptimizelyDecision +from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption as DecideOption from optimizely.helpers import enums from . import base from optimizely import optimizely, decision_service @@ -60,6 +61,23 @@ def test_user_context(self): self.assertEqual("firefox", uc.get_user_attributes()["browser"]) self.assertEqual("red", uc.get_user_attributes()["color"]) + def test_user_and_attributes_as_json(self): + """ + tests user context as json + """ + uc = OptimizelyUserContext(self.optimizely, "test_user") + + # set an attribute + uc.set_attribute("browser", "safari") + + # set expected json obj + expected_json = { + "user_id": uc.user_id, + "attributes": uc.get_user_attributes(), + } + + self.assertEqual(uc.as_json(), expected_json) + def test_attributes_are_cloned_when_passed_to_user_context(self): user_id = 'test_user' attributes = {"browser": "chrome"} @@ -759,7 +777,7 @@ def test_decide__option__include_reasons__feature_test(self): 'User "test_user" is in variation "control" of experiment test_experiment.' ] - self.assertEquals(expected_reasons, actual.reasons) + self.assertEqual(expected_reasons, actual.reasons) def test_decide__option__include_reasons__feature_rollout(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -775,7 +793,7 @@ def test_decide__option__include_reasons__feature_rollout(self): 'User "test_user" is in the traffic group of targeting rule 1.' ] - self.assertEquals(expected_reasons, actual.reasons) + self.assertEqual(expected_reasons, actual.reasons) def test_decide__option__enabled_flags_only(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -1135,7 +1153,7 @@ def test_decide_reasons__hit_everyone_else_rule__fails_bucketing(self): 'Bucketed into an empty traffic range. Returning nil.' ] - self.assertEquals(expected_reasons, actual.reasons) + self.assertEqual(expected_reasons, actual.reasons) def test_decide_reasons__hit_everyone_else_rule(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -1156,7 +1174,7 @@ def test_decide_reasons__hit_everyone_else_rule(self): 'User "abcde" meets conditions for targeting rule "Everyone Else".' ] - self.assertEquals(expected_reasons, actual.reasons) + self.assertEqual(expected_reasons, actual.reasons) def test_decide_reasons__hit_rule2__fails_bucketing(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -1179,7 +1197,7 @@ def test_decide_reasons__hit_rule2__fails_bucketing(self): 'Bucketed into an empty traffic range. Returning nil.' ] - self.assertEquals(expected_reasons, actual.reasons) + self.assertEqual(expected_reasons, actual.reasons) def test_decide_reasons__hit_user_profile_service(self): user_id = 'test_user' @@ -1215,7 +1233,7 @@ def save(self, user_profile): expected_reasons = [('Returning previously activated variation ID "control" of experiment ' '"test_experiment" for user "test_user" from user profile.')] - self.assertEquals(expected_reasons, actual.reasons) + self.assertEqual(expected_reasons, actual.reasons) def test_decide_reasons__forced_variation(self): user_id = 'test_user' @@ -1232,7 +1250,7 @@ def test_decide_reasons__forced_variation(self): expected_reasons = [('Variation "control" is mapped to experiment ' '"test_experiment" and user "test_user" in the forced variation map')] - self.assertEquals(expected_reasons, actual.reasons) + self.assertEqual(expected_reasons, actual.reasons) def test_decide_reasons__whitelisted_variation(self): user_id = 'user_1' @@ -1246,4 +1264,35 @@ def test_decide_reasons__whitelisted_variation(self): expected_reasons = ['User "user_1" is forced in variation "control".'] - self.assertEquals(expected_reasons, actual.reasons) + self.assertEqual(expected_reasons, actual.reasons) + + def test_init__invalid_default_decide_options(self): + """ + Test to confirm that default decide options passed not as a list will trigger setting + self.deafulat_decide_options as an empty list. + """ + invalid_decide_options = {"testKey": "testOption"} + + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(default_decide_options=invalid_decide_options) + + self.assertEqual(opt_obj.default_decide_options, []) + + def test_decide_experiment(self): + """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + Also confirm that no impression event is processed. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=(decision_service.Decision(mock_experiment, + mock_variation, enums.DecisionSources.FEATURE_TEST), []), + ): + user_context = opt_obj.create_user_context('test_user') + decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT]) + self.assertTrue(decision.enabled, "decision should be enabled")