44# You may obtain a copy of the License at
55#
66# http://www.apache.org/licenses/LICENSE-2.0
7-
87# Unless required by applicable law or agreed to in writing, software
98# distributed under the License is distributed on an "AS IS" BASIS,
109# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1110# See the License for the specific language governing permissions and
1211# limitations under the License.
1312
1413import json
14+ from unittest .mock import patch
1515
1616from optimizely import optimizely , project_config
1717from optimizely import optimizely_config
18+ from optimizely import logger
1819from . import base
1920
2021
@@ -23,7 +24,8 @@ def setUp(self):
2324 base .BaseTest .setUp (self )
2425 opt_instance = optimizely .Optimizely (json .dumps (self .config_dict_with_features ))
2526 self .project_config = opt_instance .config_manager .get_config ()
26- self .opt_config_service = optimizely_config .OptimizelyConfigService (self .project_config )
27+ self .opt_config_service = optimizely_config .OptimizelyConfigService (self .project_config ,
28+ logger = logger .SimpleLogger ())
2729
2830 self .expected_config = {
2931 'sdk_key' : 'features-test' ,
@@ -1452,7 +1454,7 @@ def test__get_config(self):
14521454 def test__get_config__invalid_project_config (self ):
14531455 """ Test that get_config returns None when invalid project config supplied. """
14541456
1455- opt_service = optimizely_config .OptimizelyConfigService ({"key" : "invalid" })
1457+ opt_service = optimizely_config .OptimizelyConfigService ({"key" : "invalid" }, None )
14561458 self .assertIsNone (opt_service .get_config ())
14571459
14581460 def test__get_experiments_maps (self ):
@@ -1473,6 +1475,81 @@ def test__get_experiments_maps(self):
14731475
14741476 self .assertEqual (expected_id_map , self .to_dict (actual_id_map ))
14751477
1478+ def test__duplicate_experiment_keys (self ):
1479+ """ Test that multiple features don't have the same experiment key. """
1480+
1481+ # update the test datafile with an additional feature flag with the same experiment rule key
1482+ new_experiment = {
1483+ 'key' : 'test_experiment' , # added duplicate "test_experiment"
1484+ 'status' : 'Running' ,
1485+ 'layerId' : '8' ,
1486+ "audienceConditions" : [
1487+ "or" ,
1488+ "11160"
1489+ ],
1490+ 'audienceIds' : ['11160' ],
1491+ 'id' : '111137' ,
1492+ 'forcedVariations' : {},
1493+ 'trafficAllocation' : [
1494+ {'entityId' : '222242' , 'endOfRange' : 8000 },
1495+ {'entityId' : '' , 'endOfRange' : 10000 }
1496+ ],
1497+ 'variations' : [
1498+ {
1499+ 'id' : '222242' ,
1500+ 'key' : 'control' ,
1501+ 'variables' : [],
1502+ }
1503+ ],
1504+ }
1505+
1506+ new_feature = {
1507+ 'id' : '91117' ,
1508+ 'key' : 'new_feature' ,
1509+ 'experimentIds' : ['111137' ],
1510+ 'rolloutId' : '' ,
1511+ 'variables' : [
1512+ {'id' : '127' , 'key' : 'is_working' , 'defaultValue' : 'true' , 'type' : 'boolean' },
1513+ {'id' : '128' , 'key' : 'environment' , 'defaultValue' : 'devel' , 'type' : 'string' },
1514+ {'id' : '129' , 'key' : 'cost' , 'defaultValue' : '10.99' , 'type' : 'double' },
1515+ {'id' : '130' , 'key' : 'count' , 'defaultValue' : '999' , 'type' : 'integer' },
1516+ {'id' : '131' , 'key' : 'variable_without_usage' , 'defaultValue' : '45' , 'type' : 'integer' },
1517+ {'id' : '132' , 'key' : 'object' , 'defaultValue' : '{"test": 12}' , 'type' : 'string' ,
1518+ 'subType' : 'json' },
1519+ {'id' : '133' , 'key' : 'true_object' , 'defaultValue' : '{"true_test": 23.54}' , 'type' : 'json' },
1520+ ],
1521+ }
1522+
1523+ # add new experiment rule with the same key and a new feature with the same rule key
1524+ self .config_dict_with_features ['experiments' ].append (new_experiment )
1525+ self .config_dict_with_features ['featureFlags' ].append (new_feature )
1526+
1527+ config_with_duplicate_key = self .config_dict_with_features
1528+ opt_instance = optimizely .Optimizely (json .dumps (config_with_duplicate_key ))
1529+ self .project_config = opt_instance .config_manager .get_config ()
1530+
1531+ with patch ('optimizely.logger.SimpleLogger.warning' ) as mock_logger :
1532+ self .opt_config_service = optimizely_config .OptimizelyConfigService (self .project_config ,
1533+ logger = logger .SimpleLogger ())
1534+
1535+ actual_key_map , actual_id_map = self .opt_config_service ._get_experiments_maps ()
1536+
1537+ self .assertIsInstance (actual_key_map , dict )
1538+ for exp in actual_key_map .values ():
1539+ self .assertIsInstance (exp , optimizely_config .OptimizelyExperiment )
1540+
1541+ # Assert that the warning method of the mock logger was called with the expected message
1542+ expected_warning_message = f"Duplicate experiment keys found in datafile: { new_experiment ['key' ]} "
1543+ mock_logger .assert_called_with (expected_warning_message )
1544+
1545+ # assert we get ID of the duplicated experiment
1546+ assert actual_key_map .get ('test_experiment' ).id == "111137"
1547+
1548+ # assert we get one duplicated experiment
1549+ keys_list = list (actual_key_map .keys ())
1550+ assert "test_experiment" in keys_list , "Key 'test_experiment' not found in actual key map"
1551+ assert keys_list .count ("test_experiment" ) == 1 , "Key 'test_experiment' found more than once in actual key map"
1552+
14761553 def test__get_features_map (self ):
14771554 """ Test that get_features_map returns expected features map. """
14781555
@@ -1674,7 +1751,7 @@ def test_get_audiences(self):
16741751 error_handler = None
16751752 )
16761753
1677- config_service = optimizely_config .OptimizelyConfigService (proj_conf )
1754+ config_service = optimizely_config .OptimizelyConfigService (proj_conf , logger = logger . SimpleLogger () )
16781755
16791756 for audience in config_service .audiences :
16801757 self .assertIsInstance (audience , optimizely_config .OptimizelyAudience )
@@ -1742,7 +1819,7 @@ def test_stringify_audience_conditions_all_cases(self):
17421819 '("us" OR ("female" AND "adult")) AND ("fr" AND ("male" OR "adult"))'
17431820 ]
17441821
1745- config_service = optimizely_config .OptimizelyConfigService (config )
1822+ config_service = optimizely_config .OptimizelyConfigService (config , None )
17461823
17471824 for i in range (len (audiences_input )):
17481825 result = config_service .stringify_conditions (audiences_input [i ], audiences_map )
@@ -1760,7 +1837,7 @@ def test_optimizely_audience_conversion(self):
17601837 error_handler = None
17611838 )
17621839
1763- config_service = optimizely_config .OptimizelyConfigService (proj_conf )
1840+ config_service = optimizely_config .OptimizelyConfigService (proj_conf , None )
17641841
17651842 for audience in config_service .audiences :
17661843 self .assertIsInstance (audience , optimizely_config .OptimizelyAudience )
@@ -1776,7 +1853,7 @@ def test_get_variations_from_experiments_map(self):
17761853 error_handler = None
17771854 )
17781855
1779- config_service = optimizely_config .OptimizelyConfigService (proj_conf )
1856+ config_service = optimizely_config .OptimizelyConfigService (proj_conf , None )
17801857
17811858 experiments_key_map , experiments_id_map = config_service ._get_experiments_maps ()
17821859
0 commit comments