3
3
*/
4
4
import axios from 'axios' ;
5
5
import * as td from 'testdouble' ;
6
- import mock from 'xhr-mock' ;
6
+ import mock , { MockResponse } from 'xhr-mock' ;
7
7
8
8
import {
9
9
IAssignmentTestCase ,
@@ -16,19 +16,19 @@ import {
16
16
import { IAssignmentHooks } from '../assignment-hooks' ;
17
17
import { IAssignmentLogger } from '../assignment-logger' ;
18
18
import { IConfigurationStore } from '../configuration-store' ;
19
- import { MAX_EVENT_QUEUE_SIZE } from '../constants' ;
19
+ import { MAX_EVENT_QUEUE_SIZE , POLL_INTERVAL_MS , POLL_JITTER_PCT } from '../constants' ;
20
20
import { OperatorType } from '../dto/rule-dto' ;
21
21
import { EppoValue } from '../eppo_value' ;
22
22
import ExperimentConfigurationRequestor from '../experiment-configuration-requestor' ;
23
23
import HttpClient from '../http-client' ;
24
24
25
- import EppoClient from './eppo-client' ;
25
+ import EppoClient , { ExperimentConfigurationRequestParameters } from './eppo-client' ;
26
26
27
27
// eslint-disable-next-line @typescript-eslint/no-var-requires
28
28
const packageJson = require ( '../../package.json' ) ;
29
29
30
30
class TestConfigurationStore implements IConfigurationStore {
31
- private store = { } ;
31
+ private store : Record < string , string > = { } ;
32
32
33
33
public get < T > ( key : string ) : T {
34
34
const rval = this . store [ key ] ;
@@ -1101,3 +1101,172 @@ describe(' EppoClient getAssignment From Obfuscated RAC', () => {
1101
1101
} ) ;
1102
1102
}
1103
1103
} ) ;
1104
+
1105
+ describe ( 'Eppo Client constructed with configuration request parameters' , ( ) => {
1106
+ let client : EppoClient ;
1107
+ let storage : IConfigurationStore ;
1108
+ let requestConfiguration : ExperimentConfigurationRequestParameters ;
1109
+ let mockServerResponseFunc : ( res : MockResponse ) => MockResponse ;
1110
+
1111
+ const racBody = JSON . stringify ( readMockRacResponse ( MOCK_RAC_RESPONSE_FILE ) ) ;
1112
+ const flagKey = 'randomization_algo' ;
1113
+ const subjectForGreenVariation = 'subject-identiferA' ;
1114
+
1115
+ const maxRetryDelay = POLL_INTERVAL_MS * POLL_JITTER_PCT ;
1116
+
1117
+ beforeAll ( ( ) => {
1118
+ mock . setup ( ) ;
1119
+ mock . get ( / r a n d o m i z e d _ a s s i g n m e n t \/ v 3 \/ c o n f i g * / , ( _req , res ) => {
1120
+ return mockServerResponseFunc ( res ) ;
1121
+ } ) ;
1122
+ } ) ;
1123
+
1124
+ beforeEach ( ( ) => {
1125
+ storage = new TestConfigurationStore ( ) ;
1126
+ requestConfiguration = {
1127
+ apiKey : 'dummy key' ,
1128
+ sdkName : 'js-client-sdk-common' ,
1129
+ sdkVersion : packageJson . version ,
1130
+ } ;
1131
+ mockServerResponseFunc = ( res ) => res . status ( 200 ) . body ( racBody ) ;
1132
+
1133
+ // We only want to fake setTimeout() and clearTimeout()
1134
+ jest . useFakeTimers ( {
1135
+ advanceTimers : true ,
1136
+ doNotFake : [
1137
+ 'Date' ,
1138
+ 'hrtime' ,
1139
+ 'nextTick' ,
1140
+ 'performance' ,
1141
+ 'queueMicrotask' ,
1142
+ 'requestAnimationFrame' ,
1143
+ 'cancelAnimationFrame' ,
1144
+ 'requestIdleCallback' ,
1145
+ 'cancelIdleCallback' ,
1146
+ 'setImmediate' ,
1147
+ 'clearImmediate' ,
1148
+ 'setInterval' ,
1149
+ 'clearInterval' ,
1150
+ ] ,
1151
+ } ) ;
1152
+ } ) ;
1153
+
1154
+ afterEach ( ( ) => {
1155
+ jest . clearAllTimers ( ) ;
1156
+ jest . useRealTimers ( ) ;
1157
+ } ) ;
1158
+
1159
+ afterAll ( ( ) => {
1160
+ mock . teardown ( ) ;
1161
+ } ) ;
1162
+
1163
+ it ( 'Fetches initial configuration' , async ( ) => {
1164
+ client = new EppoClient ( storage , requestConfiguration ) ;
1165
+ client . setIsGracefulFailureMode ( false ) ;
1166
+ // no configuration loaded
1167
+ let variation = client . getAssignment ( subjectForGreenVariation , flagKey ) ;
1168
+ expect ( variation ) . toBeNull ( ) ;
1169
+ // have client fetch configurations
1170
+ await client . fetchFlagConfigurations ( ) ;
1171
+ variation = client . getAssignment ( subjectForGreenVariation , flagKey ) ;
1172
+ expect ( variation ) . toBe ( 'green' ) ;
1173
+ } ) ;
1174
+
1175
+ it . each ( [
1176
+ { pollAfterSuccessfulInitialization : false } ,
1177
+ { pollAfterSuccessfulInitialization : true } ,
1178
+ ] ) ( 'retries initial configuration request with config %p' , async ( configModification ) => {
1179
+ let callCount = 0 ;
1180
+ mockServerResponseFunc = ( res ) => {
1181
+ if ( ++ callCount === 1 ) {
1182
+ // Throw an error for the first call
1183
+ return res . status ( 500 ) ;
1184
+ } else {
1185
+ // Return a mock object for subsequent calls
1186
+ return res . status ( 200 ) . body ( racBody ) ;
1187
+ }
1188
+ } ;
1189
+
1190
+ const { pollAfterSuccessfulInitialization } = configModification ;
1191
+ requestConfiguration = {
1192
+ ...requestConfiguration ,
1193
+ pollAfterSuccessfulInitialization,
1194
+ } ;
1195
+ client = new EppoClient ( storage , requestConfiguration ) ;
1196
+ client . setIsGracefulFailureMode ( false ) ;
1197
+ // no configuration loaded
1198
+ let variation = client . getAssignment ( subjectForGreenVariation , flagKey ) ;
1199
+ expect ( variation ) . toBeNull ( ) ;
1200
+
1201
+ // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
1202
+ const fetchPromise = client . fetchFlagConfigurations ( ) ;
1203
+
1204
+ // Advance timers mid-init to allow retrying
1205
+ await jest . advanceTimersByTimeAsync ( maxRetryDelay ) ;
1206
+
1207
+ // Await so it can finish its initialization before this test proceeds
1208
+ await fetchPromise ;
1209
+
1210
+ variation = client . getAssignment ( subjectForGreenVariation , flagKey ) ;
1211
+ expect ( variation ) . toBe ( 'green' ) ;
1212
+ expect ( callCount ) . toBe ( 2 ) ;
1213
+
1214
+ await jest . advanceTimersByTimeAsync ( POLL_INTERVAL_MS ) ;
1215
+ // By default, no more polling
1216
+ expect ( callCount ) . toBe ( pollAfterSuccessfulInitialization ? 3 : 2 ) ;
1217
+ } ) ;
1218
+
1219
+ it . each ( [
1220
+ { pollAfterFailedInitialization : false , throwOnFailedInitialization : false } ,
1221
+ { pollAfterFailedInitialization : false , throwOnFailedInitialization : true } ,
1222
+ { pollAfterFailedInitialization : true , throwOnFailedInitialization : false } ,
1223
+ { pollAfterFailedInitialization : true , throwOnFailedInitialization : true } ,
1224
+ ] ) ( 'initial configuration request fails with config %p' , async ( configModification ) => {
1225
+ let callCount = 0 ;
1226
+ mockServerResponseFunc = ( res ) => {
1227
+ if ( ++ callCount === 1 ) {
1228
+ // Throw an error for initialization call
1229
+ return res . status ( 500 ) ;
1230
+ } else {
1231
+ // Return a mock object for subsequent calls
1232
+ return res . status ( 200 ) . body ( racBody ) ;
1233
+ }
1234
+ } ;
1235
+
1236
+ const { pollAfterFailedInitialization, throwOnFailedInitialization } = configModification ;
1237
+
1238
+ // Note: fake time does not play well with errors bubbled up after setTimeout (event loop,
1239
+ // timeout queue, message queue stuff) so we don't allow retries when rethrowing.
1240
+ const numInitialRequestRetries = 0 ;
1241
+
1242
+ requestConfiguration = {
1243
+ ...requestConfiguration ,
1244
+ numInitialRequestRetries,
1245
+ throwOnFailedInitialization,
1246
+ pollAfterFailedInitialization,
1247
+ } ;
1248
+ client = new EppoClient ( storage , requestConfiguration ) ;
1249
+ client . setIsGracefulFailureMode ( false ) ;
1250
+ // no configuration loaded
1251
+ expect ( client . getAssignment ( subjectForGreenVariation , flagKey ) ) . toBeNull ( ) ;
1252
+
1253
+ // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
1254
+ if ( throwOnFailedInitialization ) {
1255
+ await expect ( client . fetchFlagConfigurations ( ) ) . rejects . toThrow ( ) ;
1256
+ } else {
1257
+ await expect ( client . fetchFlagConfigurations ( ) ) . resolves . toBeUndefined ( ) ;
1258
+ }
1259
+ expect ( callCount ) . toBe ( 1 ) ;
1260
+ // still no configuration loaded
1261
+ expect ( client . getAssignment ( subjectForGreenVariation , flagKey ) ) . toBeNull ( ) ;
1262
+
1263
+ // Advance timers so a post-init poll can take place
1264
+ await jest . advanceTimersByTimeAsync ( POLL_INTERVAL_MS * 1.5 ) ;
1265
+
1266
+ // if pollAfterFailedInitialization = true, we will poll later and get a config, otherwise not
1267
+ expect ( callCount ) . toBe ( pollAfterFailedInitialization ? 2 : 1 ) ;
1268
+ expect ( client . getAssignment ( subjectForGreenVariation , flagKey ) ) . toBe (
1269
+ pollAfterFailedInitialization ? 'green' : null ,
1270
+ ) ;
1271
+ } ) ;
1272
+ } ) ;
0 commit comments