@@ -12,14 +12,15 @@ import {
12
12
ensureNonContextualSubjectAttributes ,
13
13
} from '../attributes' ;
14
14
import { IPrecomputedConfigurationResponse } from '../configuration' ;
15
- import { IConfigurationStore } from '../configuration-store/configuration-store' ;
15
+ import { IConfigurationStore , ISyncStore } from '../configuration-store/configuration-store' ;
16
16
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store' ;
17
17
import { DEFAULT_POLL_INTERVAL_MS , MAX_EVENT_QUEUE_SIZE , POLL_JITTER_PCT } from '../constants' ;
18
18
import FetchHttpClient from '../http-client' ;
19
19
import {
20
20
FormatEnum ,
21
21
IObfuscatedPrecomputedBandit ,
22
22
PrecomputedFlag ,
23
+ Variation ,
23
24
VariationType ,
24
25
} from '../interfaces' ;
25
26
import { decodeBase64 , encodeBase64 , getMD5Hash } from '../obfuscation' ;
@@ -1027,3 +1028,249 @@ describe('Precomputed Bandit Store', () => {
1027
1028
loggerWarnSpy . mockRestore ( ) ;
1028
1029
} ) ;
1029
1030
} ) ;
1031
+
1032
+ describe ( 'flag overrides' , ( ) => {
1033
+ let client : EppoPrecomputedClient ;
1034
+ let mockLogger : IAssignmentLogger ;
1035
+ let overridesStore : ISyncStore < Variation > ;
1036
+ let flagStorage : IConfigurationStore < PrecomputedFlag > ;
1037
+ let subject : Subject ;
1038
+
1039
+ const precomputedFlagKey = 'mock-flag' ;
1040
+ const hashedPrecomputedFlagKey = getMD5Hash ( precomputedFlagKey ) ;
1041
+
1042
+ const mockPrecomputedFlag : PrecomputedFlag = {
1043
+ flagKey : hashedPrecomputedFlagKey ,
1044
+ variationKey : encodeBase64 ( 'a' ) ,
1045
+ variationValue : encodeBase64 ( 'variation-a' ) ,
1046
+ allocationKey : encodeBase64 ( 'allocation-a' ) ,
1047
+ doLog : true ,
1048
+ variationType : VariationType . STRING ,
1049
+ extraLogging : { } ,
1050
+ } ;
1051
+
1052
+ beforeEach ( ( ) => {
1053
+ flagStorage = new MemoryOnlyConfigurationStore ( ) ;
1054
+ flagStorage . setEntries ( { [ hashedPrecomputedFlagKey ] : mockPrecomputedFlag } ) ;
1055
+ mockLogger = td . object < IAssignmentLogger > ( ) ;
1056
+ overridesStore = new MemoryOnlyConfigurationStore < Variation > ( ) ;
1057
+ subject = {
1058
+ subjectKey : 'test-subject' ,
1059
+ subjectAttributes : { attr1 : 'value1' } ,
1060
+ } ;
1061
+
1062
+ client = new EppoPrecomputedClient ( {
1063
+ precomputedFlagStore : flagStorage ,
1064
+ subject,
1065
+ overridesStore,
1066
+ } ) ;
1067
+ client . setAssignmentLogger ( mockLogger ) ;
1068
+ } ) ;
1069
+
1070
+ it ( 'returns override values for all supported types' , ( ) => {
1071
+ overridesStore . setEntries ( {
1072
+ 'string-flag' : {
1073
+ key : 'override-variation' ,
1074
+ value : 'override-string' ,
1075
+ } ,
1076
+ 'boolean-flag' : {
1077
+ key : 'override-variation' ,
1078
+ value : true ,
1079
+ } ,
1080
+ 'numeric-flag' : {
1081
+ key : 'override-variation' ,
1082
+ value : 42.5 ,
1083
+ } ,
1084
+ 'json-flag' : {
1085
+ key : 'override-variation' ,
1086
+ value : '{"foo": "bar"}' ,
1087
+ } ,
1088
+ } ) ;
1089
+
1090
+ expect ( client . getStringAssignment ( 'string-flag' , 'default' ) ) . toBe ( 'override-string' ) ;
1091
+ expect ( client . getBooleanAssignment ( 'boolean-flag' , false ) ) . toBe ( true ) ;
1092
+ expect ( client . getNumericAssignment ( 'numeric-flag' , 0 ) ) . toBe ( 42.5 ) ;
1093
+ expect ( client . getJSONAssignment ( 'json-flag' , { } ) ) . toEqual ( { foo : 'bar' } ) ;
1094
+ } ) ;
1095
+
1096
+ it ( 'does not log assignments when override is applied' , ( ) => {
1097
+ overridesStore . setEntries ( {
1098
+ [ precomputedFlagKey ] : {
1099
+ key : 'override-variation' ,
1100
+ value : 'override-value' ,
1101
+ } ,
1102
+ } ) ;
1103
+
1104
+ client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1105
+
1106
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 0 ) ;
1107
+ } ) ;
1108
+
1109
+ it ( 'uses normal assignment when no override exists for flag' , ( ) => {
1110
+ // Set override for a different flag
1111
+ overridesStore . setEntries ( {
1112
+ 'other-flag' : {
1113
+ key : 'override-variation' ,
1114
+ value : 'override-value' ,
1115
+ } ,
1116
+ } ) ;
1117
+
1118
+ const result = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1119
+
1120
+ // Should get the normal assignment value from mockPrecomputedFlag
1121
+ expect ( result ) . toBe ( 'variation-a' ) ;
1122
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1123
+ } ) ;
1124
+
1125
+ it ( 'uses normal assignment when no overrides store is configured' , ( ) => {
1126
+ // Create client without overrides store
1127
+ const clientWithoutOverrides = new EppoPrecomputedClient ( {
1128
+ precomputedFlagStore : flagStorage ,
1129
+ subject,
1130
+ } ) ;
1131
+ clientWithoutOverrides . setAssignmentLogger ( mockLogger ) ;
1132
+
1133
+ const result = clientWithoutOverrides . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1134
+
1135
+ // Should get the normal assignment value from mockPrecomputedFlag
1136
+ expect ( result ) . toBe ( 'variation-a' ) ;
1137
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1138
+ } ) ;
1139
+
1140
+ it ( 'respects override after initial assignment without override' , ( ) => {
1141
+ // First call without override
1142
+ const initialAssignment = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1143
+ expect ( initialAssignment ) . toBe ( 'variation-a' ) ;
1144
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1145
+
1146
+ // Set override and make second call
1147
+ overridesStore . setEntries ( {
1148
+ [ precomputedFlagKey ] : {
1149
+ key : 'override-variation' ,
1150
+ value : 'override-value' ,
1151
+ } ,
1152
+ } ) ;
1153
+
1154
+ const overriddenAssignment = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1155
+ expect ( overriddenAssignment ) . toBe ( 'override-value' ) ;
1156
+ // No additional logging should occur when using override
1157
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1158
+ } ) ;
1159
+
1160
+ it ( 'reverts to normal assignment after removing override' , ( ) => {
1161
+ // Set initial override
1162
+ overridesStore . setEntries ( {
1163
+ [ precomputedFlagKey ] : {
1164
+ key : 'override-variation' ,
1165
+ value : 'override-value' ,
1166
+ } ,
1167
+ } ) ;
1168
+
1169
+ const overriddenAssignment = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1170
+ expect ( overriddenAssignment ) . toBe ( 'override-value' ) ;
1171
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 0 ) ;
1172
+
1173
+ // Remove override and make second call
1174
+ overridesStore . setEntries ( { } ) ;
1175
+
1176
+ const normalAssignment = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1177
+ expect ( normalAssignment ) . toBe ( 'variation-a' ) ;
1178
+ // Should log the normal assignment
1179
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1180
+ } ) ;
1181
+
1182
+ describe ( 'setOverridesStore' , ( ) => {
1183
+ it ( 'applies overrides after setting store' , ( ) => {
1184
+ // Create client without overrides store
1185
+ const clientWithoutOverrides = new EppoPrecomputedClient ( {
1186
+ precomputedFlagStore : flagStorage ,
1187
+ subject,
1188
+ } ) ;
1189
+ clientWithoutOverrides . setAssignmentLogger ( mockLogger ) ;
1190
+
1191
+ // Initial call without override store
1192
+ const initialAssignment = clientWithoutOverrides . getStringAssignment (
1193
+ precomputedFlagKey ,
1194
+ 'default' ,
1195
+ ) ;
1196
+ expect ( initialAssignment ) . toBe ( 'variation-a' ) ;
1197
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1198
+
1199
+ // Set overrides store with override
1200
+ overridesStore . setEntries ( {
1201
+ [ precomputedFlagKey ] : {
1202
+ key : 'override-variation' ,
1203
+ value : 'override-value' ,
1204
+ } ,
1205
+ } ) ;
1206
+ clientWithoutOverrides . setOverridesStore ( overridesStore ) ;
1207
+
1208
+ // Call after setting override store
1209
+ const overriddenAssignment = clientWithoutOverrides . getStringAssignment (
1210
+ precomputedFlagKey ,
1211
+ 'default' ,
1212
+ ) ;
1213
+ expect ( overriddenAssignment ) . toBe ( 'override-value' ) ;
1214
+ // No additional logging should occur when using override
1215
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1216
+ } ) ;
1217
+
1218
+ it ( 'reverts to normal assignment after unsetting store' , ( ) => {
1219
+ // Set initial override
1220
+ overridesStore . setEntries ( {
1221
+ [ precomputedFlagKey ] : {
1222
+ key : 'override-variation' ,
1223
+ value : 'override-value' ,
1224
+ } ,
1225
+ } ) ;
1226
+
1227
+ client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1228
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 0 ) ;
1229
+
1230
+ // Unset overrides store
1231
+ client . unsetOverridesStore ( ) ;
1232
+
1233
+ const normalAssignment = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1234
+ expect ( normalAssignment ) . toBe ( 'variation-a' ) ;
1235
+ // Should log the normal assignment
1236
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1237
+ } ) ;
1238
+
1239
+ it ( 'switches between different override stores' , ( ) => {
1240
+ // Create a second override store
1241
+ const secondOverridesStore = new MemoryOnlyConfigurationStore < Variation > ( ) ;
1242
+
1243
+ // Set up different overrides in each store
1244
+ overridesStore . setEntries ( {
1245
+ [ precomputedFlagKey ] : {
1246
+ key : 'override-1' ,
1247
+ value : 'value-1' ,
1248
+ } ,
1249
+ } ) ;
1250
+
1251
+ secondOverridesStore . setEntries ( {
1252
+ [ precomputedFlagKey ] : {
1253
+ key : 'override-2' ,
1254
+ value : 'value-2' ,
1255
+ } ,
1256
+ } ) ;
1257
+
1258
+ // Start with first override store
1259
+ const firstOverride = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1260
+ expect ( firstOverride ) . toBe ( 'value-1' ) ;
1261
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 0 ) ;
1262
+
1263
+ // Switch to second override store
1264
+ client . setOverridesStore ( secondOverridesStore ) ;
1265
+ const secondOverride = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1266
+ expect ( secondOverride ) . toBe ( 'value-2' ) ;
1267
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 0 ) ;
1268
+
1269
+ // Switch back to first override store
1270
+ client . setOverridesStore ( overridesStore ) ;
1271
+ const backToFirst = client . getStringAssignment ( precomputedFlagKey , 'default' ) ;
1272
+ expect ( backToFirst ) . toBe ( 'value-1' ) ;
1273
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 0 ) ;
1274
+ } ) ;
1275
+ } ) ;
1276
+ } ) ;
0 commit comments