|
714 | 714 | decision_result = decision_service.get_variation_for_feature_rollout(config, feature_flag, user_context)
|
715 | 715 | expect(decision_result.decision).to eq(nil)
|
716 | 716 | expect(decision_result.reasons).to eq([
|
717 |
| - "User 'user_1' does not meet the conditions for targeting rule '1'.", |
718 |
| - "User 'user_1' does not meet the conditions for targeting rule '2'.", |
719 |
| - "User 'user_1' does not meet the conditions for targeting rule 'Everyone Else'." |
720 |
| - ]) |
| 717 | + "User 'user_1' does not meet the conditions for targeting rule '1'.", |
| 718 | + "User 'user_1' does not meet the conditions for targeting rule '2'.", |
| 719 | + "User 'user_1' does not meet the conditions for targeting rule 'Everyone Else'." |
| 720 | + ]) |
721 | 721 |
|
722 | 722 | # verify we tried to bucket in all targeting rules and the everyone else rule
|
723 | 723 | expect(Optimizely::Audience).to have_received(:user_meets_audience_conditions?).once
|
|
937 | 937 | expect(reasons).to eq(["Variation 'control' is mapped to experiment '111127' and user 'test_user_2' in the forced variation map"])
|
938 | 938 | end
|
939 | 939 | end
|
| 940 | + describe 'CMAB experiments' do |
| 941 | + describe 'when user is in traffic allocation' do |
| 942 | + it 'should return correct variation and CMAB UUID from CMAB service' do |
| 943 | + # Create a CMAB experiment configuration |
| 944 | + cmab_experiment = { |
| 945 | + 'id' => '111150', |
| 946 | + 'key' => 'cmab_experiment', |
| 947 | + 'status' => 'Running', |
| 948 | + 'layerId' => '111150', |
| 949 | + 'audienceIds' => [], |
| 950 | + 'forcedVariations' => {}, |
| 951 | + 'variations' => [ |
| 952 | + {'id' => '111151', 'key' => 'variation_1'}, |
| 953 | + {'id' => '111152', 'key' => 'variation_2'} |
| 954 | + ], |
| 955 | + 'trafficAllocation' => [ |
| 956 | + {'entityId' => '111151', 'endOfRange' => 5000}, |
| 957 | + {'entityId' => '111152', 'endOfRange' => 10_000} |
| 958 | + ], |
| 959 | + 'cmab' => {'trafficAllocation' => 5000} |
| 960 | + } |
| 961 | + user_context = project_instance.create_user_context('test_user', {}) |
| 962 | + |
| 963 | + # Mock experiment lookup to return our CMAB experiment |
| 964 | + allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment) |
| 965 | + allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true) |
| 966 | + |
| 967 | + # Mock audience evaluation to pass |
| 968 | + allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []]) |
| 969 | + |
| 970 | + # Mock bucketer to return a valid entity ID (user is in traffic allocation) |
| 971 | + allow(decision_service.bucketer).to receive(:bucket_to_entity_id) |
| 972 | + .with(config, cmab_experiment, 'test_user', 'test_user') |
| 973 | + .and_return(['$', []]) |
| 974 | + |
| 975 | + # Mock CMAB service to return a decision |
| 976 | + allow(spy_cmab_service).to receive(:get_decision) |
| 977 | + .with(config, user_context, '111150', []) |
| 978 | + .and_return(Optimizely::CmabDecision.new(variation_id: '111151', cmab_uuid: 'test-cmab-uuid-123')) |
| 979 | + |
| 980 | + # Mock variation lookup |
| 981 | + allow(config).to receive(:get_variation_from_id_by_experiment_id) |
| 982 | + .with('111150', '111151') |
| 983 | + .and_return({'id' => '111151', 'key' => 'variation_1'}) |
| 984 | + |
| 985 | + variation_result = decision_service.get_variation(config, '111150', user_context) |
| 986 | + |
| 987 | + expect(variation_result.variation_id).to eq('111151') |
| 988 | + expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-123') |
| 989 | + expect(variation_result.error).to eq(false) |
| 990 | + expect(variation_result.reasons).to include( |
| 991 | + "User 'test_user' is in variation 'variation_1' of experiment '111150'." |
| 992 | + ) |
| 993 | + |
| 994 | + # Verify CMAB service was called |
| 995 | + expect(spy_cmab_service).to have_received(:get_decision).once |
| 996 | + end |
| 997 | + end |
| 998 | + |
| 999 | + describe 'when user is not in traffic allocation' do |
| 1000 | + it 'should return nil variation and log traffic allocation message' do |
| 1001 | + cmab_experiment = { |
| 1002 | + 'id' => '111150', |
| 1003 | + 'key' => 'cmab_experiment', |
| 1004 | + 'status' => 'Running', |
| 1005 | + 'layerId' => '111150', |
| 1006 | + 'audienceIds' => [], |
| 1007 | + 'forcedVariations' => {}, |
| 1008 | + 'variations' => [ |
| 1009 | + {'id' => '111151', 'key' => 'variation_1'} |
| 1010 | + ], |
| 1011 | + 'trafficAllocation' => [ |
| 1012 | + {'entityId' => '111151', 'endOfRange' => 10_000} |
| 1013 | + ], |
| 1014 | + 'cmab' => {'trafficAllocation' => 1000} |
| 1015 | + } |
| 1016 | + user_context = project_instance.create_user_context('test_user', {}) |
| 1017 | + |
| 1018 | + # Mock experiment lookup to return our CMAB experiment |
| 1019 | + allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment) |
| 1020 | + allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true) |
| 1021 | + |
| 1022 | + # Mock audience evaluation to pass |
| 1023 | + allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []]) |
| 1024 | + |
| 1025 | + variation_result = decision_service.get_variation(config, '111150', user_context) |
| 1026 | + |
| 1027 | + expect(variation_result.variation_id).to eq(nil) |
| 1028 | + expect(variation_result.cmab_uuid).to eq(nil) |
| 1029 | + expect(variation_result.error).to eq(false) |
| 1030 | + expect(variation_result.reasons).to include( |
| 1031 | + 'User "test_user" not in CMAB experiment "cmab_experiment" due to traffic allocation.' |
| 1032 | + ) |
| 1033 | + |
| 1034 | + # Verify CMAB service was not called since user is not in traffic allocation |
| 1035 | + expect(spy_cmab_service).not_to have_received(:get_decision) |
| 1036 | + end |
| 1037 | + end |
| 1038 | + |
| 1039 | + describe 'when CMAB service returns an error' do |
| 1040 | + it 'should return nil variation and include error in reasons' do |
| 1041 | + cmab_experiment = { |
| 1042 | + 'id' => '111150', |
| 1043 | + 'key' => 'cmab_experiment', |
| 1044 | + 'status' => 'Running', |
| 1045 | + 'layerId' => '111150', |
| 1046 | + 'audienceIds' => [], |
| 1047 | + 'forcedVariations' => {}, |
| 1048 | + 'variations' => [ |
| 1049 | + {'id' => '111151', 'key' => 'variation_1'} |
| 1050 | + ], |
| 1051 | + 'trafficAllocation' => [ |
| 1052 | + {'entityId' => '111151', 'endOfRange' => 10_000} |
| 1053 | + ], |
| 1054 | + 'cmab' => {'trafficAllocation' => 5000} |
| 1055 | + } |
| 1056 | + user_context = project_instance.create_user_context('test_user', {}) |
| 1057 | + |
| 1058 | + # Mock experiment lookup to return our CMAB experiment |
| 1059 | + allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment) |
| 1060 | + allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true) |
| 1061 | + |
| 1062 | + # Mock audience evaluation to pass |
| 1063 | + allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []]) |
| 1064 | + |
| 1065 | + # Mock bucketer to return a valid entity ID (user is in traffic allocation) |
| 1066 | + allow(decision_service.bucketer).to receive(:bucket_to_entity_id) |
| 1067 | + .with(config, cmab_experiment, 'test_user', 'test_user') |
| 1068 | + .and_return(['$', []]) |
| 1069 | + |
| 1070 | + # Mock CMAB service to return an error |
| 1071 | + allow(spy_cmab_service).to receive(:get_decision) |
| 1072 | + .with(config, user_context, '111150', []) |
| 1073 | + .and_raise(StandardError.new('CMAB service error')) |
| 1074 | + |
| 1075 | + variation_result = decision_service.get_variation(config, '111150', user_context) |
| 1076 | + |
| 1077 | + expect(variation_result.variation_id).to be_nil |
| 1078 | + expect(variation_result.cmab_uuid).to be_nil |
| 1079 | + expect(variation_result.error).to eq(true) |
| 1080 | + expect(variation_result.reasons).to include( |
| 1081 | + "Failed to fetch CMAB decision for experiment 'cmab_experiment'" |
| 1082 | + ) |
| 1083 | + |
| 1084 | + # Verify CMAB service was called but errored |
| 1085 | + expect(spy_cmab_service).to have_received(:get_decision).once |
| 1086 | + end |
| 1087 | + end |
| 1088 | + |
| 1089 | + describe 'when user has forced variation' do |
| 1090 | + it 'should return forced variation and skip CMAB service call' do |
| 1091 | + # Use a real experiment from the datafile and modify it to be a CMAB experiment |
| 1092 | + real_experiment = config.get_experiment_from_key('test_experiment') |
| 1093 | + cmab_experiment = real_experiment.dup |
| 1094 | + cmab_experiment['cmab'] = {'trafficAllocation' => 5000} |
| 1095 | + |
| 1096 | + user_context = project_instance.create_user_context('test_user', {}) |
| 1097 | + |
| 1098 | + # Set up forced variation first (using real experiment that exists in datafile) |
| 1099 | + decision_service.set_forced_variation(config, 'test_experiment', 'test_user', 'variation') |
| 1100 | + |
| 1101 | + # Mock the experiment to be a CMAB experiment after setting forced variation |
| 1102 | + allow(config).to receive(:get_experiment_from_id).with('111127').and_return(cmab_experiment) |
| 1103 | + allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true) |
| 1104 | + |
| 1105 | + # Add spy for bucket_to_entity_id method |
| 1106 | + allow(decision_service.bucketer).to receive(:bucket_to_entity_id).and_call_original |
| 1107 | + |
| 1108 | + variation_result = decision_service.get_variation(config, '111127', user_context) |
| 1109 | + |
| 1110 | + expect(variation_result.variation_id).to eq('111129') |
| 1111 | + expect(variation_result.cmab_uuid).to be_nil |
| 1112 | + expect(variation_result.error).to eq(false) |
| 1113 | + expect(variation_result.reasons).to include( |
| 1114 | + "Variation 'variation' is mapped to experiment '111127' and user 'test_user' in the forced variation map" |
| 1115 | + ) |
| 1116 | + |
| 1117 | + # Verify CMAB service was not called since user has forced variation |
| 1118 | + expect(spy_cmab_service).not_to have_received(:get_decision) |
| 1119 | + # Verify bucketer was not called since forced variations short-circuit bucketing |
| 1120 | + expect(decision_service.bucketer).not_to have_received(:bucket_to_entity_id) |
| 1121 | + end |
| 1122 | + end |
| 1123 | + |
| 1124 | + describe 'when user has whitelisted variation' do |
| 1125 | + it 'should return whitelisted variation and skip CMAB service call' do |
| 1126 | + # Create a CMAB experiment with whitelisted users |
| 1127 | + cmab_experiment = { |
| 1128 | + 'id' => '111150', |
| 1129 | + 'key' => 'cmab_experiment', |
| 1130 | + 'status' => 'Running', |
| 1131 | + 'layerId' => '111150', |
| 1132 | + 'audienceIds' => [], |
| 1133 | + 'forcedVariations' => { |
| 1134 | + 'whitelisted_user' => '111151' # User is whitelisted to variation_1 |
| 1135 | + }, |
| 1136 | + 'variations' => [ |
| 1137 | + {'id' => '111151', 'key' => 'variation_1'}, |
| 1138 | + {'id' => '111152', 'key' => 'variation_2'} |
| 1139 | + ], |
| 1140 | + 'trafficAllocation' => [ |
| 1141 | + {'entityId' => '111151', 'endOfRange' => 5000}, |
| 1142 | + {'entityId' => '111152', 'endOfRange' => 10_000} |
| 1143 | + ], |
| 1144 | + 'cmab' => {'trafficAllocation' => 5000} |
| 1145 | + } |
| 1146 | + user_context = project_instance.create_user_context('whitelisted_user', {}) |
| 1147 | + |
| 1148 | + # Mock experiment lookup to return our CMAB experiment |
| 1149 | + allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment) |
| 1150 | + allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true) |
| 1151 | + |
| 1152 | + # Mock the get_whitelisted_variation_id method directly |
| 1153 | + allow(decision_service).to receive(:get_whitelisted_variation_id) |
| 1154 | + .with(config, '111150', 'whitelisted_user') |
| 1155 | + .and_return(['111151', "User 'whitelisted_user' is whitelisted into variation 'variation_1' of experiment '111150'."]) |
| 1156 | + |
| 1157 | + variation_result = decision_service.get_variation(config, '111150', user_context) |
| 1158 | + |
| 1159 | + expect(variation_result.variation_id).to eq('111151') |
| 1160 | + expect(variation_result.cmab_uuid).to be_nil |
| 1161 | + expect(variation_result.error).to eq(false) |
| 1162 | + expect(variation_result.reasons).to include( |
| 1163 | + "User 'whitelisted_user' is whitelisted into variation 'variation_1' of experiment '111150'." |
| 1164 | + ) |
| 1165 | + # Verify CMAB service was not called since user is whitelisted |
| 1166 | + expect(spy_cmab_service).not_to have_received(:get_decision) |
| 1167 | + end |
| 1168 | + end |
| 1169 | + end |
940 | 1170 | end
|
0 commit comments