13
13
* See the License for the specific language governing permissions and
14
14
* limitations under the License.
15
15
*/
16
- import { describe , it , expect , beforeEach , afterEach , vi , assert , Mock } from 'vitest' ;
16
+ import { describe , it , expect , beforeEach , afterEach , vi , assert , Mock , beforeAll , afterAll } from 'vitest' ;
17
17
import { sprintf } from '../utils/fns' ;
18
18
import { keyBy } from '../utils/fns' ;
19
- import projectConfig , { ProjectConfig , Region } from './project_config' ;
19
+ import projectConfig , { ProjectConfig , getHoldoutsForFlag } from './project_config' ;
20
20
import { FEATURE_VARIABLE_TYPES , LOG_LEVEL } from '../utils/enums' ;
21
21
import testDatafile from '../tests/test_data' ;
22
22
import configValidator from '../utils/config_validator' ;
@@ -32,11 +32,20 @@ import {
32
32
import { getMockLogger } from '../tests/mock/mock_logger' ;
33
33
import { VariableType } from '../shared_types' ;
34
34
import { OptimizelyError } from '../error/optimizly_error' ;
35
+ import { mock } from 'node:test' ;
35
36
36
37
const buildLogMessageFromArgs = ( args : any [ ] ) => sprintf ( args [ 1 ] , ...args . splice ( 2 ) ) ;
37
38
const cloneDeep = ( obj : any ) => JSON . parse ( JSON . stringify ( obj ) ) ;
38
39
const logger = getMockLogger ( ) ;
39
40
41
+ const mockHoldoutToggle = vi . hoisted ( ( ) => vi . fn ( ) ) ;
42
+
43
+ vi . mock ( '../feature_toggle' , ( ) => {
44
+ return {
45
+ holdout : mockHoldoutToggle ,
46
+ } ;
47
+ } ) ;
48
+
40
49
describe ( 'createProjectConfig' , ( ) => {
41
50
let configObj : ProjectConfig ;
42
51
@@ -298,6 +307,191 @@ describe('createProjectConfig - cmab experiments', () => {
298
307
} ) ;
299
308
} ) ;
300
309
310
+ const getHoldoutDatafile = ( ) => {
311
+ const datafile = testDatafile . getTestDecideProjectConfig ( ) ;
312
+
313
+ // Add holdouts to the datafile
314
+ datafile . holdouts = [
315
+ {
316
+ id : 'holdout_id_1' ,
317
+ key : 'holdout_1' ,
318
+ status : 'Running' ,
319
+ includeFlags : [ ] ,
320
+ excludeFlags : [ ] ,
321
+ audienceIds : [ '13389130056' ] ,
322
+ audienceConditions : [ 'or' , '13389130056' ] ,
323
+ variations : [
324
+ {
325
+ id : 'var_id_1' ,
326
+ key : 'holdout_variation_1' ,
327
+ variables : [ ]
328
+ }
329
+ ] ,
330
+ trafficAllocation : [
331
+ {
332
+ entityId : 'var_id_1' ,
333
+ endOfRange : 5000
334
+ }
335
+ ]
336
+ } ,
337
+ {
338
+ id : 'holdout_id_2' ,
339
+ key : 'holdout_2' ,
340
+ status : 'Running' ,
341
+ includeFlags : [ ] ,
342
+ excludeFlags : [ 'feature_3' ] ,
343
+ audienceIds : [ ] ,
344
+ audienceConditions : [ ] ,
345
+ variations : [
346
+ {
347
+ id : 'var_id_2' ,
348
+ key : 'holdout_variation_2' ,
349
+ variables : [ ]
350
+ }
351
+ ] ,
352
+ trafficAllocation : [
353
+ {
354
+ entityId : 'var_id_2' ,
355
+ endOfRange : 1000
356
+ }
357
+ ]
358
+ } ,
359
+ {
360
+ id : 'holdout_id_3' ,
361
+ key : 'holdout_3' ,
362
+ status : 'Draft' ,
363
+ includeFlags : [ 'feature_1' ] ,
364
+ excludeFlags : [ ] ,
365
+ audienceIds : [ ] ,
366
+ audienceConditions : [ ] ,
367
+ variations : [
368
+ {
369
+ id : 'var_id_2' ,
370
+ key : 'holdout_variation_2' ,
371
+ variables : [ ]
372
+ }
373
+ ] ,
374
+ trafficAllocation : [
375
+ {
376
+ entityId : 'var_id_2' ,
377
+ endOfRange : 1000
378
+ }
379
+ ]
380
+ }
381
+ ] ;
382
+
383
+ return datafile ;
384
+ }
385
+
386
+ describe ( 'createProjectConfig - holdouts, feature toggle is on' , ( ) => {
387
+ beforeAll ( ( ) => {
388
+ mockHoldoutToggle . mockReturnValue ( true ) ;
389
+ } ) ;
390
+
391
+ afterAll ( ( ) => {
392
+ mockHoldoutToggle . mockReset ( ) ;
393
+ } ) ;
394
+
395
+ it ( 'should populate holdouts fields correctly' , function ( ) {
396
+ const datafile = getHoldoutDatafile ( ) ;
397
+
398
+ mockHoldoutToggle . mockReturnValue ( true ) ;
399
+
400
+ const configObj = projectConfig . createProjectConfig ( JSON . parse ( JSON . stringify ( datafile ) ) ) ;
401
+
402
+ expect ( configObj . holdouts ) . toHaveLength ( 3 ) ;
403
+ configObj . holdouts . forEach ( ( holdout , i ) => {
404
+ expect ( holdout ) . toEqual ( expect . objectContaining ( datafile . holdouts [ i ] ) ) ;
405
+ expect ( holdout . variationKeyMap ) . toEqual (
406
+ keyBy ( datafile . holdouts [ i ] . variations , 'key' )
407
+ ) ;
408
+ } ) ;
409
+
410
+ expect ( configObj . holdoutIdMap ) . toEqual ( {
411
+ holdout_id_1 : configObj . holdouts [ 0 ] ,
412
+ holdout_id_2 : configObj . holdouts [ 1 ] ,
413
+ holdout_id_3 : configObj . holdouts [ 2 ] ,
414
+ } ) ;
415
+
416
+ expect ( configObj . globalHoldouts ) . toHaveLength ( 2 ) ;
417
+ expect ( configObj . globalHoldouts ) . toEqual ( [
418
+ configObj . holdouts [ 0 ] , // holdout_1 has empty includeFlags
419
+ configObj . holdouts [ 1 ] // holdout_2 has empty includeFlags
420
+ ] ) ;
421
+
422
+ expect ( configObj . includedHoldouts ) . toEqual ( {
423
+ feature_1 : [ configObj . holdouts [ 2 ] ] , // holdout_3 includes feature_1
424
+ } ) ;
425
+
426
+ expect ( configObj . excludedHoldouts ) . toEqual ( {
427
+ feature_3 : [ configObj . holdouts [ 1 ] ] // holdout_2 excludes feature_3
428
+ } ) ;
429
+
430
+ expect ( configObj . flagHoldoutsMap ) . toEqual ( { } ) ;
431
+ } ) ;
432
+
433
+ it ( 'should handle empty holdouts array' , function ( ) {
434
+ const datafile = testDatafile . getTestProjectConfig ( ) ;
435
+
436
+ const configObj = projectConfig . createProjectConfig ( datafile ) ;
437
+
438
+ expect ( configObj . holdouts ) . toEqual ( [ ] ) ;
439
+ expect ( configObj . holdoutIdMap ) . toEqual ( { } ) ;
440
+ expect ( configObj . globalHoldouts ) . toEqual ( [ ] ) ;
441
+ expect ( configObj . includedHoldouts ) . toEqual ( { } ) ;
442
+ expect ( configObj . excludedHoldouts ) . toEqual ( { } ) ;
443
+ expect ( configObj . flagHoldoutsMap ) . toEqual ( { } ) ;
444
+ } ) ;
445
+
446
+ it ( 'should handle undefined includeFlags and excludeFlags in holdout' , function ( ) {
447
+ const datafile = getHoldoutDatafile ( ) ;
448
+ datafile . holdouts [ 0 ] . includeFlags = undefined ;
449
+ datafile . holdouts [ 0 ] . excludeFlags = undefined ;
450
+
451
+ const configObj = projectConfig . createProjectConfig ( JSON . parse ( JSON . stringify ( datafile ) ) ) ;
452
+
453
+ expect ( configObj . holdouts ) . toHaveLength ( 3 ) ;
454
+ expect ( configObj . holdouts [ 0 ] . includeFlags ) . toEqual ( [ ] ) ;
455
+ expect ( configObj . holdouts [ 0 ] . excludeFlags ) . toEqual ( [ ] ) ;
456
+ } ) ;
457
+ } ) ;
458
+
459
+ describe ( 'getHoldoutsForFlag: feature toggle is on' , ( ) => {
460
+ beforeAll ( ( ) => {
461
+ mockHoldoutToggle . mockReturnValue ( true ) ;
462
+ } ) ;
463
+
464
+ afterAll ( ( ) => {
465
+ mockHoldoutToggle . mockReset ( ) ;
466
+ } ) ;
467
+
468
+ it ( 'should return all applicable holdouts for a flag' , ( ) => {
469
+ const datafile = getHoldoutDatafile ( ) ;
470
+ const configObj = projectConfig . createProjectConfig ( JSON . parse ( JSON . stringify ( datafile ) ) ) ;
471
+
472
+ const feature1Holdouts = getHoldoutsForFlag ( configObj , 'feature_1' ) ;
473
+ expect ( feature1Holdouts ) . toHaveLength ( 3 ) ;
474
+ expect ( feature1Holdouts ) . toEqual ( [
475
+ configObj . holdouts [ 0 ] ,
476
+ configObj . holdouts [ 1 ] ,
477
+ configObj . holdouts [ 2 ] ,
478
+ ] ) ;
479
+
480
+ const feature2Holdouts = getHoldoutsForFlag ( configObj , 'feature_2' ) ;
481
+ expect ( feature2Holdouts ) . toHaveLength ( 2 ) ;
482
+ expect ( feature2Holdouts ) . toEqual ( [
483
+ configObj . holdouts [ 0 ] ,
484
+ configObj . holdouts [ 1 ] ,
485
+ ] ) ;
486
+
487
+ const feature3Holdouts = getHoldoutsForFlag ( configObj , 'feature_3' ) ;
488
+ expect ( feature3Holdouts ) . toHaveLength ( 1 ) ;
489
+ expect ( feature3Holdouts ) . toEqual ( [
490
+ configObj . holdouts [ 0 ] ,
491
+ ] ) ;
492
+ } ) ;
493
+ } ) ;
494
+
301
495
describe ( 'getExperimentId' , ( ) => {
302
496
let testData : Record < string , any > ;
303
497
let configObj : ProjectConfig ;
0 commit comments