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 , vi } from 'vitest' ;
16
+ import { describe , it , expect , vi , MockInstance , beforeEach } from 'vitest' ;
17
17
import { DecisionService } from '.' ;
18
18
import { getMockLogger } from '../../tests/mock/mock_logger' ;
19
+ import OptimizelyUserContext from '../../optimizely_user_context' ;
20
+ import { bucket } from '../bucketer' ;
21
+ import { getTestProjectConfig , getTestProjectConfigWithFeatures } from '../../tests/test_data' ;
22
+ import { createProjectConfig , ProjectConfig } from '../../project_config/project_config' ;
23
+ import { Experiment } from '../../shared_types' ;
24
+ import { CONTROL_ATTRIBUTES } from '../../utils/enums' ;
25
+
26
+ import {
27
+ USER_HAS_NO_FORCED_VARIATION ,
28
+ VALID_BUCKETING_ID ,
29
+ SAVED_USER_VARIATION ,
30
+ SAVED_VARIATION_NOT_FOUND ,
31
+ } from 'log_message' ;
32
+
33
+ import {
34
+ EXPERIMENT_NOT_RUNNING ,
35
+ RETURNING_STORED_VARIATION ,
36
+ USER_NOT_IN_EXPERIMENT ,
37
+ USER_FORCED_IN_VARIATION ,
38
+ EVALUATING_AUDIENCES_COMBINED ,
39
+ AUDIENCE_EVALUATION_RESULT_COMBINED ,
40
+ USER_IN_ROLLOUT ,
41
+ USER_NOT_IN_ROLLOUT ,
42
+ FEATURE_HAS_NO_EXPERIMENTS ,
43
+ USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE ,
44
+ USER_NOT_BUCKETED_INTO_TARGETING_RULE ,
45
+ USER_BUCKETED_INTO_TARGETING_RULE ,
46
+ NO_ROLLOUT_EXISTS ,
47
+ USER_MEETS_CONDITIONS_FOR_TARGETING_RULE ,
48
+ } from '../decision_service/index' ;
19
49
20
50
type MockLogger = ReturnType < typeof getMockLogger > ;
21
51
@@ -35,7 +65,7 @@ type DecisionServiceInstance = {
35
65
decisionService : DecisionService ;
36
66
}
37
67
38
- const getDecisionService = ( opt : DecisionServiceInstanceOpt ) : DecisionServiceInstance => {
68
+ const getDecisionService = ( opt : DecisionServiceInstanceOpt = { } ) : DecisionServiceInstance => {
39
69
const logger = opt . logger ? getMockLogger ( ) : undefined ;
40
70
const userProfileService = opt . userProfileService ? {
41
71
lookup : vi . fn ( ) ,
@@ -55,70 +85,144 @@ const getDecisionService = (opt: DecisionServiceInstanceOpt): DecisionServiceIns
55
85
} ;
56
86
} ;
57
87
88
+ const mockBucket : MockInstance < typeof bucket > = vi . hoisted ( ( ) => vi . fn ( ) ) ;
89
+
90
+ vi . mock ( '../bucketer' , ( ) => ( {
91
+ bucket : mockBucket ,
92
+ } ) ) ;
93
+
58
94
const testGetVariationWithoutUserProfileService = ( decisonService : DecisionServiceInstance ) => {
59
95
60
96
}
97
+ const cloneDeep = ( d : any ) => JSON . parse ( JSON . stringify ( d ) ) ;
98
+
99
+ const testData = getTestProjectConfig ( ) ;
100
+ const testDataWithFeatures = getTestProjectConfigWithFeatures ( ) ;
101
+
102
+ const verifyBucketCall = (
103
+ call : number ,
104
+ projectConfig : ProjectConfig ,
105
+ experiment : Experiment ,
106
+ user : OptimizelyUserContext ,
107
+ ) => {
108
+ const {
109
+ experimentId,
110
+ experimentKey,
111
+ userId,
112
+ trafficAllocationConfig,
113
+ experimentKeyMap,
114
+ experimentIdMap,
115
+ groupIdMap,
116
+ variationIdMap,
117
+ bucketingId,
118
+ } = mockBucket . mock . calls [ call ] [ 0 ] ;
119
+ expect ( experimentId ) . toBe ( experiment . id ) ;
120
+ expect ( experimentKey ) . toBe ( experiment . key ) ;
121
+ expect ( userId ) . toBe ( user . getUserId ( ) ) ;
122
+ expect ( trafficAllocationConfig ) . toBe ( experiment . trafficAllocation ) ;
123
+ expect ( experimentKeyMap ) . toBe ( projectConfig . experimentKeyMap ) ;
124
+ expect ( experimentIdMap ) . toBe ( projectConfig . experimentIdMap ) ;
125
+ expect ( groupIdMap ) . toBe ( projectConfig . groupIdMap ) ;
126
+ expect ( variationIdMap ) . toBe ( projectConfig . variationIdMap ) ;
127
+ expect ( bucketingId ) . toBe ( user . getAttributes ( ) [ CONTROL_ATTRIBUTES . BUCKETING_ID ] || user . getUserId ( ) ) ;
128
+ } ;
61
129
62
130
describe ( 'DecisionService' , ( ) => {
63
131
describe ( 'getVariation' , function ( ) {
64
- it ( 'should return the correct variation for the given experiment key and user ID for a running experiment' , ( ) => {
65
- user = new OptimizelyUserContext ( {
66
- shouldIdentifyUser : false ,
67
- optimizely : { } ,
132
+ beforeEach ( ( ) => {
133
+ mockBucket . mockClear ( ) ;
134
+ } ) ;
135
+
136
+ it ( 'should return the correct variation from bucketer for the given experiment key and user ID for a running experiment' , ( ) => {
137
+ const user = new OptimizelyUserContext ( {
138
+ optimizely : { } as any ,
68
139
userId : 'tester'
69
140
} ) ;
70
- var fakeDecisionResponse = {
141
+
142
+ const config = createProjectConfig ( cloneDeep ( testData ) ) ;
143
+
144
+ const fakeDecisionResponse = {
71
145
result : '111128' ,
72
146
reasons : [ ] ,
73
147
} ;
74
- experiment = configObj . experimentIdMap [ '111127' ] ;
75
- bucketerStub . returns ( fakeDecisionResponse ) ; // contains variation ID of the 'control' variation from `test_data`
76
- assert . strictEqual (
77
- 'control' ,
78
- decisionServiceInstance . getVariation ( configObj , experiment , user ) . result
79
- ) ;
80
- sinon . assert . calledOnce ( bucketerStub ) ;
148
+
149
+ const experiment = config . experimentIdMap [ '111127' ] ;
150
+
151
+ mockBucket . mockReturnValue ( fakeDecisionResponse ) ; // contains variation ID of the 'control' variation from `test_data`
152
+
153
+ const { decisionService } = getDecisionService ( ) ;
154
+
155
+ const variation = decisionService . getVariation ( config , experiment , user ) ;
156
+
157
+ expect ( variation . result ) . toBe ( 'control' ) ;
158
+
159
+ expect ( mockBucket ) . toHaveBeenCalledTimes ( 1 ) ;
160
+ verifyBucketCall ( 0 , config , experiment , user ) ;
81
161
} ) ;
82
162
83
- // it('should return the whitelisted variation if the user is whitelisted', function() {
84
- // user = new OptimizelyUserContext({
85
- // shouldIdentifyUser: false,
86
- // optimizely: {},
87
- // userId: 'user2'
88
- // });
89
- // experiment = configObj.experimentIdMap['122227'];
90
- // assert.strictEqual(
91
- // 'variationWithAudience',
92
- // decisionServiceInstance.getVariation(configObj, experiment, user).result
93
- // );
94
- // sinon.assert.notCalled(bucketerStub);
95
- // assert.strictEqual(1, mockLogger.debug.callCount);
96
- // assert.strictEqual(1, mockLogger.info.callCount);
163
+ it ( 'should return the whitelisted variation if the user is whitelisted' , function ( ) {
164
+ const user = new OptimizelyUserContext ( {
165
+ optimizely : { } as any ,
166
+ userId : 'user2'
167
+ } ) ;
97
168
98
- // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user2'] );
169
+ const config = createProjectConfig ( cloneDeep ( testData ) ) ;
99
170
100
- // assert.deepEqual(mockLogger.info.args[0], [USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience']);
101
- // });
171
+ const experiment = config . experimentIdMap [ '122227' ] ;
102
172
103
- // it('should return null if the user does not meet audience conditions', function() {
104
- // user = new OptimizelyUserContext({
105
- // shouldIdentifyUser: false,
106
- // optimizely: {},
107
- // userId: 'user3'
108
- // });
109
- // experiment = configObj.experimentIdMap['122227'];
110
- // assert.isNull(
111
- // decisionServiceInstance.getVariation(configObj, experiment, user, { foo: 'bar' }).result
112
- // );
173
+ const { decisionService, logger } = getDecisionService ( { logger : true } ) ;
174
+
175
+ const variation = decisionService . getVariation ( config , experiment , user ) ;
113
176
114
- // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user3']);
177
+ expect ( variation . result ) . toBe ( 'variationWithAudience' ) ;
178
+ expect ( mockBucket ) . not . toHaveBeenCalled ( ) ;
179
+ expect ( logger ?. debug ) . toHaveBeenCalledTimes ( 1 ) ;
180
+ expect ( logger ?. info ) . toHaveBeenCalledTimes ( 1 ) ;
115
181
116
- // assert.deepEqual(mockLogger.debug.args[1], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]);
182
+ expect ( logger ?. debug ) . toHaveBeenNthCalledWith ( 1 , USER_HAS_NO_FORCED_VARIATION , 'user2' ) ;
183
+ expect ( logger ?. info ) . toHaveBeenNthCalledWith ( 1 , USER_FORCED_IN_VARIATION , 'user2' , 'variationWithAudience' ) ;
184
+ } ) ;
117
185
118
- // assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']);
186
+ it ( 'should return null if the user does not meet audience conditions' , ( ) => {
187
+ const user = new OptimizelyUserContext ( {
188
+ optimizely : { } as any ,
189
+ userId : 'user2'
190
+ } ) ;
119
191
120
- // assert.deepEqual(mockLogger.info.args[1], [USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences']);
121
- // });
192
+ const config = createProjectConfig ( cloneDeep ( testData ) ) ;
193
+
194
+ const experiment = config . experimentIdMap [ '122227' ] ;
195
+
196
+ const { decisionService, logger } = getDecisionService ( { logger : true } ) ;
197
+
198
+ const variation = decisionService . getVariation ( config , experiment , user ) ;
199
+
200
+ expect ( variation . result ) . toBe ( 'variationWithAudience' ) ;
201
+ expect ( mockBucket ) . not . toHaveBeenCalled ( ) ;
202
+ expect ( logger ?. debug ) . toHaveBeenCalledTimes ( 1 ) ;
203
+ expect ( logger ?. info ) . toHaveBeenCalledTimes ( 1 ) ;
204
+
205
+ expect ( logger ?. debug ) . toHaveBeenNthCalledWith ( 1 , USER_HAS_NO_FORCED_VARIATION , 'user2' ) ;
206
+ expect ( logger ?. info ) . toHaveBeenNthCalledWith ( 1 , USER_FORCED_IN_VARIATION , 'user2' , 'variationWithAudience' ) ;
207
+
208
+ user = new OptimizelyUserContext ( {
209
+ shouldIdentifyUser : false ,
210
+ optimizely : { } ,
211
+ userId : 'user3'
212
+ } ) ;
213
+ experiment = configObj . experimentIdMap [ '122227' ] ;
214
+ assert . isNull (
215
+ decisionServiceInstance . getVariation ( configObj , experiment , user , { foo : 'bar' } ) . result
216
+ ) ;
217
+
218
+ assert . deepEqual ( mockLogger . debug . args [ 0 ] , [ USER_HAS_NO_FORCED_VARIATION , 'user3' ] ) ;
219
+
220
+ assert . deepEqual ( mockLogger . debug . args [ 1 ] , [ EVALUATING_AUDIENCES_COMBINED , 'experiment' , 'testExperimentWithAudiences' , JSON . stringify ( [ "11154" ] ) ] ) ;
221
+
222
+ assert . deepEqual ( mockLogger . info . args [ 0 ] , [ AUDIENCE_EVALUATION_RESULT_COMBINED , 'experiment' , 'testExperimentWithAudiences' , 'FALSE' ] ) ;
223
+
224
+ assert . deepEqual ( mockLogger . info . args [ 1 ] , [ USER_NOT_IN_EXPERIMENT , 'user3' , 'testExperimentWithAudiences' ] ) ;
225
+ } ) ;
122
226
123
227
// it('should return null if the experiment is not running', function() {
124
228
// user = new OptimizelyUserContext({
0 commit comments