@@ -14,6 +14,7 @@ import {
14
14
validateTestAssignments ,
15
15
} from '../../test/testHelpers' ;
16
16
import { IAssignmentLogger } from '../assignment-logger' ;
17
+ import { AssignmentCache } from '../cache/abstract-assignment-cache' ;
17
18
import {
18
19
IConfigurationWire ,
19
20
IObfuscatedPrecomputedConfigurationResponse ,
@@ -23,7 +24,7 @@ import { IConfigurationStore } from '../configuration-store/configuration-store'
23
24
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store' ;
24
25
import { MAX_EVENT_QUEUE_SIZE , DEFAULT_POLL_INTERVAL_MS , POLL_JITTER_PCT } from '../constants' ;
25
26
import { decodePrecomputedFlag } from '../decoding' ;
26
- import { Flag , ObfuscatedFlag , VariationType } from '../interfaces' ;
27
+ import { Flag , ObfuscatedFlag , Variation , VariationType } from '../interfaces' ;
27
28
import { getMD5Hash } from '../obfuscation' ;
28
29
import { AttributeType } from '../types' ;
29
30
@@ -945,4 +946,225 @@ describe('EppoClient E2E test', () => {
945
946
) ;
946
947
} ) ;
947
948
} ) ;
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
+ } ) ;
948
1170
} ) ;
0 commit comments