@@ -14,6 +14,7 @@ import {
1414 validateTestAssignments ,
1515} from '../../test/testHelpers' ;
1616import { IAssignmentLogger } from '../assignment-logger' ;
17+ import { AssignmentCache } from '../cache/abstract-assignment-cache' ;
1718import {
1819 IConfigurationWire ,
1920 IObfuscatedPrecomputedConfigurationResponse ,
@@ -23,7 +24,7 @@ import { IConfigurationStore } from '../configuration-store/configuration-store'
2324import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store' ;
2425import { MAX_EVENT_QUEUE_SIZE , DEFAULT_POLL_INTERVAL_MS , POLL_JITTER_PCT } from '../constants' ;
2526import { decodePrecomputedFlag } from '../decoding' ;
26- import { Flag , ObfuscatedFlag , VariationType } from '../interfaces' ;
27+ import { Flag , ObfuscatedFlag , Variation , VariationType } from '../interfaces' ;
2728import { getMD5Hash } from '../obfuscation' ;
2829import { AttributeType } from '../types' ;
2930
@@ -945,4 +946,225 @@ describe('EppoClient E2E test', () => {
945946 ) ;
946947 } ) ;
947948 } ) ;
949+
950+ describe ( 'flag overrides' , ( ) => {
951+ let client : EppoClient ;
952+ let mockLogger : IAssignmentLogger ;
953+ let overrideStore : IConfigurationStore < Variation > ;
954+
955+ beforeEach ( ( ) => {
956+ storage . setEntries ( { [ flagKey ] : mockFlag } ) ;
957+ mockLogger = td . object < IAssignmentLogger > ( ) ;
958+ overrideStore = new MemoryOnlyConfigurationStore < Variation > ( ) ;
959+ client = new EppoClient ( {
960+ flagConfigurationStore : storage ,
961+ overrideStore : overrideStore ,
962+ } ) ;
963+ client . setAssignmentLogger ( mockLogger ) ;
964+ client . useNonExpiringInMemoryAssignmentCache ( ) ;
965+ } ) ;
966+
967+ it ( 'returns override values for all supported types' , ( ) => {
968+ overrideStore . setEntries ( {
969+ 'string-flag' : {
970+ key : 'override-variation' ,
971+ value : 'override-string' ,
972+ } ,
973+ 'boolean-flag' : {
974+ key : 'override-variation' ,
975+ value : true ,
976+ } ,
977+ 'numeric-flag' : {
978+ key : 'override-variation' ,
979+ value : 42.5 ,
980+ } ,
981+ 'json-flag' : {
982+ key : 'override-variation' ,
983+ value : '{"foo": "bar"}' ,
984+ } ,
985+ } ) ;
986+
987+ expect ( client . getStringAssignment ( 'string-flag' , 'subject-10' , { } , 'default' ) ) . toBe (
988+ 'override-string' ,
989+ ) ;
990+ expect ( client . getBooleanAssignment ( 'boolean-flag' , 'subject-10' , { } , false ) ) . toBe ( true ) ;
991+ expect ( client . getNumericAssignment ( 'numeric-flag' , 'subject-10' , { } , 0 ) ) . toBe ( 42.5 ) ;
992+ expect ( client . getJSONAssignment ( 'json-flag' , 'subject-10' , { } , { } ) ) . toEqual ( { foo : 'bar' } ) ;
993+ } ) ;
994+
995+ it ( 'does not log assignments when override is applied' , ( ) => {
996+ overrideStore . setEntries ( {
997+ [ flagKey ] : {
998+ key : 'override-variation' ,
999+ value : 'override-value' ,
1000+ } ,
1001+ } ) ;
1002+
1003+ client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1004+
1005+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 0 ) ;
1006+ } ) ;
1007+
1008+ it ( 'includes override details in assignment details' , ( ) => {
1009+ overrideStore . setEntries ( {
1010+ [ flagKey ] : {
1011+ key : 'override-variation' ,
1012+ value : 'override-value' ,
1013+ } ,
1014+ } ) ;
1015+
1016+ const result = client . getStringAssignmentDetails (
1017+ flagKey ,
1018+ 'subject-10' ,
1019+ { foo : 3 } ,
1020+ 'default' ,
1021+ ) ;
1022+
1023+ expect ( result ) . toMatchObject ( {
1024+ variation : 'override-value' ,
1025+ evaluationDetails : {
1026+ flagEvaluationCode : 'MATCH' ,
1027+ flagEvaluationDescription : 'Flag override applied' ,
1028+ } ,
1029+ } ) ;
1030+ } ) ;
1031+
1032+ it ( 'does not update assignment cache when override is applied' , ( ) => {
1033+ const mockAssignmentCache = td . object < AssignmentCache > ( ) ;
1034+ td . when ( mockAssignmentCache . has ( td . matchers . anything ( ) ) ) . thenReturn ( false ) ;
1035+ td . when ( mockAssignmentCache . set ( td . matchers . anything ( ) ) ) . thenReturn ( ) ;
1036+ client . useCustomAssignmentCache ( mockAssignmentCache ) ;
1037+
1038+ overrideStore . setEntries ( {
1039+ [ flagKey ] : {
1040+ key : 'override-variation' ,
1041+ value : 'override-value' ,
1042+ } ,
1043+ } ) ;
1044+
1045+ // First call with override
1046+ client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1047+
1048+ // Verify cache was not used at all
1049+ expect ( td . explain ( mockAssignmentCache . set ) . callCount ) . toBe ( 0 ) ;
1050+
1051+ // Remove override
1052+ overrideStore . setEntries ( { } ) ;
1053+
1054+ // Second call without override
1055+ client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1056+
1057+ // Now cache should be used
1058+ expect ( td . explain ( mockAssignmentCache . set ) . callCount ) . toBe ( 1 ) ;
1059+ } ) ;
1060+
1061+ it ( 'uses normal assignment when no override exists for flag' , ( ) => {
1062+ // Set override for a different flag
1063+ overrideStore . setEntries ( {
1064+ 'other-flag' : {
1065+ key : 'override-variation' ,
1066+ value : 'override-value' ,
1067+ } ,
1068+ } ) ;
1069+
1070+ const result = client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1071+
1072+ // Should get the normal assignment value from mockFlag
1073+ expect ( result ) . toBe ( variationA . value ) ;
1074+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1075+ } ) ;
1076+
1077+ it ( 'uses normal assignment when no overrides store is configured' , ( ) => {
1078+ // Create client without overrides store
1079+ const clientWithoutOverrides = new EppoClient ( {
1080+ flagConfigurationStore : storage ,
1081+ } ) ;
1082+ clientWithoutOverrides . setAssignmentLogger ( mockLogger ) ;
1083+
1084+ const result = clientWithoutOverrides . getStringAssignment (
1085+ flagKey ,
1086+ 'subject-10' ,
1087+ { } ,
1088+ 'default' ,
1089+ ) ;
1090+
1091+ // Should get the normal assignment value from mockFlag
1092+ expect ( result ) . toBe ( variationA . value ) ;
1093+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1094+ } ) ;
1095+
1096+ it ( 'respects override after initial assignment without override' , ( ) => {
1097+ // First call without override
1098+ const initialAssignment = client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1099+ expect ( initialAssignment ) . toBe ( variationA . value ) ;
1100+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1101+
1102+ // Set override and make second call
1103+ overrideStore . setEntries ( {
1104+ [ flagKey ] : {
1105+ key : 'override-variation' ,
1106+ value : 'override-value' ,
1107+ } ,
1108+ } ) ;
1109+
1110+ const overriddenAssignment = client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1111+ expect ( overriddenAssignment ) . toBe ( 'override-value' ) ;
1112+ // No additional logging should occur when using override
1113+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1114+ } ) ;
1115+
1116+ it ( 'reverts to normal assignment after removing override' , ( ) => {
1117+ // Set initial override
1118+ overrideStore . setEntries ( {
1119+ [ flagKey ] : {
1120+ key : 'override-variation' ,
1121+ value : 'override-value' ,
1122+ } ,
1123+ } ) ;
1124+
1125+ const overriddenAssignment = client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1126+ expect ( overriddenAssignment ) . toBe ( 'override-value' ) ;
1127+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 0 ) ;
1128+
1129+ // Remove override and make second call
1130+ overrideStore . setEntries ( { } ) ;
1131+
1132+ const normalAssignment = client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1133+ expect ( normalAssignment ) . toBe ( variationA . value ) ;
1134+ // Should log the normal assignment
1135+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toBe ( 1 ) ;
1136+ } ) ;
1137+
1138+ it ( 'reverts to normal assignment after unsetting overrides store' , ( ) => {
1139+ overrideStore . setEntries ( {
1140+ [ flagKey ] : {
1141+ key : 'override-variation' ,
1142+ value : 'override-value' ,
1143+ } ,
1144+ } ) ;
1145+
1146+ client . unsetOverrideStore ( ) ;
1147+
1148+ const normalAssignment = client . getStringAssignment ( flagKey , 'subject-10' , { } , 'default' ) ;
1149+ expect ( normalAssignment ) . toBe ( variationA . value ) ;
1150+ } ) ;
1151+
1152+ it ( 'returns a mapping of flag key to variation key for all active overrides' , ( ) => {
1153+ overrideStore . setEntries ( {
1154+ [ flagKey ] : {
1155+ key : 'override-variation' ,
1156+ value : 'override-value' ,
1157+ } ,
1158+ 'other-flag' : {
1159+ key : 'other-variation' ,
1160+ value : 'other-value' ,
1161+ } ,
1162+ } ) ;
1163+
1164+ expect ( client . getOverrideVariationKeys ( ) ) . toEqual ( {
1165+ [ flagKey ] : 'override-variation' ,
1166+ 'other-flag' : 'other-variation' ,
1167+ } ) ;
1168+ } ) ;
1169+ } ) ;
9481170} ) ;
0 commit comments