@@ -12,14 +12,15 @@ import {
1212 ensureNonContextualSubjectAttributes ,
1313} from '../attributes' ;
1414import { IPrecomputedConfigurationResponse } from '../configuration' ;
15- import { IConfigurationStore } from '../configuration-store/configuration-store' ;
15+ import { IConfigurationStore , ISyncStore } from '../configuration-store/configuration-store' ;
1616import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store' ;
1717import { DEFAULT_POLL_INTERVAL_MS , MAX_EVENT_QUEUE_SIZE , POLL_JITTER_PCT } from '../constants' ;
1818import FetchHttpClient from '../http-client' ;
1919import {
2020 FormatEnum ,
2121 IObfuscatedPrecomputedBandit ,
2222 PrecomputedFlag ,
23+ Variation ,
2324 VariationType ,
2425} from '../interfaces' ;
2526import { decodeBase64 , encodeBase64 , getMD5Hash } from '../obfuscation' ;
@@ -1027,3 +1028,249 @@ describe('Precomputed Bandit Store', () => {
10271028 loggerWarnSpy . mockRestore ( ) ;
10281029 } ) ;
10291030} ) ;
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