@@ -10,6 +10,11 @@ import ApiEndpoints from '../api-endpoints';
10
10
import { IAssignmentEvent , IAssignmentLogger } from '../assignment-logger' ;
11
11
import { BanditEvaluation , BanditEvaluator } from '../bandit-evaluator' ;
12
12
import { IBanditEvent , IBanditLogger } from '../bandit-logger' ;
13
+ import {
14
+ IConfigurationWire ,
15
+ IPrecomputedConfiguration ,
16
+ IObfuscatedPrecomputedConfigurationResponse ,
17
+ } from '../configuration' ;
13
18
import ConfigurationRequestor from '../configuration-requestor' ;
14
19
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store' ;
15
20
import { Evaluator , FlagEvaluation } from '../evaluator' ;
@@ -19,7 +24,8 @@ import {
19
24
} from '../flag-evaluation-details-builder' ;
20
25
import FetchHttpClient from '../http-client' ;
21
26
import { BanditVariation , BanditParameters , Flag } from '../interfaces' ;
22
- import { Attributes , ContextAttributes } from '../types' ;
27
+ import { attributeEncodeBase64 , setSaltOverrideForTests } from '../obfuscation' ;
28
+ import { Attributes , BanditActions , ContextAttributes } from '../types' ;
23
29
24
30
import EppoClient , { IAssignmentDetails } from './eppo-client' ;
25
31
@@ -554,6 +560,7 @@ describe('EppoClient Bandits E2E test', () => {
554
560
afterAll ( ( ) => {
555
561
mockEvaluateFlag . mockClear ( ) ;
556
562
mockEvaluateBandit . mockClear ( ) ;
563
+ jest . restoreAllMocks ( ) ;
557
564
} ) ;
558
565
559
566
it ( 'handles bandit actions appropriately' , async ( ) => {
@@ -619,4 +626,82 @@ describe('EppoClient Bandits E2E test', () => {
619
626
} ) ;
620
627
} ) ;
621
628
} ) ;
629
+
630
+ describe ( 'precomputed bandits' , ( ) => {
631
+ const bob = 'bob' ;
632
+
633
+ const bobInfo : ContextAttributes = {
634
+ numericAttributes : { age : 30 , account_age : 10 } ,
635
+ categoricalAttributes : { country : 'UK' , gender_identity : 'male' } ,
636
+ } ;
637
+
638
+ const bobActions : Record < string , BanditActions > = {
639
+ banner_bandit_flag : {
640
+ nike : {
641
+ numericAttributes : { brand_affinity : - 2.5 } ,
642
+ categoricalAttributes : { loyalty_tier : 'bronze' } ,
643
+ } ,
644
+ adidas : {
645
+ numericAttributes : { brand_affinity : - 2.5 } ,
646
+ categoricalAttributes : { loyalty_tier : 'bronze' } ,
647
+ } ,
648
+ reebok : {
649
+ numericAttributes : { brand_affinity : - 2.5 } ,
650
+ categoricalAttributes : { loyalty_tier : 'bronze' } ,
651
+ } ,
652
+ } ,
653
+ } ;
654
+
655
+ function getPrecomputedResults (
656
+ client : EppoClient ,
657
+ subjectKey : string ,
658
+ subjectAttributes : ContextAttributes ,
659
+ banditActions : Record < string , BanditActions > ,
660
+ ) : IPrecomputedConfiguration {
661
+ const precomputedResults = client . getPrecomputedConfiguration (
662
+ subjectKey ,
663
+ subjectAttributes ,
664
+ banditActions ,
665
+ ) ;
666
+
667
+ const { precomputed } = JSON . parse ( precomputedResults ) as IConfigurationWire ;
668
+ if ( ! precomputed ) {
669
+ fail ( 'precomputed result was not parsed' ) ;
670
+ }
671
+ return precomputed ;
672
+ }
673
+
674
+ describe ( 'obfuscated results' , ( ) => {
675
+ beforeEach ( ( ) => {
676
+ setSaltOverrideForTests ( new Uint8Array ( [ 101 , 112 , 112 , 111 ] ) ) ; // e p p o => "ZXBwbw=="
677
+ } ) ;
678
+
679
+ afterAll ( ( ) => {
680
+ setSaltOverrideForTests ( null ) ;
681
+ } ) ;
682
+
683
+ it ( 'obfuscates precomputed bandits' , ( ) => {
684
+ const bannerBanditFlagMd5 = '3ac89e06235484aa6f2aec8c33109a02' ;
685
+ const brandAffinityB64 = 'YnJhbmRfYWZmaW5pdHk=' ;
686
+ const loyaltyTierB64 = 'bG95YWx0eV90aWVy' ;
687
+ const bronzeB64 = 'YnJvbnpl' ;
688
+ const adidasB64 = 'YWRpZGFz' ;
689
+ const modelB64 = 'MTIz' ; // 123
690
+
691
+ const precomputed = getPrecomputedResults ( client , bob , bobInfo , bobActions ) ;
692
+
693
+ const response = JSON . parse (
694
+ precomputed . response ,
695
+ ) as IObfuscatedPrecomputedConfigurationResponse ;
696
+
697
+ const numericAttrs = response . bandits [ bannerBanditFlagMd5 ] [ 'actionNumericAttributes' ] ;
698
+ const categoricalAttrs =
699
+ response . bandits [ bannerBanditFlagMd5 ] [ 'actionCategoricalAttributes' ] ;
700
+ expect ( response . bandits [ bannerBanditFlagMd5 ] [ 'action' ] ) . toEqual ( adidasB64 ) ;
701
+ expect ( response . bandits [ bannerBanditFlagMd5 ] [ 'modelVersion' ] ) . toEqual ( modelB64 ) ;
702
+ expect ( categoricalAttrs [ loyaltyTierB64 ] ) . toEqual ( bronzeB64 ) ;
703
+ expect ( numericAttrs [ brandAffinityB64 ] ) . toEqual ( attributeEncodeBase64 ( - 2.5 ) ) ;
704
+ } ) ;
705
+ } ) ;
706
+ } ) ;
622
707
} ) ;
0 commit comments