Skip to content

Commit dcdd9de

Browse files
committed
cmab service: init
1 parent f4e3a1f commit dcdd9de

File tree

4 files changed

+535
-1
lines changed

4 files changed

+535
-1
lines changed
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
import { describe, it, expect, vi, Mocked, Mock, MockInstance, beforeEach, afterEach } from 'vitest';
2+
3+
import { DefaultCmabService } from './cmab_service';
4+
import { getMockSyncCache } from '../../../tests/mock/mock_cache';
5+
import { ProjectConfig } from '../../../project_config/project_config';
6+
import { OptimizelyDecideOption, UserAttributes } from '../../../shared_types';
7+
import OptimizelyUserContext from '../../../optimizely_user_context';
8+
import { validate as uuidValidate } from 'uuid';
9+
10+
const mockProjectConfig = (): ProjectConfig => ({
11+
experimentIdMap: {
12+
'1234': {
13+
id: '1234',
14+
key: 'cmab_1',
15+
cmab: {
16+
attributeIds: ['66', '77', '88'],
17+
}
18+
},
19+
'5678': {
20+
id: '5678',
21+
key: 'cmab_2',
22+
cmab: {
23+
attributeIds: ['66', '99'],
24+
}
25+
},
26+
},
27+
attributeKeyMap: {
28+
'country': {
29+
id: '66',
30+
},
31+
'age': {
32+
id: '77',
33+
},
34+
'language': {
35+
id: '88',
36+
},
37+
'gender': {
38+
id: '99',
39+
},
40+
}
41+
} as any);
42+
43+
const mockUserContext = (userId: string, attributes: UserAttributes): OptimizelyUserContext => new OptimizelyUserContext({
44+
userId,
45+
attributes,
46+
} as any);
47+
48+
describe('DefaultCmabService', () => {
49+
it('should fetch and return the variation from cmabClient using correct parameters', async () => {
50+
const mockCmabClient = {
51+
fetchVariation: vi.fn().mockResolvedValue('123'),
52+
};
53+
54+
const cmabService = new DefaultCmabService({
55+
cmabCache: getMockSyncCache(),
56+
cmabClient: mockCmabClient,
57+
});
58+
59+
const projectConfig = mockProjectConfig();
60+
const userContext = mockUserContext('user123', {
61+
country: 'US',
62+
age: '25',
63+
gender: 'male',
64+
});
65+
66+
const experimentId = '1234';
67+
const variation = await cmabService.getVariation(projectConfig, userContext, experimentId, []);
68+
69+
expect(variation.variationId).toEqual('123');
70+
expect(uuidValidate(variation.cmabUuid)).toBe(true);
71+
72+
expect(mockCmabClient.fetchVariation).toHaveBeenCalledOnce();
73+
const [experimentIdArg, userIdArg, attributesArg, cmabUuidArg] = mockCmabClient.fetchVariation.mock.calls[0];
74+
expect(experimentIdArg).toEqual(experimentId);
75+
expect(userIdArg).toEqual(userContext.getUserId());
76+
expect(attributesArg).toEqual({
77+
country: 'US',
78+
age: '25',
79+
});
80+
});
81+
82+
it('should filter attributes based on experiment cmab attributeIds before fetching variation', async () => {
83+
const mockCmabClient = {
84+
fetchVariation: vi.fn().mockResolvedValue('123'),
85+
};
86+
87+
const cmabService = new DefaultCmabService({
88+
cmabCache: getMockSyncCache(),
89+
cmabClient: mockCmabClient,
90+
});
91+
92+
const projectConfig = mockProjectConfig();
93+
const userContext = mockUserContext('user123', {
94+
country: 'US',
95+
age: '25',
96+
language: 'en',
97+
gender: 'male'
98+
});
99+
100+
await cmabService.getVariation(projectConfig, userContext, '1234', []);
101+
await cmabService.getVariation(projectConfig, userContext, '5678', []);
102+
103+
expect(mockCmabClient.fetchVariation).toHaveBeenCalledTimes(2);
104+
expect(mockCmabClient.fetchVariation.mock.calls[0][2]).toEqual({
105+
country: 'US',
106+
age: '25',
107+
language: 'en',
108+
});
109+
expect(mockCmabClient.fetchVariation.mock.calls[1][2]).toEqual({
110+
country: 'US',
111+
gender: 'male'
112+
});
113+
});
114+
115+
it('should cache the variation and return the same variation if relevant attributes have not changed', async () => {
116+
const mockCmabClient = {
117+
fetchVariation: vi.fn().mockResolvedValueOnce('123')
118+
.mockResolvedValueOnce('456')
119+
.mockResolvedValueOnce('789'),
120+
};
121+
122+
const cmabService = new DefaultCmabService({
123+
cmabCache: getMockSyncCache(),
124+
cmabClient: mockCmabClient,
125+
});
126+
127+
const projectConfig = mockProjectConfig();
128+
const userContext11 = mockUserContext('user123', {
129+
country: 'US',
130+
age: '25',
131+
language: 'en',
132+
gender: 'male'
133+
});
134+
135+
const variation11 = await cmabService.getVariation(projectConfig, userContext11, '1234', []);
136+
137+
const userContext12 = mockUserContext('user123', {
138+
country: 'US',
139+
age: '25',
140+
language: 'en',
141+
gender: 'female'
142+
});
143+
144+
const variation12 = await cmabService.getVariation(projectConfig, userContext12, '1234', []);
145+
expect(variation11.variationId).toEqual('123');
146+
expect(variation12.variationId).toEqual('123');
147+
expect(variation11.cmabUuid).toEqual(variation12.cmabUuid);
148+
149+
expect(mockCmabClient.fetchVariation).toHaveBeenCalledTimes(1);
150+
151+
const userContext21 = mockUserContext('user456', {
152+
country: 'BD',
153+
age: '30',
154+
});
155+
156+
const variation21 = await cmabService.getVariation(projectConfig, userContext21, '5678', []);
157+
158+
const userContext22 = mockUserContext('user456', {
159+
country: 'BD',
160+
age: '35',
161+
});
162+
163+
const variation22 = await cmabService.getVariation(projectConfig, userContext22, '5678', []);
164+
expect(variation21.variationId).toEqual('456');
165+
expect(variation22.variationId).toEqual('456');
166+
expect(variation21.cmabUuid).toEqual(variation22.cmabUuid);
167+
168+
expect(mockCmabClient.fetchVariation).toHaveBeenCalledTimes(2);
169+
});
170+
171+
it('should not mix up the cache between different experiments', async () => {
172+
const mockCmabClient = {
173+
fetchVariation: vi.fn().mockResolvedValueOnce('123')
174+
.mockResolvedValueOnce('456')
175+
.mockResolvedValueOnce('789'),
176+
};
177+
178+
const cmabService = new DefaultCmabService({
179+
cmabCache: getMockSyncCache(),
180+
cmabClient: mockCmabClient,
181+
});
182+
183+
const projectConfig = mockProjectConfig();
184+
const userContext = mockUserContext('user123', {
185+
country: 'US',
186+
age: '25',
187+
});
188+
189+
const variation1 = await cmabService.getVariation(projectConfig, userContext, '1234', []);
190+
191+
const variation2 = await cmabService.getVariation(projectConfig, userContext, '5678', []);
192+
193+
expect(variation1.variationId).toEqual('123');
194+
expect(variation2.variationId).toEqual('456');
195+
expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid);
196+
});
197+
198+
it('should not mix up the cache between different users', async () => {
199+
const mockCmabClient = {
200+
fetchVariation: vi.fn().mockResolvedValueOnce('123')
201+
.mockResolvedValueOnce('456')
202+
.mockResolvedValueOnce('789'),
203+
};
204+
205+
const cmabService = new DefaultCmabService({
206+
cmabCache: getMockSyncCache(),
207+
cmabClient: mockCmabClient,
208+
});
209+
210+
const projectConfig = mockProjectConfig();
211+
212+
const userContext1 = mockUserContext('user123', {
213+
country: 'US',
214+
age: '25',
215+
});
216+
217+
const userContext2 = mockUserContext('user456', {
218+
country: 'US',
219+
age: '25',
220+
});
221+
222+
const variation1 = await cmabService.getVariation(projectConfig, userContext1, '1234', []);
223+
224+
const variation2 = await cmabService.getVariation(projectConfig, userContext2, '1234', []);
225+
expect(variation1.variationId).toEqual('123');
226+
expect(variation2.variationId).toEqual('456');
227+
expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid);
228+
229+
expect(mockCmabClient.fetchVariation).toHaveBeenCalledTimes(2);
230+
});
231+
232+
it('should invalidate the cache and fetch a new variation if relevant attributes have changed', async () => {
233+
const mockCmabClient = {
234+
fetchVariation: vi.fn().mockResolvedValueOnce('123')
235+
.mockResolvedValueOnce('456'),
236+
};
237+
238+
const cmabService = new DefaultCmabService({
239+
cmabCache: getMockSyncCache(),
240+
cmabClient: mockCmabClient,
241+
});
242+
243+
const projectConfig = mockProjectConfig();
244+
const userContext1 = mockUserContext('user123', {
245+
country: 'US',
246+
age: '25',
247+
language: 'en',
248+
gender: 'male'
249+
});
250+
251+
const variation1 = await cmabService.getVariation(projectConfig, userContext1, '1234', []);
252+
253+
const userContext2 = mockUserContext('user123', {
254+
country: 'US',
255+
age: '50',
256+
language: 'en',
257+
gender: 'male'
258+
});
259+
260+
const variation2 = await cmabService.getVariation(projectConfig, userContext2, '1234', []);
261+
expect(variation1.variationId).toEqual('123');
262+
expect(variation2.variationId).toEqual('456');
263+
expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid);
264+
265+
expect(mockCmabClient.fetchVariation).toHaveBeenCalledTimes(2);
266+
});
267+
268+
it('should ignore the cache and fetch variation if IGNORE_CMAB_CACHE option is provided', async () => {
269+
const mockCmabClient = {
270+
fetchVariation: vi.fn().mockResolvedValueOnce('123')
271+
.mockResolvedValueOnce('456'),
272+
};
273+
274+
const cmabService = new DefaultCmabService({
275+
cmabCache: getMockSyncCache(),
276+
cmabClient: mockCmabClient,
277+
});
278+
279+
const projectConfig = mockProjectConfig();
280+
const userContext = mockUserContext('user123', {
281+
country: 'US',
282+
age: '25',
283+
language: 'en',
284+
gender: 'male'
285+
});
286+
287+
const variation1 = await cmabService.getVariation(projectConfig, userContext, '1234', []);
288+
289+
const variation2 = await cmabService.getVariation(projectConfig, userContext, '1234', [
290+
OptimizelyDecideOption.IGNORE_CMAB_CACHE,
291+
]);
292+
293+
const variation3 = await cmabService.getVariation(projectConfig, userContext, '1234', []);
294+
295+
expect(variation1.variationId).toEqual('123');
296+
expect(variation2.variationId).toEqual('456');
297+
expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid);
298+
299+
expect(variation3.variationId).toEqual('123');
300+
expect(variation3.cmabUuid).toEqual(variation1.cmabUuid);
301+
302+
expect(mockCmabClient.fetchVariation).toHaveBeenCalledTimes(2);
303+
});
304+
305+
it('should reset the cache before fetching variation if RESET_CMAB_CACHE option is provided', async () => {
306+
const mockCmabClient = {
307+
fetchVariation: vi.fn().mockResolvedValueOnce('123')
308+
.mockResolvedValueOnce('456')
309+
.mockResolvedValueOnce('789')
310+
.mockResolvedValueOnce('101112'),
311+
};
312+
313+
const cmabService = new DefaultCmabService({
314+
cmabCache: getMockSyncCache(),
315+
cmabClient: mockCmabClient,
316+
});
317+
318+
const projectConfig = mockProjectConfig();
319+
const userContext1 = mockUserContext('user123', {
320+
country: 'US',
321+
age: '25'
322+
});
323+
324+
const userContext2 = mockUserContext('user456', {
325+
country: 'US',
326+
age: '50'
327+
});
328+
329+
const variation1 = await cmabService.getVariation(projectConfig, userContext1, '1234', []);
330+
expect(variation1.variationId).toEqual('123');
331+
332+
const variation2 = await cmabService.getVariation(projectConfig, userContext2, '1234', []);
333+
expect(variation2.variationId).toEqual('456');
334+
335+
const variation3 = await cmabService.getVariation(projectConfig, userContext1, '1234', [
336+
OptimizelyDecideOption.RESET_CMAB_CACHE,
337+
]);
338+
expect(variation3.variationId).toEqual('789');
339+
340+
const variation4 = await cmabService.getVariation(projectConfig, userContext2, '1234', []);
341+
expect(variation4.variationId).toEqual('101112');
342+
});
343+
344+
it('should invalidate the cache and fetch a new variation if INVALIDATE_USER_CMAB_CACHE option is provided', async () => {
345+
const mockCmabClient = {
346+
fetchVariation: vi.fn().mockResolvedValueOnce('123')
347+
.mockResolvedValueOnce('456'),
348+
};
349+
350+
const cmabService = new DefaultCmabService({
351+
cmabCache: getMockSyncCache(),
352+
cmabClient: mockCmabClient,
353+
});
354+
355+
const projectConfig = mockProjectConfig();
356+
const userContext = mockUserContext('user123', {
357+
country: 'US',
358+
age: '25',
359+
language: 'en',
360+
gender: 'male'
361+
});
362+
363+
const variation1 = await cmabService.getVariation(projectConfig, userContext, '1234', []);
364+
365+
const variation2 = await cmabService.getVariation(projectConfig, userContext, '1234', [
366+
OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE,
367+
]);
368+
369+
const variation3 = await cmabService.getVariation(projectConfig, userContext, '1234', []);
370+
371+
expect(variation1.variationId).toEqual('123');
372+
expect(variation2.variationId).toEqual('456');
373+
expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid);
374+
expect(variation3.variationId).toEqual('456');
375+
expect(variation2.cmabUuid).toEqual(variation3.cmabUuid);
376+
377+
expect(mockCmabClient.fetchVariation).toHaveBeenCalledTimes(2);
378+
});
379+
});

0 commit comments

Comments
 (0)