Skip to content

Commit 0fd1d58

Browse files
Merge pull request #813 from splitio/SDK-8407_e2e_tests
Large Segments tests: readiness and streaming mode
2 parents 64d1cbc + cbcd0c9 commit 0fd1d58

11 files changed

+283
-22
lines changed

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio",
3-
"version": "10.27.0",
3+
"version": "10.27.1-rc.0",
44
"description": "Split SDK",
55
"files": [
66
"README.md",
@@ -40,7 +40,7 @@
4040
"node": ">=6"
4141
},
4242
"dependencies": {
43-
"@splitsoftware/splitio-commons": "1.16.1-rc.3",
43+
"@splitsoftware/splitio-commons": "1.16.1-rc.5",
4444
"@types/google.analytics": "0.0.40",
4545
"@types/ioredis": "^4.28.0",
4646
"bloom-filters": "^3.0.0",

src/__tests__/browserSuites/push-synchronization.spec.js

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import unboundedMessage from '../mocks/message.V2.UNBOUNDED.1457552650000.json';
1717
import boundedZlibMessage from '../mocks/message.V2.BOUNDED.ZLIB.1457552651000.json';
1818
import keylistGzipMessage from '../mocks/message.V2.KEYLIST.GZIP.1457552652000.json';
1919
import segmentRemovalMessage from '../mocks/message.V2.SEGMENT_REMOVAL.1457552653000.json';
20+
import unboundedMyLargeSegmentsMessage from '../mocks/message.MY_LARGE_SEGMENTS_UPDATE.UNBOUNDED.1457552650000.json';
21+
import myLargeSegmentRemovalMessage from '../mocks/message.MY_LARGE_SEGMENTS_UPDATE.SEGMENT_REMOVAL.1457552653000.json';
2022

2123
import authPushEnabledNicolas from '../mocks/auth.pushEnabled.nicolas@split.io.json';
2224
import authPushEnabledNicolasAndMarcio from '../mocks/auth.pushEnabled.nicolas@split.io.marcio@split.io.json';
@@ -49,6 +51,9 @@ const config = {
4951
},
5052
urls: baseUrls,
5153
streamingEnabled: true,
54+
sync: {
55+
largeSegmentsEnabled: true
56+
}
5257
};
5358
const settings = settingsFactory(config);
5459

@@ -68,17 +73,19 @@ const MILLIS_KEYLIST_FALLBACK = 1300;
6873
const MILLIS_BOUNDED = 1400;
6974
const MILLIS_KEYLIST = 1500;
7075
const MILLIS_SEGMENT_REMOVAL = 1600;
76+
const MILLIS_UNBOUNDED_FETCH_LS = 1700;
77+
const MILLIS_SEGMENT_REMOVAL_LS = 2100;
7178

7279
/**
7380
* Sequence of calls:
7481
* 0.0 secs: initial SyncAll (/splitChanges, /mySegments/*), auth, SSE connection
75-
* 0.1 secs: SSE connection opened -> syncAll (/splitChanges, /mySegments/*)
82+
* 0.1 secs: SSE connection opened -> syncAll (/splitChanges, /mySegments/*, /myLargeSegments/*)
7683
* 0.2 secs: SPLIT_UPDATE event -> /splitChanges
7784
* 0.3 secs: SPLIT_UPDATE event with old changeNumber
7885
* 0.4 secs: MY_SEGMENTS_UPDATE event -> /mySegments/nicolas@split.io
7986
* 0.5 secs: SPLIT_KILL event -> /splitChanges
8087
* 0.6 secs: creates a new client -> new auth and SSE connection
81-
* 0.7 secs: SSE connection opened -> syncAll (/splitChanges, /mySegments/*)
88+
* 0.7 secs: SSE connection opened -> syncAll (/splitChanges, /mySegments/*, /myLargeSegments/*)
8289
* 0.8 secs: MY_SEGMENTS_UPDATE event for new client (with payload).
8390
* 0.9 secs: MY_SEGMENTS_UPDATE event for new client (with empty payload).
8491
* 1.0 secs: creates more clients
@@ -88,9 +95,12 @@ const MILLIS_SEGMENT_REMOVAL = 1600;
8895
* 1.4 secs: MY_SEGMENTS_UPDATE_V2 BoundedFetchRequest event.
8996
* 1.5 secs: MY_SEGMENTS_UPDATE_V2 KeyList event.
9097
* 1.6 secs: MY_SEGMENTS_UPDATE_V2 SegmentRemoval event.
98+
* 1.7 secs: MY_LARGE_SEGMENTS_UPDATE UnboundedFetchRequest event, with 241 ms delay for 'nicolas@split.io' (hash('nicolas@split.io') % 300)
99+
* 1.941 secs: /myLargeSegments/* fetch due to unbounded MY_LARGE_SEGMENTS_UPDATE event -> SPLIT_UPDATE event
100+
* 2.1 secs: MY_LARGE_SEGMENTS_UPDATE SegmentRemoval event -> SPLIT_UPDATE event
91101
*/
92102
export function testSynchronization(fetchMock, assert) {
93-
assert.plan(38);
103+
assert.plan(44);
94104
fetchMock.reset();
95105

96106
let start, splitio, client, otherClient, keylistAddClient, keylistRemoveClient, bitmapTrueClient, sharedClients = [];
@@ -236,6 +246,31 @@ export function testSynchronization(fetchMock, assert) {
236246
assert.deepEqual(sharedClients.map(c => c.getTreatment('splitters')), ['off', 'on', 'off', 'on'], 'evaluation before segment removal');
237247
bitmapTrueClient.once(bitmapTrueClient.Event.SDK_UPDATE, () => {
238248
assert.deepEqual(sharedClients.map(c => c.getTreatment('splitters')), ['off', 'off', 'off', 'off'], 'evaluation after segment removal');
249+
});
250+
251+
eventSourceInstance.emitMessage(segmentRemovalMessage);
252+
}, MILLIS_SEGMENT_REMOVAL - MILLIS_MORE_CLIENTS);
253+
254+
setTimeout(() => {
255+
assert.equal(client.getTreatment('in_large_segment'), 'no', 'evaluation before myLargeSegment fetch');
256+
257+
const timestampUnboundEvent = Date.now();
258+
const EXPECTED_DELAY = 241;
259+
260+
client.once(client.Event.SDK_UPDATE, () => {
261+
assert.true(nearlyEqual(Date.now() - timestampUnboundEvent, EXPECTED_DELAY), 'SDK_UPDATE after fetching myLargeSegments with a delay');
262+
assert.equal(client.getTreatment('in_large_segment'), 'yes', 'evaluation after myLargeSegment fetch');
263+
});
264+
265+
eventSourceInstance.emitMessage(unboundedMyLargeSegmentsMessage);
266+
}, MILLIS_UNBOUNDED_FETCH_LS - MILLIS_MORE_CLIENTS);
267+
268+
setTimeout(() => {
269+
assert.equal(client.getTreatment('in_large_segment'), 'yes', 'evaluation before large segment removal');
270+
assert.deepEqual(sharedClients.map(c => c.getTreatment('in_large_segment')), ['no', 'no', 'no', 'no'], 'evaluation before segment removal');
271+
272+
client.once(client.Event.SDK_UPDATE, () => {
273+
assert.equal(client.getTreatment('in_large_segment'), 'no', 'evaluation after large segment removal');
239274

240275
// destroy shared clients and then main client
241276
Promise.all(sharedClients.map(c => c.destroy()))
@@ -252,8 +287,8 @@ export function testSynchronization(fetchMock, assert) {
252287
});
253288
});
254289

255-
eventSourceInstance.emitMessage(segmentRemovalMessage);
256-
}, MILLIS_SEGMENT_REMOVAL - MILLIS_MORE_CLIENTS);
290+
eventSourceInstance.emitMessage(myLargeSegmentRemovalMessage);
291+
}, MILLIS_SEGMENT_REMOVAL_LS - MILLIS_MORE_CLIENTS);
257292
});
258293
}, MILLIS_MORE_CLIENTS - MILLIS_NEW_CLIENT);
259294

@@ -283,7 +318,7 @@ export function testSynchronization(fetchMock, assert) {
283318
authParams += `&users=${encodeURIComponent(keylistAddKey)}&users=${encodeURIComponent(keylistRemoveKey)}&users=${encodeURIComponent(bitmapTrueKey)}`;
284319
fetchMock.getOnce(url(settings, `/v2/auth?s=1.1&${authParams}`), { status: 200, body: authPushEnabledNicolasAndMarcio });
285320

286-
// initial split and mySegments sync
321+
// initial sync
287322
fetchMock.getOnce(url(settings, '/splitChanges?s=1.1&since=-1'), function (url, opts) {
288323
const lapse = Date.now() - start;
289324
assert.true(nearlyEqual(lapse, 0), 'initial sync');
@@ -294,6 +329,7 @@ export function testSynchronization(fetchMock, assert) {
294329
if (hasNoCacheHeader(opts)) assert.fail('request must not include `Cache-Control` header');
295330
return { status: 200, body: mySegmentsNicolasMock1 };
296331
});
332+
fetchMock.getOnce(url(settings, '/myLargeSegments/nicolas%40split.io'), { status: 200, body: { myLargeSegments: [] } });
297333

298334
// split and segment sync after SSE opened
299335
fetchMock.getOnce(url(settings, '/splitChanges?s=1.1&since=1457552620999'), function (url, opts) {
@@ -306,6 +342,7 @@ export function testSynchronization(fetchMock, assert) {
306342
if (hasNoCacheHeader(opts)) assert.fail('request must not include `Cache-Control` header');
307343
return { status: 200, body: mySegmentsNicolasMock1 };
308344
});
345+
fetchMock.getOnce(url(settings, '/myLargeSegments/nicolas%40split.io'), { status: 200, body: { myLargeSegments: [] } });
309346

310347
// fetch due to SPLIT_UPDATE event
311348
fetchMock.getOnce(url(settings, '/splitChanges?s=1.1&since=1457552620999'), function (url, opts) {
@@ -326,13 +363,15 @@ export function testSynchronization(fetchMock, assert) {
326363
return { status: 200, body: splitChangesMock4 };
327364
});
328365

329-
// initial fetch of mySegments for new client
366+
// initial fetch of mySegments and myLargeSegments for new client
330367
fetchMock.getOnce(url(settings, '/mySegments/marcio%40split.io'), function (url, opts) {
331368
if (hasNoCacheHeader(opts)) assert.fail('request must not include `Cache-Control` header');
332369
return { status: 200, body: mySegmentsMarcio };
333370
});
371+
fetchMock.getOnce(url(settings, '/myLargeSegments/marcio%40split.io'), { status: 200, body: { myLargeSegments: [] } });
372+
334373

335-
// split and mySegment sync after second SSE opened
374+
// sync after second SSE opened
336375
fetchMock.getOnce(url(settings, '/splitChanges?s=1.1&since=1457552650000'), function (url, opts) {
337376
const lapse = Date.now() - start;
338377
assert.true(nearlyEqual(lapse, MILLIS_SECOND_SSE_OPEN), 'sync after second SSE connection is opened');
@@ -347,6 +386,9 @@ export function testSynchronization(fetchMock, assert) {
347386
if (hasNoCacheHeader(opts)) assert.fail('request must not include `Cache-Control` header');
348387
return { status: 200, body: mySegmentsMarcio };
349388
});
389+
fetchMock.get({ url: url(settings, '/myLargeSegments/nicolas%40split.io'), repeat: 2 }, { status: 200, body: { myLargeSegments: [] } });
390+
fetchMock.get({ url: url(settings, '/myLargeSegments/marcio%40split.io'), repeat: 2 }, { status: 200, body: { myLargeSegments: [] } });
391+
350392
// 3 unbounded fetch requests
351393
fetchMock.get({ url: url(settings, '/mySegments/nicolas%40split.io'), repeat: 3 }, function (url, opts) {
352394
if (!hasNoCacheHeader(opts)) assert.fail('request must not include `Cache-Control` header');
@@ -357,15 +399,25 @@ export function testSynchronization(fetchMock, assert) {
357399
return { status: 200, body: mySegmentsMarcio };
358400
});
359401

360-
// initial fetch of mySegments for other clients + sync after third SSE opened + 3 unbounded fetch requests
402+
// initial fetch of mySegments and myLargeSegments for other clients + sync after third SSE opened + 3 unbounded fetch requests for mySegments
361403
fetchMock.getOnce(url(settings, '/splitChanges?s=1.1&since=1457552650000'), { status: 200, body: { splits: [], since: 1457552650000, till: 1457552650000 } });
362404
fetchMock.get({ url: url(settings, '/mySegments/key1'), repeat: 5 }, { status: 200, body: { mySegments: [] } });
363405
fetchMock.get({ url: url(settings, '/mySegments/key3'), repeat: 5 }, { status: 200, body: { mySegments: [{ name: 'splitters' }] } });
364406
fetchMock.get({ url: url(settings, `/mySegments/${bitmapTrueKey}`), repeat: 5 }, { status: 200, body: { mySegments: [] } });
407+
fetchMock.get({ url: url(settings, '/myLargeSegments/key1'), repeat: 2 }, { status: 200, body: { myLargeSegments: [] } });
408+
fetchMock.get({ url: url(settings, '/myLargeSegments/key3'), repeat: 2 }, { status: 200, body: { myLargeSegments: [] } });
409+
fetchMock.get({ url: url(settings, `/myLargeSegments/${bitmapTrueKey}`), repeat: 2 }, { status: 200, body: { myLargeSegments: [] } });
365410

366411
// bounded fetch request
367412
fetchMock.get(url(settings, `/mySegments/${bitmapTrueKey}`), { status: 200, body: { mySegments: [{ name: 'splitters' }] } });
368413

414+
// unbounded myLargeSegments fetch requests
415+
fetchMock.getOnce(url(settings, '/myLargeSegments/nicolas%40split.io'), { status: 200, body: { myLargeSegments: ['employees', 'splitters'] } });
416+
fetchMock.getOnce(url(settings, '/myLargeSegments/marcio%40split.io'), { status: 200, body: { myLargeSegments: [] } });
417+
fetchMock.getOnce(url(settings, '/myLargeSegments/key1'), { status: 200, body: { myLargeSegments: [] } });
418+
fetchMock.getOnce(url(settings, '/myLargeSegments/key3'), { status: 200, body: { myLargeSegments: [] } });
419+
fetchMock.getOnce(url(settings, `/myLargeSegments/${bitmapTrueKey}`), { status: 200, body: { myLargeSegments: [] } });
420+
369421
fetchMock.get(new RegExp('.*'), function (url) {
370422
assert.fail('unexpected GET request with url: ' + url);
371423
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { SplitFactory } from '../../';
2+
3+
// Mocks
4+
import mySegments from '../mocks/mysegments.nicolas@split.io.json';
5+
import myLargeSegments from '../mocks/mylargesegments.employees.json';
6+
import { nearlyEqual } from '../testUtils';
7+
8+
const FF = {
9+
name: 'FF',
10+
status: 'ACTIVE',
11+
conditions: [{
12+
matcherGroup: {
13+
combiner: 'AND',
14+
matchers: []
15+
}
16+
}]
17+
};
18+
19+
const FF_WITH_SEGMENTS = {
20+
name: 'FF_WITH_SEGMENTS',
21+
status: 'ACTIVE',
22+
conditions: [{
23+
matcherGroup: {
24+
combiner: 'AND',
25+
matchers: [{
26+
matcherType: 'IN_SEGMENT',
27+
userDefinedSegmentMatcherData: {
28+
segmentName: 'A'
29+
}
30+
}]
31+
}
32+
}]
33+
};
34+
35+
const FF_WITH_LARGE_SEGMENTS = {
36+
name: 'FF_WITH_LARGE_SEGMENTS',
37+
status: 'ACTIVE',
38+
conditions: [{
39+
matcherGroup: {
40+
combiner: 'AND',
41+
matchers: [{
42+
matcherType: 'IN_LARGE_SEGMENT',
43+
userDefinedSegmentMatcherData: {
44+
segmentName: 'A'
45+
}
46+
}]
47+
}
48+
}]
49+
};
50+
51+
const waitConfig = {
52+
core: {
53+
authorizationKey: '<fake-token>',
54+
key: 'emi@split.io'
55+
},
56+
urls: {
57+
sdk: 'https://sdk.baseurl/largeSegments',
58+
},
59+
sync: {
60+
largeSegmentsEnabled: true
61+
},
62+
streamingEnabled: false
63+
};
64+
65+
const noWaitConfig = {
66+
...waitConfig,
67+
startup: {
68+
waitForLargeSegments: false
69+
}
70+
};
71+
72+
const SEGMENTS_DELAY = 50;
73+
const LARGE_SEGMENTS_DELAY = 100;
74+
const TEST_END_DELAY = 150;
75+
76+
export default function (fetchMock, assert) {
77+
78+
const testCases = [
79+
{ waitForLargeSegments: true, featureFlagsWithSegments: true, featureFlagsWithLS: true },
80+
{ waitForLargeSegments: true, featureFlagsWithSegments: true, featureFlagsWithLS: false },
81+
{ waitForLargeSegments: true, featureFlagsWithSegments: false, featureFlagsWithLS: true },
82+
{ waitForLargeSegments: true, featureFlagsWithSegments: false, featureFlagsWithLS: false },
83+
{ waitForLargeSegments: false, featureFlagsWithSegments: true, featureFlagsWithLS: true },
84+
{ waitForLargeSegments: false, featureFlagsWithSegments: true, featureFlagsWithLS: false },
85+
{ waitForLargeSegments: false, featureFlagsWithSegments: false, featureFlagsWithLS: true },
86+
{ waitForLargeSegments: false, featureFlagsWithSegments: false, featureFlagsWithLS: false },
87+
88+
// Special cases where large segments are not supported for the given SDK key: `/myLargeSegments/*` responds with 403 and there cannot be FFs with large segments
89+
{ waitForLargeSegments: true, featureFlagsWithSegments: true, featureFlagsWithLS: false, myLargeSegmentsForbidden: true },
90+
{ waitForLargeSegments: false, featureFlagsWithSegments: true, featureFlagsWithLS: false, myLargeSegmentsForbidden: true },
91+
];
92+
93+
testCases.forEach(({ waitForLargeSegments, featureFlagsWithSegments, featureFlagsWithLS, myLargeSegmentsForbidden }) => {
94+
95+
const config = waitForLargeSegments ? waitConfig : noWaitConfig;
96+
97+
const splitChangesMock = {
98+
since: -1,
99+
till: 1457552620999,
100+
splits: [FF, featureFlagsWithSegments && FF_WITH_SEGMENTS, featureFlagsWithLS && FF_WITH_LARGE_SEGMENTS].filter(ff => ff)
101+
};
102+
103+
// smart ready: if FFs are not using segments (or LS) we don't need to wait for them
104+
const SDK_READY_DELAY = Math.max(
105+
featureFlagsWithSegments ? SEGMENTS_DELAY : 0,
106+
featureFlagsWithLS && waitForLargeSegments ? LARGE_SEGMENTS_DELAY : 0
107+
);
108+
109+
// emit SDK_UPDATE if large segments arrive after SDK_READY event is emitted and FFs are using them
110+
const shouldEmitSdkUpdate = waitForLargeSegments === false && featureFlagsWithLS === true && (LARGE_SEGMENTS_DELAY > SEGMENTS_DELAY || featureFlagsWithSegments === false);
111+
112+
assert.test(t => {
113+
fetchMock.getOnce(config.urls.sdk + '/splitChanges?s=1.1&since=-1', { status: 200, body: splitChangesMock });
114+
fetchMock.getOnce(config.urls.sdk + '/splitChanges?s=1.1&since=1457552620999', { status: 200, body: { since: 1457552620999, till: 1457552620999, splits: [] } });
115+
fetchMock.getOnce(config.urls.sdk + '/mySegments/emi%40split.io', { status: 200, body: mySegments }, { delay: SEGMENTS_DELAY });
116+
fetchMock.getOnce(config.urls.sdk + '/myLargeSegments/emi%40split.io', { status: myLargeSegmentsForbidden ? 403 : 200, body: myLargeSegments }, { delay: LARGE_SEGMENTS_DELAY });
117+
118+
// smart pausing: if FFs are not using segments (or LS) we don't need to fetch them
119+
if (featureFlagsWithSegments) fetchMock.getOnce(config.urls.sdk + '/mySegments/shared', { status: 200, body: mySegments }, { delay: SEGMENTS_DELAY });
120+
if (featureFlagsWithLS) fetchMock.getOnce(config.urls.sdk + '/myLargeSegments/shared', { status: myLargeSegmentsForbidden ? 403 : 200, body: myLargeSegments }, { delay: LARGE_SEGMENTS_DELAY });
121+
122+
const splitio = SplitFactory(config);
123+
const client = splitio.client();
124+
125+
const start = Date.now();
126+
client.once(client.Event.SDK_READY, () => {
127+
assert.true(nearlyEqual(Date.now() - start, SDK_READY_DELAY));
128+
129+
splitio.client('shared').ready().then(() => {
130+
assert.true(nearlyEqual(Date.now() - start, 2 * SDK_READY_DELAY));
131+
});
132+
});
133+
134+
let updateEmitted = false;
135+
136+
client.once(client.Event.SDK_UPDATE, () => {
137+
assert.true(nearlyEqual(Date.now() - start, LARGE_SEGMENTS_DELAY));
138+
updateEmitted = true;
139+
});
140+
141+
setTimeout(() => {
142+
assert.true(updateEmitted === shouldEmitSdkUpdate);
143+
client.destroy().then(() => { t.end(); });
144+
}, TEST_END_DELAY);
145+
});
146+
147+
});
148+
149+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "message",
3+
"data": "{\"data\":\"{\\\"type\\\":\\\"MY_LARGE_SEGMENTS_UPDATE\\\",\\\"changeNumber\\\":1457552653000,\\\"largeSegments\\\":[\\\"harnessians\\\",\\\"splitters\\\"],\\\"c\\\": 0,\\\"u\\\": 3,\\\"d\\\":\\\"\\\"}\"}"
4+
}

0 commit comments

Comments
 (0)