diff --git a/package-lock.json b/package-lock.json index bf1a63fb9..fd1396791 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "10.23.2-rc.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "1.9.2-rc.0", + "@splitsoftware/splitio-commons": "1.9.2-rc.1", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -802,9 +802,9 @@ "dev": true }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.9.2-rc.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.2-rc.0.tgz", - "integrity": "sha512-sKsaIAELQDwA/LUpZflVkkrtBzaghrz1tqnpLV3iVeiXBWK3Im3pEHoMm4bwa3wtoawbInrFbIdN+oIXrCHkRw==", + "version": "1.9.2-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.2-rc.1.tgz", + "integrity": "sha512-a9NYF8gh8QRstWuVwLZ55YxeOwbAWL/dFralfxvRcBX1zVwUfKMvyW0bPKzXUzqTbvxsfWI4PV95xFgbQk/m/Q==", "dependencies": { "tslib": "^2.3.1" }, @@ -7036,9 +7036,9 @@ } }, "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/tty-browserify": { "version": "0.0.1", @@ -8312,9 +8312,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "1.9.2-rc.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.2-rc.0.tgz", - "integrity": "sha512-sKsaIAELQDwA/LUpZflVkkrtBzaghrz1tqnpLV3iVeiXBWK3Im3pEHoMm4bwa3wtoawbInrFbIdN+oIXrCHkRw==", + "version": "1.9.2-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.2-rc.1.tgz", + "integrity": "sha512-a9NYF8gh8QRstWuVwLZ55YxeOwbAWL/dFralfxvRcBX1zVwUfKMvyW0bPKzXUzqTbvxsfWI4PV95xFgbQk/m/Q==", "requires": { "tslib": "^2.3.1" } @@ -13162,9 +13162,9 @@ } }, "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tty-browserify": { "version": "0.0.1", diff --git a/package.json b/package.json index f8780ffa3..ff68ea6e5 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "node": ">=6" }, "dependencies": { - "@splitsoftware/splitio-commons": "1.9.2-rc.0", + "@splitsoftware/splitio-commons": "1.9.2-rc.1", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", diff --git a/src/__tests__/browserSuites/fetch-specific-splits.spec.js b/src/__tests__/browserSuites/fetch-specific-splits.spec.js index 14dc031ea..a15f68b42 100644 --- a/src/__tests__/browserSuites/fetch-specific-splits.spec.js +++ b/src/__tests__/browserSuites/fetch-specific-splits.spec.js @@ -1,3 +1,4 @@ +import sinon from 'sinon'; import { SplitFactory } from '../../'; import { splitFilters, queryStrings, groupedFilters } from '../mocks/fetchSpecificSplits'; @@ -14,7 +15,7 @@ const baseConfig = { export default function fetchSpecificSplits(fetchMock, assert) { - assert.plan(splitFilters.length); + assert.plan(splitFilters.length+1); for (let i = 0; i < splitFilters.length; i++) { const urls = { sdk: 'https://sdkurl' + i }; @@ -45,4 +46,41 @@ export default function fetchSpecificSplits(fetchMock, assert) { } } + + // Flag sets + assert.test(async (t) => { + + const splitFilters = [{ type: 'bySet', values: ['set_x ', 'set_x', 'set_3', 'set_2', 'set_3', 'set_ww', 'invalid+', '_invalid', '4_valid'] }]; + const baseUrls = { sdk: 'https://sdk.baseurl' }; + + const config = { + ...baseConfig, + urls: baseUrls, + debug: 'WARN', + sync: { + splitFilters + } + }; + + const logSpy = sinon.spy(console, 'log'); + + fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } }); + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=4_valid,set_2,set_3,set_ww,set_x', function () { + t.pass('flag set query correctly formed'); + return { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }; + }); + + const factory = SplitFactory(config); + + const client = factory.client(); + client.ready().then(() => { + t.true(logSpy.calledWithExactly('[WARN] splitio => settings: bySet filter value "set_x " has extra whitespace, trimming.')); + t.true(logSpy.calledWithExactly('[WARN] splitio => settings: you passed invalid+, flag set must adhere to the regular expressions /^[a-z0-9][_a-z0-9]{0,49}$/. This means a flag set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. invalid+ was discarded.')); + t.true(logSpy.calledWithExactly('[WARN] splitio => settings: you passed _invalid, flag set must adhere to the regular expressions /^[a-z0-9][_a-z0-9]{0,49}$/. This means a flag set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. _invalid was discarded.')); + logSpy.restore(); + factory.client().destroy().then(() => { + t.end(); + }); + }); + }, 'FlagSets config'); } diff --git a/src/__tests__/browserSuites/flag-sets.spec.js b/src/__tests__/browserSuites/flag-sets.spec.js new file mode 100644 index 000000000..cae4fddc1 --- /dev/null +++ b/src/__tests__/browserSuites/flag-sets.spec.js @@ -0,0 +1,204 @@ +import { SplitFactory } from '../..'; + +import splitChange2 from '../mocks/splitChanges.since.-1.till.1602796638344.json'; +import splitChange1 from '../mocks/splitchanges.since.1602796638344.till.1602797638344.json'; +import splitChange0 from '../mocks/splitchanges.since.1602797638344.till.1602798638344.json'; + +const baseUrls = { sdk: 'https://sdk.baseurl' }; + +const baseConfig = { + core: { + authorizationKey: '', + key: 'nicolas@split.io' + }, + urls: baseUrls, + scheduler: { featuresRefreshRate: 0.01 }, + streamingEnabled: false +}; + +export default function flagSets(fetchMock, t) { + fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } }); + + t.test(async (assert) => { + let factory; + let manager; + + // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=set_1,set_2', function () { + return { status: 200, body: splitChange2}; + }); + + // Receive split change with 1 split belonging to set_1 only + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344&sets=set_1,set_2', function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 1, 'only one feature flag should be added'); + assert.true(storedFlags[0].name === 'workm'); + assert.deepEqual(storedFlags[0].sets, ['set_1','set_2']); + + // send split change + return { status: 200, body: splitChange1}; + }); + + // Receive split change with 1 split belonging to set_3 only + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602797638344&sets=set_1,set_2', function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 1); + assert.true(storedFlags[0].name === 'workm'); + assert.deepEqual(storedFlags[0].sets, ['set_1'], 'the feature flag should be updated'); + + // send split change + return { status: 200, body: splitChange0}; + }); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602798638344&sets=set_1,set_2', async function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 0, 'the feature flag should be removed'); + await factory.client().destroy(); + assert.end(); + + return { status: 200, body: {} }; + }); + + // Initialize a factory with polling and sets set_1 & set_2 configured. + const splitFilters = [{ type: 'bySet', values: ['set_1','set_2'] }]; + factory = SplitFactory({ ...baseConfig, sync: { splitFilters }}); + await factory.client().ready(); + manager = factory.manager(); + + }, 'Polling - SDK with sets configured updates flags according to sets'); + + t.test(async (assert) => { + let factory; + let manager; + + // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', function () { + return { status: 200, body: splitChange2}; + }); + + // Receive split change with 1 split belonging to set_1 only + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344', function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 2, 'every feature flag should be added'); + assert.true(storedFlags[0].name === 'workm'); + assert.true(storedFlags[1].name === 'workm_set_3'); + assert.deepEqual(storedFlags[0].sets, ['set_1','set_2']); + assert.deepEqual(storedFlags[1].sets, ['set_3']); + + // send split change + return { status: 200, body: splitChange1}; + }); + + // Receive split change with 1 split belonging to set_3 only + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602797638344', function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 2); + assert.true(storedFlags[0].name === 'workm'); + assert.true(storedFlags[1].name === 'workm_set_3'); + assert.deepEqual(storedFlags[0].sets, ['set_1'], 'the feature flag should be updated'); + assert.deepEqual(storedFlags[1].sets, ['set_3'], 'the feature flag should remain as it was'); + + // send split change + return { status: 200, body: splitChange0}; + }); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602798638344', async function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 2); + assert.true(storedFlags[0].name === 'workm'); + assert.true(storedFlags[1].name === 'workm_set_3'); + assert.deepEqual(storedFlags[0].sets, ['set_3'], 'the feature flag should be updated'); + assert.deepEqual(storedFlags[1].sets, ['set_3'], 'the feature flag should remain as it was'); + await factory.client().destroy(); + assert.end(); + return { status: 200, body: {} }; + }); + + // Initialize a factory with polling and no sets configured. + factory = SplitFactory(baseConfig); + await factory.client().ready(); + manager = factory.manager(); + + }, 'Poling - SDK with no sets configured does not take sets into account when updating flags'); + + // EVALUATION + + t.test(async (assert) => { + fetchMock.reset(); + assert.plan(8); + + let factory, client = []; + + fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } }); + // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=set_1', function () { + return { status: 200, body: splitChange2}; + }); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344&sets=set_1', async function () { + // stored feature flags before update + assert.deepEqual(client.getTreatmentsByFlagSet('set_1'), {workm: 'on'}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet('set_2'), {}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet('set_3'), {}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_1'), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_2'), {}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_3'), {}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSets(['set_1','set_2','set_3']), {workm: 'on'}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(['set_1','set_2','set_3']), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated'); + await client.destroy(); + assert.end(); + + // send split change + return { status: 200, body: splitChange1}; + }); + + // Initialize a factory with set_1 configured. + const splitFilters = [{ type: 'bySet', values: ['set_1'] }]; + factory = SplitFactory({ ...baseConfig, sync: { splitFilters }}); + client = factory.client(); + await client.ready(); + + }, 'SDK with sets configured can only evaluate configured sets'); + + t.test(async (assert) => { + fetchMock.reset(); + assert.plan(8); + + let factory, client = []; + + fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } }); + // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', function () { + return { status: 200, body: splitChange2}; + }); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344', async function () { + // stored feature flags before update + assert.deepEqual(client.getTreatmentsByFlagSet('set_1'), {workm: 'on'}, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet('set_2'), {workm: 'on'}, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet('set_3'), { workm_set_3: 'on' }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_1'), { workm: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_2'), { workm: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_3'), { workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSets(['set_1','set_2','set_3']), { workm: 'on', workm_set_3: 'on' }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(['set_1','set_2','set_3']), { workm: { treatment: 'on', config: null }, workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + await client.destroy(); + assert.end(); + + // send split change + return { status: 200, body: splitChange1}; + }); + + factory = SplitFactory(baseConfig); + client = factory.client(); + await client.ready(); + + }, 'SDK with no sets configured can evaluate any set'); + +} diff --git a/src/__tests__/browserSuites/push-flag-sets.spec.js b/src/__tests__/browserSuites/push-flag-sets.spec.js new file mode 100644 index 000000000..d2d7b2a6f --- /dev/null +++ b/src/__tests__/browserSuites/push-flag-sets.spec.js @@ -0,0 +1,262 @@ +import { SplitFactory } from '../..'; +import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; +window.EventSource = EventSourceMock; + +import notification1 from '../mocks/message.SPLIT_UPDATE.FS.1.json'; +import notification2 from '../mocks/message.SPLIT_UPDATE.FS.2.json'; +import notification3 from '../mocks/message.SPLIT_UPDATE.FS.3.json'; +import notification4None from '../mocks/message.SPLIT_UPDATE.FS.4None.json'; +import notification4 from '../mocks/message.SPLIT_UPDATE.FS.4.json'; +import notification5 from '../mocks/message.SPLIT_UPDATE.FS.5.json'; +import notificationKill from '../mocks/message.SPLIT_UPDATE.FS.kill.json'; + +import authPushEnabled from '../mocks/auth.pushEnabled.nicolas@split.io.json'; + +const baseUrls = { + sdk: 'https://sdk.baseurl', + auth: 'https://auth.baseurl/api' +}; + +const baseConfig = { + core: { + authorizationKey: '', + key: 'nicolas@split.io' + }, + urls: baseUrls, +}; + +const MILLIS_FIRST_SPLIT_UPDATE_EVENT = 100; +const MILLIS_SECOND_SPLIT_UPDATE_EVENT = 200; +const MILLIS_THIRD_SPLIT_UPDATE_EVENT = 300; +const MILLIS_FOURTH_SPLIT_UPDATE_EVENT = 400; + +export function testFlagSets(fetchMock, t) { + fetchMock.reset(); + + fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } }); + + fetchMock.get(baseUrls.auth + '/v2/auth?users=nicolas%40split.io', function () { + return { status: 200, body: authPushEnabled }; + }); + fetchMock.get(baseUrls.sdk + '/splitChanges?since=-1', function () { + return { status: 200, body: { splits: [], since: -1, till: 0}}; + }); + fetchMock.get(baseUrls.sdk + '/splitChanges?since=0', function () { + return { status: 200, body: { splits: [], since: 0, till: 1 }}; + }); + fetchMock.get(baseUrls.sdk + '/splitChanges?since=-1&sets=set_1,set_2', function () { + return { status: 200, body: { splits: [], since: -1, till: 0 }}; + }); + fetchMock.get(baseUrls.sdk + '/splitChanges?since=0&sets=set_1,set_2', function () { + return { status: 200, body: { splits: [], since: 0, till: 1 }}; + }); + + + const configWithSets = { + ...baseConfig, + sync: { + splitFilters: [{type: 'bySet', values: ['set_1', 'set_2']}] + } + }; + + t.test(async (assert) => { + + assert.plan(3); + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '1 - initialized without flags'); + client.once(client.Event.SDK_UPDATE, async () => { + assert.equal(manager.splits().length, 1, '1 - update is processed and the flag is stored'); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '1 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notification1); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(baseConfig); + client = splitio.client(); + manager = splitio.manager(); + + }, 'SDK with no sets configured does not exclude updates'); + + t.test(async (assert) => { + + assert.plan(5); + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '2 - initialized without flags'); + // Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.split('workm').sets, ['set_1', 'set_2'], '2 - update is processed and the flag is stored'); + }); + eventSourceInstance.emitMessage(notification2); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":["set_1"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.split('workm').sets, ['set_1'], '2 - update is processed and the flag is updated'); + }); + eventSourceInstance.emitMessage(notification3); + }, MILLIS_SECOND_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":[] + client.once(client.Event.SDK_UPDATE, async () => { + assert.deepEqual(manager.splits().length, 0, '2 - update is processed and the flag is removed'); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '2 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notification4None); + }, MILLIS_THIRD_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(configWithSets); + client = splitio.client(); + manager = splitio.manager(); + + }, 'SDK with sets configured deletes flag when change with empty sets is received'); + + t.test(async (assert) => { + + assert.plan(6); + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '3 - initialized without flags'); + // Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.split('workm').sets, ['set_1', 'set_2'], '3 - update is processed and the flag is stored'); + }); + eventSourceInstance.emitMessage(notification2); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":["set_1"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.split('workm').sets, ['set_1'], '3 - update is processed and the flag is updated'); + }); + eventSourceInstance.emitMessage(notification3); + }, MILLIS_SECOND_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":["set_3"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.splits().length, 0, '3 - update is processed and the flag is removed'); + }); + eventSourceInstance.emitMessage(notification4); + }, MILLIS_THIRD_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":["set_3", "set_4"] + client.once(client.Event.SDK_UPDATE, async () => { + assert.deepEqual(manager.splits().length, 0, '3 - update is processed and flag is not added to the storage'); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '3 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notification5); + }, MILLIS_FOURTH_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(configWithSets); + client = splitio.client(); + manager = splitio.manager(); + + }, 'SDK with sets configured deletes flag when change with non-matching sets is received'); + + t.test(async (assert) => { + + assert.plan(5); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=2&sets=set_1,set_2', function () { + assert.pass('4 - A fetch is triggered due to the SPLIT_KILL'); + return { status: 200, body: { splits: [], since: 2, till: 3 }}; + }); + + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '4 - initialized without flags'); + // Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + client.once(client.Event.SDK_UPDATE, () => { + assert.equal(manager.split('workm').killed, false, '4 - update is processed and the flag is stored'); + }); + eventSourceInstance.emitMessage(notification2); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_KILL for flag + client.once(client.Event.SDK_UPDATE, async () => { + assert.equal(manager.split('workm').killed, true, '4 - update is processed and the flag is updated'); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '4 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notificationKill); + }, MILLIS_SECOND_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(configWithSets); + client = splitio.client(); + manager = splitio.manager(); + + }, 'SDK with sets configured updates flag when a SPLIT_KILL is received'); + + t.test(async (assert) => { + + assert.plan(4); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1&sets=set_1,set_2', function () { + assert.pass('5 - A fetch is triggered due to the SPLIT_KILL'); + return { status: 200, body: { splits: [], since: 1, till: 5 }}; + }); + + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '5 - initialized without flags'); + + // Receive a SPLIT_KILL for flag + client.once(client.Event.SDK_UPDATE, async () => { + assert.deepEqual(manager.splits(), [], '5 - storage is not modified since flag is not present. '); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '5 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notificationKill); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(configWithSets); + client = splitio.client(); + manager = splitio.manager(); + + }, 'SDK with sets configured does not update flag when a SPLIT_KILL is received for a non-existing flag'); + +} diff --git a/src/__tests__/browserSuites/telemetry.spec.js b/src/__tests__/browserSuites/telemetry.spec.js index d58e56923..5d0c8f576 100644 --- a/src/__tests__/browserSuites/telemetry.spec.js +++ b/src/__tests__/browserSuites/telemetry.spec.js @@ -20,147 +20,222 @@ const config = { streamingEnabled: false }; -export default async function telemetryBrowserSuite(fetchMock, assert) { - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', 500); - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', { status: 200, body: splitChangesMock1 }); - fetchMock.getOnce(baseUrls.sdk + '/mySegments/user-key', 500); - fetchMock.getOnce(baseUrls.sdk + '/mySegments/user-key', { status: 200, body: { 'mySegments': [ 'one_segment'] } }); - - // We need to handle all requests properly - fetchMock.postOnce(baseUrls.events + '/testImpressions/bulk', 200); - fetchMock.postOnce(baseUrls.events + '/testImpressions/count', 200); - fetchMock.postOnce(baseUrls.events + '/events/bulk', 200); - +const SplitFactoryForTest = (config) => { // Overwrite Math.random to instantiate factory with telemetry const originalMathRandom = Math.random; Math.random = () => 0.001; - const splitio = SplitFactory(config, ({ settings }) => { - assert.equal(settings.scheduler.telemetryRefreshRate, 60000); + const factory = SplitFactory(config, ({ settings }) => { settings.scheduler.telemetryRefreshRate = 1000; // set below minimum to validate matrics/usage requests }); Math.random = originalMathRandom; // restore + return factory; +}; - const client = splitio.client(); - - const finish = (function* () { - yield; - client.destroy(); - assert.end(); - })(); - - let lastSync; - - // 1st metrics/usage call due to telemetryRefreshRate set in 1 second - fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', (url, opts) => { - const data = JSON.parse(opts.body); - - // Validate last successful sync - assert.deepEqual(Object.keys(data.lS), ['ms', 'sp', 'te'], 'Successful splitChanges, mySegments and metrics/config requests'); - lastSync = data.lS; delete data.lS; - - // Validate http and method latencies - const getLatencyCount = buckets => buckets ? buckets.reduce((accum, entry) => accum + entry, 0) : 0; - assert.equal(getLatencyCount(data.hL.sp), 2, 'Two latency metrics for splitChanges GET request'); - assert.equal(getLatencyCount(data.hL.ms), 2, 'Two latency metrics for mySegments GET request'); - assert.equal(getLatencyCount(data.hL.te), 1, 'One latency metric for telemetry config POST request'); - assert.equal(getLatencyCount(data.mL.t), 2, 'Two latency metrics for getTreatment (one not ready usage'); - assert.equal(getLatencyCount(data.mL.ts), 1, 'One latency metric for getTreatments'); - assert.equal(getLatencyCount(data.mL.tc), 1, 'One latency metric for getTreatmentWithConfig'); - assert.equal(getLatencyCount(data.mL.tcs), 1, 'One latency metric for getTreatmentsWithConfig'); - assert.equal(getLatencyCount(data.mL.tr), 1, 'One latency metric for track'); - delete data.hL; delete data.mL; - - // @TODO check if iDe value is correct - assert.deepEqual(data, { - mE: {}, hE: { sp: { 500: 1 }, ms: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [], ufs: { sp: 0, ms: 0 } - }, 'metrics/usage JSON payload should be the expected'); - - finish.next(); - return 200; - }); - - // 2nd metrics/usage call due to destroy - fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', (url, opts) => { - const data = JSON.parse(opts.body); - - assert.deepEqual(data.lS, lastSync, 'last successful sync hasn\'t change'); - delete data.lS; - - assert.true(data.sL > 0, 'sessionLengthMs must be defined'); - delete data.sL; - - // @TODO check if iDe value is correct - assert.deepEqual(data, { - mL: {}, mE: {}, hE: {}, hL: {}, // errors and latencies were popped - tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [], ufs: { sp: 0, ms: 0 } - }, '2nd metrics/usage JSON payload should be the expected'); - return 200; - }); - - fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/config', (url, opts) => { - const data = JSON.parse(opts.body); - - assert.true(data.tR > 0, 'timeUntilReady is larger than 0'); - delete data.tR; // delete to validate other properties - - assert.deepEqual(data, { - oM: 0, st: 'memory', aF: 1, rF: 0, sE: false, - rR: { sp: 99999, ms: 60, im: 300, ev: 60, te: 1 } /* override featuresRefreshRate */, - uO: { s: true, e: true, a: false, st: false, t: true } /* override sdk, events and telemetry URLs */, - iQ: 30000, eQ: 500, iM: 0, iL: false, hP: false, nR: 1 /* 1 non ready usage */, t: [], i: [], uC: 2 /* Default GRANTED */, - fsT: 0, fsI: 0 - }, 'metrics/config JSON payload should be the expected'); +export default async function telemetryBrowserSuite(fetchMock, t) { + + t.test(async (assert) => { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', 500); + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', { status: 200, body: splitChangesMock1 }); + fetchMock.getOnce(baseUrls.sdk + '/mySegments/user-key', 500); + fetchMock.getOnce(baseUrls.sdk + '/mySegments/user-key', { status: 200, body: { 'mySegments': [ 'one_segment'] } }); + + // We need to handle all requests properly + fetchMock.postOnce(baseUrls.events + '/testImpressions/bulk', 200); + fetchMock.postOnce(baseUrls.events + '/testImpressions/count', 200); + fetchMock.postOnce(baseUrls.events + '/events/bulk', 200); + + const splitio = SplitFactoryForTest(config); + const client = splitio.client(); + + const finish = (function* () { + yield; + client.destroy(); + assert.end(); + })(); + + let lastSync; + + // 1st metrics/usage call due to telemetryRefreshRate set in 1 second + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', (url, opts) => { + const data = JSON.parse(opts.body); + + // Validate last successful sync + assert.deepEqual(Object.keys(data.lS), ['ms', 'sp', 'te'], 'Successful splitChanges, mySegments and metrics/config requests'); + lastSync = data.lS; delete data.lS; + + // Validate http and method latencies + const getLatencyCount = buckets => buckets ? buckets.reduce((accum, entry) => accum + entry, 0) : 0; + assert.equal(getLatencyCount(data.hL.sp), 2, 'Two latency metrics for splitChanges GET request'); + assert.equal(getLatencyCount(data.hL.ms), 2, 'Two latency metrics for mySegments GET request'); + assert.equal(getLatencyCount(data.hL.te), 1, 'One latency metric for telemetry config POST request'); + assert.equal(getLatencyCount(data.mL.t), 2, 'Two latency metrics for getTreatment (one not ready usage'); + assert.equal(getLatencyCount(data.mL.ts), 1, 'One latency metric for getTreatments'); + assert.equal(getLatencyCount(data.mL.tc), 1, 'One latency metric for getTreatmentWithConfig'); + assert.equal(getLatencyCount(data.mL.tcs), 1, 'One latency metric for getTreatmentsWithConfig'); + assert.equal(getLatencyCount(data.mL.tr), 1, 'One latency metric for track'); + delete data.hL; delete data.mL; + + // @TODO check if iDe value is correct + assert.deepEqual(data, { + mE: {}, hE: { sp: { 500: 1 }, ms: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [], ufs: { sp: 0, ms: 0 } + }, 'metrics/usage JSON payload should be the expected'); + + finish.next(); + return 200; + }); + + // 2nd metrics/usage call due to destroy + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', (url, opts) => { + const data = JSON.parse(opts.body); + + assert.deepEqual(data.lS, lastSync, 'last successful sync hasn\'t change'); + delete data.lS; + + assert.true(data.sL > 0, 'sessionLengthMs must be defined'); + delete data.sL; + + // @TODO check if iDe value is correct + assert.deepEqual(data, { + mL: {}, mE: {}, hE: {}, hL: {}, // errors and latencies were popped + tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [], ufs: { sp: 0, ms: 0 } + }, '2nd metrics/usage JSON payload should be the expected'); + return 200; + }); + + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/config', (url, opts) => { + const data = JSON.parse(opts.body); + + assert.true(data.tR > 0, 'timeUntilReady is larger than 0'); + delete data.tR; // delete to validate other properties + + assert.deepEqual(data, { + oM: 0, st: 'memory', aF: 1, rF: 0, sE: false, + rR: { sp: 99999, ms: 60, im: 300, ev: 60, te: 1 } /* override featuresRefreshRate */, + uO: { s: true, e: true, a: false, st: false, t: true } /* override sdk, events and telemetry URLs */, + iQ: 30000, eQ: 500, iM: 0, iL: false, hP: false, nR: 1 /* 1 non ready usage */, t: [], i: [], uC: 2 /* Default GRANTED */, + fsT: 0, fsI: 0 + }, 'metrics/config JSON payload should be the expected'); + + finish.next(); + + return 200; + }); + + assert.equal(client.getTreatment('always_on'), 'control', 'Non ready usage.'); + + await client.ready(); + + // treatments and results are only validated so we know for sure when the function was actually running to compare the metrics. + assert.equal(client.getTreatment('always_on'), 'on', 'Evaluation was correct.'); + assert.equal(client.getTreatment('always_on', () => { }), 'control', 'We should return control with invalid input.'); + + assert.deepEqual(client.getTreatmentWithConfig('split_with_config'), { + treatment: 'o.n', + config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' + }, 'Evaluation with config was correct.'); + assert.deepEqual(client.getTreatmentWithConfig('split_with_config', () => { }), { + treatment: 'control', + config: null + }, 'Evaluation with config returned control state for invalid input.'); + + assert.deepEqual(client.getTreatments(['always_on', 'always_off']), { always_on: 'on', always_off: 'off' }, 'Evaluations were correct.'); + assert.deepEqual(client.getTreatments(['always_on', 'always_off', null], () => { }), { always_on: 'control', always_off: 'control' }, 'We should return map of controls with invalid input.'); + + assert.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'always_on', null]), + { + split_with_config: { + treatment: 'o.n', + config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' + }, + always_on: { + treatment: 'on', + config: null + } + } + , 'Evaluations with config were correct.'); + assert.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'always_on', null], () => { }), + { + split_with_config: { + treatment: 'control', + config: null + }, + always_on: { + treatment: 'control', + config: null + } + }, + 'Evaluations with config returned control states for invalid input.'); - finish.next(); + assert.equal(client.track('someTT', 'someEvent'), true, 'Event was queued'); + assert.equal(client.track('someTT', null), false, 'Invalid input.'); - return 200; }); - assert.equal(client.getTreatment('always_on'), 'control', 'Non ready usage.'); - - await client.ready(); - - // treatments and results are only validated so we know for sure when the function was actually running to compare the metrics. - assert.equal(client.getTreatment('always_on'), 'on', 'Evaluation was correct.'); - assert.equal(client.getTreatment('always_on', () => { }), 'control', 'We should return control with invalid input.'); - - assert.deepEqual(client.getTreatmentWithConfig('split_with_config'), { - treatment: 'o.n', - config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' - }, 'Evaluation with config was correct.'); - assert.deepEqual(client.getTreatmentWithConfig('split_with_config', () => { }), { - treatment: 'control', - config: null - }, 'Evaluation with config returned control state for invalid input.'); - - assert.deepEqual(client.getTreatments(['always_on', 'always_off']), { always_on: 'on', always_off: 'off' }, 'Evaluations were correct.'); - assert.deepEqual(client.getTreatments(['always_on', 'always_off', null], () => { }), { always_on: 'control', always_off: 'control' }, 'We should return map of controls with invalid input.'); - - assert.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'always_on', null]), - { - split_with_config: { - treatment: 'o.n', - config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' - }, - always_on: { - treatment: 'on', - config: null - } - } - , 'Evaluations with config were correct.'); - assert.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'always_on', null], () => { }), - { - split_with_config: { - treatment: 'control', - config: null - }, - always_on: { - treatment: 'control', - config: null - } + // Flag sets + const baseConfig = { + core: { + authorizationKey: '', + key: 'nicolas@split.io' }, - 'Evaluations with config returned control states for invalid input.'); - - assert.equal(client.track('someTT', 'someEvent'), true, 'Event was queued'); - assert.equal(client.track('someTT', null), false, 'Invalid input.'); + urls: baseUrls, + scheduler: { + telemetryRefreshRate: 60 + }, + streamingEnabled: false + }; + + t.test(async (assert) => { + let factory; + const splitFilters = [{ type: 'bySet', values: ['a', '_b', 'a', 'a', 'c', 'd', '_d'] }]; + + fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } }); + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=a,c,d', { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }); + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', 200); + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/config', (url, opts) => { + const data = JSON.parse(opts.body); + + assert.true(data.tR > 0, 'timeUntilReady is larger than 0'); + assert.equal(data.fsT, 7, 'unique flag sets total should be 7'); + assert.equal(data.fsI, 4, 'flagset ignored should be 4'); + factory.client().destroy().then(() => { + assert.end(); + }); + + return 200; + }); + + factory = SplitFactoryForTest({...baseConfig, sync: {splitFilters}}); + + }, 'SDK with sets configured has sets information in config POST'); + + t.test(async (assert) => { + assert.plan(8); + let factory; + const splitFilters = [{ type: 'bySet', values: ['a', 'b'] }]; + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=a,c,d', { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }); + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', (url, opts) => { + const data = JSON.parse(opts.body); + + assert.deepEqual(data.mL.tf, [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 'Latencies stats'); + assert.deepEqual(data.mL.tfs, [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 'Latencies stats'); + assert.deepEqual(data.mL.tcf, [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 'Latencies stats'); + assert.deepEqual(data.mL.tcfs, [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 'Latencies stats'); + + factory.client().destroy().then(() => { + assert.end(); + }); + + return 200; + }); + fetchMock.postOnce(baseUrls.telemetry + '/v1/metrics/usage', 200); + + factory = SplitFactoryForTest({...baseConfig, sync: {splitFilters}}); + const client = factory.client(); + assert.deepEqual(client.getTreatmentsByFlagSet('a'),[]); + assert.deepEqual(client.getTreatmentsByFlagSets(['a']),[]); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('a'),[]); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(['a']),[]); + + }, 'SDK with sets configured has evaluation by sets telemetry in stats POST'); } diff --git a/src/__tests__/mocks/message.SPLIT_UPDATE.FS.1.json b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.1.json new file mode 100644 index 000000000..a8d69166b --- /dev/null +++ b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.1.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":1694548198907,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":2,\\\"pcn\\\":1,\\\"c\\\":0,\\\"d\\\":\\\"eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTYwMjc5OTYzODM0NCwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJzZXRzIjpbXSwiY29uZGl0aW9ucyI6W3siY29uZGl0aW9uVHlwZSI6IldISVRFTElTVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJtYXRjaGVyVHlwZSI6IldISVRFTElTVCIsIm5lZ2F0ZSI6ZmFsc2UsIndoaXRlbGlzdE1hdGNoZXJEYXRhIjp7IndoaXRlbGlzdCI6WyJhZG1pbiIsIm1hdXJvIiwibmljbyJdfX1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfV0sImxhYmVsIjoid2hpdGVsaXN0ZWQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoidXNlciJ9LCJtYXRjaGVyVHlwZSI6IklOX1NFR01FTlQiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6eyJzZWdtZW50TmFtZSI6Im1hdXItMiJ9fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6IlY0Iiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJ2NSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG1hdXItMiJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6IlY0Iiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJ2NSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19\\\"}\"}" +} diff --git a/src/__tests__/mocks/message.SPLIT_UPDATE.FS.2.json b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.2.json new file mode 100644 index 000000000..2df299899 --- /dev/null +++ b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.2.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":1694548363040,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":2,\\\"pcn\\\":1,\\\"c\\\":0,\\\"d\\\":\\\"eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjIsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMSIsInNldF8yIl0sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0\\\"}\"}" +} diff --git a/src/__tests__/mocks/message.SPLIT_UPDATE.FS.3.json b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.3.json new file mode 100644 index 000000000..d7e1aee32 --- /dev/null +++ b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.3.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":1694548363039,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":3,\\\"pcn\\\":2,\\\"c\\\":0,\\\"d\\\":\\\"eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjMsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMSJdLCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibmV3X3NlZ21lbnQifSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjB9LHsidHJlYXRtZW50IjoiZnJlZSIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG5ld19zZWdtZW50In0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6bnVsbCwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjoxMDB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19\\\"}\"}" +} diff --git a/src/__tests__/mocks/message.SPLIT_UPDATE.FS.4.json b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.4.json new file mode 100644 index 000000000..0806febf2 --- /dev/null +++ b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.4.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":1694548363039,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":4,\\\"pcn\\\":3,\\\"c\\\":0,\\\"d\\\":\\\"eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjQsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMyJdLCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibmV3X3NlZ21lbnQifSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjB9LHsidHJlYXRtZW50IjoiZnJlZSIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG5ld19zZWdtZW50In0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6bnVsbCwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjoxMDB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19\\\"}\"}" +} diff --git a/src/__tests__/mocks/message.SPLIT_UPDATE.FS.4None.json b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.4None.json new file mode 100644 index 000000000..ef90c8f9a --- /dev/null +++ b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.4None.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":1694548363039,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":4,\\\"pcn\\\":3,\\\"c\\\":0,\\\"d\\\":\\\"eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjUsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6W10sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0\\\"}\"}" +} diff --git a/src/__tests__/mocks/message.SPLIT_UPDATE.FS.5.json b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.5.json new file mode 100644 index 000000000..c03ffddc1 --- /dev/null +++ b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.5.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":1694548665831,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":5,\\\"pcn\\\":4,\\\"c\\\":0,\\\"d\\\":\\\"eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjUsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMyIsInNldF80Il0sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0\\\"}\"}" +} diff --git a/src/__tests__/mocks/message.SPLIT_UPDATE.FS.kill.json b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.kill.json new file mode 100644 index 000000000..b00248517 --- /dev/null +++ b/src/__tests__/mocks/message.SPLIT_UPDATE.FS.kill.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"-OT-rGuSwz:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:NDIxNjU0NTUyNw==\",\"timestamp\":1694549324214,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_KILL\\\",\\\"changeNumber\\\":5,\\\"defaultTreatment\\\":\\\"off\\\",\\\"splitName\\\":\\\"workm\\\"}\"}" +} diff --git a/src/__tests__/mocks/splitchanges.since.-1.till.1602796638344.json b/src/__tests__/mocks/splitchanges.since.-1.till.1602796638344.json new file mode 100644 index 000000000..1c69f3b8a --- /dev/null +++ b/src/__tests__/mocks/splitchanges.since.-1.till.1602796638344.json @@ -0,0 +1,150 @@ +{ + "splits": [ + { + "trafficTypeName": "client", + "name": "workm", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": {}, + "sets": ["set_1", "set_2"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 0 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 100 }, + { "treatment": "conta", "size": 0 } + ], + "label": "in segment new_segment" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "client", + "name": "workm_set_3", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": {}, + "sets": ["set_3"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 0 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 100 }, + { "treatment": "conta", "size": 0 } + ], + "label": "in segment new_segment" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1602796638344 +} diff --git a/src/__tests__/mocks/splitchanges.since.1602796638344.till.1602797638344.json b/src/__tests__/mocks/splitchanges.since.1602796638344.till.1602797638344.json new file mode 100644 index 000000000..5ec998208 --- /dev/null +++ b/src/__tests__/mocks/splitchanges.since.1602796638344.till.1602797638344.json @@ -0,0 +1,78 @@ +{ + "splits": [ + { + "trafficTypeName": "client", + "name": "workm", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602797638344, + "algo": 2, + "configurations": {}, + "sets": ["set_1"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 0 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 100 }, + { "treatment": "conta", "size": 0 } + ], + "label": "in segment new_segment" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + } + ], + "since": 1602796638344, + "till": 1602797638344 +} diff --git a/src/__tests__/mocks/splitchanges.since.1602797638344.till.1602798638344.json b/src/__tests__/mocks/splitchanges.since.1602797638344.till.1602798638344.json new file mode 100644 index 000000000..9384d1c17 --- /dev/null +++ b/src/__tests__/mocks/splitchanges.since.1602797638344.till.1602798638344.json @@ -0,0 +1,78 @@ +{ + "splits": [ + { + "trafficTypeName": "client", + "name": "workm", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602798638344, + "algo": 2, + "configurations": {}, + "sets": ["set_3"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 0 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 100 }, + { "treatment": "conta", "size": 0 } + ], + "label": "in segment new_segment" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + } + ], + "since": 1602797638344, + "till": 1602798638344 +} diff --git a/src/__tests__/nodeSuites/fetch-specific-splits.spec.js b/src/__tests__/nodeSuites/fetch-specific-splits.spec.js index a8a92b4de..6470b0a32 100644 --- a/src/__tests__/nodeSuites/fetch-specific-splits.spec.js +++ b/src/__tests__/nodeSuites/fetch-specific-splits.spec.js @@ -13,7 +13,7 @@ const baseConfig = { export default function fetchSpecificSplits(fetchMock, assert) { - assert.plan(splitFilters.length); + assert.plan(splitFilters.length+1); for (let i = 0; i < splitFilters.length; i++) { const urls = { sdk: 'https://sdkurl' + i }; @@ -43,4 +43,32 @@ export default function fetchSpecificSplits(fetchMock, assert) { } } + + // Flag sets + assert.test(async (t) => { + + const splitFilters = [{ type: 'bySet', values: ['set_x ', 'set_x', 'set_3', 'set_2', 'set_3', 'set_ww', 'invalid+', '_invalid', '4_valid'] }]; + const baseUrls = { sdk: 'https://sdk.baseurl' }; + + const config = { + ...baseConfig, + urls: baseUrls, + sync: { + splitFilters + } + }; + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=4_valid,set_2,set_3,set_ww,set_x', async function () { + t.pass('flag set query correctly formed'); + return { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }; + }); + + const factory = SplitFactory(config); + const client = factory.client(); + + client.ready().then(async () => { + await client.destroy(); + t.end(); + }); + }, 'FlagSets config'); } diff --git a/src/__tests__/nodeSuites/flag-sets.spec.js b/src/__tests__/nodeSuites/flag-sets.spec.js new file mode 100644 index 000000000..db857ad6a --- /dev/null +++ b/src/__tests__/nodeSuites/flag-sets.spec.js @@ -0,0 +1,215 @@ +import { SplitFactory } from '../..'; +import { mockSegmentChanges } from '../testUtils'; + + +import splitChange2 from '../mocks/splitChanges.since.-1.till.1602796638344.json'; +import splitChange1 from '../mocks/splitchanges.since.1602796638344.till.1602797638344.json'; +import splitChange0 from '../mocks/splitchanges.since.1602797638344.till.1602798638344.json'; + +const baseUrls = { sdk: 'https://sdk.baseurl' }; + +const baseConfig = { + core: { + authorizationKey: '', + }, + urls: baseUrls, + scheduler: { featuresRefreshRate: 0.01 }, + streamingEnabled: false, +}; + +const key = 'emma'; + +export default function flagSets(fetchMock, t) { + mockSegmentChanges(fetchMock, new RegExp(baseUrls.sdk + '/segmentChanges/*'), ['some_key']); + + t.test(async (assert) => { + assert.plan(7); + let factory, manager, client = []; + + // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=set_1,set_2', function () { + return { status: 200, body: splitChange2}; + }); + + // Receive split change with 1 split belonging to set_1 only + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344&sets=set_1,set_2', function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 1, 'only one feature flag should be added'); + assert.true(storedFlags[0].name === 'workm'); + assert.deepEqual(storedFlags[0].sets, ['set_1','set_2']); + + // send split change + return { status: 200, body: splitChange1}; + }); + + // Receive split change with 1 split belonging to set_3 only + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602797638344&sets=set_1,set_2', function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 1); + assert.true(storedFlags[0].name === 'workm'); + assert.deepEqual(storedFlags[0].sets, ['set_1'], 'the feature flag should be updated'); + + // send split change + return { status: 200, body: splitChange0}; + }); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602798638344&sets=set_1,set_2', async function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 0, 'the feature flag should be removed'); + await client.destroy(); + client = []; + assert.end(); + + return { status: 200, body: {} }; + }); + + // Initialize a factory with polling and sets set_1 & set_2 configured. + const splitFilters = [{ type: 'bySet', values: ['set_1','set_2'] }]; + factory = SplitFactory({ ...baseConfig, sync: { splitFilters }}); + client = factory.client(); + await client.ready(); + manager = factory.manager(); + + }, 'Polling - SDK with sets configured updates flags according to sets'); + + t.test(async (assert) => { + assert.plan(15); + + let factory, manager, client = []; + + // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', function () { + return { status: 200, body: splitChange2}; + }); + + // Receive split change with 1 split belonging to set_1 only + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344', function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 2, 'every feature flag should be added'); + assert.true(storedFlags[0].name === 'workm'); + assert.true(storedFlags[1].name === 'workm_set_3'); + assert.deepEqual(storedFlags[0].sets, ['set_1','set_2']); + assert.deepEqual(storedFlags[1].sets, ['set_3']); + + // send split change + return { status: 200, body: splitChange1}; + }); + + // Receive split change with 1 split belonging to set_3 only + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602797638344', function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 2); + assert.true(storedFlags[0].name === 'workm'); + assert.true(storedFlags[1].name === 'workm_set_3'); + assert.deepEqual(storedFlags[0].sets, ['set_1'], 'the feature flag should be updated'); + assert.deepEqual(storedFlags[1].sets, ['set_3'], 'the feature flag should remain as it was'); + + // send split change + return { status: 200, body: splitChange0}; + }); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602798638344', async function () { + // stored feature flags before update + const storedFlags = manager.splits(); + assert.true(storedFlags.length === 2); + assert.true(storedFlags[0].name === 'workm'); + assert.true(storedFlags[1].name === 'workm_set_3'); + assert.deepEqual(storedFlags[0].sets, ['set_3'], 'the feature flag should be updated'); + assert.deepEqual(storedFlags[1].sets, ['set_3'], 'the feature flag should remain as it was'); + await client.destroy(); + assert.end(); + return { status: 200, body: {} }; + }); + + // Initialize a factory with polling and no sets configured. + factory = SplitFactory(baseConfig); + client = factory.client(); + await client.ready(); + manager = factory.manager(); + + }, 'Poling - SDK with no sets configured does not take sets into account when updating flags'); + + // EVALUATION + + t.test(async (assert) => { + fetchMock.reset(); + assert.plan(8); + + let factory, client = []; + + mockSegmentChanges(fetchMock, new RegExp(baseUrls.sdk + '/segmentChanges/*'), []); + fetchMock.post('*', 200); + // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=set_1', function () { + return { status: 200, body: splitChange2}; + }); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344&sets=set_1', async function () { + // stored feature flags before update + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_1'), {workm: 'on'}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_2'), {}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_3'), {}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_1'), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_2'), {}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_3'), {}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSets(key, ['set_1','set_2','set_3']), {workm: 'on'}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(key, ['set_1','set_2','set_3']), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated'); + await client.destroy(); + assert.end(); + + // send split change + return { status: 200, body: splitChange1}; + }); + + // Initialize a factory with set_1 configured. + const splitFilters = [{ type: 'bySet', values: ['set_1'] }]; + factory = SplitFactory({ ...baseConfig, sync: { splitFilters }}); + client = factory.client(); + await client.ready(); + + }, 'SDK with sets configured can only evaluate configured sets'); + + t.test(async (assert) => { + fetchMock.reset(); + assert.plan(8); + + let factory, client = []; + + mockSegmentChanges(fetchMock, new RegExp(baseUrls.sdk + '/segmentChanges/*'), []); + fetchMock.post('*', 200); + + // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', function () { + return { status: 200, body: splitChange2}; + }); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344', async function () { + // stored feature flags before update + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_1'), {workm: 'on'}, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_2'), {workm: 'on'}, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_3'), { workm_set_3: 'on' }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_1'), { workm: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_2'), { workm: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_3'), { workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSets(key, ['set_1','set_2','set_3']), { workm: 'on', workm_set_3: 'on' }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(key, ['set_1','set_2','set_3']), { workm: { treatment: 'on', config: null }, workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + await client.destroy(); + assert.end(); + + // send split change + return { status: 200, body: splitChange1}; + }); + + + factory = SplitFactory(baseConfig); + client = factory.client(); + await client.ready(); + + }, 'SDK with no sets configured can evaluate any set'); + +} diff --git a/src/__tests__/nodeSuites/push-flag-sets.spec.js b/src/__tests__/nodeSuites/push-flag-sets.spec.js new file mode 100644 index 000000000..d504a5084 --- /dev/null +++ b/src/__tests__/nodeSuites/push-flag-sets.spec.js @@ -0,0 +1,267 @@ +import { SplitFactory } from '../..'; +import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; +import { __setEventSource } from '../../platform/getEventSource/node'; +import { mockSegmentChanges } from '../testUtils'; + +import notification1 from '../mocks/message.SPLIT_UPDATE.FS.1.json'; +import notification2 from '../mocks/message.SPLIT_UPDATE.FS.2.json'; +import notification3 from '../mocks/message.SPLIT_UPDATE.FS.3.json'; +import notification4None from '../mocks/message.SPLIT_UPDATE.FS.4None.json'; +import notification4 from '../mocks/message.SPLIT_UPDATE.FS.4.json'; +import notification5 from '../mocks/message.SPLIT_UPDATE.FS.5.json'; +import notificationKill from '../mocks/message.SPLIT_UPDATE.FS.kill.json'; + +import authPushEnabled from '../mocks/auth.pushEnabled.node.json'; + +const baseUrls = { + sdk: 'https://sdk.baseurl', + auth: 'https://auth.baseurl/api' +}; + +const baseConfig = { + core: { + authorizationKey: '', + }, + urls: baseUrls, +}; + +const MILLIS_FIRST_SPLIT_UPDATE_EVENT = 100; +const MILLIS_SECOND_SPLIT_UPDATE_EVENT = 200; +const MILLIS_THIRD_SPLIT_UPDATE_EVENT = 300; +const MILLIS_FOURTH_SPLIT_UPDATE_EVENT = 400; + +export function testFlagSets(fetchMock, t) { + fetchMock.reset(); + __setEventSource(EventSourceMock); + + mockSegmentChanges(fetchMock, new RegExp(baseUrls.sdk + '/segmentChanges/*'), ['some-key']); + fetchMock.post('*', 200); + + fetchMock.get(baseUrls.auth + '/v2/auth', function (url, opts) { + if (!opts.headers['Authorization']) t.fail('`/v2/auth` request must include `Authorization` header'); + t.pass('auth success'); + return { status: 200, body: authPushEnabled }; + }); + fetchMock.get(baseUrls.sdk + '/splitChanges?since=-1', function () { + return { status: 200, body: { splits: [], since: -1, till: 0}}; + }); + fetchMock.get(baseUrls.sdk + '/splitChanges?since=0', function () { + return { status: 200, body: { splits: [], since: 0, till: 1 }}; + }); + fetchMock.get(baseUrls.sdk + '/splitChanges?since=-1&sets=set_1,set_2', function () { + return { status: 200, body: { splits: [], since: -1, till: 0 }}; + }); + fetchMock.get(baseUrls.sdk + '/splitChanges?since=0&sets=set_1,set_2', function () { + return { status: 200, body: { splits: [], since: 0, till: 1 }}; + }); + + const configWithSets = { + ...baseConfig, + sync: { + splitFilters: [{type: 'bySet', values: ['set_1', 'set_2']}] + } + }; + + t.test(async (assert) => { + + assert.plan(3); + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '1 - initialized without flags'); + client.once(client.Event.SDK_UPDATE, async () => { + assert.equal(manager.splits().length, 1, '1 - update is processed and the flag is stored'); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '1 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notification1); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(baseConfig); + client = splitio.client(); + manager = splitio.manager(); + + }, 'SDK with no sets configured does not exclude updates'); + + t.test(async (assert) => { + + assert.plan(5); + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '2 - initialized without flags'); + // Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.split('workm').sets, ['set_1', 'set_2'], '2 - update is processed and the flag is stored'); + }); + eventSourceInstance.emitMessage(notification2); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":["set_1"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.split('workm').sets, ['set_1'], '2 - update is processed and the flag is updated'); + }); + eventSourceInstance.emitMessage(notification3); + }, MILLIS_SECOND_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":[] + client.once(client.Event.SDK_UPDATE, async () => { + assert.deepEqual(manager.splits().length, 0, '2 - update is processed and the flag is removed'); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '2 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notification4None); + }, MILLIS_THIRD_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(configWithSets); + client = splitio.client(); + manager = splitio.manager(); + + }, 'SDK with sets configured deletes flag when change with empty sets is received'); + + t.test(async (assert) => { + + assert.plan(6); + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '3 - initialized without flags'); + // Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.split('workm').sets, ['set_1', 'set_2'], '3 - update is processed and the flag is stored'); + }); + eventSourceInstance.emitMessage(notification2); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":["set_1"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.split('workm').sets, ['set_1'], '3 - update is processed and the flag is updated'); + }); + eventSourceInstance.emitMessage(notification3); + }, MILLIS_SECOND_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":["set_3"] + client.once(client.Event.SDK_UPDATE, () => { + assert.deepEqual(manager.splits().length, 0, '3 - update is processed and the flag is removed'); + }); + eventSourceInstance.emitMessage(notification4); + }, MILLIS_THIRD_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_UPDATE with "sets":["set_3", "set_4"] + client.once(client.Event.SDK_UPDATE, async () => { + assert.deepEqual(manager.splits().length, 0, '3 - update is processed and flag is not added to the storage'); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '3 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notification5); + }, MILLIS_FOURTH_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(configWithSets); + client = splitio.client(); + manager = splitio.manager(); + + }, 'SDK with sets configured deletes flag when change with non-matching sets is received'); + + t.test(async (assert) => { + + assert.plan(5); + + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=2&sets=set_1,set_2', async function () { + assert.pass('4 - A fetch is triggered due to the SPLIT_KILL'); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '4 - streaming is closed after destroy'); + assert.end(); + return { status: 200, body: { splits: [], since: 2, till: 3 }}; + }); + + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '4 - initialized without flags'); + // Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + client.once(client.Event.SDK_UPDATE, () => { + assert.equal(manager.split('workm').killed, false, '4 - update is processed and the flag is stored'); + }); + eventSourceInstance.emitMessage(notification2); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + setTimeout(() => { + // Receive a SPLIT_KILL for flag + client.once(client.Event.SDK_UPDATE, async () => { + assert.equal(manager.split('workm').killed, true, '4 - update is processed and the flag is updated'); + + }); + eventSourceInstance.emitMessage(notificationKill); + }, MILLIS_SECOND_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(configWithSets); + client = splitio.client(); + manager = splitio.manager(); + await client.ready(); + + }, 'SDK with sets configured updates flag when a SPLIT_KILL is received'); + + t.test(async (assert) => { + assert.plan(4); + + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1&sets=set_1,set_2', function () { + assert.pass('5 - A fetch is triggered due to the SPLIT_KILL'); + return { status: 200, body: { splits: [], since: 1, till: 5 }}; + }); + + let splitio, client, manager = []; + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + assert.deepEqual(manager.splits(), [], '5 - initialized without flags'); + + // Receive a SPLIT_KILL for flag + client.once(client.Event.SDK_UPDATE, async () => { + assert.deepEqual(manager.splits(), [], '5 - storage is not modified since flag is not present. '); + await client.destroy(); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, '5 - streaming is closed after destroy'); + assert.end(); + }); + eventSourceInstance.emitMessage(notificationKill); + }, MILLIS_FIRST_SPLIT_UPDATE_EVENT); + + }); + + splitio = SplitFactory(configWithSets); + client = splitio.client(); + manager = splitio.manager(); + await client.ready(); + + }, 'SDK with sets configured does not update flag when a SPLIT_KILL is received for a non-existing flag'); +} diff --git a/src/__tests__/online/browser.spec.js b/src/__tests__/online/browser.spec.js index a96dfe9b7..00218fb53 100644 --- a/src/__tests__/online/browser.spec.js +++ b/src/__tests__/online/browser.spec.js @@ -19,6 +19,7 @@ import readyPromiseSuite from '../browserSuites/ready-promise.spec'; import fetchSpecificSplits from '../browserSuites/fetch-specific-splits.spec'; import userConsent from '../browserSuites/user-consent.spec'; import singleSync from '../browserSuites/single-sync.spec'; +import flagSets from '../browserSuites/flag-sets.spec'; import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; @@ -132,6 +133,8 @@ tape('## E2E CI Tests ##', function (assert) { assert.test('E2E / Fetch specific splits', fetchSpecificSplits.bind(null, fetchMock)); /* Validate single sync */ assert.test('E2E / Single sync', singleSync.bind(null, fetchMock)); + /* Validate flag sets */ + assert.test('E2E / flag sets', flagSets.bind(null, fetchMock)); //If we change the mocks, we need to clear localstorage. Cleaning up after testing ensures "fresh data". localStorage.clear(); diff --git a/src/__tests__/online/node.spec.js b/src/__tests__/online/node.spec.js index 81304cce2..9cb9eb873 100644 --- a/src/__tests__/online/node.spec.js +++ b/src/__tests__/online/node.spec.js @@ -19,6 +19,7 @@ import fetchSpecificSplits from '../nodeSuites/fetch-specific-splits.spec'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; +import flagSets from '../nodeSuites/flag-sets.spec'; const config = { core: { @@ -83,5 +84,8 @@ tape('## Node JS - E2E CI Tests ##', async function (assert) { /* Validate fetching specific splits */ assert.test('E2E / Fetch specific splits', fetchSpecificSplits.bind(null, fetchMock)); + /* Validate flag sets */ + assert.test('E2E / Flag sets', flagSets.bind(null, fetchMock)); + assert.end(); }); diff --git a/src/__tests__/push/browser.spec.js b/src/__tests__/push/browser.spec.js index f4d510371..2ff84728d 100644 --- a/src/__tests__/push/browser.spec.js +++ b/src/__tests__/push/browser.spec.js @@ -7,6 +7,7 @@ import { testSynchronizationRetries } from '../browserSuites/push-synchronizatio import { testFallbacking } from '../browserSuites/push-fallbacking.spec'; import { testRefreshToken } from '../browserSuites/push-refresh-token.spec'; import { testSplitKillOnReadyFromCache } from '../browserSuites/push-corner-cases.spec'; +import { testFlagSets } from '../browserSuites/push-flag-sets.spec'; fetchMock.config.overwriteRoutes = false; Math.random = () => 0.5; // SDKs without telemetry @@ -38,5 +39,8 @@ tape('## Browser JS - E2E CI Tests for PUSH ##', function (assert) { // Corner cases assert.test('E2E / PUSH corner case: SPLIT_KILL notification must not emit SDK_READY if the SDK is ready from cache', testSplitKillOnReadyFromCache.bind(null, fetchMock)); + // Validate flag sets + assert.test('E2E / PUSH flag sets', testFlagSets.bind(null, fetchMock)); + assert.end(); }); diff --git a/src/__tests__/push/node.spec.js b/src/__tests__/push/node.spec.js index b72949c9e..6345e15db 100644 --- a/src/__tests__/push/node.spec.js +++ b/src/__tests__/push/node.spec.js @@ -6,6 +6,7 @@ import { testSynchronization } from '../nodeSuites/push-synchronization.spec'; import { testSynchronizationRetries } from '../nodeSuites/push-synchronization-retries.spec'; import { testFallbacking } from '../nodeSuites/push-fallbacking.spec'; import { testRefreshToken } from '../nodeSuites/push-refresh-token.spec'; +import { testFlagSets } from '../nodeSuites/push-flag-sets.spec'; fetchMock.config.overwriteRoutes = false; @@ -36,5 +37,8 @@ tape('## Node JS - E2E CI Tests for PUSH ##', async function (assert) { assert.test('E2E / PUSH refresh token and connection delay', testRefreshToken.bind(null, fetchMock)); + /* Validate flag sets */ + assert.test('E2E / PUSH flag sets', testFlagSets.bind(null, fetchMock)); + assert.end(); });