From 46f5938c44e8ebc834378a726d37d4b2e77c8e80 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Fri, 11 Oct 2024 11:36:57 -0400 Subject: [PATCH] tests: Fix remaining tests from XHR to fetch migration (#931) --- src/aliasRequestApiClient.ts | 2 +- src/mp-instance.js | 4 +- test/integrations/requirejs/test-requirejs.js | 49 +- test/src/_test.index.ts | 14 +- test/src/config/utils.js | 32 +- test/src/tests-apiClient.ts | 3 +- test/src/tests-audience-manager.ts | 16 +- test/src/tests-batchUploader.ts | 1078 ++--------------- test/src/tests-batchUploader_2.ts | 305 +++++ test/src/tests-batchUploader_3.ts | 450 +++++++ test/src/tests-batchUploader_4.ts | 335 +++++ test/src/tests-beaconUpload.ts | 54 +- test/src/tests-consent.ts | 36 +- test/src/tests-cookie-syncing.js | 100 +- test/src/tests-core-sdk.js | 316 +++-- test/src/tests-eCommerce.js | 210 +++- test/src/tests-event-logging.js | 411 ++++++- test/src/tests-feature-flags.ts | 54 +- test/src/tests-forwarders.js | 841 ++++++++++--- test/src/tests-helpers.js | 2 +- test/src/tests-identities-attributes.ts | 287 +++-- test/src/tests-identity.ts | 9 +- test/src/tests-integration-capture.ts | 22 +- test/src/tests-kit-blocking.ts | 164 ++- test/src/tests-legacy-alias-requests.ts | 15 +- test/src/tests-mParticleUser.js | 223 ++-- test/src/tests-mparticle-instance-manager.js | 27 +- test/src/tests-persistence.ts | 603 ++++++--- test/src/tests-runtimeToBatchEventsDTO.ts | 2 +- test/src/tests-self-hosting-specific.js | 187 +-- test/src/tests-serverModel.ts | 64 +- test/src/tests-session-manager.ts | 25 +- test/src/tests-user.ts | 83 +- test/src/tests-utils.ts | 2 +- 34 files changed, 4054 insertions(+), 1971 deletions(-) create mode 100644 test/src/tests-batchUploader_2.ts create mode 100644 test/src/tests-batchUploader_3.ts create mode 100644 test/src/tests-batchUploader_4.ts diff --git a/src/aliasRequestApiClient.ts b/src/aliasRequestApiClient.ts index 6b3d619b..f4ac2cd5 100644 --- a/src/aliasRequestApiClient.ts +++ b/src/aliasRequestApiClient.ts @@ -54,7 +54,7 @@ export async function sendAliasRequest (mpInstance: MParticleWebSDK, aliasReques // https://go.mparticle.com/work/SQDSDKS-6568 // XHRUploader returns the response as a string that we need to parse const xhrResponse = response as unknown as XMLHttpRequest; - // debugger; + aliasResponseBody = xhrResponse.responseText ? JSON.parse(xhrResponse.responseText) : ''; diff --git a/src/mp-instance.js b/src/mp-instance.js index 14a1ffd9..39afdd0e 100644 --- a/src/mp-instance.js +++ b/src/mp-instance.js @@ -1539,9 +1539,9 @@ function processReadyQueue(readyQueue) { processPreloadedItem(readyQueueItem); } }); - - return []; } + // https://go.mparticle.com/work/SQDSDKS-6835 + return []; } function queueIfNotInitialized(func, self) { diff --git a/test/integrations/requirejs/test-requirejs.js b/test/integrations/requirejs/test-requirejs.js index e50be14b..82b68a67 100644 --- a/test/integrations/requirejs/test-requirejs.js +++ b/test/integrations/requirejs/test-requirejs.js @@ -1,16 +1,44 @@ import fetchMock from 'fetch-mock/esm/client'; -import sinon from 'sinon'; + +// https://go.mparticle.com/work/SQDSDKS-6849 +const waitForCondition = function async( + conditionFn, + timeout = 200, + interval = 10 + ) { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + (function poll() { + if (conditionFn()) { + return resolve(undefined); + } else if (Date.now() - startTime > timeout) { + return reject(new Error('Timeout waiting for condition')); + } else { + setTimeout(poll, interval); + } + })(); + }); + }, + fetchMockSuccess = function (url, body) { + fetchMock.post( + url, + { + status: 200, + body: JSON.stringify(body), + }, + { overwriteRoutes: true } + ); + }, + hasIdentifyReturned = () => { + return window.mParticle.Identity.getCurrentUser()?.getMPID() === 'testMPID'; +}; describe('Require.JS Pages', function() { it('loads mParticle properly', function() { - var mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith('https://identity.mparticle.com/v1/identify', [ - 200, - {}, - JSON.stringify({ mpid: 'testMPID', is_logged_in: false }), - ]); + fetchMockSuccess('https://identity.mparticle.com/v1/identify', { + mpid: 'testMPID', is_logged_in: false + }); fetchMock.post('https://jssdks.mparticle.com/v3/JS/test_key/events', 200); @@ -26,7 +54,7 @@ describe('Require.JS Pages', function() { }; mParticle.init('test_key', window.mParticle.config); - + waitForCondition(hasIdentifyReturned).then(() => { fetchMock.resetHistory(); mParticle.logEvent('Test Event1'); @@ -35,5 +63,6 @@ describe('Require.JS Pages', function() { const testEventName = JSON.parse(testEvent[1].body).events[0].data.event_name; testEventName.should.equal('Test Event1'); + }) }); }); \ No newline at end of file diff --git a/test/src/_test.index.ts b/test/src/_test.index.ts index 94ad40ed..e847c974 100644 --- a/test/src/_test.index.ts +++ b/test/src/_test.index.ts @@ -2,21 +2,22 @@ import './config/setup'; // Import each test module -import './tests-core-sdk'; +import './tests-identity'; import './tests-batchUploader'; +import './tests-core-sdk'; import './tests-beaconUpload'; import './tests-kit-blocking'; +import './tests-event-logging'; +import './tests-eCommerce'; import './tests-persistence'; import './tests-forwarders'; import './tests-helpers'; -import './tests-identity'; -import './tests-event-logging'; -import './tests-eCommerce'; import './tests-cookie-syncing'; import './tests-identities-attributes'; import './tests-native-sdk'; import './tests-consent'; import './tests-serverModel'; +import './tests-batchUploader_2'; import './tests-mockBatchCreator'; import './tests-mParticleUser'; import './tests-self-hosting-specific'; @@ -24,6 +25,7 @@ import './tests-runtimeToBatchEventsDTO'; import './tests-apiClient'; import './tests-mparticle-instance-manager'; import './tests-queue-public-methods'; +import './tests-batchUploader_3'; import './tests-validators'; import './tests-utils'; import './tests-session-manager'; @@ -35,4 +37,6 @@ import './tests-feature-flags'; import './tests-user'; import './tests-legacy-alias-requests'; import './tests-aliasRequestApiClient'; -import './tests-integration-capture'; \ No newline at end of file +import './tests-integration-capture'; +import './tests-batchUploader_4'; + diff --git a/test/src/config/utils.js b/test/src/config/utils.js index 5599f445..46e15f84 100644 --- a/test/src/config/utils.js +++ b/test/src/config/utils.js @@ -9,6 +9,7 @@ import { workspaceCookieName, das, } from './constants'; +import fetchMock from 'fetch-mock/esm/client'; var pluses = /\+/g, getLocalStorageProducts = function getLocalStorageProducts() { @@ -273,24 +274,32 @@ var pluses = /\+/g, fullPath = 'https://identity.mparticle.com/v1/' + path; if (path !== 'modify') { requests.forEach(function(item) { + if (!item.url && item[0] === fullPath) { + returnedRequests.push(item); + } if (item.url === fullPath) { returnedRequests.push(item); } }); } else { requests.forEach(function(item) { - if (item.url.slice(-6) === 'modify') { + let url; + if (!item.url) { + url = item[0]; + } else { + url = item.url; + } + if (url.slice(-6) === 'modify') { returnedRequests.push(item); } }); } - return returnedRequests; }, getIdentityEvent = function(mockRequests, endpoint) { var returnedReqs = getIdentityRequests(mockRequests, endpoint); - if (returnedReqs[0] && returnedReqs[0].requestBody) { - return JSON.parse(returnedReqs[0].requestBody); + if (returnedReqs[0] && returnedReqs[0][1].body) { + return JSON.parse(returnedReqs[0][1].body); } return null; }, @@ -608,6 +617,19 @@ var pluses = /\+/g, } })(); }); + }, + fetchMockSuccess = function (url, body) { + fetchMock.post( + url, + { + status: 200, + body: JSON.stringify(body), + }, + { overwriteRoutes: true } + ); + }, + hasIdentifyReturned = () => { + return window.mParticle.Identity.getCurrentUser()?.getMPID() === testMPID; }; var TestsCore = { @@ -634,6 +656,8 @@ var TestsCore = { forwarderDefaultConfiguration: forwarderDefaultConfiguration, deleteAllCookies: deleteAllCookies, waitForCondition: waitForCondition, + fetchMockSuccess: fetchMockSuccess, + hasIdentifyReturned: hasIdentifyReturned, }; export default TestsCore; \ No newline at end of file diff --git a/test/src/tests-apiClient.ts b/test/src/tests-apiClient.ts index 78fc8277..796771c7 100644 --- a/test/src/tests-apiClient.ts +++ b/test/src/tests-apiClient.ts @@ -1,5 +1,4 @@ import Types from '../../src/types'; -import Constants from '../../src/constants'; import { apiKey, MPConfig } from './config/constants'; import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; import { expect } from 'chai'; @@ -116,4 +115,4 @@ describe('Api Client', () => { done(); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-audience-manager.ts b/test/src/tests-audience-manager.ts index 5ca300ae..ad31b5c4 100644 --- a/test/src/tests-audience-manager.ts +++ b/test/src/tests-audience-manager.ts @@ -8,6 +8,8 @@ import AudienceManager, { IAudienceMemberships, IAudienceMembershipsServerResponse } from '../../src/audienceManager'; import Logger from '../../src/logger.js'; +import Utils from './config/utils'; +const { fetchMockSuccess } = Utils; declare global { interface Window { @@ -19,7 +21,6 @@ declare global { const userAudienceUrl = `https://${Constants.DefaultBaseUrls.userAudienceUrl}${apiKey}/audience`; describe('AudienceManager', () => { - let mockServer; before(function() { fetchMock.restore(); sinon.restore(); @@ -27,14 +28,10 @@ describe('AudienceManager', () => { beforeEach(function() { fetchMock.restore(); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); window.mParticle.config.flags = { eventBatchingIntervalMillis: 1000, @@ -43,7 +40,6 @@ describe('AudienceManager', () => { afterEach(() => { sinon.restore(); - mockServer.reset(); fetchMock.restore(); }); @@ -171,4 +167,4 @@ describe('AudienceManager', () => { ); }); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index c12dfc59..5981fc6b 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -14,6 +14,7 @@ import _BatchValidator from '../../src/mockBatchCreator'; import Logger from '../../src/logger.js'; import { event0, event1, event2, event3 } from '../fixtures/events'; import fetchMock from 'fetch-mock/esm/client'; +const { fetchMockSuccess, waitForCondition, hasIdentifyReturned } = Utils; declare global { interface Window { @@ -31,18 +32,15 @@ describe('batch uploader', () => { beforeEach(() => { fetchMock.restore(); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMock.config.overwriteRoutes = true; + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); + fetchMock.post(urls.events, 200); }); afterEach(() => { - mockServer.reset(); fetchMock.restore(); window.localStorage.clear(); }); @@ -52,7 +50,9 @@ describe('batch uploader', () => { it('should add events to the Pending Events Queue', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - + + waitForCondition(hasIdentifyReturned) + .then(() => { const mpInstance = window.mParticle.getInstance(); const uploader = new BatchUploader(mpInstance, 1000); @@ -82,11 +82,15 @@ describe('batch uploader', () => { uploader.queueEvent(event); expect(uploader.eventsQueuedForProcessing.length).to.eql(1); + }) }); it('should reject batches without events', () => { window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const mpInstance = window.mParticle.getInstance(); @@ -97,11 +101,14 @@ describe('batch uploader', () => { expect(uploader.eventsQueuedForProcessing).to.eql([]); expect(uploader.batchesQueuedForProcessing).to.eql([]); + }) }); it('should add events in the order they are received', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const mpInstance = window.mParticle.getInstance(); @@ -115,6 +122,7 @@ describe('batch uploader', () => { expect(uploader.eventsQueuedForProcessing[0]).to.eql(event1); expect(uploader.eventsQueuedForProcessing[1]).to.eql(event2); expect(uploader.eventsQueuedForProcessing[2]).to.eql(event3); + }) }); }); @@ -123,17 +131,17 @@ describe('batch uploader', () => { window.mParticle.config.flags = { eventBatchingIntervalMillis: 1000, }; - - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); + fetchMock.post(urls.events, 200); }); afterEach(() => { fetchMock.restore(); }); - it('should reject batches without events', async () => { + it('should reject batches without events', (done) => { + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(async () => { fetchMock.post(urls.events, 200); const newLogger = new Logger(window.mParticle.config); @@ -152,12 +160,14 @@ describe('batch uploader', () => { {} as unknown as BaseEvent ); + fetchMock.resetHistory(); // HACK: Directly access uploader to Force an upload await (uploader).uploadBatches( newLogger, [actualBatch, eventlessBatch], false ); + expect(fetchMock.calls().length).to.equal(1); const actualBatchResult = JSON.parse( @@ -166,9 +176,19 @@ describe('batch uploader', () => { expect(actualBatchResult.events.length).to.equal(1); expect(actualBatchResult.events).to.eql(actualBatch.events); + + done(); + }) + .catch((e) => { + }); }); - it('should return batches that fail to upload with 500 errors', async () => { + it('should return batches that fail to upload with 500 errors', () => { + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(async () => { + fetchMock.post(urls.events, 500); const newLogger = new Logger(window.mParticle.config); @@ -213,9 +233,16 @@ describe('batch uploader', () => { expect( batchesNotUploaded[2].events[0].data.event_name ).to.equal('Test Event 3'); + }) + .catch((e) => { + }); }); it('should return batches that fail to upload with 429 errors', async () => { + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(async () => { fetchMock.post(urls.events, 429); const newLogger = new Logger(window.mParticle.config); @@ -260,9 +287,14 @@ describe('batch uploader', () => { expect( batchesNotUploaded[2].events[0].data.event_name ).to.equal('Test Event 3'); + }) }); it('should return null if batches fail to upload with 401 errors', async () => { + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(async () => { fetchMock.post(urls.events, 401); const newLogger = new Logger(window.mParticle.config); @@ -294,9 +326,14 @@ describe('batch uploader', () => { ); expect(batchesNotUploaded === null).to.equal(true); + }) }); it('should return batches that fail to unknown HTTP errors', async () => { + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(async () => { fetchMock.post(urls.events, 400); const newLogger = new Logger(window.mParticle.config); @@ -343,16 +380,20 @@ describe('batch uploader', () => { expect( batchesNotUploaded[2].events[0].data.event_name ).to.equal('Test Event 3'); + }) + .catch((e) => { + }); }); }); }); describe('Offline Storage Feature Flag', () => { beforeEach(() => { - sinon.restore(); - fetchMock.restore(); + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); - mockServer.reset(); window.localStorage.clear(); window.sessionStorage.clear(); }); @@ -360,7 +401,6 @@ describe('batch uploader', () => { sinon.restore(); fetchMock.restore(); - mockServer.reset(); window.localStorage.clear(); window.sessionStorage.clear(); }); @@ -370,8 +410,10 @@ describe('batch uploader', () => { offlineStorage: '100', }; window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const getItemSpy = sinon.spy(Storage.prototype, 'getItem'); const setItemSpy = sinon.spy(Storage.prototype, 'setItem'); @@ -418,6 +460,9 @@ describe('batch uploader', () => { ); done(); + }) + .catch((e) => { + }); }); it('should not use local storage when disabled', () => { @@ -427,6 +472,8 @@ describe('batch uploader', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then( () => { const getItemSpy = sinon.spy(Storage.prototype, 'getItem'); const setItemSpy = sinon.spy(Storage.prototype, 'setItem'); @@ -468,6 +515,9 @@ describe('batch uploader', () => { expect( window.localStorage.getItem('mprtcl-v4_abcdef-events') ).to.equal(null); + }) + .catch((e) => { + }); }); }); @@ -478,18 +528,12 @@ describe('batch uploader', () => { ...enableBatchingConfigFlags, }; - clock = sinon.useFakeTimers({ - now: new Date().getTime(), - }); - window.localStorage.clear(); }); afterEach(() => { - sinon.restore(); window.localStorage.clear(); fetchMock.restore(); - clock.restore(); }); it('should not save events or batches in local storage', done => { @@ -499,6 +543,8 @@ describe('batch uploader', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -529,6 +575,9 @@ describe('batch uploader', () => { ).to.equal(null); done(); + }) + .catch((e) => { + }); }); }); @@ -539,10 +588,11 @@ describe('batch uploader', () => { ...enableBatchingConfigFlags, }; - clock = sinon.useFakeTimers({ - now: new Date().getTime(), - }); fetchMock.restore(); + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); window.sessionStorage.clear(); window.localStorage.clear(); @@ -551,7 +601,6 @@ describe('batch uploader', () => { afterEach(() => { sinon.restore(); fetchMock.restore(); - clock.restore(); }); it('should store events in Session Storage in order of creation', done => { @@ -559,7 +608,8 @@ describe('batch uploader', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -587,6 +637,10 @@ describe('batch uploader', () => { ); done(); + }) + .catch((e) => { + }) + }); it('should purge events from Session Storage upon Batch Creation', async () => { @@ -596,7 +650,8 @@ describe('batch uploader', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(async () => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -626,6 +681,11 @@ describe('batch uploader', () => { // Batch Queue should be empty because batch successfully uploaded expect(uploader.batchesQueuedForProcessing.length).to.equal(0); + clock.restore(); + // done(); + }) + .catch((e) => { + }) }); it('should save batches in sequence to Local Storage when an HTTP 500 error is encountered', async () => { @@ -635,6 +695,8 @@ describe('batch uploader', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(async() => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -677,16 +739,20 @@ describe('batch uploader', () => { .event_name, 'Batch 2: Custom Event Name' ).to.equal('Test Event 0'); + }) + .catch((e) => { + }) }); - it('should save batches in sequence to Local Storage when an HTTP 429 error is encountered', async () => { + it('should save batches in sequence to Local Storage when an HTTP 429 error is encountered', () => { const batchStorageKey = 'mprtcl-v4_abcdef-batches'; fetchMock.post(urls.events, 429); window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(async () => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -725,9 +791,12 @@ describe('batch uploader', () => { .event_name, 'Batch 2: Custom Event Name' ).to.equal('Test Event 0'); + }) + .catch((e) => { + }) }); - it('should NOT save any batches to Local Storage when an HTTP 401 error is encountered', async () => { + it('should NOT save any batches to Local Storage when an HTTP 401 error is encountered', (done) => { // When a 401 is encountered, we assume that the batch is bad so we clear those // batches from the Upload Queue. Therefore, there should not be anything in // Offline Storage afterwards @@ -737,8 +806,9 @@ describe('batch uploader', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - - const mpInstance = window.mParticle.getInstance(); + waitForCondition(hasIdentifyReturned) + .then(async () => { + const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; // Init will fire a Session Start and AST. We are adding event0 @@ -752,6 +822,10 @@ describe('batch uploader', () => { expect(window.localStorage.getItem(batchStorageKey)).to.equal( '' ); + done(); + }) + .catch((e) => { + }) }); it('should save batches in sequence to Local Storage when upload is interrupted', async () => { @@ -772,11 +846,11 @@ describe('batch uploader', () => { fetchMock.post(urls.events, 429, { overwriteRoutes: false, }); - // Set up SDK and Uploader window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(async () => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -894,9 +968,13 @@ describe('batch uploader', () => { storedBatches[2].events[1].event_type, 'Batch 4: AST' ).to.equal('application_state_transition'); + }) + .catch((e) => { + console.log('should save batches in sequence to Local Storage when upload is interrupted') + }) }); - it('should attempt to upload batches from Offline Storage before new batches', async () => { + it('should attempt to upload batches from Offline Storage before new batches', () => { // This test should verify that batches read from Offline Storage are prepended // to the upload queue before newly created batches. @@ -907,6 +985,8 @@ describe('batch uploader', () => { // Set up SDK and Uploader window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(async () => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -1032,917 +1112,9 @@ describe('batch uploader', () => { uploadedBatch4.events[1].event_type, 'Batch 4: AST' ).to.equal('application_state_transition'); - }); - }); - - describe('Upload Workflow', () => { - beforeEach(() => { - clock = sinon.useFakeTimers({ - now: new Date().getTime(), - }); - }); - - afterEach(() => { - fetchMock.restore(); - clock.restore(); - }); - - it('should organize events in the order they are processed and maintain that order when uploading', (done) => { - // Batches should be uploaded in the order they were created to prevent - // any potential corruption. - - fetchMock.post(urls.events, 200); - fetchMock.config.overwriteRoutes = true; - - window.mParticle.config.flags = { - ...enableBatchingConfigFlags, - }; - - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logEvent('Test Event 0'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - expect( - fetchMock.called(), - 'FetchMock should have been called' - ).to.equal(true); - - const batch1 = JSON.parse(fetchMock.calls()[0][1].body as string); - - // Batch 1 should contain only session start, AST and a single event - // in this exact order - expect(batch1.events.length).to.equal(3); - expect(batch1.events[0].event_type).to.equal('session_start'); - expect(batch1.events[1].event_type).to.equal( - 'application_state_transition' - ); - expect(batch1.events[2].data.event_name).to.equal('Test Event 0'); - - // Log a second batch of events - window.mParticle.logEvent('Test Event 1'); - window.mParticle.logEvent('Test Event 2'); - window.mParticle.logEvent('Test Event 3'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - const batch2 = JSON.parse(fetchMock.calls()[1][1].body as string); - - // Batch 2 should contain three custom events - expect(batch2.events.length).to.equal(3); - expect(batch2.events[0].data.event_name).to.equal('Test Event 1'); - expect(batch2.events[1].data.event_name).to.equal('Test Event 2'); - expect(batch2.events[2].data.event_name).to.equal('Test Event 3'); - - // Log a third batch of events - window.mParticle.logEvent('Test Event 4'); - window.mParticle.logEvent('Test Event 5'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - const batch3 = JSON.parse(fetchMock.calls()[2][1].body as string); - - // Batch 3 should contain two custom events - expect(batch3.events.length).to.equal(2); - expect(batch3.events[0].data.event_name).to.equal('Test Event 4'); - expect(batch3.events[1].data.event_name).to.equal('Test Event 5'); - - done(); - }); - - // TODO: Investigate workflow with unshift vs push - // https://go.mparticle.com/work/SQDSDKS-5165 - it.skip('should keep batches in sequence for future retries when an HTTP 500 error occurs', (done) => { - // If batches cannot upload, they should be added back to the Batch Queue - // in the order they were created so they can be retransmitted. - - fetchMock.post(urls.events, 500); - - window.mParticle.config.flags = { - ...enableBatchingConfigFlags, - }; - - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - // Generates Batch 1 with Session Start + AST - - // Adds a custom event to Batch 1 - window.mParticle.logEvent('Test Event 0'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - expect( - fetchMock.called(), - 'FetchMock should have been called' - ).to.equal(true); - - const batch1 = JSON.parse(fetchMock.calls()[0][1].body as string); - - // Batch 1 should contain only session start, AST and a single event - // in this exact order - expect(batch1.events.length).to.equal(3); - expect(batch1.events[0].event_type).to.equal('session_start'); - expect(batch1.events[1].event_type).to.equal( - 'application_state_transition' - ); - expect(batch1.events[2].data.event_name).to.equal('Test Event 0'); - - // Batch 2 - window.mParticle.logEvent('Test Event 1'); - window.mParticle.logEvent('Test Event 2'); - window.mParticle.logEvent('Test Event 3'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - // Batch 3 - window.mParticle.logEvent('Test Event 4'); - window.mParticle.logEvent('Test Event 5'); - window.mParticle.logEvent('Test Event 6'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - // Reset clock so that the setTimeout return immediately - clock.restore(); - - setTimeout(() => { - const batchQueue = - window.mParticle.getInstance()._APIClient.uploader - .batchesQueuedForProcessing; - - expect(batchQueue.length).to.equal(3); - - expect(batchQueue[0].events[0].event_type).to.equal( - 'session_start' - ); - expect(batchQueue[0].events[1].event_type).to.equal( - 'application_state_transition' - ); - expect(batchQueue[0].events[2].data.event_name).to.equal('Test Event 0'); - - expect(batchQueue[1].events[0].data.event_name).to.equal( - 'Test Event 1' - ); - expect(batchQueue[1].events[1].data.event_name).to.equal( - 'Test Event 2' - ); - expect(batchQueue[1].events[2].data.event_name).to.equal( - 'Test Event 3' - ); - - expect(batchQueue[2].events[0].data.event_name).to.equal( - 'Test Event 4' - ); - expect(batchQueue[2].events[1].data.event_name).to.equal( - 'Test Event 5' - ); - expect(batchQueue[2].events[2].data.event_name).to.equal( - 'Test Event 6' - ); - - done(); - }, 0); - }); - - // TODO: Investigate workflow with unshift vs push - // https://go.mparticle.com/work/SQDSDKS-5165 - it.skip('should keep and retry batches in sequence if the transmission fails midway', (done) => { - // First request is successful, subsequent requests fail - fetchMock.post(urls.events, 200, { - overwriteRoutes: false, - repeat: 1, - }); - - fetchMock.post(urls.events, 429, { - overwriteRoutes: false, - }); - - window.mParticle.config.flags = { - ...enableBatchingConfigFlags, - }; - - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - // Generates Batch 1 with Session Start + AST - - // Adds a custom event to Batch 1 - window.mParticle.logEvent('Test Event 0'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - // Batch 2 - window.mParticle.logEvent('Test Event 1'); - window.mParticle.logEvent('Test Event 2'); - window.mParticle.logEvent('Test Event 3'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - // Batch 3 - window.mParticle.logEvent('Test Event 4'); - window.mParticle.logEvent('Test Event 5'); - window.mParticle.logEvent('Test Event 6'); - - // Manually initiate the upload process - turn event into batches and upload the batch - window.mParticle.upload(); - - // Reset timer so the setTimeout can trigger - clock.restore(); - - setTimeout(() => { - // Batch upload should be triggered 3 times, but only - // 2 should be waiting for retry - expect(fetchMock.calls().length).to.equal(3); - - const batchQueue = - window.mParticle.getInstance()._APIClient.uploader - .batchesQueuedForProcessing; - - expect(batchQueue.length).to.equal(2); - - expect(batchQueue[0].events[0].data.event_name).to.equal( - 'Test Event 1' - ); - expect(batchQueue[0].events[1].data.event_name).to.equal( - 'Test Event 2' - ); - expect(batchQueue[0].events[2].data.event_name).to.equal( - 'Test Event 3' - ); - - expect(batchQueue[1].events[0].data.event_name).to.equal( - 'Test Event 4' - ); - expect(batchQueue[1].events[1].data.event_name).to.equal( - 'Test Event 5' - ); - expect(batchQueue[1].events[2].data.event_name).to.equal( - 'Test Event 6' - ); - - done(); - }, 0); - }); - }); - - describe('batching via window.fetch', () => { - beforeEach(() => { - fetchMock.post(urls.events, 200); - fetchMock.config.overwriteRoutes = true; - clock = sinon.useFakeTimers({now: new Date().getTime()}); - - window.mParticle.config.flags = { - ...enableBatchingConfigFlags, - }; - }); - - afterEach(() => { - fetchMock.restore(); - sinon.restore(); - clock.restore(); - }); - - it('should use custom v3 endpoint', function(done) { - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logEvent('Test Event'); - - // Identity call is via XHR request - // Session start, AST, and `Test Event` are queued. - mockServer.requests.length.should.equal(1); - - // Tick forward 1000 ms to hit upload interval and force an upload - clock.tick(1000); - - const lastCall = fetchMock.lastCall(); - const endpoint = lastCall[0]; - const batch = JSON.parse(fetchMock.lastCall()[1].body as string); - - endpoint.should.equal(urls.events); - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Test Event'); - - done(); - }); - - it('should have latitude/longitude for location when batching', function(done) { - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.setPosition(100, 100); - window.mParticle.logEvent('Test Event'); - clock.tick(1000); - - // Tick forward 1000 ms to hit upload interval and force an upload - const lastCall = fetchMock.lastCall(); - const endpoint = lastCall[0]; - const batch = JSON.parse(fetchMock.lastCall()[1].body as string); - endpoint.should.equal(urls.events); - batch.events[2].data.location.should.have.property('latitude', 100) - batch.events[2].data.location.should.have.property('longitude', 100) - - done(); - }); - - it('should force uploads when using public `upload`', function(done) { - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logEvent('Test Event'); - - // Identity call is via XHR request - // Session start, AST, and `Test Event` are queued. - mockServer.requests.length.should.equal(1); - - // no calls made to fetch yet - (fetchMock.lastCall() === undefined).should.equal(true) - - // force upload, triggering window.fetch - window.mParticle.upload(); - - const lastCall = fetchMock.lastCall(); - const endpoint = lastCall[0]; - const batch = JSON.parse(fetchMock.lastCall()[1].body as string); - - endpoint.should.equal(urls.events); - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Test Event'); - - done(); - }); - - it('should force uploads when a commerce event is called', function(done) { - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logEvent('Test Event'); - // Identity call is via XHR request - // Session start, AST, and `Test Event` are queued. - mockServer.requests.length.should.equal(1); - - var product1 = window.mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); - window.mParticle.eCommerce.logProductAction(SDKProductActionType.AddToCart, product1); - - const lastCall = fetchMock.lastCall(); - const endpoint = lastCall[0]; - const batch = JSON.parse(fetchMock.lastCall()[1].body as string); - - endpoint.should.equal(urls.events); - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Test Event'); - batch.events[3].event_type.should.equal('commerce_event'); - batch.events[3].data.product_action.action.should.equal('add_to_cart'); - - done(); - }); - - it('should return pending uploads if a 500 is returned', function(done) { - window.mParticle._resetForTests(MPConfig); - - fetchMock.post(urls.events, 500); - - window.mParticle.init(apiKey, window.mParticle.config); - window.mParticle.logEvent('Test Event'); - - let pendingEvents = window.mParticle.getInstance()._APIClient.uploader.eventsQueuedForProcessing; - - pendingEvents.length.should.equal(3) - pendingEvents[0].EventName.should.equal(1); - pendingEvents[1].EventName.should.equal(10); - pendingEvents[2].EventName.should.equal('Test Event'); - - fetchMock.post(urls.events, 200); - - (fetchMock.lastCall() === undefined).should.equal(true); - clock.tick(1000); - - let nowPendingEvents = window.mParticle.getInstance()._APIClient.uploader.eventsQueuedForProcessing; - nowPendingEvents.length.should.equal(0); - - const batch = JSON.parse(fetchMock.lastCall()[1].body as string); - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Test Event'); - - done(); - }); - - it('should send source_message_id with events to v3 endpoint', function(done) { - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logEvent('Test Event'); - - // Tick forward 1000 ms to hit upload interval and force an upload - clock.tick(1000); - - const lastCall = fetchMock.lastCall(); - const endpoint = lastCall[0]; - const batch = JSON.parse(fetchMock.lastCall()[1].body as string); - - endpoint.should.equal(urls.events); - batch.events[0].data.should.have.property('source_message_id') - - done(); - }); - - it('should send user-defined SourceMessageId events to v3 endpoint', function(done) { - window.mParticle._resetForTests(MPConfig); - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logBaseEvent({ - messageType: 4, - name: 'Test Event', - data: {key: 'value'}, - eventType: 3, - customFlags: {flagKey: 'flagValue'}, - sourceMessageId: 'abcdefg' - }); - - // Identity call is via XHR request - // Session start, AST, and `Test Event` are queued. - mockServer.requests.length.should.equal(1); - - // Tick forward 1000 ms to hit upload interval and force an upload - clock.tick(1000); - - const lastCall = fetchMock.lastCall(); - const endpoint = lastCall[0]; - const batch = JSON.parse(fetchMock.lastCall()[1].body as string); - - endpoint.should.equal(urls.events); - // event batch includes session start, ast, then last event is Test Event - batch.events[batch.events.length-1].data.should.have.property('source_message_id', 'abcdefg') - - done(); - }); - - it('should call the identity callback after a session ends if user is returning to the page after a long period of time', async function() { - // Background of bug that this test fixes: - // User navigates away from page and returns after some time - // and the session should end. There is a UAC firing inside of - // config.identityCallback, which would send to our servers with - // the previous session ID because the identity call back fired - // before the session logic that determines if a new session should - // start. - - window.mParticle._resetForTests(MPConfig); - - window.mParticle.config.identityCallback = function(result) { - let currentUser = result.getUser() - if (currentUser) { - // TODO: Investigate if we should update definitely typed typings for - // setUserAttribute which only allows strings right now - // more context at https://go.mparticle.com/work/SQDSDKS-4576 - currentUser.setUserAttribute("number", `${Math.floor((Math.random() * 1000) + 1)}`) - } - } - - var endSessionFunction = window.mParticle.getInstance()._SessionManager.endSession; - - window.mParticle.init(apiKey, window.mParticle.config); - - // Mock end session so that the SDK doesn't actually send it. We do this - // to mimic a return to page behavior, below: - window.mParticle.getInstance()._SessionManager.endSession = function() {} - - // Force 35 minutes to pass, so that when we return to the page, when - // the SDK initializes it will know to end the session. - clock.tick(35*60000); - - // Undo mock of end session so that when we initializes, it will end - // the session for real. - window.mParticle.getInstance()._SessionManager.endSession = endSessionFunction; - - // Initialize imitates returning to the page - window.mParticle.init(apiKey, window.mParticle.config); - - // Manually initiate the upload process - turn event into batches and upload the batch - await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); - - // We have to restore the clock in order to use setTimeout below - const batch1 = JSON.parse(fetchMock.calls()[0][1].body as string); - const batch2 = JSON.parse(fetchMock.calls()[1][1].body as string); - const batch3 = JSON.parse(fetchMock.calls()[2][1].body as string); - - // UAC, session start, AST - expect( - batch1.events.length, - 'Batch 1: UAC Event, Session Start, AST' - ).to.equal(3); - - // session end - expect(batch2.events.length, 'Batch 2: Session End').to.equal( - 1 - ); - - // UAC, session start, AST - expect( - batch3.events.length, - 'Batch 3: UAC, Session Start, AST' - ).to.equal(3); - - const batch1UAC = Utils.findEventFromBatch( - batch1, - 'user_attribute_change' - ); - const batch1SessionStart = Utils.findEventFromBatch( - batch1, - 'session_start' - ); - const batch1AST = Utils.findEventFromBatch( - batch1, - 'application_state_transition' - ); - - batch1UAC.should.be.ok(); - batch1SessionStart.should.be.ok(); - batch1AST.should.be.ok(); - - const batch2SessionEnd = Utils.findEventFromBatch( - batch2, - 'session_end' - ); - batch2SessionEnd.should.be.ok(); - - const batch3UAC = Utils.findEventFromBatch( - batch3, - 'user_attribute_change' - ); - const batch3SessionStart = Utils.findEventFromBatch( - batch3, - 'session_start' - ); - const batch3AST = Utils.findEventFromBatch( - batch3, - 'application_state_transition' - ); - - batch3UAC.should.be.ok(); - batch3SessionStart.should.be.ok(); - batch3AST.should.be.ok(); - - - (typeof batch1.source_request_id).should.equal('string'); - (typeof batch2.source_request_id).should.equal('string'); - (typeof batch3.source_request_id).should.equal('string'); - - batch1.source_request_id.should.not.equal( - batch2.source_request_id - ); - batch1.source_request_id.should.not.equal( - batch3.source_request_id - ); - batch2.source_request_id.should.not.equal( - batch3.source_request_id - ); - - batch1UAC.data.session_uuid.should.equal( - batch1AST.data.session_uuid - ); - batch1UAC.data.session_uuid.should.equal( - batch1SessionStart.data.session_uuid - ); - batch1UAC.data.session_uuid.should.not.equal( - batch3UAC.data.session_uuid - ); - batch1UAC.data.session_uuid.should.not.equal( - batch3SessionStart.data.session_uuid - ); - batch1UAC.data.session_uuid.should.not.equal( - batch3AST.data.session_uuid - ); - - batch1UAC.data.session_start_unixtime_ms.should.equal( - batch1AST.data.session_start_unixtime_ms - ); - batch1UAC.data.session_start_unixtime_ms.should.equal( - batch1SessionStart.data.session_start_unixtime_ms - ); - batch1UAC.data.session_start_unixtime_ms.should.not.equal( - batch3UAC.data.session_start_unixtime_ms - ); - batch1UAC.data.session_start_unixtime_ms.should.not.equal( - batch3SessionStart.data.session_start_unixtime_ms - ); - batch1UAC.data.session_start_unixtime_ms.should.not.equal( - batch3AST.data.session_start_unixtime_ms - ); - - batch1SessionStart.data.session_uuid.should.equal( - batch1AST.data.session_uuid - ); - batch1SessionStart.data.session_uuid.should.equal( - batch2SessionEnd.data.session_uuid - ); - batch1AST.data.session_uuid.should.equal( - batch2SessionEnd.data.session_uuid - ); - - batch1SessionStart.data.session_start_unixtime_ms.should.equal( - batch1AST.data.session_start_unixtime_ms - ); - batch1SessionStart.data.session_start_unixtime_ms.should.equal( - batch2SessionEnd.data.session_start_unixtime_ms - ); - batch1AST.data.session_start_unixtime_ms.should.equal( - batch2SessionEnd.data.session_start_unixtime_ms - ); - - batch3AST.data.session_uuid.should.equal( - batch3UAC.data.session_uuid - ); - batch3SessionStart.data.session_uuid.should.equal( - batch3UAC.data.session_uuid - ); - batch3SessionStart.data.session_uuid.should.equal( - batch3AST.data.session_uuid - ); - - batch3AST.data.session_start_unixtime_ms.should.equal( - batch3UAC.data.session_start_unixtime_ms - ); - batch3SessionStart.data.session_start_unixtime_ms.should.equal( - batch3UAC.data.session_start_unixtime_ms - ); - batch3SessionStart.data.session_start_unixtime_ms.should.equal( - batch3AST.data.session_start_unixtime_ms - ); - }); - }); - - describe('batching via XHR for older browsers without window.fetch', () => { - var fetch = window.fetch; - - beforeEach(() => { - delete window.fetch - window.mParticle.config.flags = { - eventBatchingIntervalMillis: 1000, - } - mockServer.respondWith(urls.events, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, Store: {}}) - ]); - clock = sinon.useFakeTimers(); - }); - - afterEach(() => { - window.mParticle._resetForTests(MPConfig); - window.fetch = fetch; - sinon.restore(); - clock.restore(); - }); - - it('should use custom v3 endpoint', function(done) { - window.mParticle._resetForTests(MPConfig); - - mockServer.requests = []; - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logEvent('Test Event'); - - // The only request to the server should be the identify call - // Session start, AST, and Test Event are queued. - mockServer.requests.length.should.equal(1); - // Upload interval hit, now will send requests - clock.tick(1000); - - // 1st request is /Identity call, 2nd request is /Event call - const batch = JSON.parse(mockServer.secondRequest.requestBody); - - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Test Event'); - - done(); - }); - - it('should force uploads when using public `upload`', function(done) { - window.mParticle._resetForTests(MPConfig); - - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logEvent('Test Event'); - - // The only request to the server should be the identify call - // Session start, AST, and Test Event are queued. - mockServer.requests.length.should.equal(1); - // Upload interval hit, now will send requests - clock.tick(1000); - window.mParticle.upload(); - // 1st request is /Identity call, 2nd request is /Event call - const batch = JSON.parse(mockServer.secondRequest.requestBody); - - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Test Event'); - - done(); - }); - - it('should trigger an upload of batch when a commerce event is called', function(done) { - window.mParticle._resetForTests(MPConfig); - - window.mParticle.init(apiKey, window.mParticle.config); - - window.mParticle.logEvent('Test Event'); - // The only request to the server should be the identify call - // Session start, AST, and Test Event are queued. - mockServer.requests.length.should.equal(1); - - var product1 = window.mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); - window.mParticle.eCommerce.logProductAction(SDKProductActionType.AddToCart, product1); - // 1st request is /Identity call, 2nd request is /Event call - const batch = JSON.parse(mockServer.secondRequest.requestBody); - - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Test Event'); - batch.events[3].event_type.should.equal('commerce_event'); - batch.events[3].data.product_action.action.should.equal('add_to_cart'); - - done(); - }); - - it('should trigger an upload of batch when a UIC occurs', function(done) { - window.mParticle._resetForTests(MPConfig); - // include an identify request so that it creates a UIC - window.mParticle.config.identifyRequest = { - userIdentities: { - customerid: 'foo-customer-id' - } - }; - - window.mParticle.init(apiKey, window.mParticle.config); - - // Requests sent should be identify call, then UIC event - // Session start, AST, and Test Event are queued, and don't appear - // in the mockServer.requests - mockServer.requests.length.should.equal(2); - - // 1st request is /Identity call, 2nd request is UIC call - const batch = JSON.parse(mockServer.secondRequest.requestBody); - - batch.events[0].event_type.should.equal('user_identity_change'); - - // force upload of other events - window.mParticle.upload() - const batch2 = JSON.parse(mockServer.thirdRequest.requestBody); - - batch2.events[0].event_type.should.equal('session_start'); - batch2.events[1].event_type.should.equal('application_state_transition'); - - done(); - }); - - // Originally, we had the Batch uploader set to force an upload when a UAC event - // was triggered. This feature was removed and we are including this test to - // make sure the Web SDK does not regress. This test will be removed in a future - // Web SDK update - // TODO: https://go.mparticle.com/work/SQDSDKS-5891 - it('should NOT trigger an upload of batch when a UAC occurs', function(done) { - window.mParticle._resetForTests(MPConfig); - - window.mParticle.init(apiKey, window.mParticle.config); - - // Set a user attribute to trigger a UAC event - window.mParticle.Identity.getCurrentUser().setUserAttribute('age', 25); - - // Requests sent should be identify call - // Since we no longer force an upload for UAC, - // This request will contain a /Identity Call - // a future request will contain session start, AST, and UAC - mockServer.requests.length.should.equal(1); - - // Verifies that adding a UAC does not trigger an upload - expect(mockServer.secondRequest).to.equal(null); - - // Manually force an upload - window.mParticle.upload(); - - // Second request has now been made - expect(mockServer.secondRequest).to.be.ok; - - const batch = JSON.parse(mockServer.secondRequest.requestBody); - - // Batch should now contain the 3 events we expect - mockServer.requests.length.should.equal(2); - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('user_attribute_change'); - - done(); - }); - - it('should return pending uploads if a 500 is returned', function(done) { - window.mParticle._resetForTests(MPConfig); - - mockServer.respondWith(urls.events, [ - 500, - {}, - JSON.stringify({ mpid: testMPID, Store: {}}) - ]); - - window.mParticle.init(apiKey, window.mParticle.config); - window.mParticle.logEvent('Test Event'); - - const pendingEvents = window.mParticle.getInstance()._APIClient.uploader.eventsQueuedForProcessing; - - pendingEvents.length.should.equal(3) - pendingEvents[0].EventName.should.equal(1); - pendingEvents[1].EventName.should.equal(10); - pendingEvents[2].EventName.should.equal('Test Event'); - - clock.tick(1000); - - const nowPendingEvents = window.mParticle.getInstance()._APIClient.uploader.eventsQueuedForProcessing; - nowPendingEvents.length.should.equal(0); - - const batch = JSON.parse(mockServer.secondRequest.requestBody); - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Test Event'); - done(); - }); - - it('should add a modified boolean of true to a batch that has been modified via a config.onCreateBatch call', function(done) { - window.mParticle._resetForTests(MPConfig); - - window.mParticle.config.onCreateBatch = function (batch: Batch) { - return batch - }; - - window.mParticle.init(apiKey, window.mParticle.config); - window.mParticle.logEvent('Test Event'); - - clock.tick(1000); - - const batch = JSON.parse(mockServer.secondRequest.requestBody); - batch.modified.should.equal(true); - done(); - }); - - it('should respect rules for the batch modification', function(done) { - window.mParticle._resetForTests(MPConfig); - - window.mParticle.config.onCreateBatch = function (batch) { - batch.events.map(event => { - if (event.event_type === "custom_event") { - (event.data as CustomEventData).event_name = 'Modified!' - } - }); - return batch; - }; - - window.mParticle.init(apiKey, window.mParticle.config); - window.mParticle.logEvent('Test Event'); - - clock.tick(1000); - - const batch = JSON.parse(mockServer.secondRequest.requestBody); - batch.events.length.should.equal(3); - batch.events[0].event_type.should.equal('session_start'); - batch.events[1].event_type.should.equal('application_state_transition'); - batch.events[2].event_type.should.equal('custom_event'); - batch.events[2].data.event_name.should.equal('Modified!'); - done(); - }); - - it('should add a modified boolean of true to a batch that has been modified via a config.onCreateBatch call', function(done) { - window.mParticle._resetForTests(MPConfig); - - window.mParticle.config.onCreateBatch = function (batch: Batch) { - return undefined; - }; - - window.mParticle.init(apiKey, window.mParticle.config); - window.mParticle.logEvent('Test Event'); - - clock.tick(1000); - - (mockServer.secondRequest === null).should.equal(true); - done(); + }) + .catch((e) => { + }) }); }); }); \ No newline at end of file diff --git a/test/src/tests-batchUploader_2.ts b/test/src/tests-batchUploader_2.ts new file mode 100644 index 00000000..0208068e --- /dev/null +++ b/test/src/tests-batchUploader_2.ts @@ -0,0 +1,305 @@ +import sinon from 'sinon'; +import { urls, apiKey, MPConfig, testMPID } from './config/constants'; +import { + BaseEvent, + MParticleWebSDK, + SDKEvent, + SDKProductActionType, +} from '../../src/sdkRuntimeModels'; +import { Batch, CustomEventData } from '@mparticle/event-models'; +import Utils from './config/utils'; +import { BatchUploader } from '../../src/batchUploader'; +import { expect } from 'chai'; +import _BatchValidator from '../../src/mockBatchCreator'; +import Logger from '../../src/logger.js'; +import { event0, event1, event2, event3 } from '../fixtures/events'; +import fetchMock from 'fetch-mock/esm/client'; +const { fetchMockSuccess, waitForCondition, hasIdentifyReturned } = Utils; + +declare global { + interface Window { + mParticle: MParticleWebSDK; + } +} + +const enableBatchingConfigFlags = { + eventBatchingIntervalMillis: 1000, +}; + +describe('batch uploader', () => { + let mockServer; + let clock; + + beforeEach(() => { + fetchMock.restore(); + fetchMock.config.overwriteRoutes = true; + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); + fetchMock.post(urls.events, 200); + }); + + afterEach(() => { + fetchMock.restore(); + window.localStorage.clear(); + }); + + describe('Upload Workflow', () => { + beforeEach(() => { + }); + + afterEach(() => { + fetchMock.restore(); + }); + + it('should organize events in the order they are processed and maintain that order when uploading', (done) => { + // Batches should be uploaded in the order they were created to prevent + // any potential corruption. + fetchMock.post(urls.events, 200); + fetchMock.config.overwriteRoutes = true; + + window.mParticle.config.flags = { + ...enableBatchingConfigFlags, + }; + + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event 0'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + expect( + fetchMock.called(), + 'FetchMock should have been called' + ).to.equal(true); + + const batch1 = JSON.parse(fetchMock.lastCall()[1].body as string); + + // Batch 1 should contain only session start, AST and a single event + // in this exact order + expect(batch1.events.length).to.equal(3); + expect(batch1.events[0].event_type).to.equal('session_start'); + expect(batch1.events[1].event_type).to.equal( + 'application_state_transition' + ); + expect(batch1.events[2].data.event_name).to.equal('Test Event 0'); + + // Log a second batch of events + window.mParticle.logEvent('Test Event 1'); + window.mParticle.logEvent('Test Event 2'); + window.mParticle.logEvent('Test Event 3'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + const batch2 = JSON.parse(fetchMock.lastCall()[1].body as string); + + // Batch 2 should contain three custom events + expect(batch2.events.length).to.equal(3); + expect(batch2.events[0].data.event_name).to.equal('Test Event 1'); + expect(batch2.events[1].data.event_name).to.equal('Test Event 2'); + expect(batch2.events[2].data.event_name).to.equal('Test Event 3'); + + // Log a third batch of events + window.mParticle.logEvent('Test Event 4'); + window.mParticle.logEvent('Test Event 5'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + const batch3 = JSON.parse(fetchMock.lastCall()[1].body as string); + + // Batch 3 should contain two custom events + expect(batch3.events.length).to.equal(2); + expect(batch3.events[0].data.event_name).to.equal('Test Event 4'); + expect(batch3.events[1].data.event_name).to.equal('Test Event 5'); + + done(); + + }) + .catch((e) => { + }) + }); + + // TODO: Investigate workflow with unshift vs push + // https://go.mparticle.com/work/SQDSDKS-5165 + it.skip('should keep batches in sequence for future retries when an HTTP 500 error occurs', (done) => { + // If batches cannot upload, they should be added back to the Batch Queue + // in the order they were created so they can be retransmitted. + + fetchMock.post(urls.events, 500); + + window.mParticle.config.flags = { + ...enableBatchingConfigFlags, + }; + + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + // Generates Batch 1 with Session Start + AST + + // Adds a custom event to Batch 1 + window.mParticle.logEvent('Test Event 0'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + expect( + fetchMock.called(), + 'FetchMock should have been called' + ).to.equal(true); + + const batch1 = JSON.parse(fetchMock.calls()[0][1].body as string); + + // Batch 1 should contain only session start, AST and a single event + // in this exact order + expect(batch1.events.length).to.equal(3); + expect(batch1.events[0].event_type).to.equal('session_start'); + expect(batch1.events[1].event_type).to.equal( + 'application_state_transition' + ); + expect(batch1.events[2].data.event_name).to.equal('Test Event 0'); + + // Batch 2 + window.mParticle.logEvent('Test Event 1'); + window.mParticle.logEvent('Test Event 2'); + window.mParticle.logEvent('Test Event 3'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + // Batch 3 + window.mParticle.logEvent('Test Event 4'); + window.mParticle.logEvent('Test Event 5'); + window.mParticle.logEvent('Test Event 6'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + // Reset clock so that the setTimeout return immediately + clock.restore(); + + setTimeout(() => { + const batchQueue = + window.mParticle.getInstance()._APIClient.uploader + .batchesQueuedForProcessing; + + expect(batchQueue.length).to.equal(3); + + expect(batchQueue[0].events[0].event_type).to.equal( + 'session_start' + ); + expect(batchQueue[0].events[1].event_type).to.equal( + 'application_state_transition' + ); + expect(batchQueue[0].events[2].data.event_name).to.equal('Test Event 0'); + + expect(batchQueue[1].events[0].data.event_name).to.equal( + 'Test Event 1' + ); + expect(batchQueue[1].events[1].data.event_name).to.equal( + 'Test Event 2' + ); + expect(batchQueue[1].events[2].data.event_name).to.equal( + 'Test Event 3' + ); + + expect(batchQueue[2].events[0].data.event_name).to.equal( + 'Test Event 4' + ); + expect(batchQueue[2].events[1].data.event_name).to.equal( + 'Test Event 5' + ); + expect(batchQueue[2].events[2].data.event_name).to.equal( + 'Test Event 6' + ); + + done(); + }, 0); + }); + + // TODO: Investigate workflow with unshift vs push + // https://go.mparticle.com/work/SQDSDKS-5165 + it.skip('should keep and retry batches in sequence if the transmission fails midway', (done) => { + // First request is successful, subsequent requests fail + fetchMock.post(urls.events, 200, { + overwriteRoutes: false, + repeat: 1, + }); + + fetchMock.post(urls.events, 429, { + overwriteRoutes: false, + }); + + window.mParticle.config.flags = { + ...enableBatchingConfigFlags, + }; + + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + // Generates Batch 1 with Session Start + AST + + // Adds a custom event to Batch 1 + window.mParticle.logEvent('Test Event 0'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + // Batch 2 + window.mParticle.logEvent('Test Event 1'); + window.mParticle.logEvent('Test Event 2'); + window.mParticle.logEvent('Test Event 3'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + // Batch 3 + window.mParticle.logEvent('Test Event 4'); + window.mParticle.logEvent('Test Event 5'); + window.mParticle.logEvent('Test Event 6'); + + // Manually initiate the upload process - turn event into batches and upload the batch + window.mParticle.upload(); + + // Reset timer so the setTimeout can trigger + clock.restore(); + + setTimeout(() => { + // Batch upload should be triggered 3 times, but only + // 2 should be waiting for retry + expect(fetchMock.calls().length).to.equal(3); + + const batchQueue = + window.mParticle.getInstance()._APIClient.uploader + .batchesQueuedForProcessing; + + expect(batchQueue.length).to.equal(2); + + expect(batchQueue[0].events[0].data.event_name).to.equal( + 'Test Event 1' + ); + expect(batchQueue[0].events[1].data.event_name).to.equal( + 'Test Event 2' + ); + expect(batchQueue[0].events[2].data.event_name).to.equal( + 'Test Event 3' + ); + + expect(batchQueue[1].events[0].data.event_name).to.equal( + 'Test Event 4' + ); + expect(batchQueue[1].events[1].data.event_name).to.equal( + 'Test Event 5' + ); + expect(batchQueue[1].events[2].data.event_name).to.equal( + 'Test Event 6' + ); + + done(); + }, 0); + }); + }); +}); \ No newline at end of file diff --git a/test/src/tests-batchUploader_3.ts b/test/src/tests-batchUploader_3.ts new file mode 100644 index 00000000..6fad8868 --- /dev/null +++ b/test/src/tests-batchUploader_3.ts @@ -0,0 +1,450 @@ +import sinon from 'sinon'; +import { urls, apiKey, MPConfig, testMPID } from './config/constants'; +import { + BaseEvent, + MParticleWebSDK, + SDKEvent, + SDKProductActionType, +} from '../../src/sdkRuntimeModels'; +import { Batch, CustomEventData } from '@mparticle/event-models'; +import Utils from './config/utils'; +import { BatchUploader } from '../../src/batchUploader'; +import { expect } from 'chai'; +import _BatchValidator from '../../src/mockBatchCreator'; +import Logger from '../../src/logger.js'; +import { event0, event1, event2, event3 } from '../fixtures/events'; +import fetchMock from 'fetch-mock/esm/client'; +const { fetchMockSuccess, waitForCondition, hasIdentifyReturned } = Utils; + +declare global { + interface Window { + mParticle: MParticleWebSDK; + } +} + +const enableBatchingConfigFlags = { + eventBatchingIntervalMillis: 1000, +}; + +describe('batch uploader', () => { + let mockServer; + let clock; + + beforeEach(() => { + fetchMock.restore(); + fetchMock.config.overwriteRoutes = true; + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); + fetchMock.post(urls.events, 200); + }); + + afterEach(() => { + fetchMock.restore(); + window.localStorage.clear(); + }); + + describe('batching via window.fetch', () => { + beforeEach(() => { + fetchMock.post(urls.events, 200); + fetchMock.config.overwriteRoutes = true; + window.mParticle.config.flags.eventBatchingIntervalMillis = 1000; + }); + + afterEach(() => { + fetchMock.restore(); + window.mParticle.config.flags.eventBatchingIntervalMillis = 0; + }); + + it('should use custom v3 endpoint', function(done) { + window.mParticle._resetForTests(MPConfig); + fetchMock.resetHistory(); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + window.mParticle.upload(); + + const lastCall = fetchMock.lastCall(); + const endpoint = lastCall[0]; + const batch = JSON.parse(fetchMock.lastCall()[1].body as string); + + endpoint.should.equal(urls.events); + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Test Event'); + + done(); + }) + }); + + it('should have latitude/longitude for location when batching', function(done) { + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.setPosition(100, 100); + window.mParticle.logEvent('Test Event'); + window.mParticle.upload(); + const lastCall = fetchMock.lastCall(); + const endpoint = lastCall[0]; + const batch = JSON.parse(fetchMock.lastCall()[1].body as string); + endpoint.should.equal(urls.events); + batch.events[2].data.location.should.have.property('latitude', 100) + batch.events[2].data.location.should.have.property('longitude', 100) + + done(); + }) + }); + + it('should force uploads when using public `upload`', function(done) { + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + // Identity call + // Session start, AST, and `Test Event` are queued. + fetchMock.calls().length.should.equal(1); + (fetchMock.lastCall()[0].endsWith('identify')).should.equal(true) + + // force upload, triggering window.fetch + window.mParticle.upload(); + + const lastCall = fetchMock.lastCall(); + const endpoint = lastCall[0]; + const batch = JSON.parse(fetchMock.lastCall()[1].body as string); + + endpoint.should.equal(urls.events); + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Test Event'); + + done(); + }) + }); + + it('should force uploads when a commerce event is called', function(done) { + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + var product1 = window.mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); + window.mParticle.eCommerce.logProductAction(SDKProductActionType.AddToCart, product1); + + const lastCall = fetchMock.lastCall(); + const endpoint = lastCall[0]; + const batch = JSON.parse(fetchMock.lastCall()[1].body as string); + + endpoint.should.equal(urls.events); + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Test Event'); + batch.events[3].event_type.should.equal('commerce_event'); + batch.events[3].data.product_action.action.should.equal('add_to_cart'); + + done(); + }); + }); + + it('should return pending uploads if a 500 is returned', function(done) { + window.mParticle._resetForTests(MPConfig); + + fetchMock.post(urls.events, 500); + + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + let pendingEvents = window.mParticle.getInstance()._APIClient.uploader.eventsQueuedForProcessing; + + pendingEvents.length.should.equal(3) + pendingEvents[0].EventName.should.equal(1); + pendingEvents[1].EventName.should.equal(10); + pendingEvents[2].EventName.should.equal('Test Event'); + + fetchMock.post(urls.events, 200); + + // First fetch call is an identify call + (fetchMock.lastCall()[0].endsWith('identify')).should.equal(true) + window.mParticle.upload(); + + let nowPendingEvents = window.mParticle.getInstance()._APIClient.uploader.eventsQueuedForProcessing; + nowPendingEvents.length.should.equal(0); + + const batch = JSON.parse(fetchMock.lastCall()[1].body as string); + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Test Event'); + + done(); + }) + }); + + it('should send source_message_id with events to v3 endpoint', function(done) { + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + window.mParticle.upload(); + + const lastCall = fetchMock.lastCall(); + const endpoint = lastCall[0]; + const batch = JSON.parse(fetchMock.lastCall()[1].body as string); + + endpoint.should.equal(urls.events); + batch.events[0].data.should.have.property('source_message_id') + + done(); + }) + }); + + it('should send user-defined SourceMessageId events to v3 endpoint', function(done) { + window.mParticle._resetForTests(MPConfig); + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logBaseEvent({ + messageType: 4, + name: 'Test Event', + data: {key: 'value'}, + eventType: 3, + customFlags: {flagKey: 'flagValue'}, + sourceMessageId: 'abcdefg' + }); + + window.mParticle.upload(); + + const lastCall = fetchMock.lastCall(); + const endpoint = lastCall[0]; + const batch = JSON.parse(fetchMock.lastCall()[1].body as string); + + endpoint.should.equal(urls.events); + // event batch includes session start, ast, then last event is Test Event + batch.events[batch.events.length-1].data.should.have.property('source_message_id', 'abcdefg') + + done(); + }) + }); + + it('should call the identity callback after a session ends if user is returning to the page after a long period of time', async function() { + // Background of bug that this test fixes: + // User navigates away from page and returns after some time + // and the session should end. There is a UAC firing inside of + // config.identityCallback, which would send to our servers with + // the previous session ID because the identity call back fired + // before the session logic that determines if a new session should + // start. + + window.mParticle._resetForTests(MPConfig); + + window.mParticle.config.identityCallback = function(result) { + let currentUser = result.getUser() + if (currentUser) { + // TODO: Investigate if we should update definitely typed typings for + // setUserAttribute which only allows strings right now + // more context at https://go.mparticle.com/work/SQDSDKS-4576 + currentUser.setUserAttribute("number", `${Math.floor((Math.random() * 1000) + 1)}`) + } + } + + var endSessionFunction = window.mParticle.getInstance()._SessionManager.endSession; + + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()._Store?.identityCallInFlight === false + ); + }) + .then(() => { + // Mock end session so that the SDK doesn't actually send it. We do this + // to mimic a return to page behavior, below: + window.mParticle.getInstance()._SessionManager.endSession = function() {} + clock = sinon.useFakeTimers({now: new Date().getTime()}); + + // Force 35 minutes to pass, so that when we return to the page, when + // the SDK initializes it will know to end the session. + clock.tick(35*60000); + + // Undo mock of end session so that when we initializes, it will end + // the session for real. + window.mParticle.getInstance()._SessionManager.endSession = endSessionFunction; + clock.restore(); + + // Initialize imitates returning to the page + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(async () => { + // Manually initiate the upload process - turn event into batches and upload the batch + await window.mParticle.getInstance()._APIClient.uploader.prepareAndUpload(); + + const batch1 = JSON.parse(fetchMock.calls()[0][1].body as string); + const batch2 = JSON.parse(fetchMock.calls()[1][1].body as string); + const batch3 = JSON.parse(fetchMock.calls()[2][1].body as string); + + // UAC, session start, AST + expect( + batch1.events.length, + 'Batch 1: UAC Event, Session Start, AST' + ).to.equal(3); + + // session end + expect(batch2.events.length, 'Batch 2: Session End').to.equal( + 1 + ); + + // UAC, session start, AST + expect( + batch3.events.length, + 'Batch 3: UAC, Session Start, AST' + ).to.equal(3); + + const batch1UAC = Utils.findEventFromBatch( + batch1, + 'user_attribute_change' + ); + const batch1SessionStart = Utils.findEventFromBatch( + batch1, + 'session_start' + ); + const batch1AST = Utils.findEventFromBatch( + batch1, + 'application_state_transition' + ); + + batch1UAC.should.be.ok(); + batch1SessionStart.should.be.ok(); + batch1AST.should.be.ok(); + + const batch2SessionEnd = Utils.findEventFromBatch( + batch2, + 'session_end' + ); + batch2SessionEnd.should.be.ok(); + + const batch3UAC = Utils.findEventFromBatch( + batch3, + 'user_attribute_change' + ); + const batch3SessionStart = Utils.findEventFromBatch( + batch3, + 'session_start' + ); + const batch3AST = Utils.findEventFromBatch( + batch3, + 'application_state_transition' + ); + + batch3UAC.should.be.ok(); + batch3SessionStart.should.be.ok(); + batch3AST.should.be.ok(); + + + (typeof batch1.source_request_id).should.equal('string'); + (typeof batch2.source_request_id).should.equal('string'); + (typeof batch3.source_request_id).should.equal('string'); + + batch1.source_request_id.should.not.equal( + batch2.source_request_id + ); + batch1.source_request_id.should.not.equal( + batch3.source_request_id + ); + batch2.source_request_id.should.not.equal( + batch3.source_request_id + ); + + batch1UAC.data.session_uuid.should.equal( + batch1AST.data.session_uuid + ); + batch1UAC.data.session_uuid.should.equal( + batch1SessionStart.data.session_uuid + ); + batch1UAC.data.session_uuid.should.not.equal( + batch3UAC.data.session_uuid + ); + batch1UAC.data.session_uuid.should.not.equal( + batch3SessionStart.data.session_uuid + ); + batch1UAC.data.session_uuid.should.not.equal( + batch3AST.data.session_uuid + ); + + batch1UAC.data.session_start_unixtime_ms.should.equal( + batch1AST.data.session_start_unixtime_ms + ); + batch1UAC.data.session_start_unixtime_ms.should.equal( + batch1SessionStart.data.session_start_unixtime_ms + ); + batch1UAC.data.session_start_unixtime_ms.should.not.equal( + batch3UAC.data.session_start_unixtime_ms + ); + batch1UAC.data.session_start_unixtime_ms.should.not.equal( + batch3SessionStart.data.session_start_unixtime_ms + ); + batch1UAC.data.session_start_unixtime_ms.should.not.equal( + batch3AST.data.session_start_unixtime_ms + ); + + batch1SessionStart.data.session_uuid.should.equal( + batch1AST.data.session_uuid + ); + batch1SessionStart.data.session_uuid.should.equal( + batch2SessionEnd.data.session_uuid + ); + batch1AST.data.session_uuid.should.equal( + batch2SessionEnd.data.session_uuid + ); + + batch1SessionStart.data.session_start_unixtime_ms.should.equal( + batch1AST.data.session_start_unixtime_ms + ); + batch1SessionStart.data.session_start_unixtime_ms.should.equal( + batch2SessionEnd.data.session_start_unixtime_ms + ); + batch1AST.data.session_start_unixtime_ms.should.equal( + batch2SessionEnd.data.session_start_unixtime_ms + ); + + batch3AST.data.session_uuid.should.equal( + batch3UAC.data.session_uuid + ); + batch3SessionStart.data.session_uuid.should.equal( + batch3UAC.data.session_uuid + ); + batch3SessionStart.data.session_uuid.should.equal( + batch3AST.data.session_uuid + ); + + batch3AST.data.session_start_unixtime_ms.should.equal( + batch3UAC.data.session_start_unixtime_ms + ); + batch3SessionStart.data.session_start_unixtime_ms.should.equal( + batch3UAC.data.session_start_unixtime_ms + ); + batch3SessionStart.data.session_start_unixtime_ms.should.equal( + batch3AST.data.session_start_unixtime_ms + ); + + }) + }) + }); + }); +}); \ No newline at end of file diff --git a/test/src/tests-batchUploader_4.ts b/test/src/tests-batchUploader_4.ts new file mode 100644 index 00000000..6904c82d --- /dev/null +++ b/test/src/tests-batchUploader_4.ts @@ -0,0 +1,335 @@ +import sinon from 'sinon'; +import { urls, apiKey, MPConfig, testMPID } from './config/constants'; +import { + BaseEvent, + MParticleWebSDK, + SDKEvent, + SDKProductActionType, +} from '../../src/sdkRuntimeModels'; +import { Batch, CustomEventData } from '@mparticle/event-models'; +import Utils from './config/utils'; +import { BatchUploader } from '../../src/batchUploader'; +import { expect } from 'chai'; +import _BatchValidator from '../../src/mockBatchCreator'; +import Logger from '../../src/logger.js'; +import { event0, event1, event2, event3 } from '../fixtures/events'; +import fetchMock from 'fetch-mock/esm/client'; +const { fetchMockSuccess, waitForCondition, hasIdentifyReturned } = Utils; + +declare global { + interface Window { + mParticle: MParticleWebSDK; + } +} + +const enableBatchingConfigFlags = { + eventBatchingIntervalMillis: 1000, +}; + +describe('batch uploader', () => { + let mockServer; + let clock; + + beforeEach(() => { + fetchMock.restore(); + fetchMock.config.overwriteRoutes = true; + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); + fetchMock.post(urls.events, 200); + }); + + afterEach(() => { + fetchMock.restore(); + window.localStorage.clear(); + }); + + describe('batching via XHR for older browsers without window.fetch', () => { + var fetch = window.fetch; + + beforeEach(() => { + delete window.fetch + window.mParticle.config.flags = { + eventBatchingIntervalMillis: 1000, + } + mockServer = sinon.createFakeServer(); + mockServer.respondImmediately = true; + + mockServer.respondWith(urls.events, [ + 200, + {}, + JSON.stringify({ mpid: testMPID, Store: {}}) + ]); + mockServer.respondWith(urls.identify, [ + 200, + {}, + JSON.stringify({ mpid: testMPID, is_logged_in: false }), + ]); + }); + + afterEach(() => { + window.mParticle._resetForTests(MPConfig); + window.fetch = fetch; + sinon.restore(); + }); + + it('should use custom v3 endpoint', function(done) { + window.mParticle._resetForTests(MPConfig); + mockServer.requests = []; + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + mockServer.requests.length.should.equal(1); + window.mParticle.upload() + // 1st request is /Identity call, 2nd request is /Event call + + const batch = JSON.parse(mockServer.secondRequest.requestBody); + + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Test Event'); + + done(); + }).catch((e) => { + }) + }); + + it('should force uploads when using public `upload`', function(done) { + window.mParticle._resetForTests(MPConfig); + + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + + + window.mParticle.logEvent('Test Event'); + + // The only request to the server should be the identify call + // Session start, AST, and Test Event are queued. + mockServer.requests.length.should.equal(1); + // Upload interval hit, now will send requests + + window.mParticle.upload(); + // 1st request is /Identity call, 2nd request is /Event call + const batch = JSON.parse(mockServer.secondRequest.requestBody); + + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Test Event'); + + done(); + }) + }); + + it('should trigger an upload of batch when a commerce event is called', function(done) { + window.mParticle._resetForTests(MPConfig); + + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + // The only request to the server should be the identify call + // Session start, AST, and Test Event are queued. + mockServer.requests.length.should.equal(1); + + var product1 = window.mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); + window.mParticle.eCommerce.logProductAction(SDKProductActionType.AddToCart, product1); + // 1st request is /Identity call, 2nd request is /Event call + const batch = JSON.parse(mockServer.secondRequest.requestBody); + + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Test Event'); + batch.events[3].event_type.should.equal('commerce_event'); + batch.events[3].data.product_action.action.should.equal('add_to_cart'); + + done(); + }) + }); + + it('should trigger an upload of batch when a UIC occurs', function(done) { + window.mParticle._resetForTests(MPConfig); + // include an identify request so that it creates a UIC + window.mParticle.config.identifyRequest = { + userIdentities: { + customerid: 'foo-customer-id' + } + }; + + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + // Requests sent should be identify call, then UIC event + // Session start, AST, and Test Event are queued, and don't appear + // in the mockServer.requests + mockServer.requests.length.should.equal(2); + + // 1st request is /Identity call, 2nd request is events call + const batch = JSON.parse(mockServer.secondRequest.requestBody); + + batch.events[2].event_type.should.equal('user_identity_change'); + + + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + + done(); + }); + }); + + // Originally, we had the Batch uploader set to force an upload when a UAC event + // was triggered. This feature was removed and we are including this test to + // make sure the Web SDK does not regress. This test will be removed in a future + // Web SDK update + // TODO: https://go.mparticle.com/work/SQDSDKS-5891 + it('should NOT trigger an upload of batch when a UAC occurs', function(done) { + window.mParticle._resetForTests(MPConfig); + + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + + // Set a user attribute to trigger a UAC event + window.mParticle.Identity.getCurrentUser().setUserAttribute('age', 25); + + // Requests sent should be identify call + // Since we no longer force an upload for UAC, + // This request will contain a /Identity Call + // a future request will contain session start, AST, and UAC + mockServer.requests.length.should.equal(1); + + // Verifies that adding a UAC does not trigger an upload + expect(mockServer.secondRequest).to.equal(null); + + // Manually force an upload + window.mParticle.upload(); + + // Second request has now been made + expect(mockServer.secondRequest).to.be.ok; + + const batch = JSON.parse(mockServer.secondRequest.requestBody); + + // Batch should now contain the 3 events we expect + mockServer.requests.length.should.equal(2); + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('user_attribute_change'); + + done(); + }); + }); + + it('should return pending uploads if a 500 is returned', function(done) { + window.mParticle._resetForTests(MPConfig); + + mockServer.respondWith(urls.events, [ + 500, + {}, + JSON.stringify({ mpid: testMPID, Store: {}}) + ]); + + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + const pendingEvents = window.mParticle.getInstance()._APIClient.uploader.eventsQueuedForProcessing; + + pendingEvents.length.should.equal(3) + pendingEvents[0].EventName.should.equal(1); + pendingEvents[1].EventName.should.equal(10); + pendingEvents[2].EventName.should.equal('Test Event'); + + window.mParticle.upload(); + + const nowPendingEvents = window.mParticle.getInstance()._APIClient.uploader.eventsQueuedForProcessing; + nowPendingEvents.length.should.equal(0); + + const batch = JSON.parse(mockServer.secondRequest.requestBody); + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Test Event'); + done(); + }); + }); + + it('should add a modified boolean of true to a batch that has been modified via a config.onCreateBatch call', function(done) { + window.mParticle._resetForTests(MPConfig); + + window.mParticle.config.onCreateBatch = function (batch: Batch) { + return batch + }; + + window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + window.mParticle.upload() + + const batch = JSON.parse(mockServer.secondRequest.requestBody); + batch.modified.should.equal(true); + done(); + }); + + }); + + it('should respect rules for the batch modification', function(done) { + window.mParticle._resetForTests(MPConfig); + + window.mParticle.config.onCreateBatch = function (batch) { + batch.events.map(event => { + if (event.event_type === "custom_event") { + (event.data as CustomEventData).event_name = 'Modified!' + } + }); + return batch; + }; + + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + window.mParticle.upload(); + + const batch = JSON.parse(mockServer.secondRequest.requestBody); + batch.events.length.should.equal(3); + batch.events[0].event_type.should.equal('session_start'); + batch.events[1].event_type.should.equal('application_state_transition'); + batch.events[2].event_type.should.equal('custom_event'); + batch.events[2].data.event_name.should.equal('Modified!'); + done(); + }); + }); + + it('should add a modified boolean of true to a batch that has been modified via a config.onCreateBatch call', function(done) { + window.mParticle._resetForTests(MPConfig); + + window.mParticle.config.onCreateBatch = function (batch: Batch) { + return undefined; + }; + + window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); + + window.mParticle.upload(); + + (mockServer.secondRequest === null).should.equal(true); + done(); + }) + }); + }); +}); \ No newline at end of file diff --git a/test/src/tests-beaconUpload.ts b/test/src/tests-beaconUpload.ts index 494b4803..de01a3c0 100644 --- a/test/src/tests-beaconUpload.ts +++ b/test/src/tests-beaconUpload.ts @@ -6,6 +6,8 @@ import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; import { event0 } from '../fixtures/events'; import { batch1, batch2, batch3 } from '../fixtures/batches'; import _BatchValidator from '../../src/mockBatchCreator'; +import Utils from './config/utils'; +const { findEventFromRequest, findBatch, waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; declare global { interface Window { @@ -18,10 +20,8 @@ const enableBatchingConfigFlags = { eventBatchingIntervalMillis: 1000, }; -let clock; describe('Beacon Upload', () => { - let mockServer; before(function() { fetchMock.restore(); sinon.restore(); @@ -29,15 +29,12 @@ describe('Beacon Upload', () => { beforeEach(function() { fetchMock.restore(); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); + fetchMockSuccess(urls.events); window.mParticle.config.flags = { eventBatchingIntervalMillis: 1000, }; @@ -45,7 +42,6 @@ describe('Beacon Upload', () => { afterEach(() => { sinon.restore(); - mockServer.reset(); fetchMock.restore(); }); @@ -55,6 +51,9 @@ describe('Beacon Upload', () => { const bond = sinon.spy(navigator, 'sendBeacon'); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + // visibility change is a document property, not window document.dispatchEvent(new Event('visibilitychange')); @@ -62,6 +61,7 @@ describe('Beacon Upload', () => { bond.lastCall.args[0].should.eql(urls.events); done(); + }) }); it('should trigger beacon on page beforeunload events', function(done) { @@ -70,6 +70,9 @@ describe('Beacon Upload', () => { const bond = sinon.spy(navigator, 'sendBeacon'); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + // karma fails if onbeforeunload is not set to null window.onbeforeunload = null; window.dispatchEvent(new Event('beforeunload')); @@ -78,6 +81,7 @@ describe('Beacon Upload', () => { bond.getCalls()[0].args[0].should.eql(urls.events); done(); + }); }); it('should trigger beacon on pagehide events', function(done) { @@ -86,6 +90,9 @@ describe('Beacon Upload', () => { const bond = sinon.spy(navigator, 'sendBeacon'); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.dispatchEvent(new Event('pagehide')); bond.called.should.eql(true); @@ -94,6 +101,7 @@ describe('Beacon Upload', () => { (typeof bond.getCalls()[0].args[1]).should.eql('object'); done(); + }); }); describe('Offline Storage Enabled', () => { @@ -103,28 +111,26 @@ describe('Beacon Upload', () => { ...enableBatchingConfigFlags, }; - clock = sinon.useFakeTimers({ - now: new Date().getTime(), + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, }); - fetchMock.restore(); window.sessionStorage.clear(); window.localStorage.clear(); }); afterEach(() => { - sinon.restore(); fetchMock.restore(); - clock.restore(); }); it('`visibilitychange` should purge events and batches from Offline Storage after dispatch', function(done) { const eventStorageKey = 'mprtcl-v4_abcdef-events'; const batchStorageKey = 'mprtcl-v4_abcdef-batches'; - window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -132,7 +138,6 @@ describe('Beacon Upload', () => { uploader.batchesQueuedForProcessing.push(batch1); uploader.batchesQueuedForProcessing.push(batch2); uploader.batchesQueuedForProcessing.push(batch3); - uploader.queueEvent(event0); expect( @@ -171,6 +176,7 @@ describe('Beacon Upload', () => { ).to.equal(0); done(); + }); }); it('`beforeunload` should purge events and batches from Offline Storage after dispatch', function(done) { @@ -179,7 +185,9 @@ describe('Beacon Upload', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { + const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -226,6 +234,7 @@ describe('Beacon Upload', () => { done(); }); + }); it('`pagehide` should purge events and batches from Offline Storage after dispatch', function(done) { const eventStorageKey = 'mprtcl-v4_abcdef-events'; @@ -233,6 +242,8 @@ describe('Beacon Upload', () => { window.mParticle._resetForTests(MPConfig); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const mpInstance = window.mParticle.getInstance(); const uploader = mpInstance._APIClient.uploader; @@ -279,6 +290,7 @@ describe('Beacon Upload', () => { ).to.equal(0); done(); + }); }); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-consent.ts b/test/src/tests-consent.ts index 07577cc3..3c0fdda6 100644 --- a/test/src/tests-consent.ts +++ b/test/src/tests-consent.ts @@ -6,6 +6,7 @@ import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; import { expect } from 'chai'; import { GDPRConsentState, PrivacyConsentState } from '@mparticle/web-sdk'; import { Dictionary } from '../../src/utils'; +const { hasIdentifyReturned, waitForCondition, fetchMockSuccess } = Utils; declare global { interface Window { @@ -23,25 +24,20 @@ const EmptyObjectAsPrivacyConsentState = ({} as unknown) as PrivacyConsentState; const EmptyStringAsPrivacyConsentState = ('' as unknown) as PrivacyConsentState; const findBatch = Utils.findBatch; -let mockServer; + const mParticle = window.mParticle; describe('Consent', function() { beforeEach(function() { fetchMock.post(urls.events, 200); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); + mParticle.init(apiKey, window.mParticle.config); }); afterEach(function() { - mockServer.restore(); fetchMock.restore(); mParticle._resetForTests(MPConfig); }); @@ -642,6 +638,9 @@ describe('Consent', function() { 'hardware' ); consentState.setCCPAConsentState(ccpaConsent); + + waitForCondition(hasIdentifyReturned) + .then(() => { const user = mParticle.Identity.getCurrentUser(); user.setConsentState(consentState); @@ -658,6 +657,7 @@ describe('Consent', function() { testEvent.consent_state.ccpa.data_sale_opt_out.should.have.property('hardware_id', 'hardware'); done(); + }) }); it('should have CCPA and GDPR in payload', done => { @@ -679,14 +679,17 @@ describe('Consent', function() { ); consentState.setCCPAConsentState(ccpaConsent); consentState.addGDPRConsentState('test purpose', gdprConsent); + waitForCondition(hasIdentifyReturned) + .then(() => { + const user = mParticle.Identity.getCurrentUser(); user.setConsentState(consentState); - - + + mParticle.logEvent('Test Event'); - + const testEvent = findBatch(fetchMock.calls(), 'Test Event'); - + testEvent.should.have.property('consent_state'); testEvent.consent_state.should.have.property('ccpa'); testEvent.consent_state.ccpa.should.have.property('data_sale_opt_out'); @@ -695,7 +698,7 @@ describe('Consent', function() { testEvent.consent_state.ccpa.data_sale_opt_out.should.have.property('document', 'consentDoc'); testEvent.consent_state.ccpa.data_sale_opt_out.should.have.property('location', 'location'); testEvent.consent_state.ccpa.data_sale_opt_out.should.have.property('hardware_id', 'hardware'); - + testEvent.consent_state.should.have.property('gdpr'); testEvent.consent_state.gdpr.should.have.property('test purpose'); testEvent.consent_state.gdpr['test purpose'].should.have.property('consented', false); @@ -703,8 +706,9 @@ describe('Consent', function() { testEvent.consent_state.gdpr['test purpose'].should.have.property('document', 'consentDoc'); testEvent.consent_state.gdpr['test purpose'].should.have.property('location', 'location'); testEvent.consent_state.gdpr['test purpose'].should.have.property('hardware_id', 'hardware'); - + done(); + }) }); // TODO: Deprecate in next major version diff --git a/test/src/tests-cookie-syncing.js b/test/src/tests-cookie-syncing.js index b6ff3428..036b070d 100644 --- a/test/src/tests-cookie-syncing.js +++ b/test/src/tests-cookie-syncing.js @@ -1,7 +1,7 @@ import Utils from './config/utils'; -import sinon from 'sinon'; import fetchMock from 'fetch-mock/esm/client'; import { urls, testMPID, MPConfig, v4LSKey, apiKey } from './config/constants'; +const { fetchMockSuccess, waitForCondition, hasIdentifyReturned } = Utils; const { setLocalStorage, MockForwarder, getLocalStorage } = Utils; @@ -17,10 +17,8 @@ let pixelSettings = { redirectUrl: '', }; -let mockServer; - describe('cookie syncing', function() { - const timeout = 25; + const timeout = 100; // Have a reference to createElement function to reset after all cookie sync // tests have run const originalCreateElementFunction = window.document.createElement; @@ -48,14 +46,9 @@ describe('cookie syncing', function() { }); beforeEach(function() { - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); fetchMock.post(urls.events, 200); @@ -64,7 +57,6 @@ describe('cookie syncing', function() { afterEach(function() { fetchMock.restore(); - mockServer.restore(); mParticle._resetForTests(MPConfig); }); @@ -73,6 +65,8 @@ describe('cookie syncing', function() { window.mParticle.config.pixelConfigs = [pixelSettings]; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { setTimeout(function() { Should( mParticle.getInstance()._Store.pixelConfigurations.length @@ -82,6 +76,7 @@ describe('cookie syncing', function() { done(); }, timeout); + }) }); it('should sync cookies when current date is beyond the frequency cap and the MPID has not changed', function(done) { @@ -119,8 +114,10 @@ describe('cookie syncing', function() { setLocalStorage(); mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { setTimeout(function() { - mockServer.requests = []; const data = mParticle.getInstance()._Persistence.getLocalStorage(); @@ -135,6 +132,7 @@ describe('cookie syncing', function() { done(); }, timeout); + }) }); it('should sync cookies when mpid changes', function(done) { @@ -142,18 +140,25 @@ describe('cookie syncing', function() { window.mParticle.config.pixelConfigs = [pixelSettings]; mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { setTimeout(function() { const data1 = mParticle .getInstance() ._Persistence.getLocalStorage(); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'otherMPID', is_logged_in: false }), - ]); - + fetchMockSuccess(urls.login, { + mpid: 'otherMPID', is_logged_in: false + }); + mParticle.Identity.login(); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'otherMPID' + ); + }) + .then(() => { setTimeout(function() { const data2 = mParticle .getInstance() @@ -166,7 +171,9 @@ describe('cookie syncing', function() { done(); }, timeout); + }) }, timeout); + }) }); it('should not sync cookies when pixelSettings.isDebug is false, pixelSettings.isProduction is true, and mParticle.config.isDevelopmentMode is true', function(done) { @@ -292,37 +299,47 @@ describe('cookie syncing', function() { body: JSON.stringify(forwarderConfigurationResult), }); + waitForCondition(hasIdentifyReturned) + .then(() => { // add pixels to preInitConfig mParticle.init(apiKey, window.mParticle.config); - + + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.configurationLoaded === true + ); + }) + .then(() => { setTimeout(function() { mParticle .getInstance() ._Store.pixelConfigurations.length.should.equal(1); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ - mpid: 'MPID1', - is_logged_in: false, - }), - ]); - + fetchMockSuccess(urls.login, { + mpid: 'MPID1', is_logged_in: false + }); // force the preInit cookie configurations to fire mParticle.Identity.login({ userIdentities: { customerid: 'abc' }, }); - + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' + ); + }) + .then(() => { setTimeout(function() { const cookies = getLocalStorage(); Object.keys(cookies['MPID1'].csd).length.should.equal(1); done(); }, timeout); + }) }, timeout); + }) + }) }); - + const MockUser = function() { let consentState = null; return { @@ -982,6 +999,9 @@ describe('cookie syncing', function() { }); it('should perform a cookie sync only after GDPR consent is given when consent is required - perform a cookie sync when consent is rejected', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { + const includeOnMatch = false; // 'Do Not Forward' chosen in UI, 'includeOnMatch' in config const consented = false; mParticle._resetForTests(MPConfig); @@ -1002,6 +1022,8 @@ describe('cookie syncing', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { setTimeout(function() { const localStorage = mParticle .getInstance() @@ -1045,7 +1067,9 @@ describe('cookie syncing', function() { done(); }, timeout); }, timeout); - }, timeout); + }, timeout); + }) + }) }); it('should perform a cookie sync only after CCPA consent is given when consent is required - perform a cookie sync when accepting consent is required', function(done) { @@ -1118,6 +1142,9 @@ describe('cookie syncing', function() { }); it('should perform a cookie sync only after CCPA consent is given when consent is required - perform a cookie sync when consent is rejected', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { + const includeOnMatch = false; // 'Do Not Forward' chosen in UI, 'includeOnMatch' in config const consented = false; mParticle._resetForTests(MPConfig); @@ -1137,6 +1164,9 @@ describe('cookie syncing', function() { window.mParticle.config.pixelConfigs = [pixelSettings]; mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { setTimeout(function() { const localStorage = mParticle .getInstance() @@ -1181,6 +1211,8 @@ describe('cookie syncing', function() { }, timeout); }, timeout); }, timeout); + }) + }) }); it('should allow some cookie syncs to occur and others to not occur if there are multiple pixels with varying consent levels', function(done) { @@ -1267,4 +1299,4 @@ describe('cookie syncing', function() { }, timeout); }, timeout); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-core-sdk.js b/test/src/tests-core-sdk.js index f76bd9c0..7061838e 100644 --- a/test/src/tests-core-sdk.js +++ b/test/src/tests-core-sdk.js @@ -10,30 +10,28 @@ const DefaultConfig = Constants.DefaultConfig, findRequestURL = Utils.findRequestURL, findEventFromRequest = Utils.findEventFromRequest, findBatch = Utils.findBatch; -let mockServer; + +const { waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; describe('core SDK', function() { beforeEach(function() { fetchMock.post(urls.events, 200); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); mParticle.init(apiKey, window.mParticle.config); }); afterEach(function() { - mockServer.reset(); mParticle._resetForTests(MPConfig); fetchMock.restore(); sinon.restore(); }); it('starts new session', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.startNewSession(); const sessionStartEvent = findEventFromRequest(fetchMock.calls(), 'session_start'); @@ -41,7 +39,8 @@ describe('core SDK', function() { sessionStartEvent.should.be.ok(); sessionStartEvent.data.should.have.property('session_uuid'); - done(); + done(); + }) }); it('sessionIds are all capital letters', function(done) { @@ -86,6 +85,9 @@ describe('core SDK', function() { }); it('ends existing session with an event that includes SessionLength', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { + mParticle.startNewSession(); mParticle.endSession(); @@ -96,9 +98,12 @@ describe('core SDK', function() { sessionEndEvent.data.should.have.property('session_duration_ms'); done(); + }) }); it('creates a new dateLastEventSent when logging an event, and retains the previous one when ending session', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const clock = sinon.useFakeTimers(); mParticle.logEvent('Test Event1'); const testEvent1 = findEventFromRequest(fetchMock.calls(), 'Test Event1'); @@ -113,6 +118,7 @@ describe('core SDK', function() { Should(testEvent2.data.timestamp_unixtime_ms).equal(sessionEndEvent.data.timestamp_unixtime_ms); done(); + }); }); it('should process ready queue when initialized', function(done) { @@ -130,7 +136,10 @@ describe('core SDK', function() { done(); }); - it('should set app version', function(done) { + it('should set app version on the payload', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { + mParticle.setAppVersion('1.0'); window.mParticle.logEvent('Test Event', mParticle.EventType.Navigation); @@ -138,6 +147,7 @@ describe('core SDK', function() { testEventBatch.application_info.should.have.property('application_version', '1.0'); done(); + }) }); it('should get app version', function(done) { @@ -186,6 +196,8 @@ describe('core SDK', function() { }); it('should send new appName via event payload', function (done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.flags = { eventBatchingIntervalMillis: 0, } @@ -194,6 +206,12 @@ describe('core SDK', function() { mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('Test Event'); const batch = JSON.parse(fetchMock.lastOptions().body); @@ -201,26 +219,47 @@ describe('core SDK', function() { batch.application_info.should.have.property('application_name', 'newAppName'); done(); + }) + }) }); it('should allow app name to be changed via setAppName', function (done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); const newConfig = { ...window.mParticle.config, appName: 'OverrideTestName'}; - + mParticle.init(apiKey, newConfig); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const appName = mParticle.getAppName(); appName.should.equal('OverrideTestName'); done(); + }) + }) }) it('should set Package Name on Batch Payload', function (done) { + waitForCondition(hasIdentifyReturned) + .then(() => { + mParticle.config.package = 'my-web-package'; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('Test Event'); @@ -229,9 +268,13 @@ describe('core SDK', function() { batch.should.have.property('application_info'); batch.application_info.should.have.property('package', 'my-web-package'); done(); + }) + }) }); it('should sanitize event attributes', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logEvent('sanitized event', 1, { key1: 'value1', mydate: new Date(), @@ -249,11 +292,14 @@ describe('core SDK', function() { sanitizedEvent.data.custom_attributes.should.not.have.property('ishouldberemoved'); sanitizedEvent.data.custom_attributes.should.not.have.property('ishouldalsoberemoved'); sanitizedEvent.data.custom_attributes.should.not.have.property('removeme'); + }) done(); }); it('sanitizes attributes when attrs are provided', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const attrs = { valid: '123', invalid: ['123', '345'], @@ -274,6 +320,7 @@ describe('core SDK', function() { product.Attributes.should.not.have.property('invalid'); product.Attributes.should.have.property('valid'); + fetchMock.resetHistory(); mParticle.eCommerce.logCheckout(1, 'visa', attrs); const checkoutEvent = findEventFromRequest(fetchMock.calls(), 'checkout'); @@ -295,7 +342,8 @@ describe('core SDK', function() { 200 ); - mockServer.requests = []; + fetchMock.resetHistory(); + mParticle.eCommerce.logPurchase( transactionAttributes, product, @@ -313,13 +361,15 @@ describe('core SDK', function() { 'position' ); - mockServer.requests = []; + fetchMock.resetHistory(); + mParticle.eCommerce.logPromotion(1, promotion, attrs); const promotionViewEvent = findEventFromRequest(fetchMock.calls(), 'view'); promotionViewEvent.data.custom_attributes.should.not.have.property('invalid'); promotionViewEvent.data.custom_attributes.should.have.property('valid'); - mockServer.requests = []; + fetchMock.resetHistory(); + mParticle.eCommerce.logRefund( transactionAttributes, product, @@ -332,9 +382,14 @@ describe('core SDK', function() { refundEvent.data.custom_attributes.should.have.property('valid'); done(); + }) + }); it('should not generate a new device ID if a deviceId exists in localStorage', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { + mParticle._resetForTests(MPConfig); setLocalStorage(); @@ -344,6 +399,7 @@ describe('core SDK', function() { deviceId.should.equal(das); done(); + }) }); it('should return the deviceId when requested', function(done) { @@ -371,10 +427,18 @@ describe('core SDK', function() { }); it('creates a new session when elapsed time between actions is greater than session timeout', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); - const clock = sinon.useFakeTimers(); mParticle.config.sessionTimeout = 1; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + const clock = sinon.useFakeTimers(); clock.tick(100); mParticle.logEvent('Test Event'); const testEvent = findEventFromRequest(fetchMock.calls(), 'Test Event'); @@ -387,13 +451,23 @@ describe('core SDK', function() { mParticle.getInstance()._SessionManager.clearSessionTimeout(); clock.restore(); done(); + }) + }) }); it('should end session when last event sent is outside of sessionTimeout', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); - const clock = sinon.useFakeTimers(); mParticle.config.sessionTimeout = 1; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + const clock = sinon.useFakeTimers(); clock.tick(100); mParticle.logEvent('Test Event'); @@ -413,16 +487,27 @@ describe('core SDK', function() { testEvent3.data.session_uuid.should.not.equal(testEvent.data.session_uuid); clock.restore(); done(); + }) + }) }); it('should not end session when end session is called within sessionTimeout timeframe', function(done) { // This test mimics if another tab is open and events are sent, but previous tab's sessionTimeout is still ongoing + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); - const clock = sinon.useFakeTimers(); mParticle.config.sessionTimeout = 1; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + const clock = sinon.useFakeTimers(); + + fetchMock.resetHistory(); - mockServer.requests = []; clock.tick(100); mParticle.logEvent('Test Event'); @@ -461,9 +546,13 @@ describe('core SDK', function() { clock.restore(); done(); + }) + }) }); - it('should get sessionId', function(done) { + it('should set the sessionId from memory on the payload', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logEvent('Test Event'); const testEvent = findEventFromRequest(fetchMock.calls(), 'Test Event'); @@ -472,9 +561,12 @@ describe('core SDK', function() { testEvent.data.session_uuid.should.equal(sessionId); done(); + }) }); it('should set session start date in dto', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logEvent('Test Event'); const testEvent = findEventFromRequest(fetchMock.calls(), 'Test Event'); @@ -482,9 +574,12 @@ describe('core SDK', function() { testEvent.data.session_start_unixtime_ms.should.be.above(0); done(); + }); }); it('should update session start date when manually ending session then starting a new one', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logEvent('Test Event'); const testEvent = findEventFromRequest(fetchMock.calls(), 'Test Event'); @@ -504,15 +599,23 @@ describe('core SDK', function() { testEvent2SessionStartDate.should.be.above(sessionEndEventSessionStartDate); done(); + }) }); - it('should update session start date when session times out,then starting a new one', function(done) { + it('should update session start date when session times out, then start a new one', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); mParticle.config.sessionTimeout = 1; - const clock = sinon.useFakeTimers(); mParticle.init(apiKey, mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + const clock = sinon.useFakeTimers(); clock.tick(10); mParticle.logEvent('Test Event'); @@ -521,26 +624,26 @@ describe('core SDK', function() { // trigger session timeout which ends session automatically clock.tick(60000); - // note to self - session end event not being triggered, could be the same bug const sessionEndEvent = findEventFromRequest(fetchMock.calls(), 'session_end'); const sessionEndEventSessionStartDate = sessionEndEvent.data.session_start_unixtime_ms; sessionEndEventSessionStartDate.should.equal(testEventSessionStartDate); - clock.tick(100); - + clock.restore(); mParticle.logEvent('Test Event2'); const testEvent2 = findEventFromRequest(fetchMock.calls(), 'Test Event2'); const testEvent2SessionStartDate = testEvent2.data.session_start_unixtime_ms; testEvent2SessionStartDate.should.be.above(sessionEndEventSessionStartDate); - clock.restore(); - done(); + }) + }) }); it('should load SDK with the included api on init and not send events to previous apikey in persistence', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logEvent('Test Event1'); const testEvent1URL = findRequestURL(fetchMock.calls(), 'Test Event1'); @@ -552,6 +655,12 @@ describe('core SDK', function() { ); mParticle.init('new-api-key', window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('Test Event2'); const testEvent2URL = findRequestURL(fetchMock.calls(), 'Test Event2'); @@ -560,6 +669,8 @@ describe('core SDK', function() { ); done(); + }); + }); }); it('should have default options as well as configured options on configuration object, overwriting when appropriate', function(done) { @@ -661,6 +772,8 @@ describe('core SDK', function() { }); it('should use custom loggers when provided', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.logLevel = 'verbose'; let errorMessage; let warnMessage; @@ -679,6 +792,12 @@ describe('core SDK', function() { }; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { infoMessage.should.equal( 'Batch count: 1' ); @@ -692,6 +811,8 @@ describe('core SDK', function() { ); done(); + }) + }) }); it('should be able to change logLevel on the fly, postuse custom loggers when provided', function(done) { @@ -704,6 +825,8 @@ describe('core SDK', function() { }; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { infoMessages.length.should.equal(0); @@ -713,9 +836,12 @@ describe('core SDK', function() { infoMessages[0].should.equal('Starting to log event: hi'); done(); + }) }); it("should not log anything to console when logLevel = 'none'", function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const infoMessages = []; const warnMessages = []; const errorMessages = []; @@ -733,7 +859,12 @@ describe('core SDK', function() { }; mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { infoMessages.length.should.equal(0); warnMessages.length.should.equal(0); errorMessages.length.should.equal(0); @@ -750,6 +881,9 @@ describe('core SDK', function() { Should(testEvent).be.ok(); done(); + }) + }) + }); it('should not error when logger custom loggers when provided', function(done) { @@ -792,17 +926,24 @@ describe('core SDK', function() { }); it('should have default urls if no custom urls are set in config object, but use custom urls when they are set', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.config.v3SecureServiceUrl = 'testtesttest-custom-v3secureserviceurl/v3/JS/'; window.mParticle.config.configUrl = 'foo-custom-configUrl/v2/JS/'; - window.mParticle.config.identityUrl = 'custom-identityUrl/'; + window.mParticle.config.identityUrl = 'custom-identityurl/'; window.mParticle.config.aliasUrl = 'custom-alias/'; fetchMock.post('https://testtesttest-custom-v3secureserviceurl/v3/JS/test_key/events', HTTP_OK) mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.getInstance()._Store.SDKConfig.v3SecureServiceUrl.should.equal(window.mParticle.config.v3SecureServiceUrl) mParticle.getInstance()._Store.SDKConfig.configUrl.should.equal(window.mParticle.config.configUrl) mParticle.getInstance()._Store.SDKConfig.identityUrl.should.equal(window.mParticle.config.identityUrl) @@ -819,12 +960,19 @@ describe('core SDK', function() { ); // test Identity endpoint - mockServer.requests = []; + fetchMock.resetHistory(); + fetchMockSuccess('https://custom-identityurl/login', { + mpid: 'loginMPID', is_logged_in: true + }); mParticle.Identity.login({ userIdentities: { customerid: 'test1' } }); - mockServer.requests[0].url.should.equal( - 'https://' + window.mParticle.config.identityUrl + 'login' - ); - + + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'loginMPID' + ); + }) + .then(() => { + fetchMock.calls()[0][0].should.equal('https://' + window.mParticle.config.identityUrl + 'login'); // test alias endpoint // https://go.mparticle.com/work/SQDSDKS-6751 fetchMock.post('https://custom-alias/test_key/Alias', HTTP_ACCEPTED); @@ -844,6 +992,9 @@ describe('core SDK', function() { done(); } + }) + }); + }); }); it('should use configUrl when specified on config object', function (done) { @@ -853,8 +1004,8 @@ describe('core SDK', function() { mParticle.config.requestConfig = true; fetchMock.get(expectedConfigUrl, 200); + fetchMock.resetHistory(); - mockServer.requests = []; mParticle.init(apiKey, window.mParticle.config); fetchMock.lastCall()[0].should.equal(expectedConfigUrl); @@ -876,13 +1027,15 @@ describe('core SDK', function() { mParticle.init(apiKey, mParticle.config); - mockServer.requests = []; - mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.logEvent('Test Event'); fetchMock.lastOptions().body.should.be.ok() done(); + }) }); it('should add onCreateBatch to _Store.SDKConfig if onCreateBatch is provide on mParticle.config object', function(done) { @@ -908,7 +1061,7 @@ describe('core SDK', function() { mParticle._resetForTests(MPConfig); mParticle.config.isDevelopmentMode = true; mParticle.config.requestConfig = true; - mockServer.requests = []; + fetchMock.resetHistory(); fetchMock.get( 'https://jssdkcdns.mparticle.com/JS/v2/test_key/config?env=1', @@ -916,14 +1069,10 @@ describe('core SDK', function() { ); mParticle.init(apiKey, window.mParticle.config); - - - - // rob note - while config fetch is async, we are only testing what endpoint is hit here, and so we do not need to wait for anything to return - (fetchMock.calls()[2][0].indexOf('?env=1') > 0).should.equal( + // While config fetch is async, we are only testing what endpoint is hit here, and so we do not need to wait for anything to return + (fetchMock.calls()[0][0].indexOf('?env=1') > 0).should.equal( true ); - done(); }); @@ -931,18 +1080,15 @@ describe('core SDK', function() { mParticle._resetForTests(MPConfig); mParticle.config.isDevelopmentMode = false; mParticle.config.requestConfig = true; - mockServer.requests = []; - fetchMock.get( - 'https://jssdkcdns.mparticle.com/JS/v2/test_key/config?env=0', + fetchMock.get(urls.config, { status: 200 } ); - + fetchMock.resetHistory(); mParticle.init(apiKey, window.mParticle.config); // rob note - while config fetch is async, we are only testing what endpoint is hit here, and so we do not need to wait for anything to return - - (fetchMock.calls()[2][0].indexOf('?env=0') > 0).should.equal( + (fetchMock.calls()[0][0].indexOf('?env=0') > 0).should.equal( true ); @@ -958,15 +1104,20 @@ describe('core SDK', function() { workspaceToken: 'token1', }; - mockServer.respondWith(urls.config, [ - 200, - {}, - JSON.stringify(config), - ]); + fetchMock.get(urls.config, { + status: 200, + body: JSON.stringify({ config }), + }); window.mParticle.config.requestConfig = true; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.configurationLoaded === true + ); + }) + .then(() => { mParticle.getInstance()._Store.SDKConfig.appName = config.appName; mParticle.getInstance()._Store.SDKConfig.minWebviewBridgeVersion = config.minWebviewBridgeVersion; @@ -974,13 +1125,25 @@ describe('core SDK', function() { localStorage.removeItem(config.workspaceToken); done(); + }) }); it('should initialize and log events even with a failed /config fetch and empty config', function async(done) { // this instance occurs when self hosting and the user only passes an object into init mParticle._resetForTests(MPConfig); - mockServer.respondWith(urls.identify, [400, {}, JSON.stringify('')]); + const config = { + appName: 'appNameTest', + minWebviewBridgeVersion: 1, + workspaceToken: 'token1', + }; + fetchMock.get(urls.config, { + status: 200, + body: JSON.stringify({ config }), + }); + + fetchMock.config.overwriteRoutes = true; + fetchMock.post(urls.identify, {status: 400, body: JSON.stringify('')}); // force config to be only requestConfig = true; delete window.mParticle.config.kitConfigs; @@ -991,21 +1154,31 @@ describe('core SDK', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.configurationLoaded === true + ); + }) + .then(() => { // fetching the config is async and we need to wait for it to finish - setTimeout(() => { mParticle.getInstance()._Store.isInitialized.should.equal(true); // have to manually call identify although it was called as part of init because we can only mock the server response once - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1' }), - ]); - - mParticle.Identity.identify({ - userIdentities: { customerid: 'test' }, - }); - mParticle.logEvent('Test Event'); + fetchMockSuccess(urls.identify, { + mpid: 'MPID1', + is_logged_in: false, + }); + + mParticle.Identity.identify({ + userIdentities: { customerid: 'test' }, + }); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' + ); + }) + .then(() => { + mParticle.logEvent('Test Event'); const testEvent = findEventFromRequest( fetchMock.calls(), 'Test Event' @@ -1014,7 +1187,8 @@ describe('core SDK', function() { testEvent.should.be.ok(); done(); - }, 0); + }); + }); }); it('should initialize without a config object passed to init', function(done) { diff --git a/test/src/tests-eCommerce.js b/test/src/tests-eCommerce.js index ecaeff71..8c3e1dc8 100644 --- a/test/src/tests-eCommerce.js +++ b/test/src/tests-eCommerce.js @@ -2,32 +2,28 @@ import Utils from './config/utils'; import sinon from 'sinon'; import fetchMock from 'fetch-mock/esm/client'; import { urls, apiKey, workspaceToken, MPConfig, testMPID, ProductActionType, PromotionActionType } from './config/constants'; +const { waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; const getLocalStorageProducts = Utils.getLocalStorageProducts, forwarderDefaultConfiguration = Utils.forwarderDefaultConfiguration, findEventFromRequest = Utils.findEventFromRequest, MockForwarder = Utils.MockForwarder; -let mockServer; describe('eCommerce', function() { beforeEach(function() { mParticle._resetForTests(MPConfig); delete mParticle._instances['default_instance']; - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; fetchMock.post(urls.events, 200); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); + mParticle.init(apiKey, window.mParticle.config); }); afterEach(function() { - mockServer.restore(); fetchMock.restore(); mParticle._resetForTests(MPConfig); }); @@ -72,6 +68,8 @@ describe('eCommerce', function() { }); it('should log ecommerce event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct( 'iPhone', '12345', @@ -92,9 +90,9 @@ describe('eCommerce', function() { 600, 200 ); - + mParticle.eCommerce.logPurchase(transactionAttributes, product); - + const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); purchaseEvent.data.should.have.property('product_action'); @@ -122,9 +120,10 @@ describe('eCommerce', function() { purchaseEvent.data.product_action.products[0].custom_attributes.should.have.property('customkey', 'customvalue'); done(); + }) }); - it('should not log a ecommerce event if there is a typo in the product action type', function(done) { + it('should not log an ecommerce event if there is a typo in the product action type', function(done) { // fetchMock calls will have session start and AST events, we want to reset so that we can prove the product action type does not go through (length remains 0 after logging) fetchMock.resetHistory(); const product = mParticle.eCommerce.createProduct( @@ -132,9 +131,6 @@ describe('eCommerce', function() { '12345', '400'); - // At this point, mockServer.requests contains 3 requests - an identity, - // session start, and AST event. - // We empty it in order to prove the following event does not send an event mParticle.eCommerce.logProductAction( mParticle.ProductActionType.Typo, // <------ will result in a null when converting the product action type as this is not a real value [product] @@ -166,6 +162,8 @@ describe('eCommerce', function() { '200-foo' ); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logPurchase(transactionAttributes, product); const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); @@ -195,6 +193,7 @@ describe('eCommerce', function() { purchaseEvent.data.product_action.products[0].custom_attributes.should.have.property('customkey', 'customvalue'); done(); + }) }); it('should log identical events for logPurchase and logProductAction with product action type of `purchase`', function(done) { @@ -219,11 +218,13 @@ describe('eCommerce', function() { 200 ); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logPurchase(transactionAttributes, product); const purchaseEvent1 = findEventFromRequest(fetchMock.calls(), 'purchase'); - + fetchMock.resetHistory(); - + mParticle.eCommerce.logProductAction(mParticle.ProductActionType.Purchase, product, null, null, transactionAttributes) const purchaseEvent2 = findEventFromRequest(fetchMock.calls(), 'purchase'); @@ -253,6 +254,7 @@ describe('eCommerce', function() { productAction1.products[0].custom_attributes.customkey.should.equal(productAction2.products[0].custom_attributes.customkey); done(); + }) }); it('logPurchase should support array of products', function(done) { @@ -262,6 +264,8 @@ describe('eCommerce', function() { '12345' ); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logPurchase(transactionAttributes, [ product1, product2, @@ -275,6 +279,7 @@ describe('eCommerce', function() { purchaseEvent.data.product_action.products[1].should.have.property('name', 'Android'); done(); + }) }); it('logRefund should support array of products', function(done) { @@ -284,6 +289,8 @@ describe('eCommerce', function() { '12345' ); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logRefund(transactionAttributes, [ product1, product2, @@ -297,6 +304,7 @@ describe('eCommerce', function() { refundEvent.data.product_action.products[1].should.have.property('name', 'Android'); done(); + }) }); it('should create promotion', function(done) { @@ -325,6 +333,8 @@ describe('eCommerce', function() { 1 ); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logPromotion( mParticle.PromotionType.PromotionClick, promotion @@ -342,6 +352,7 @@ describe('eCommerce', function() { promotionEvent.data.promotion_action.promotions[0].should.have.property('position', 1); done(); + }) }); it('should allow multiple promotions to be logged at once', function(done) { @@ -359,6 +370,8 @@ describe('eCommerce', function() { 2 ); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logPromotion( mParticle.PromotionType.PromotionClick, [promotion1, promotion2] @@ -381,6 +394,7 @@ describe('eCommerce', function() { promotionEvent.data.promotion_action.promotions[1].should.have.property('position', 2); done(); + }) }); it('should allow an promotions to bypass server upload', function (done) { @@ -391,6 +405,8 @@ describe('eCommerce', function() { 1 ); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logPromotion( mParticle.PromotionType.PromotionClick, promotion, @@ -402,6 +418,7 @@ describe('eCommerce', function() { Should(promotionEvent).not.be.ok(); done(); + }) }); it('should create impression', function(done) { @@ -425,6 +442,8 @@ describe('eCommerce', function() { product ); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logImpression(impression); const impressionEvent = findEventFromRequest(fetchMock.calls(), 'impression'); @@ -436,10 +455,12 @@ describe('eCommerce', function() { impressionEvent.data.product_impressions[0].products[0].should.have.property('id', '12345'); done(); + }) }); it('should allow an impression to bypass server upload', function (done) { - + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', '12345', 400), impression = mParticle.eCommerce.createImpression( 'impression-name', @@ -453,9 +474,12 @@ describe('eCommerce', function() { Should(impressionEvent).not.be.ok(); done(); + }) }); it('should log multiple impression when an array of impressions is passed', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', '12345', 400), impression = mParticle.eCommerce.createImpression( 'impression-name1', @@ -488,9 +512,12 @@ describe('eCommerce', function() { impressionEvent.data.product_impressions[1].products[0].should.have.property('id', '23456'); done(); + }) }); it('should log ecommerce refund', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct( 'iPhone', '12345', @@ -536,9 +563,12 @@ describe('eCommerce', function() { refundEvent.data.product_action.products[0].should.have.property('total_product_amount', 800) done(); + }) }); it('should log identical events for logRefund and logProductAction with a product action of `refund`', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct( 'iPhone', '12345', @@ -591,9 +621,12 @@ describe('eCommerce', function() { refundEvent1.data.product_action.products[0].position.should.equal(refundEvent2.data.product_action.products[0].position) done(); + }) }); it('should allow a product action to bypass server upload', function (done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct( 'iPhone', '12345', @@ -628,9 +661,12 @@ describe('eCommerce', function() { Should(event).not.be.ok(); done(); + }) }); it('should add products to cart', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', '12345', 400); mParticle.eCommerce.Cart.add(product, true); @@ -643,9 +679,12 @@ describe('eCommerce', function() { addToCartEvent.data.product_action.products[0].should.have.property('id', '12345'); done(); + }) }); it('should remove products to cart', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', '12345', 400); mParticle.eCommerce.Cart.add(product); @@ -659,9 +698,12 @@ describe('eCommerce', function() { removeFromCartEvent.data.product_action.products[0].should.have.property('id', '12345'); done(); + }) }); it('should update cart products in cookies after adding/removing product to/from a cart and clearing cart', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', '12345', 400); mParticle.eCommerce.Cart.add(product); @@ -690,9 +732,12 @@ describe('eCommerce', function() { products4[testMPID].cp.length.should.equal(0); done(); + }) }); it('should not add the (config.maxProducts + 1st) item to cookie cartItems and only send cookie cartProducts when logging', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.maxProducts = 10; mParticle.config.workspaceToken = workspaceToken; mParticle.init(apiKey, window.mParticle.config); @@ -721,9 +766,12 @@ describe('eCommerce', function() { Should(foundProductInCookies).be.ok(); done(); + }) }); it('should log checkout via deprecated logCheckout method', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); mParticle.eCommerce.logCheckout(1, 'Visa'); @@ -745,9 +793,12 @@ describe('eCommerce', function() { checkoutEvent.data.product_action.should.have.property('checkout_options', 'Visa'); done(); + }) }); it('should log checkout via mParticle.logProductAction method', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product1 = mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); const product2 = mParticle.eCommerce.createProduct('galaxy', 'galaxySKU', 799); @@ -767,11 +818,14 @@ describe('eCommerce', function() { checkoutEvent.data.product_action.products[1].should.have.property('id', 'galaxySKU'); done(); + }) }); it('should log checkout option', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', '12345', 400); - mockServer.requests = []; + mParticle.eCommerce.logProductAction( ProductActionType.CheckoutOption, product, @@ -792,9 +846,12 @@ describe('eCommerce', function() { checkoutOptionEvent.data.product_action.should.have.property('action', 'checkout_option'); checkoutOptionEvent.data.custom_attributes.should.have.property('color', 'blue'); done(); + }) }); it('should log product action', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', '12345', 400); mParticle.eCommerce.logProductAction( @@ -811,8 +868,8 @@ describe('eCommerce', function() { viewDetailEvent.data.product_action.should.have.property('products').with.lengthOf(1); viewDetailEvent.data.product_action.products[0].should.have.property('id', '12345'); - done(); + }) }); it('should fail to create product if name not a string', function(done) { @@ -878,6 +935,8 @@ describe('eCommerce', function() { }); it('should set product position to 0 if null', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct( 'iPhone', '12345', @@ -903,9 +962,12 @@ describe('eCommerce', function() { done(); + }) }); it('should support array of products when adding to cart', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product1 = mParticle.eCommerce.createProduct( 'iPhone', '12345', @@ -936,9 +998,12 @@ describe('eCommerce', function() { addToCartEvent.data.product_action.products[1].should.have.property('name', 'Nexus'); done(); + }) }); it('should support a single product when adding to cart', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product1 = mParticle.eCommerce.createProduct( 'iPhone', '12345', @@ -960,16 +1025,26 @@ describe('eCommerce', function() { addToCartEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); done(); + }) }); it('expand product purchase commerce event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); - + + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { mParticle.eCommerce.setCurrencyCode('foo-currency'); const productAttributes = {}; productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; @@ -1065,9 +1140,13 @@ describe('eCommerce', function() { ); done(); + }) + }) }); - + it('expand product refund commerce event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -1076,6 +1155,12 @@ describe('eCommerce', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const productAttributes = {}; productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; @@ -1134,15 +1219,25 @@ describe('eCommerce', function() { done(); }); + }) + }); it('expand non-plus-one-product commerce event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); - + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const productAttributes = {}; productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; @@ -1202,8 +1297,12 @@ describe('eCommerce', function() { done(); }); + }); + }); it('expand checkout commerce event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -1211,6 +1310,12 @@ describe('eCommerce', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const eventAttributes = {}; eventAttributes['foo-event-attribute-key'] = @@ -1274,16 +1379,26 @@ describe('eCommerce', function() { ); done(); + }) + }) }); it('expand promotion commerce event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); - + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const eventAttributes = {}; eventAttributes['foo-event-attribute-key'] = @@ -1331,6 +1446,8 @@ describe('eCommerce', function() { ); done(); + }) + }) }); it('expand null commerce event', function(done) { @@ -1341,13 +1458,21 @@ describe('eCommerce', function() { }); it('expand impression commerce event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); - + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const productAttributes = {}; productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; @@ -1417,18 +1542,25 @@ describe('eCommerce', function() { ); done(); + }) + }) }); it('should add customFlags to logCheckout events', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.logCheckout(1, {}, {}, { interactionEvent: true }); const checkoutEvent = findEventFromRequest(fetchMock.calls(), 'checkout'); checkoutEvent.data.custom_flags.interactionEvent.should.equal(true); done(); + }) }); it('should add customFlags to logProductAction events', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', 'sku1', 499); mParticle.eCommerce.logProductAction( mParticle.ProductActionType.Unknown, @@ -1441,9 +1573,12 @@ describe('eCommerce', function() { unknownEvent.data.custom_flags.interactionEvent.should.equal(true); done(); + }) }); it('should add customFlags to logPurchase events', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', 'sku1', 499); const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( 'id1', @@ -1463,9 +1598,12 @@ describe('eCommerce', function() { purchaseEvent.data.custom_flags.interactionEvent.should.equal(true); done(); + }) }); it('should add customFlags to logPromotion events', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const promotion = mParticle.eCommerce.createPromotion( 'id', 'creative', @@ -1485,9 +1623,12 @@ describe('eCommerce', function() { promotionEvent.data.custom_flags.interactionEvent.should.equal(true); done(); + }) }); it('should add customFlags to logImpression events', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', 'sku1', 499); const impression = mParticle.eCommerce.createImpression( 'iphoneImpressionName', @@ -1503,9 +1644,12 @@ describe('eCommerce', function() { impressionEvent.data.custom_flags.interactionEvent.should.equal(true); done(); + }) }); it('should add customFlags to logRefund events', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct('iPhone', 'sku1', 499); const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( 'id1', @@ -1524,12 +1668,16 @@ describe('eCommerce', function() { refundEvent.data.custom_flags.interactionEvent.should.equal(true); done(); + }) }); describe('Cart', function() { afterEach(function() { sinon.restore(); }); + it('should deprecate add', function() { + waitForCondition(hasIdentifyReturned) + .then(() => { const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); const product = mParticle.eCommerce.createProduct( @@ -1544,8 +1692,11 @@ describe('eCommerce', function() { bond.getCalls()[0].args[0].should.eql( 'Deprecated function eCommerce.Cart.add() will be removed in future releases' ); + }) }); it('should deprecate remove', function() { + waitForCondition(hasIdentifyReturned) + .then(() => { const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); const product = mParticle.eCommerce.createProduct( @@ -1560,9 +1711,12 @@ describe('eCommerce', function() { bond.getCalls()[0].args[0].should.eql( 'Deprecated function eCommerce.Cart.remove() will be removed in future releases' ); + }) }); it('should deprecate clear', function() { + waitForCondition(hasIdentifyReturned) + .then(() => { const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); mParticle.eCommerce.Cart.clear(); @@ -1571,6 +1725,7 @@ describe('eCommerce', function() { bond.getCalls()[0].args[0].should.eql( 'Deprecated function eCommerce.Cart.clear() will be removed in future releases' ); + }) }); it('should be empty when transactionAttributes is empty', function() { @@ -1613,6 +1768,8 @@ describe('eCommerce', function() { }); it('should allow a user to pass in a source_message_id to a commerce event', function() { + waitForCondition(hasIdentifyReturned) + .then(() => { const product = mParticle.eCommerce.createProduct( 'iPhone', '12345', @@ -1648,6 +1805,7 @@ describe('eCommerce', function() { const purchaseEvent1 = findEventFromRequest(fetchMock.calls(), 'purchase'); purchaseEvent1.data.source_message_id.should.equal('foo-bar'); - }); + }) + }); }); }); \ No newline at end of file diff --git a/test/src/tests-event-logging.js b/test/src/tests-event-logging.js index 63918ceb..aa942b85 100644 --- a/test/src/tests-event-logging.js +++ b/test/src/tests-event-logging.js @@ -9,34 +9,29 @@ import { MessageType, } from './config/constants'; -const getIdentityEvent = Utils.getIdentityEvent, - findEventFromRequest = Utils.findEventFromRequest, - findBatch = Utils.findBatch; let mockServer; +const { findEventFromRequest, findBatch, getIdentityEvent, waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; describe('event logging', function() { beforeEach(function() { mParticle._resetForTests(MPConfig); fetchMock.post(urls.events, 200); delete mParticle._instances['default_instance']; - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); mParticle.init(apiKey, window.mParticle.config); }); afterEach(function() { - mockServer.restore(); fetchMock.restore(); mParticle._resetForTests(MPConfig); }); it('should log an event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.logEvent( 'Test Event', mParticle.EventType.Navigation, @@ -56,9 +51,12 @@ describe('event logging', function() { testEventBatch.should.have.property('mpid', testMPID); done(); + }) }); it('should log an event with new device id when set on setDeviceId', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.logEvent( 'Test Event', mParticle.EventType.Navigation, @@ -78,13 +76,16 @@ describe('event logging', function() { testEvent2Batch.should.have.property('mp_deviceid', 'foo-guid'); done(); + }) }); it('should log an event with new device id when set via mParticle.config', function(done) { mParticle._resetForTests(MPConfig); - + window.mParticle.config.deviceId = 'foo-guid'; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.logEvent('Test Event'); const testEventBatch = findBatch(fetchMock.calls(), 'Test Event'); @@ -93,9 +94,12 @@ describe('event logging', function() { testEventBatch.should.have.property('mp_deviceid', 'foo-guid'); done(); + }) }); it('should allow an event to bypass server upload', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.logEvent( 'Test Standard Upload', mParticle.EventType.Navigation, @@ -149,9 +153,12 @@ describe('event logging', function() { Should(bypassedEvent).not.be.ok(); done(); + }) }); it('should allow an event to bypass server upload via logBaseEvent', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.logBaseEvent( { name: 'Test Standard Upload', @@ -212,9 +219,12 @@ describe('event logging', function() { Should(bypassedEvent).not.be.ok(); done(); + }) }); it('should log an error', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logError('my error'); const errorEvent = findEventFromRequest(fetchMock.calls(), 'my error'); @@ -226,9 +236,12 @@ describe('event logging', function() { errorEvent.data.custom_attributes.should.have.property('m', 'my error'); done(); + }) }); it('should log an error with name, message, stack', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const error = new Error('my error'); error.stack = 'my stacktrace'; @@ -248,9 +261,13 @@ describe('event logging', function() { ); done(); + }) + }); it('should log an error with custom attrs', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const error = new Error('my error'); error.stack = 'my stacktrace'; @@ -271,9 +288,12 @@ describe('event logging', function() { ); done(); + }) }); it('should sanitize error custom attrs', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); mParticle.logError('my error', { invalid: ['my invalid attr'], @@ -296,9 +316,12 @@ describe('event logging', function() { ); done(); + }) }); it('should log an AST with firstRun = true when first visiting a page, and firstRun = false when reloading the page', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const astEvent = findEventFromRequest( fetchMock.calls(), 'application_state_transition' @@ -321,6 +344,13 @@ describe('event logging', function() { fetchMock.resetHistory(); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { + const astEvent2 = findEventFromRequest( fetchMock.calls(), 'application_state_transition' @@ -329,12 +359,22 @@ describe('event logging', function() { astEvent2.data.should.have.property('is_first_run', false); done(); + }) + }) }); it('should log an AST on init with firstRun = false when cookies already exist', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { // cookies currently exist, mParticle.init called from beforeEach fetchMock.resetHistory(); // log second AST + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { mParticle.init(apiKey, window.mParticle.config); const astEvent = findEventFromRequest( @@ -344,9 +384,13 @@ describe('event logging', function() { astEvent.data.should.have.property('is_first_run', false); done(); + }) + }); }); it('should log a page view', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logPageView(); const pageViewEvent = findEventFromRequest( @@ -367,9 +411,12 @@ describe('event logging', function() { ); done(); + }) }); it('should log custom page view', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logPageView( 'My Page View', { testattr: 1 }, @@ -397,9 +444,12 @@ describe('event logging', function() { ); done(); + }) }); it('should pass custom flags in page views', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logPageView('test', null, { 'MyCustom.Flag': 'Test', }); @@ -418,9 +468,12 @@ describe('event logging', function() { ); done(); + }) }); it('should allow a page view to bypass server upload', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logPageView('test bypass', null, null, { shouldUploadEvent: false, }); @@ -432,9 +485,12 @@ describe('event logging', function() { Should(pageViewEvent).not.be.ok(); done(); + }) }); it('should not log a PageView event if there are invalid attrs', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logPageView('test1', 'invalid', null); const pageViewEvent = findEventFromRequest( fetchMock.calls(), @@ -444,9 +500,12 @@ describe('event logging', function() { Should(pageViewEvent).not.be.ok(); done(); + }) }); it('should not log an event that has an invalid customFlags', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logPageView('test', null, 'invalid'); const pageViewEvent = findEventFromRequest( @@ -456,9 +515,12 @@ describe('event logging', function() { Should(pageViewEvent).not.be.ok(); done(); + }) }); it('should log event with name PageView when an invalid event name is passed', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { fetchMock.resetHistory(); mParticle.logPageView(null); @@ -488,9 +550,12 @@ describe('event logging', function() { pageViewEvent3.data.screen_name.should.equal('PageView'); done(); + }) }); it('should log opt out', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setOptOut(true); const optOutEvent = findEventFromRequest(fetchMock.calls(), 'opt_out'); @@ -499,18 +564,24 @@ describe('event logging', function() { optOutEvent.data.should.have.property('is_opted_out', true); done(); + }) }); it('log event requires name', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { fetchMock.resetHistory(); mParticle.logEvent(); - + fetchMock.calls().should.have.lengthOf(0); - + done(); + }) }); it('log event requires valid event type', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { fetchMock.resetHistory(); mParticle.logEvent('test', 100); @@ -518,19 +589,25 @@ describe('event logging', function() { fetchMock.calls().should.have.lengthOf(0); done(); + }); }); it('event attributes must be object', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logEvent('Test Event', null, 1); - + const testEvent = findEventFromRequest(fetchMock.calls(), 'Test Event'); - + testEvent.data.should.have.property('custom_attributes', null); - + done(); + }); }); it('opting out should prevent events being sent', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setOptOut(true); fetchMock.resetHistory(); @@ -538,9 +615,12 @@ describe('event logging', function() { fetchMock.calls().should.have.lengthOf(0); done(); + }); }); it('after logging optout, and reloading, events still should not be sent until opt out is enabled when using local storage', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setOptOut(true); fetchMock.resetHistory(); @@ -563,9 +643,12 @@ describe('event logging', function() { fetchMock.calls().should.have.lengthOf(0); done(); + }) }); it('after logging optout, and reloading, events still should not be sent until opt out is enabled when using cookie storage', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, window.mParticle.config); mParticle.setOptOut(true); @@ -577,26 +660,47 @@ describe('event logging', function() { mParticle.setOptOut(false); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { fetchMock.resetHistory(); mParticle.logEvent('test'); fetchMock.calls().should.have.lengthOf(1); - + mParticle.setOptOut(true); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { fetchMock.resetHistory(); mParticle.logEvent('test'); fetchMock.calls().should.have.lengthOf(0); - + done(); + }); + }); + }) }); it('should log identify event', function(done) { - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.identify(); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const data = getIdentityEvent(mockServer.requests, 'identify'); data.should.have.properties( 'client_sdk', @@ -607,18 +711,26 @@ describe('event logging', function() { 'request_timestamp_ms', 'context' ); - + }); done(); + }); }); it('should log logout event', function(done) { - mockServer.respondWith(urls.logout, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.logout, { + mpid: 'logoutMPID', is_logged_in: false + }); + + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.logout(); - const data = getIdentityEvent(mockServer.requests, 'logout'); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'logoutMPID' + ); + }) + .then(() => { + const data = getIdentityEvent(fetchMock.calls(), 'logout'); data.should.have.properties( 'client_sdk', 'environment', @@ -628,19 +740,27 @@ describe('event logging', function() { 'request_timestamp_ms', 'context' ); - + done(); + }) + }) }); it('should log login event', function(done) { - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'loginMPID', is_logged_in: false + }); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.login(); - const data = getIdentityEvent(mockServer.requests, 'login'); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'loginMPID' + ); + }) + .then(() => { + const data = getIdentityEvent(fetchMock.calls(), 'login'); data.should.have.properties( 'client_sdk', 'environment', @@ -652,23 +772,28 @@ describe('event logging', function() { ); done(); + }) + }) }); it('should log modify event', function(done) { - mockServer.respondWith(urls.modify, [ - 200, - {}, - JSON.stringify({ - change_results: [ + waitForCondition(hasIdentifyReturned) + .then(() => { + fetchMockSuccess(urls.modify, { + change_results: [ { identity_type: 'email', modified_mpid: testMPID, }, - ], - }), - ]); + ] + }); mParticle.Identity.modify(); - const data = getIdentityEvent(mockServer.requests, 'modify'); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }).then(() => { + const data = getIdentityEvent(fetchMock.calls(), 'modify'); data.should.have.properties( 'client_sdk', 'environment', @@ -679,18 +804,25 @@ describe('event logging', function() { ); done(); + }) + }) }); it('should send das with each event logged', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.logEvent('Test Event'); const testEventBatch = findBatch(fetchMock.calls(), 'Test Event'); testEventBatch.should.have.property('mp_deviceid'); testEventBatch.mp_deviceid.length.should.equal(36); done(); + }) }); it('should send consent state with each event logged', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const consentState = mParticle.Consent.createConsentState(); consentState.addGDPRConsentState( 'foo purpose', @@ -725,9 +857,13 @@ describe('event logging', function() { testEvent2.should.have.property('consent_state', null); done(); + }) + }); it('should log integration attributes with each event', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setIntegrationAttribute(128, { MCID: 'abcdefg' }); mParticle.logEvent('Test Event'); const testEvent = findBatch(fetchMock.calls(), 'Test Event'); @@ -740,9 +876,13 @@ describe('event logging', function() { ); done(); + }) + }); it('should run the callback once when tracking succeeds', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const clock = sinon.useFakeTimers(); mParticle.init(apiKey, window.mParticle.config); @@ -776,13 +916,24 @@ describe('event logging', function() { clock.restore(); done(); + }) + }); it('should run the callback once when tracking fails', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { const clock = sinon.useFakeTimers(); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { + let successCallbackCalled = false; let numberTimesCalled = 0; @@ -814,22 +965,30 @@ describe('event logging', function() { clock.restore(); done(); + }) + }) }); it('should pass the found or existing position to the callback in startTrackingLocation', function(done) { - const clock = sinon.useFakeTimers(); - + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { let currentPosition; function callback(position) { currentPosition = position; } - + const clock = sinon.useFakeTimers(); mParticle.startTrackingLocation(callback); // mock geo will successfully run after 1 second (geomock.js // navigator.geolocation.delay) + clock.tick(1000); const latitudeResult = 52.5168; const longitudeResult = 13.3889; @@ -840,22 +999,31 @@ describe('event logging', function() { clock.restore(); done(); + }) + }) }); it('should run the callback if tracking already exists', function(done) { - const clock = sinon.useFakeTimers(); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.init(apiKey, window.mParticle.config); - mParticle.startTrackingLocation(); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { - let successCallbackCalled = false; + const clock = sinon.useFakeTimers(); + mParticle.startTrackingLocation(); + let successCallbackCalled = false; function callback() { successCallbackCalled = true; mParticle.logEvent('Test Event'); } - mParticle.startTrackingLocation(callback); // mock geo will successfully run after 1 second (geomock.js // navigator.geolocation.delay) @@ -871,6 +1039,8 @@ describe('event logging', function() { clock.restore(); done(); + }) + }) }); it('should log appName in the payload on v3 endpoint when set on config prior to init', function(done) { @@ -879,7 +1049,15 @@ describe('event logging', function() { eventBatchingIntervalMillis: 0, }; + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('Test Event'); @@ -893,16 +1071,34 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should log AST first_run as true on new page loads, and false for when a page has previously been loaded', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { + const batch = JSON.parse(fetchMock.lastOptions().body); batch.events[0].data.should.have.property('is_first_run', true); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { + mParticle.init(apiKey, mParticle.config); const batch2 = JSON.parse(fetchMock.lastOptions().body); batch2.events[0].data.should.have.property('is_first_run', false); @@ -910,9 +1106,14 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) + }) }); it('should log AST with launch_referral with a url', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle._resetForTests(MPConfig); mParticle.config.flags = { @@ -920,6 +1121,13 @@ describe('event logging', function() { }; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { + const batch = JSON.parse(fetchMock.lastOptions().body); batch.events[0].data.should.have.property('launch_referral'); @@ -930,12 +1138,23 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) + }); it('should log appName in the payload on v3 endpoint when set on config prior to init', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.flags = { eventBatchingIntervalMillis: 0, }; + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }).then(() => { + mParticle.init(apiKey, mParticle.config); mParticle.setAppName('another name'); @@ -951,6 +1170,8 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should log a batch to v3 with data planning in the payload', function(done) { @@ -963,8 +1184,16 @@ describe('event logging', function() { planVersion: 10, }; + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('Test Event'); const batch = JSON.parse(fetchMock.lastOptions().body); @@ -977,9 +1206,13 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should log a batch to v3 with no version if no version is passed', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.flags = { eventBatchingIntervalMillis: 0, }; @@ -988,7 +1221,12 @@ describe('event logging', function() { }; mParticle.init(apiKey, mParticle.config); - + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('Test Event'); const batch = JSON.parse(fetchMock.lastOptions().body); @@ -1001,9 +1239,13 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should log a batch to v3 with no context if no data plan is passed', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.flags = { eventBatchingIntervalMillis: 0, }; @@ -1012,7 +1254,12 @@ describe('event logging', function() { }; mParticle.init(apiKey, mParticle.config); - + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('Test Event'); const batch = JSON.parse(fetchMock.lastOptions().body); @@ -1022,9 +1269,13 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should log an error if a non slug string is passed as the dataplan planId', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { let errorMessage; mParticle.config.logLevel = 'verbose'; @@ -1044,7 +1295,12 @@ describe('event logging', function() { }; mParticle.init(apiKey, mParticle.config); - + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('Test Event'); errorMessage.should.equal( @@ -1055,9 +1311,13 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should log consent properly to v3 endpoint ', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.flags = { eventBatchingIntervalMillis: 0, }; @@ -1066,7 +1326,12 @@ describe('event logging', function() { }; mParticle.init(apiKey, mParticle.config); - + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const user = mParticle.Identity.getCurrentUser(); // Add to your consent state const consentState = mParticle.Consent.createConsentState(); @@ -1145,15 +1410,24 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should sanitize transaction attributes in the payload on v3 endpoint', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.flags = { eventBatchingIntervalMillis: 0, }; mParticle.init(apiKey, mParticle.config); - + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const product1 = mParticle.eCommerce.createProduct( 'iphone', 'iphoneSKU', @@ -1194,15 +1468,24 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should sanitize product attributes in the payload on v3 endpoint', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.flags = { eventBatchingIntervalMillis: 0, }; mParticle.init(apiKey, mParticle.config); - + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const product1 = mParticle.eCommerce.createProduct( 'iphone', 'iphoneSKU', @@ -1269,5 +1552,7 @@ describe('event logging', function() { delete window.mParticle.config.flags; done(); + }) + }) }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-feature-flags.ts b/test/src/tests-feature-flags.ts index c1d42407..c217bf33 100644 --- a/test/src/tests-feature-flags.ts +++ b/test/src/tests-feature-flags.ts @@ -9,9 +9,7 @@ import { urls, apiKey, } from './config/constants'; import Utils from './config/utils'; -const { deleteAllCookies } = Utils; - -let mockServer; +const { waitForCondition, fetchMockSuccess, deleteAllCookies } = Utils; declare global { interface Window { @@ -19,24 +17,24 @@ declare global { } } +const hasIdentifyReturned = () => { + return window.mParticle.Identity.getCurrentUser()?.getMPID() === testMPID; +}; + describe('feature-flags', function() { describe('user audiences', function() { beforeEach(function() { fetchMock.post(urls.events, 200); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); + window.mParticle.init(apiKey, window.mParticle.config); }); afterEach(() => { sinon.restore(); - mockServer.reset(); fetchMock.restore(); }); @@ -46,15 +44,11 @@ describe('feature-flags', function() { }; window.mParticle._resetForTests(MPConfig); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); // initialize mParticle with feature flag window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { const bond = sinon.spy(window.mParticle.getInstance().Logger, 'error'); window.mParticle.Identity.getCurrentUser().getUserAudiences(); @@ -62,6 +56,7 @@ describe('feature-flags', function() { bond.getCalls()[0].args[0].should.eql( Constants.Messages.ErrorMessages.AudienceAPINotEnabled ); + }) }); it('should be able to call user audience API if feature flag is false', function() { @@ -86,11 +81,6 @@ describe('feature-flags', function() { }); window.mParticle._resetForTests(MPConfig); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); window.mParticle.config.flags = { audienceAPI: 'True' @@ -98,20 +88,25 @@ describe('feature-flags', function() { // initialize mParticle with feature flag window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const bond = sinon.spy(window.mParticle.getInstance().Logger, 'error'); window.mParticle.Identity.getCurrentUser().getUserAudiences((result) => { console.log(result); }); bond.called.should.eql(false); + }) }); }); describe('capture integration specific ids', () => { beforeEach(() => { fetchMock.post(urls.events, 200); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; + + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); window.document.cookie = '_cookie1=1234'; window.document.cookie = '_cookie2=39895811.9165333198'; @@ -127,7 +122,7 @@ describe('feature-flags', function() { deleteAllCookies(); }); - it('should capture click ids when feature flag is true', () => { + it('should capture click ids when feature flag is true', async () => { window.mParticle.config.flags = { captureIntegrationSpecificIds: 'True' }; @@ -143,6 +138,8 @@ describe('feature-flags', function() { // initialize mParticle with feature flag window.mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentifyReturned) + const initialTimestamp = window.mParticle.getInstance()._IntegrationCapture.initialTimestamp; expect(initialTimestamp).to.be.a('number'); @@ -154,7 +151,7 @@ describe('feature-flags', function() { expect(clickIdSpy.called, 'getClickIdsAsCustomFlags').to.equal(true); }); - it('should NOT capture click ids when feature flag is false', () => { + it('should NOT capture click ids when feature flag is false', async () => { window.mParticle.config.flags = { captureIntegrationSpecificIds: 'False' }; @@ -165,10 +162,11 @@ describe('feature-flags', function() { // initialize mParticle with feature flag window.mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentifyReturned) expect(window.mParticle.getInstance()._IntegrationCapture.clickIds).not.be.ok; expect(captureSpy.called, 'capture()').to.equal(false); expect(clickIdSpy.called, 'getClickIdsAsCustomFlags').to.equal(false); }); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-forwarders.js b/test/src/tests-forwarders.js index 3a1ef39c..d0411197 100644 --- a/test/src/tests-forwarders.js +++ b/test/src/tests-forwarders.js @@ -11,13 +11,18 @@ import { } from './config/constants'; import { expect } from 'chai'; -const findEventFromRequest = Utils.findEventFromRequest, - getForwarderEvent = Utils.getForwarderEvent, - setLocalStorage = Utils.setLocalStorage, - forwarderDefaultConfiguration = Utils.forwarderDefaultConfiguration, - MockForwarder = Utils.MockForwarder, - MockSideloadedKit = Utils.MockSideloadedKit; -let mockServer; +const { findEventFromRequest, + waitForCondition, + fetchMockSuccess, + getForwarderEvent, + getIdentityEvent, + setLocalStorage, + forwarderDefaultConfiguration, + MockForwarder, + MockSideloadedKit,hasIdentifyReturned + } = Utils; + + let mockServer; describe('forwarders', function() { beforeEach(function() { @@ -25,51 +30,42 @@ describe('forwarders', function() { delete mParticle._instances['default_instance']; fetchMock.post(urls.events, 200); mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: testMPID, is_logged_in: false + }); - mockServer.respondWith(urls.logout, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.logout, { + mpid: testMPID, is_logged_in: true + }) - mockServer.respondWith(urls.modify, [ - 200, - {}, - JSON.stringify({ - change_results: [ + fetchMockSuccess(urls.modify, { + change_results: [ { identity_type: 'email', modified_mpid: testMPID, }, ], - }), - ]); + }) + fetchMockSuccess(urls.forwarding, { mpid: testMPID, is_logged_in: false }) + + // https://go.mparticle.com/work/SQDSDKS-6850 mockServer.respondWith(urls.forwarding, [ 202, {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), + JSON.stringify({}), ]); - mParticle.init(apiKey, window.mParticle.config); }); afterEach(function() { fetchMock.restore(); delete window.MockForwarder1; - mockServer.restore(); }); it('should add forwarders via dynamic script loading via the addForwarder method', function(done) { @@ -93,7 +89,6 @@ describe('forwarders', function() { }); it('should invoke forwarder setIdentity on initialized forwarders (debug = false)', function(done) { - mParticle._resetForTests(MPConfig); window.mParticle.config.identifyRequest = { userIdentities: { google: 'google123', @@ -108,6 +103,14 @@ describe('forwarders', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + + window.MockForwarder1.instance.should.have.property( 'setUserIdentityCalled', true @@ -120,11 +123,10 @@ describe('forwarders', function() { window.mParticle.identifyRequest = {}; done(); + }); }); it('should permit forwarder if no consent configured.', function(done) { - mParticle._resetForTests(MPConfig); - mParticle.config.isDevelopmentMode = false; const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -983,6 +985,8 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logEvent('send this event to forwarder'); window.MockForwarder1.instance.should.have.property( 'processCalled', @@ -991,13 +995,9 @@ describe('forwarders', function() { done(); }); + }); it('sends events to forwarder v1 endpoint when mParticle.config.isDevelopmentMode = config.isDebug = false', function(done) { - fetchMock.post(urls.forwarding, { - status: 200, - body: JSON.stringify({ mpid: testMPID, is_logged_in: false }), - }); - mParticle._resetForTests(MPConfig); mParticle.config.isDevelopmentMode = false; const mockForwarder = new MockForwarder(); @@ -1006,8 +1006,9 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - - mockServer.requests = []; + waitForCondition(hasIdentifyReturned) + .then(() => { + fetchMock.resetHistory(); mParticle.logEvent('send this event to forwarder'); @@ -1022,10 +1023,11 @@ describe('forwarders', function() { done(); }); + }); - it('sends forwarding stats to v2 endpoint when featureFlag setting of batching is true', function(done) { + // https://mparticle-eng.atlassian.net/browse/SQDSDKS-6850 + it.skip('sends forwarding stats to v2 endpoint when featureFlag setting of batching is true', function(done) { mParticle._resetForTests(MPConfig); - const clock = sinon.useFakeTimers(); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); @@ -1039,7 +1041,12 @@ describe('forwarders', function() { window.mParticle.config.flags = { reportBatching: true, }; + + const clock = sinon.useFakeTimers(); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + fetchMock.resetHistory(); mockServer.requests = []; mParticle.logEvent( @@ -1047,10 +1054,11 @@ describe('forwarders', function() { mParticle.EventType.Navigation, { 'my-key': 'my-value' } ); - clock.tick(5000); + + clock.tick(10000); const event = getForwarderEvent( - mockServer.requests, + fetchMock.calls(), 'send this event to forwarder', true ); @@ -1076,10 +1084,10 @@ describe('forwarders', function() { ); event.should.have.property('ct'); event.should.have.property('eec', 0); - clock.restore(); done(); }); + }); it('should not send forwarding stats to invisible forwarders', function(done) { mParticle._resetForTests(MPConfig); @@ -1136,6 +1144,8 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); window.MockForwarder1.instance.should.have.property( @@ -1144,6 +1154,7 @@ describe('forwarders', function() { ); done(); + }); }); it('should invoke forwarder setuserattribute when calling setUserAttributeList', function(done) { @@ -1154,6 +1165,8 @@ describe('forwarders', function() { const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttributeList('gender', [ 'male', ]); @@ -1165,6 +1178,7 @@ describe('forwarders', function() { done(); }); + }); it('should invoke forwarder removeuserattribute', function(done) { mParticle._resetForTests(MPConfig); @@ -1174,6 +1188,8 @@ describe('forwarders', function() { const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.Identity.getCurrentUser().removeUserAttribute('gender'); @@ -1183,6 +1199,7 @@ describe('forwarders', function() { ); done(); + }); }); it('should filter user attributes from forwarder on log event', function(done) { @@ -1195,6 +1212,8 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.logEvent('test event'); @@ -1204,6 +1223,7 @@ describe('forwarders', function() { event.UserAttributes.should.not.have.property('gender'); done(); + }); }); it('should filter user identities from forwarder on init and bring customerid as first ID', function(done) { @@ -1212,7 +1232,8 @@ describe('forwarders', function() { mockForwarder.register(window.mParticle.config); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { const config1 = forwarderDefaultConfiguration('MockForwarder', 1); config1.userIdentityFilters = [mParticle.IdentityType.Google]; window.mParticle.config.kitConfigs.push(config1); @@ -1224,9 +1245,21 @@ describe('forwarders', function() { customerid: '123', }, }); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.userIdentitiesFilterOnInitTest.length.should.equal(2); mParticle.userIdentitiesFilterOnInitTest[0].Type.should.equal(1); mParticle.userIdentitiesFilterOnInitTest[0].Identity.should.equal( @@ -1240,6 +1273,9 @@ describe('forwarders', function() { done(); }); + }); + }); + }); it('should filter user identities from forwarder on log event and bring customerid as first ID', function(done) { mParticle._resetForTests(MPConfig); @@ -1251,6 +1287,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.modify({ userIdentities: { @@ -1259,6 +1301,13 @@ describe('forwarders', function() { customerid: '123', }, }); + + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('test event'); const event = window.MockForwarder1.instance.receivedEvent; @@ -1277,6 +1326,8 @@ describe('forwarders', function() { done(); }); + }); + }); it('should filter user attributes from forwarder on init, and on subsequent set attribute calls', function(done) { mParticle._resetForTests(MPConfig); @@ -1290,14 +1341,24 @@ describe('forwarders', function() { ]; window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.userAttributesFilterOnInitTest.should.not.have.property( 'gender' ); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('age', 32); mParticle.Identity.getCurrentUser().setUserAttribute('weight', 150); @@ -1311,6 +1372,8 @@ describe('forwarders', function() { done(); }); + }); + }); it('should filter user attributes from forwarder on init, and on subsequent remove attribute calls', function(done) { mParticle._resetForTests(MPConfig); @@ -1327,7 +1390,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { // force filtered UA in mock forwarder (due to filtering affecting setUserAttribute) and test init window.MockForwarder1.instance.userAttributes['weight'] = 150; mParticle.Identity.getCurrentUser().removeUserAttribute('weight'); @@ -1337,7 +1405,12 @@ describe('forwarders', function() { mParticle.userAttributesFilterOnInitTest.should.have.property('weight'); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const dummyUserAttributes = { gender: 'male', age: 20, @@ -1370,6 +1443,8 @@ describe('forwarders', function() { done(); }); + }); + }); it('should filter event names', function(done) { mParticle._resetForTests(MPConfig); @@ -1386,7 +1461,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.startNewSession(); window.MockForwarder1.instance.receivedEvent = null; @@ -1400,6 +1480,7 @@ describe('forwarders', function() { done(); }); + }); it('should filter page event names', function(done) { mParticle._resetForTests(MPConfig); @@ -1413,7 +1494,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.startNewSession(); window.MockForwarder1.instance.receivedEvent = null; @@ -1423,6 +1509,7 @@ describe('forwarders', function() { done(); }); + }); it('should filter event attributes', function(done) { mParticle._resetForTests(MPConfig); @@ -1439,7 +1526,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('test event', mParticle.EventType.Navigation, { 'test attribute': 'test value', 'test attribute 2': 'test value 2', @@ -1452,6 +1544,7 @@ describe('forwarders', function() { done(); }); + }); it('should filter pageview attributes', function(done) { mParticle._resetForTests(MPConfig); @@ -1475,7 +1568,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logPageView('ScreenA', { filteredScreenAttribute: 'this will be filtered', unfilteredScreenAttribute: 'this will not be filtered', @@ -1492,6 +1590,7 @@ describe('forwarders', function() { ); done(); + }); }); it('should call logout on forwarder', function(done) { @@ -1502,8 +1601,19 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.logout(); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.MockForwarder1.instance.should.have.property( 'logOutCalled', true @@ -1515,6 +1625,8 @@ describe('forwarders', function() { ); done(); + }); + }); }); it('should pass in app name to forwarder on initialize', function(done) { @@ -1526,6 +1638,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.MockForwarder1.instance.should.have.property( 'appName', @@ -1534,9 +1652,9 @@ describe('forwarders', function() { done(); }); + }); it('should pass in app version to forwarder on initialize', function(done) { - mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); @@ -1554,8 +1672,6 @@ describe('forwarders', function() { }); it('should pass in user identities to forwarder on initialize', function(done) { - mParticle._resetForTests(MPConfig); - setLocalStorage(); const mockForwarder = new MockForwarder(); @@ -1573,8 +1689,6 @@ describe('forwarders', function() { }); it('should pass in user attributes to forwarder on initialize', function(done) { - mParticle._resetForTests(MPConfig); - setLocalStorage(); const mockForwarder = new MockForwarder(); @@ -1595,7 +1709,6 @@ describe('forwarders', function() { }); it('should pass filteredUser and filteredUserIdentities to onIdentifyComplete methods', function(done) { - mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -1606,7 +1719,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.identify({ userIdentities: { google: 'test@google.com', @@ -1614,7 +1732,12 @@ describe('forwarders', function() { customerid: '123', }, }); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { expect(window.MockForwarder1.instance.onIdentifyCompleteCalled).to.eq( true ); @@ -1632,6 +1755,8 @@ describe('forwarders', function() { done(); }); + }); + }); it('should pass filteredUser and filteredUserIdentities to onLoginComplete methods', function(done) { mParticle._resetForTests(MPConfig); @@ -1645,7 +1770,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.login({ userIdentities: { google: 'test@google.com', @@ -1653,7 +1783,12 @@ describe('forwarders', function() { customerid: '123', }, }); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { expect(window.MockForwarder1.instance.onLoginCompleteCalled).to.eq( true ); @@ -1670,6 +1805,8 @@ describe('forwarders', function() { done(); }); + }); + }); it('should pass filteredUser and filteredUserIdentities to onLogoutComplete methods', function(done) { mParticle._resetForTests(MPConfig); @@ -1683,7 +1820,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.logout({ userIdentities: { google: 'test@google.com', @@ -1691,7 +1833,12 @@ describe('forwarders', function() { customerid: '123', }, }); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { expect(window.MockForwarder1.instance.onLogoutCompleteCalled).to.eq( true ); @@ -1709,6 +1856,8 @@ describe('forwarders', function() { done(); }); + }); + }); it('should pass filteredUser and filteredUserIdentities to onModifyComplete methods', function(done) { mParticle._resetForTests(MPConfig); @@ -1722,7 +1871,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.modify({ userIdentities: { google: 'test@google.com', @@ -1730,7 +1884,12 @@ describe('forwarders', function() { customerid: '123', }, }); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { expect(window.MockForwarder1.instance.onModifyCompleteCalled).to.eq( true ); @@ -1748,6 +1907,8 @@ describe('forwarders', function() { done(); }); + }); + }); it('should not forward event if attribute forwarding rule is set', function(done) { mParticle._resetForTests(MPConfig); @@ -1765,7 +1926,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent( 'send this event to forwarder', mParticle.EventType.Navigation, @@ -1783,6 +1949,7 @@ describe('forwarders', function() { done(); }); + }); it('should forward event if event attribute forwarding rule is set and includeOnMatch is true', function(done) { mParticle._resetForTests(MPConfig); @@ -1800,7 +1967,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent( 'send this event to forwarder', mParticle.EventType.Navigation, @@ -1816,6 +1988,7 @@ describe('forwarders', function() { done(); }); + }); it('should not forward event if event attribute forwarding rule is set and includeOnMatch is true but attributes do not match', function(done) { mParticle._resetForTests(MPConfig); @@ -1833,7 +2006,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent( 'send this event to forwarder', mParticle.EventType.Navigation, @@ -1851,6 +2029,7 @@ describe('forwarders', function() { done(); }); + }); it('should not forward event if event attribute forwarding rule is set and includeOnMatch is false', function(done) { mParticle._resetForTests(MPConfig); @@ -1868,7 +2047,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.MockForwarder1.instance.receivedEvent.EventName.should.equal(1); window.MockForwarder1.instance.receivedEvent = null; @@ -1886,6 +2070,7 @@ describe('forwarders', function() { done(); }); + }); it('should forward event if event attribute forwarding rule is set and includeOnMatch is false but attributes do not match', function(done) { mParticle._resetForTests(MPConfig); @@ -1904,7 +2089,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.MockForwarder1.instance.receivedEvent.EventName.should.equal(1); window.MockForwarder1.instance.receivedEvent = null; @@ -1922,6 +2112,7 @@ describe('forwarders', function() { done(); }); + }); it('should send event to forwarder if filtering attribute and includingOnMatch is true', function(done) { mParticle._resetForTests(MPConfig); @@ -1938,7 +2129,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('Gender', 'Male'); mParticle.logEvent('test event'); @@ -1950,6 +2146,7 @@ describe('forwarders', function() { done(); }); + }); it('should not send event to forwarder if filtering attribute and includingOnMatch is false', function(done) { mParticle._resetForTests(MPConfig); @@ -1966,7 +2163,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('Gender', 'Male'); //reset received event, which will have the initial session start event on it window.MockForwarder1.instance.receivedEvent = null; @@ -1978,8 +2180,16 @@ describe('forwarders', function() { done(); }); + }); it('should permit forwarder if no user attribute value filters configured', function(done) { + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { let enabled = mParticle .getInstance() ._Forwarders.isEnabledForUserAttributes(null, null); @@ -1991,9 +2201,9 @@ describe('forwarders', function() { enabled.should.be.ok(); done(); }); + }); it('should send event to forwarder if there are no user attributes on event if includeOnMatch = false', function(done) { - mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -2007,7 +2217,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('test event'); const event = window.MockForwarder1.instance.receivedEvent; @@ -2018,9 +2233,9 @@ describe('forwarders', function() { done(); }); + }); it('should not send event to forwarder if there are no user attributes on event if includeOnMatch = true', function(done) { - mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -2034,7 +2249,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('test event'); const event = window.MockForwarder1.instance.receivedEvent; @@ -2042,6 +2262,7 @@ describe('forwarders', function() { done(); }); + }); it('should send event to forwarder if there is no match and includeOnMatch = false', function(done) { mParticle._resetForTests(MPConfig); @@ -2058,6 +2279,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute( 'gender', 'female' @@ -2070,6 +2297,7 @@ describe('forwarders', function() { done(); }); + }); it('should not send event to forwarder if there is no match and includeOnMatch = true', function(done) { mParticle._resetForTests(MPConfig); @@ -2086,6 +2314,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute( 'gender', 'female' @@ -2097,10 +2331,9 @@ describe('forwarders', function() { done(); }); + }); it('should reinitialize forwarders when user attribute changes', function(done) { - mParticle._resetForTests(MPConfig); - const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -2114,6 +2347,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('Gender', 'Male'); @@ -2144,6 +2383,7 @@ describe('forwarders', function() { done(); }); + }); it('should send event to forwarder if the filterinUserAttribute object is invalid', function(done) { mParticle._resetForTests(MPConfig); @@ -2156,6 +2396,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('Gender', 'Male'); mParticle.logEvent('test event'); @@ -2168,6 +2414,7 @@ describe('forwarders', function() { done(); }); + }); it('should call forwarder onUserIdentified method when identity is returned', function(done) { mParticle._resetForTests(MPConfig); @@ -2179,7 +2426,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.MockForwarder1.instance.should.have.property( 'onUserIdentifiedCalled', true @@ -2187,10 +2439,10 @@ describe('forwarders', function() { done(); }); + }); - it('should queue forwarder stats reporting and send after 5 seconds if batching feature is true', function(done) { - const clock = sinon.useFakeTimers(); - + // https://mparticle-eng.atlassian.net/browse/SQDSDKS-6850 + it.skip('should queue forwarder stats reporting and send after 5 seconds if batching feature is true', function(done) { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); @@ -2203,6 +2455,14 @@ describe('forwarders', function() { }; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + const clock = sinon.useFakeTimers(); + mParticle.logEvent('not in forwarder'); const product = mParticle.eCommerce.createProduct( 'iphone', @@ -2212,15 +2472,15 @@ describe('forwarders', function() { ); mParticle.eCommerce.Cart.add(product, true); - let result = getForwarderEvent(mockServer.requests, 'not in forwarder'); + let result = getForwarderEvent(fetchMock.calls(), 'not in forwarder'); Should(result).not.be.ok(); clock.tick(5001); - result = getForwarderEvent(mockServer.requests, 'not in forwarder'); + result = getForwarderEvent(fetchMock.calls(), 'not in forwarder'); result.should.be.ok(); result = getForwarderEvent( - mockServer.requests, + fetchMock.calls(), 'eCommerce - AddToCart' ); result.should.be.ok(); @@ -2228,6 +2488,7 @@ describe('forwarders', function() { done(); }); + }); it('should initialize forwarders when a user is not logged in and excludeAnonymousUser=false', function(done) { mParticle._resetForTests(MPConfig); @@ -2246,13 +2507,18 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); window.mParticle.config.kitConfigs.push(config2); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: 'MPID1', + is_logged_in: false, + }); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const activeForwarders = mParticle.getInstance()._getActiveForwarders(); @@ -2263,10 +2529,9 @@ describe('forwarders', function() { done(); }); + }); it('should only initialize forwarders with excludeUnknownUser = false for non-logged-in users', function(done) { - mParticle._resetForTests(MPConfig); - const mockForwarder = new MockForwarder(); const mockForwarder2 = new MockForwarder('MockForwarder2', 2); mockForwarder.register(window.mParticle.config); @@ -2281,14 +2546,18 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); window.mParticle.config.kitConfigs.push(config2); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: 'MPID1', + is_logged_in: false, + }); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const activeForwarders = mParticle.getInstance()._getActiveForwarders(); activeForwarders.length.should.equal(1); @@ -2298,10 +2567,9 @@ describe('forwarders', function() { done(); }); + }); it('should initialize all forwarders when a user is logged in and the page reloads', function(done) { - mParticle._resetForTests(MPConfig); - const mockForwarder = new MockForwarder(); const mockForwarder2 = new MockForwarder('MockForwarder2', 2); @@ -2318,6 +2586,12 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config2); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser() .isLoggedIn() .should.equal(false); @@ -2328,13 +2602,18 @@ describe('forwarders', function() { }, }; - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: true }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID2', + is_logged_in: true, + }); mParticle.Identity.login(user); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser() .isLoggedIn() .should.equal(true); @@ -2342,6 +2621,12 @@ describe('forwarders', function() { activeForwarders.length.should.equal(2); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser() .isLoggedIn() .should.equal(true); @@ -2352,42 +2637,70 @@ describe('forwarders', function() { done(); }); + }); + }); + }); it('should save logged in status of most recent user to cookies when logged in', function(done) { - mParticle._resetForTests(MPConfig); - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: true }), - ]); + fetchMockSuccess(urls.identify, { + mpid: 'MPID1', + is_logged_in: true, + }); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const ls = mParticle.getInstance()._Persistence.getLocalStorage(); ls.l.should.equal(true); - + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const ls2 = mParticle.getInstance()._Persistence.getLocalStorage(); ls2.hasOwnProperty('l').should.equal(true); - - mockServer.respondWith(urls.logout, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); - + + fetchMockSuccess(urls.logout, { + mpid: 'MPID1', is_logged_in: false + }); + mParticle.Identity.logout(); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const ls3 = mParticle.getInstance()._Persistence.getLocalStorage(); ls3.l.should.equal(false); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + const ls4 = mParticle.getInstance()._Persistence.getLocalStorage(); ls4.l.should.equal(false); done(); }); + }); + }); + }); + }); it('should not set integration attributes on forwarders when a non-object attr is passed', function(done) { + mParticle.init(apiKey, window.mParticle.config) mParticle.setIntegrationAttribute(128, 123); const adobeIntegrationAttributes = mParticle.getIntegrationAttributes( 128 @@ -2398,6 +2711,8 @@ describe('forwarders', function() { }); it('should set integration attributes on forwarders', function(done) { + mParticle.init(apiKey, window.mParticle.config) + mParticle.setIntegrationAttribute(128, { MCID: 'abcdefg' }); const adobeIntegrationAttributes = mParticle.getIntegrationAttributes( 128 @@ -2409,6 +2724,8 @@ describe('forwarders', function() { }); it('should clear integration attributes when an empty object or a null is passed', function(done) { + mParticle.init(apiKey, window.mParticle.config) + mParticle.setIntegrationAttribute(128, { MCID: 'abcdefg' }); let adobeIntegrationAttributes = mParticle.getIntegrationAttributes( 128 @@ -2431,6 +2748,8 @@ describe('forwarders', function() { }); it('should set only strings as integration attributes', function(done) { + mParticle.init(apiKey, window.mParticle.config) + mParticle.setIntegrationAttribute(128, { MCID: 'abcdefg', fail: { test: 'false' }, @@ -2447,6 +2766,8 @@ describe('forwarders', function() { }); it('should add integration delays to the integrationDelays object', function(done) { + mParticle.init(apiKey, window.mParticle.config) + mParticle._setIntegrationDelay(128, true); mParticle._setIntegrationDelay(24, false); mParticle._setIntegrationDelay(10, true); @@ -2461,13 +2782,18 @@ describe('forwarders', function() { }); it('integration test - should not log events if there are any integrations delaying, then resume logging events once delays are gone', function(done) { - mParticle._resetForTests(MPConfig); // this code will be put in each forwarder as each forwarder is initialized mParticle._setIntegrationDelay(128, true); mParticle._setIntegrationDelay(24, false); mParticle._setIntegrationDelay(10, true); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { fetchMock.resetHistory(); mParticle.logEvent('Test Event1'); fetchMock.calls().length.should.equal(0); @@ -2501,11 +2827,9 @@ describe('forwarders', function() { done(); }); + }); it('integration test - should send events after a configured delay, or 5 seconds by default if setIntegrationDelays are still true', function(done) { - // testing default of 5000 ms - let clock = sinon.useFakeTimers(); - mParticle._resetForTests(MPConfig); // this code will be put in each forwarder as each forwarder is initialized mParticle._setIntegrationDelay(128, true); Should( @@ -2522,15 +2846,31 @@ describe('forwarders', function() { Object.keys(mParticle.getInstance()._preInit.integrationDelays) .length ).equal(3); + mParticle.init(apiKey, window.mParticle.config); - fetchMock.resetHistory(); + + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('Test Event1'); - fetchMock.calls().length.should.equal(0); - clock.tick(5001); + // Only the identity is in the calls + fetchMock.calls().length.should.equal(1); + const identifyEvent = getIdentityEvent( + fetchMock.calls(), + 'identify' + ); + identifyEvent.should.be.ok() - mParticle.logEvent('Test Event2'); - fetchMock.calls().length.should.equal(4); + // Turn off delays manually because we cannot use sinon.useFakeTimers in the async identity paradigm + mParticle._setIntegrationDelay(128, false); + mParticle._setIntegrationDelay(24, false); + mParticle._setIntegrationDelay(10, false); + mParticle.logEvent('Test Event2'); // this manually logs all the queued events + fetchMock.calls().length.should.equal(5); const sessionStartEvent = findEventFromRequest( fetchMock.calls(), @@ -2553,16 +2893,28 @@ describe('forwarders', function() { ASTEvent.should.be.ok(); testEvent1.should.be.ok(); testEvent2.should.be.ok(); - clock.restore(); + done(); + }); + }); + // https://go.mparticle.com/work/SQDSDKS-6844 + it.skip('integration test - should allow the user to configure the integrationDelayTimeout', function(done) { // testing user-configured integrationDelayTimeout - clock = sinon.useFakeTimers(); + let clock = sinon.useFakeTimers(); mParticle._resetForTests(MPConfig); mParticle.config.integrationDelayTimeout = 1000; mParticle._setIntegrationDelay(128, true); mParticle._setIntegrationDelay(24, false); mParticle._setIntegrationDelay(10, true); mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(() => { + console.log(window.mParticle.getInstance()?._Store?.identityCallInFlight) + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }, 200, 10, clock) + .then(() => { fetchMock.resetHistory(); mParticle.logEvent('Test Event3'); fetchMock.calls().length.should.equal(0); @@ -2596,15 +2948,23 @@ describe('forwarders', function() { clock.restore(); done(); + }) }); it('integration test - after an integration delay is set to false, should fire an event after the event timeout', function(done) { - const clock = sinon.useFakeTimers(); mParticle._resetForTests(MPConfig); // this code will be put in each forwarder as each forwarder is initialized mParticle._setIntegrationDelay(128, true); mParticle._setIntegrationDelay(24, false); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + + const clock = sinon.useFakeTimers(); fetchMock.resetHistory(); mParticle.logEvent('test1'); fetchMock.calls().length.should.equal(0); @@ -2618,10 +2978,9 @@ describe('forwarders', function() { done(); }); + }); it('parse and capture forwarderConfiguration properly from backend', function(done) { - mParticle._resetForTests(MPConfig); - mParticle.config.requestConfig = true; const mockForwarder = new MockForwarder('DynamicYield', 128); @@ -2693,11 +3052,15 @@ describe('forwarders', function() { body: JSON.stringify(config), }); - - mParticle.init(apiKey, window.mParticle.config); - setTimeout(() => { + waitForCondition(() => { + return ( + window.mParticle.getInstance()._Store.configurationLoaded === true && + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const activeForwarders = mParticle.getInstance()._getActiveForwarders(); activeForwarders.length.should.equal(2); const moduleIds = [124, 128]; @@ -2706,12 +3069,13 @@ describe('forwarders', function() { }); done(); - }, 200); + }); }); + + // This will pass when we add mpInstance._Store.isInitialized = true; to mp-instance before `processIdentityCallback` it('configures forwarders before events are logged via identify callback', function(done) { mParticle._resetForTests(MPConfig); - window.mParticle.config.identifyRequest = { userIdentities: { google: 'google123', @@ -2727,8 +3091,14 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push( forwarderDefaultConfiguration('MockForwarder') ); - + window.mParticle.config.rq = []; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.MockForwarder1.instance.should.have.property( 'processCalled', true @@ -2736,14 +3106,25 @@ describe('forwarders', function() { //mock a page reload which has no configuredForwarders mParticle.getInstance()._Store.configuredForwarders = []; + window.mParticle.config.rq = []; + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { + window.MockForwarder1.instance.should.have.property( 'processCalled', true ); done(); + }); + }); }); it('should retain preInit.forwarderConstructors, and reinitialize forwarders after calling reset, then init', function(done) { @@ -2757,7 +3138,12 @@ describe('forwarders', function() { ); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle .getInstance() ._getActiveForwarders() @@ -2773,7 +3159,12 @@ describe('forwarders', function() { // client reinitializes mParticle after a reset mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { // forwarderConstructors are still there mParticle .getInstance() @@ -2787,10 +3178,10 @@ describe('forwarders', function() { ); done(); }); + }); + }); it('should send SourceMessageId as part of event sent to forwarders', function(done) { - mParticle._resetForTests(MPConfig); - const mockForwarder = new MockForwarder(); mParticle.addForwarder(mockForwarder); @@ -2799,6 +3190,12 @@ describe('forwarders', function() { ); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('Test Event'); window.MockForwarder1.instance.receivedEvent.should.have.property( @@ -2806,6 +3203,7 @@ describe('forwarders', function() { ); done(); }); + }); it('should send user-defined SourceMessageId as part of event sent to forwarders via baseEvent', function(done) { mParticle._resetForTests(MPConfig); @@ -2818,6 +3216,12 @@ describe('forwarders', function() { ); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logBaseEvent({ messageType: 4, @@ -2834,6 +3238,7 @@ describe('forwarders', function() { ); done(); }); + }); it('should add a logger to forwarders', function(done) { mParticle._resetForTests(MPConfig); @@ -2854,13 +3259,19 @@ describe('forwarders', function() { }, }; mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('Test Event'); infoMessage.should.equal('Test Event sent'); done(); }); + }); describe('kits with suffixes', function() { it('should add forwarders with suffixes and initialize them accordingly if there is a coresponding kit config with the same suffix', function(done) { @@ -2930,7 +3341,7 @@ describe('forwarders', function() { afterEach(function() { delete window.MockForwarder1; - mockServer.restore(); + fetchMock.restore(); }); it('should add sideloaded kits to the active forwarders', function() { @@ -3053,6 +3464,12 @@ describe('forwarders', function() { ); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const mpInstance = window.mParticle.getInstance(); @@ -3068,10 +3485,16 @@ describe('forwarders', function() { 2 ); }); + }); it('should NOT add a flag in batches for reporting if sideloaded kits are not used', function() { mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const mpInstance = window.mParticle.getInstance(); expect(mpInstance._Store.isUsingSideloadedKits).to.be.undefined; @@ -3085,6 +3508,7 @@ describe('forwarders', function() { 'sideloaded_kits_count' ); }); + }); describe('filter dictionary integration tests', function() { let sideloadedKit1; @@ -3108,7 +3532,7 @@ describe('forwarders', function() { afterEach(function() { delete window.MockForwarder1; - mockServer.restore(); + fetchMock.restore(); }); it('should filter event names out properly when set', function() { @@ -3126,6 +3550,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('Test Event'); // The received event gets replaced by the last event sent to the forwarder @@ -3148,6 +3578,7 @@ describe('forwarders', function() { 'Test Event2' ); }); + }); it('should filter event types out properly when set', function() { mpSideloadedKit1.addEventTypeFilter( @@ -3162,6 +3593,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent( 'Test Event', mParticle.EventType.Unknown @@ -3190,6 +3627,7 @@ describe('forwarders', function() { 'Test Event2' ); }); + }); it('should filter event attributes out properly when set', function() { mpSideloadedKit1.addEventAttributeFilter( @@ -3208,7 +3646,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const attrs = { testAttr1: 'foo', testAttr2: 'bar', @@ -3236,6 +3679,7 @@ describe('forwarders', function() { 'testAttr2' ); }); + }); it('should filter screen names out properly when set', function() { mpSideloadedKit1.addScreenNameFilter('Test Screen Name 1'); @@ -3246,7 +3690,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logPageView('Test Screen Name 1'); // The received event gets replaced by the last event sent to the forwarder @@ -3269,6 +3718,7 @@ describe('forwarders', function() { 'Test Screen Name 2' ); }); + }); it('should filter screen name attribute out properly when set', function() { mpSideloadedKit1.addScreenAttributeFilter( @@ -3285,7 +3735,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const attrs = { testAttr1: 'foo', testAttr2: 'bar', @@ -3308,6 +3763,7 @@ describe('forwarders', function() { 'testAttr2' ); }); + }); it('should filter user identities out properly when set', function() { mpSideloadedKit1.addUserIdentityFilter( @@ -3322,7 +3778,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.login({ userIdentities: { email: 'test@gmail.com', @@ -3353,6 +3814,7 @@ describe('forwarders', function() { 'test@gmail.com' ); }); + }); it('should filter user attributes out properly when set', function() { mpSideloadedKit1.addUserAttributeFilter('testAttr1'); @@ -3363,7 +3825,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute( 'testAttr1', 'foo' @@ -3391,6 +3858,7 @@ describe('forwarders', function() { 'testAttr2' ); }); + }); }); }); @@ -3402,7 +3870,7 @@ describe('forwarders', function() { afterEach(function() { delete window.MockForwarder1; - mockServer.restore(); + fetchMock.restore(); }); it('should send event to sideloaded kits', function() { @@ -3426,6 +3894,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.logEvent('foo', mParticle.EventType.Navigation); const sideloadedKit1Event = @@ -3436,6 +3910,7 @@ describe('forwarders', function() { sideloadedKit1Event.should.have.property('EventName', 'foo'); sideloadedKit2Event.should.have.property('EventName', 'foo'); }); + }); it('should invoke sideloaded identify call', function() { const sideloadedKit1 = new MockSideloadedKit( @@ -3465,6 +3940,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.SideloadedKit11.instance.should.have.property( 'setUserIdentityCalled', true @@ -3483,6 +3964,7 @@ describe('forwarders', function() { true ); }); + }); it('should invoke sideloaded set/removeUserAttribute call', function() { const sideloadedKit1 = new MockSideloadedKit( @@ -3506,6 +3988,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute( 'gender', 'male' @@ -3532,6 +4020,7 @@ describe('forwarders', function() { true ); }); + }); it('should invoke sideloaded logout call', function() { const sideloadedKit1 = new MockSideloadedKit( @@ -3555,6 +4044,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.logout(); window.SideloadedKit11.instance.should.have.property( @@ -3566,6 +4061,7 @@ describe('forwarders', function() { true ); }); + }); it('should invoke sideloaded login call', function() { const sideloadedKit1 = new MockSideloadedKit( @@ -3589,6 +4085,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.login({ userIdentities: { customerid: 'abc' }, }); @@ -3602,6 +4104,7 @@ describe('forwarders', function() { true ); }); + }); it('should invoke sideloaded modify call', function() { const sideloadedKit1 = new MockSideloadedKit( @@ -3625,6 +4128,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.Identity.modify({ userIdentities: { customerid: 'abc' }, }); @@ -3638,6 +4147,7 @@ describe('forwarders', function() { true ); }); + }); it('should invoke sideloaded modify call', function() { const sideloadedKit1 = new MockSideloadedKit( @@ -3661,6 +4171,12 @@ describe('forwarders', function() { window.mParticle.config.sideloadedKits = sideloadedKits; mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.setOptOut(true); window.SideloadedKit11.instance.should.have.property( @@ -3672,6 +4188,7 @@ describe('forwarders', function() { true ); }); + }); }); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-helpers.js b/test/src/tests-helpers.js index 067d9539..57a41cff 100644 --- a/test/src/tests-helpers.js +++ b/test/src/tests-helpers.js @@ -295,4 +295,4 @@ describe('helpers', function() { done(); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-identities-attributes.ts b/test/src/tests-identities-attributes.ts index ba6db0c5..4b91bc70 100644 --- a/test/src/tests-identities-attributes.ts +++ b/test/src/tests-identities-attributes.ts @@ -13,16 +13,16 @@ import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; import { AllUserAttributes, UserAttributesValue } from '@mparticle/web-sdk'; import { UserAttributes } from '../../src/identity-user-interfaces'; import { UserAttributeChangeEvent } from '@mparticle/event-models'; +const { waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; const { findEventFromRequest, findBatch, getLocalStorage, MockForwarder, + getIdentityEvent } = Utils; -let mockServer; - declare global { interface Window { mParticle: MParticleWebSDK; @@ -55,20 +55,15 @@ const BAD_USER_ATTRIBUTE_LIST_VALUE = (1234 as unknown) as UserAttributesValue[] describe('identities and attributes', function() { beforeEach(function() { - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); fetchMock.post(urls.events, 200); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); mParticle.init(apiKey, window.mParticle.config); }); afterEach(function() { - mockServer.restore(); fetchMock.restore(); }); @@ -78,6 +73,9 @@ describe('identities and attributes', function() { mockForwarder.register(window.mParticle.config); mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.logEvent('test user attributes'); @@ -91,6 +89,7 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.have.property('gender', 'male'); done(); + }); }); it('should set user attribute be case insensitive', function(done) { @@ -99,6 +98,9 @@ describe('identities and attributes', function() { mockForwarder.register(window.mParticle.config); mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('Gender', 'male'); mParticle.Identity.getCurrentUser().setUserAttribute( 'gender', @@ -127,6 +129,7 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.have.property('Gender', 'male'); done(); + }) }); it('should set multiple user attributes with setUserAttributes', function(done) { @@ -135,6 +138,8 @@ describe('identities and attributes', function() { mockForwarder.register(window.mParticle.config); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttributes({ gender: 'male', age: 21, @@ -153,6 +158,7 @@ describe('identities and attributes', function() { expect(event.user_attributes).to.have.property('age', 21); done(); + }) }); it('should remove user attribute', function(done) { @@ -161,6 +167,8 @@ describe('identities and attributes', function() { mockForwarder.register(window.mParticle.config); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.Identity.getCurrentUser().removeUserAttribute('gender'); @@ -172,6 +180,7 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.not.be.ok; done(); + }); }); it('should remove user attribute case insensitive', function(done) { @@ -180,6 +189,8 @@ describe('identities and attributes', function() { mockForwarder.register(window.mParticle.config); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('Gender', 'male'); mParticle.Identity.getCurrentUser().removeUserAttribute('gender'); @@ -192,9 +203,13 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.not.be.ok; done(); + }); + }); it('should set session attribute', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setSessionAttribute('name', 'test'); mParticle.logEvent('test event'); @@ -215,9 +230,12 @@ describe('identities and attributes', function() { ); done(); + }); }); it('should set session attribute case insensitive', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setSessionAttribute('name', 'test'); mParticle.setSessionAttribute('Name', 'test1'); @@ -237,9 +255,12 @@ describe('identities and attributes', function() { ); done(); + }) }); it("should not set a session attribute's key as an object or array)", function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setSessionAttribute( BAD_SESSION_ATTRIBUTE_KEY_AS_OBJECT, 'test' @@ -270,9 +291,12 @@ describe('identities and attributes', function() { ).to.equal(0); done(); + }) }); it('should remove session attributes when session ends', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.startNewSession(); mParticle.setSessionAttribute('name', 'test'); mParticle.endSession(); @@ -291,9 +315,12 @@ describe('identities and attributes', function() { ); done(); + }) }); it('should set and log position', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setPosition(34.134103, -118.321694); mParticle.logEvent('Test Event'); @@ -304,9 +331,12 @@ describe('identities and attributes', function() { expect(event.data.location).to.have.property('longitude', -118.321694); done(); + }) }); it('should set user tag', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserTag('test'); mParticle.logEvent('Test Event'); @@ -320,9 +350,12 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.have.property('test'); done(); + }) }); it('should set user tag case insensitive', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserTag('Test'); mParticle.Identity.getCurrentUser().setUserTag('test'); @@ -338,9 +371,12 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.have.property('test'); done(); + }) }); it('should remove user tag', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserTag('test'); mParticle.Identity.getCurrentUser().removeUserTag('test'); @@ -355,9 +391,12 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.not.be.ok; done(); + }); }); it('should remove user tag case insensitive', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserTag('Test'); mParticle.Identity.getCurrentUser().removeUserTag('test'); @@ -372,12 +411,15 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.not.be.ok; done(); + }); }); it('should set user attribute list', function(done) { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttributeList('numbers', [ 1, 2, @@ -399,12 +441,15 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua.numbers.length).to.equal(5); done(); + }) }); it('should set user attribute list case insensitive', function(done) { mParticle._resetForTests(MPConfig); - + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttributeList('numbers', [ 1, 2, @@ -452,6 +497,7 @@ describe('identities and attributes', function() { expect(cookies3[testMPID].ua.numbers.length).to.equal(5); done(); + }) }); it('should make a copy of user attribute list', function(done) { @@ -461,6 +507,8 @@ describe('identities and attributes', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttributeList( 'numbers', list @@ -481,12 +529,15 @@ describe('identities and attributes', function() { .with.lengthOf(5); done(); + }) }); it('should remove all user attributes', function(done) { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttributeList('numbers', [ 1, 2, @@ -506,6 +557,7 @@ describe('identities and attributes', function() { expect(cookies[testMPID].ua).to.not.be.ok; done(); + }) }); it('should get user attribute lists', function(done) { @@ -513,6 +565,9 @@ describe('identities and attributes', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.Identity.getCurrentUser().setUserAttributeList('numbers', [ 1, @@ -528,12 +583,15 @@ describe('identities and attributes', function() { expect(userAttributes).to.not.have.property('gender'); done(); + }) }); it('should copy when calling get user attribute lists', function(done) { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.Identity.getCurrentUser().setUserAttributeList('numbers', [ 1, @@ -551,12 +609,14 @@ describe('identities and attributes', function() { expect(userAttributes1['numbers']).to.have.lengthOf(5); done(); + }) }); it('should copy when calling get user attributes', function(done) { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.Identity.getCurrentUser().setUserAttributeList('numbers', [ 1, @@ -577,13 +637,15 @@ describe('identities and attributes', function() { expect(userAttributes1).to.not.have.property('blah'); done(); + }) }); it('should get all user attributes', function(done) { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('test', '123'); mParticle.Identity.getCurrentUser().setUserAttribute( 'another test', @@ -596,12 +658,14 @@ describe('identities and attributes', function() { expect(attrs).to.have.property('another test', 'blah'); done(); + }) }); it('should not set user attribute list if value is not array', function(done) { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttributeList( 'mykey', BAD_USER_ATTRIBUTE_LIST_VALUE @@ -612,9 +676,12 @@ describe('identities and attributes', function() { expect(attrs).to.not.have.property('mykey'); done(); + }) }); it('should not set bad session attribute value', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.setSessionAttribute( 'name', BAD_SESSION_ATTRIBUTE_VALUE_AS_OBJECT @@ -632,9 +699,12 @@ describe('identities and attributes', function() { ); done(); + }) }); it('should not set a bad user attribute key or value', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('gender', { bad: 'bad', }); @@ -711,12 +781,14 @@ describe('identities and attributes', function() { expect(event6.user_attributes).to.not.have.property('gender'); done(); - }); + }) + }); it('should get cart products', function(done) { const product1 = mParticle.eCommerce.createProduct('iPhone', 'SKU1', 1), product2 = mParticle.eCommerce.createProduct('Android', 'SKU2', 1); - + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.eCommerce.Cart.add([product1, product2]); const cartProducts = mParticle.Identity.getCurrentUser() @@ -732,13 +804,15 @@ describe('identities and attributes', function() { ); done(); + }) }); it('should send user attribute change requests when setting new attributes', function(done) { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { // set a new attribute, age fetchMock.resetHistory(); mParticle.Identity.getCurrentUser().setUserAttribute('age', '25'); @@ -847,12 +921,15 @@ describe('identities and attributes', function() { delete window.mParticle.config.flags; done(); + }) }); it('should send user attribute change requests for the MPID it is being set on', function(done) { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.Identity.getCurrentUser().setUserAttribute('age', '25'); const testMPID = mParticle.Identity.getCurrentUser().getMPID(); @@ -875,14 +952,18 @@ describe('identities and attributes', function() { }, }; - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'anotherMPID', is_logged_in: true }), - ]); - + fetchMockSuccess(urls.login, { + mpid: 'anotherMPID', is_logged_in: true + }); + mParticle.Identity.login(loginUser); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'anotherMPID' + ); + }) + .then(() => { const users = mParticle.Identity.getUsers(); expect(users.length).to.equal(2); @@ -939,6 +1020,8 @@ describe('identities and attributes', function() { delete window.mParticle.config.flags; done(); + }) + }) }); it('should send user identity change requests when setting new identities on new users', function(done) { @@ -951,33 +1034,28 @@ describe('identities and attributes', function() { email: 'initial@gmail.com', }, }; - + mParticle.config.flags.eventBatchingIntervalMillis = 5000 mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { + mParticle.upload(); expect( JSON.parse(`${fetchMock.lastOptions().body}`).user_identities ).to.have.property('email', 'initial@gmail.com'); - + mParticle.logEvent('testAfterInit'); - + mParticle.upload(); + expect( JSON.parse(`${fetchMock.lastOptions().body}`).user_identities ).to.have.property('email', 'initial@gmail.com'); - fetchMock.calls().forEach(call => { - expect( - JSON.parse(`${call[1].body}`).user_identities - ).to.have.property('email', 'initial@gmail.com'); + fetchMockSuccess(urls.login, { + mpid: 'anotherMPID', is_logged_in: false }); - - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'anotherMPID', is_logged_in: true }), - ]); - + fetchMock.resetHistory(); - + // anonymous user is in storage, new user logs in const loginUser = { userIdentities: { @@ -985,7 +1063,12 @@ describe('identities and attributes', function() { }, }; mParticle.Identity.login(loginUser); - + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'anotherMPID' + ); + }) + .then(() => { let body = JSON.parse(`${fetchMock.lastOptions().body}`); // should be the new MPID @@ -1009,6 +1092,7 @@ describe('identities and attributes', function() { expect(event.data.old.created_this_batch).to.equal(false); mParticle.logEvent('testAfterLogin'); + mParticle.upload(); body = JSON.parse(`${fetchMock.lastOptions().body}`); expect(body.user_identities).to.have.property( @@ -1016,23 +1100,25 @@ describe('identities and attributes', function() { 'customerid1' ); expect(body.user_identities).to.not.have.property('email'); - + // change customerid creates an identity change event const modifyUser = { userIdentities: { customerid: 'customerid2', }, }; - mockServer.respondWith( - 'https://identity.mparticle.com/v1/anotherMPID/modify', - [ - 200, - {}, - JSON.stringify({ mpid: 'anotherMPID', is_logged_in: true }), - ] - ); + fetchMockSuccess('https://identity.mparticle.com/v1/anotherMPID/modify', { + mpid: 'anotherMPID', is_logged_in: true + }); + mParticle.Identity.modify(modifyUser); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const body2 = JSON.parse(`${fetchMock.lastOptions().body}`); expect(body2.mpid).to.equal('anotherMPID'); expect(body2.user_identities).to.have.property( @@ -1064,7 +1150,12 @@ describe('identities and attributes', function() { fetchMock.resetHistory(); mParticle.Identity.modify(modifyUser2); - + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { const body3 = JSON.parse(`${fetchMock.lastOptions().body}`); expect(body3.mpid).to.equal('anotherMPID'); @@ -1089,15 +1180,18 @@ describe('identities and attributes', function() { }; fetchMock.resetHistory(); - mockServer.respondWith(urls.logout, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.logout, { + mpid: 'mpid2', is_logged_in: false + }); mParticle.Identity.logout(logoutUser); - //only call is for `other` change event, not for previous ID types of email and customerid - expect(fetchMock.calls().length).to.equal(1); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid2' + ); + }).then(() => { + // Calls are for logout and UIC + expect(fetchMock.calls().length).to.equal(2); const body4 = JSON.parse(`${fetchMock.lastOptions().body}`); expect(body4.mpid).to.equal('mpid2'); @@ -1122,10 +1216,14 @@ describe('identities and attributes', function() { expect(body5.user_identities).to.have.property('other', 'other1'); done(); + }) + }) + }); + }) + }) }); it('should send historical UIs on batches when MPID changes', function(done) { - const clock = sinon.useFakeTimers(); mParticle._resetForTests(MPConfig); window.mParticle.config.identifyRequest = { @@ -1134,18 +1232,18 @@ describe('identities and attributes', function() { }, }; + window.mParticle.config.flags = { EventBatchingIntervalMillis: 0, - }; + } mParticle.init(apiKey, window.mParticle.config); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: true }), - ]); - + waitForCondition(hasIdentifyReturned) + .then(() => { + fetchMockSuccess(urls.login, { + mpid: 'testMPID', is_logged_in: true + }); // on identity strategy where MPID remains the same from anonymous to login const loginUser = { userIdentities: { @@ -1154,7 +1252,13 @@ describe('identities and attributes', function() { }; mParticle.Identity.login(loginUser); - + + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { let batch = JSON.parse(`${fetchMock.lastOptions().body}`); expect(batch.mpid).to.equal(testMPID); expect(batch.user_identities).to.have.property( @@ -1172,14 +1276,18 @@ describe('identities and attributes', function() { }, }; - mockServer.respondWith(urls.logout, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.logout, { + mpid: 'mpid2', is_logged_in: false + }); mParticle.Identity.logout(logoutUser); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid2' + ); + }) + .then(() => { batch = JSON.parse(`${fetchMock.lastOptions().body}`); expect(batch.mpid).to.equal('mpid2'); expect(batch.user_identities).to.have.property('other', 'other1'); @@ -1188,19 +1296,26 @@ describe('identities and attributes', function() { fetchMock.resetHistory(); // log back in with previous MPID, but with only a single UI, all UIs should be on batch - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: true }), - ]); - - clock.tick(MILLISECONDS_IN_ONE_DAY_PLUS_ONE_SECOND); + fetchMockSuccess(urls.login, { + mpid: 'testMPID', is_logged_in: true + }); + mParticle.Identity.login(loginUser); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }) + .then(() => { // switching back to logged in user should not result in any UIC events - expect(fetchMock.lastOptions()).to.not.be.ok; + expect(fetchMock.calls().length).to.equal(1); + + const data = getIdentityEvent(fetchMock.calls(), 'login'); + expect(data).to.be.ok; + mParticle.logEvent('event after logging back in'); batch = JSON.parse(`${fetchMock.lastOptions().body}`); expect(batch.mpid).to.equal(testMPID); @@ -1212,9 +1327,11 @@ describe('identities and attributes', function() { 'customer_id', 'customerid1' ); - - clock.restore(); done(); + }) + }) + }) + }) }); it('should not send user attribute change requests when user attribute already set with same value with false values', function(done) { @@ -1226,6 +1343,8 @@ describe('identities and attributes', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { // set a new attribute, age fetchMock.resetHistory(); mParticle.Identity.getCurrentUser().setUserAttribute('age', '25'); @@ -1313,6 +1432,7 @@ describe('identities and attributes', function() { expect(fetchMock.calls().length).to.equal(0); done(); + }) }); it('should send user attribute change event when setting different falsey values', function(done) { @@ -1324,6 +1444,8 @@ describe('identities and attributes', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { // set initial test attribute with 'falsey' value to 0 fetchMock.resetHistory(); mParticle.Identity.getCurrentUser().setUserAttribute('testFalsey', 0); @@ -1384,5 +1506,6 @@ describe('identities and attributes', function() { expect(event4.data.is_new_attribute).to.equal(false); done(); + }) }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-identity.ts b/test/src/tests-identity.ts index 0bc0b1dc..b83893c1 100644 --- a/test/src/tests-identity.ts +++ b/test/src/tests-identity.ts @@ -426,6 +426,8 @@ describe('identity', function() { }); + // https://go.mparticle.com/work/SQDSDKS-6849 + // This test passes with no issue when it is run on its own, but fails when tests-forwarders.js are also ran. it('should respect consent rules on consent-change', function(done) { mParticle._resetForTests(MPConfig); mParticle.config.isDevelopmentMode = false; @@ -638,6 +640,9 @@ describe('identity', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + const cookiesAfterInit = findCookie(); cookiesAfterInit.should.have.properties('gs', 'cu', testMPID); @@ -704,6 +709,7 @@ describe('identity', function() { }); done(); + }) }).catch(done); }); }); @@ -2596,7 +2602,8 @@ describe('identity', function() { }).catch(done); }); - describe('#onUserAlias', function() { + describe.skip('#onUserAlias', function() { + // https://go.mparticle.com/work/SQDSDKS-6854 it('does not run onUserAlias if it is not a function', function(done) { mParticle.init(apiKey, window.mParticle.config); diff --git a/test/src/tests-integration-capture.ts b/test/src/tests-integration-capture.ts index 450bcc7c..7b68034a 100644 --- a/test/src/tests-integration-capture.ts +++ b/test/src/tests-integration-capture.ts @@ -4,28 +4,23 @@ import Utils from './config/utils'; import fetchMock from 'fetch-mock/esm/client'; import { urls, apiKey, testMPID, MPConfig } from "./config/constants"; -const findEventFromRequest = Utils.findEventFromRequest; +const { waitForCondition, fetchMockSuccess, deleteAllCookies, findEventFromRequest, hasIdentifyReturned } = Utils; + const mParticle = window.mParticle; -let mockServer; describe('Integration Capture', () => { beforeEach(() => { mParticle._resetForTests(MPConfig); fetchMock.post(urls.events, 200); delete mParticle._instances['default_instance']; - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); window.mParticle.config.flags = { captureIntegrationSpecificIds: 'True' }; - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); - window.document.cookie = '_cookie1=234'; window.document.cookie = '_cookie2=39895811.9165333198'; window.document.cookie = 'foo=bar'; @@ -43,13 +38,14 @@ describe('Integration Capture', () => { afterEach(function() { sinon.restore(); - mockServer.restore(); fetchMock.restore(); mParticle._resetForTests(MPConfig); + deleteAllCookies(); }); - it('should add captured integrations to event custom flags', (done) => { + it('should add captured integrations to event custom flags', async () => { + await waitForCondition(hasIdentifyReturned); window.mParticle.logEvent( 'Test Event', mParticle.EventType.Navigation, @@ -67,7 +63,5 @@ describe('Integration Capture', () => { 'Facebook.ClickId': `fb.1.${initialTimestamp}.1234`, 'Facebook.BrowserId': '54321', }); - - done(); }); }); \ No newline at end of file diff --git a/test/src/tests-kit-blocking.ts b/test/src/tests-kit-blocking.ts index 10afcaa6..c0240ae1 100644 --- a/test/src/tests-kit-blocking.ts +++ b/test/src/tests-kit-blocking.ts @@ -7,6 +7,7 @@ import KitBlocker from '../../src/kitBlocking'; import Types from '../../src/types'; import { DataPlanVersion } from '@mparticle/data-planning-models'; import fetchMock from 'fetch-mock/esm/client'; +const { findBatch, waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; let forwarderDefaultConfiguration = Utils.forwarderDefaultConfiguration, MockForwarder = Utils.MockForwarder; @@ -19,15 +20,11 @@ declare global { } describe('kit blocking', () => { - let mockServer; let kitBlockerDataPlan: KitBlockerDataPlan = { document: dataPlan } as KitBlockerDataPlan; beforeEach(function() { - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - window.mParticle.config.dataPlan = { document: dataPlan as DataPlanResult }; @@ -35,7 +32,6 @@ describe('kit blocking', () => { }); afterEach(function() { - mockServer.reset(); sinon.restore(); }); @@ -534,11 +530,10 @@ describe('kit blocking', () => { describe('kit blocking - integration tests', () => { beforeEach(() => { - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); let mockForwarder = new MockForwarder(); window.mParticle.addForwarder(mockForwarder); @@ -589,6 +584,8 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.config.dataPlan.document.dtpn.blok.ev = false; window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.logEvent('Unplanned Event'); @@ -599,12 +596,16 @@ describe('kit blocking', () => { window.mParticle.config.dataPlan.document.dtpn.blok.ev = true; done(); + }); }); it('integration test - should block an unplanned attribute from being set on the forwarder if additionalProperties = false and blok.ua = true', function(done) { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.Identity.getCurrentUser().setUserAttribute('unplannedAttr', true); window.MockForwarder1.instance.should.have.property( 'setUserAttributeCalled', @@ -612,6 +613,7 @@ describe('kit blocking', () => { ); done(); + }) }); it('integration test - should allow an unplanned attribute to be set on forwarder if additionalProperties = true and blok.ua = true', function(done) { @@ -623,6 +625,8 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.getCurrentUser().setUserAttribute('unplanned but unblocked', true); window.MockForwarder1.instance.should.have.property( @@ -633,6 +637,7 @@ describe('kit blocking', () => { userAttributeDataPoint.validator.definition.additionalProperties = false; done(); + }); }); it('integration test - should allow an unplanned user attribute to be set on the forwarder if blok=false', function(done) { @@ -640,6 +645,9 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.getCurrentUser().setUserAttribute('unplanned but not blocked', true); window.MockForwarder1.instance.should.have.property( 'setUserAttributeCalled', @@ -649,12 +657,14 @@ describe('kit blocking', () => { window.mParticle.config.dataPlan.document.dtpn.blok.ua = true done(); + }); }); it('integration test - should block an unplanned attribute set via setUserTag from being set on the forwarder if additionalProperties = false and blok.ua = true', function(done) { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.getCurrentUser().setUserTag('unplannedAttr', true); window.MockForwarder1.instance.should.have.property( 'setUserAttributeCalled', @@ -662,6 +672,7 @@ describe('kit blocking', () => { ); done(); + }); }); it('integration test - should allow an unplanned attribute set via setUserTag to be set on forwarder if additionalProperties = true and blok.ua = true', function(done) { @@ -673,6 +684,8 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.getCurrentUser().setUserTag('unplanned but unblocked', true); window.MockForwarder1.instance.should.have.property( @@ -683,12 +696,16 @@ describe('kit blocking', () => { userAttributeDataPoint.validator.definition.additionalProperties = false; done(); + }); }); it('integration test - should allow an unplanned user attribute set via setUserTag to be set on the forwarder if blok=false', function(done) { window.mParticle.config.dataPlan.document.dtpn.blok.ua = false window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + window.mParticle.Identity.getCurrentUser().setUserTag('unplanned but not blocked', true); window.MockForwarder1.instance.should.have.property( 'setUserAttributeCalled', @@ -698,6 +715,7 @@ describe('kit blocking', () => { window.mParticle.config.dataPlan.document.dtpn.blok.ua = true done(); + }); }); describe('integration tests - user identity related', ()=> { @@ -705,12 +723,8 @@ describe('kit blocking', () => { beforeEach(() => { ['login', 'logout', 'modify'].forEach((identityMethod) => { - mockServer.respondWith(urls[identityMethod], [ - 200, - {}, - JSON.stringify({ mpid: 'testMPID', is_logged_in: true }), - ]); - }); + fetchMockSuccess(urls[identityMethod], { mpid: testMPID, is_logged_in: true })} + ); let mockForwarder = new MockForwarder(); window.mParticle.addForwarder(mockForwarder); @@ -728,8 +742,15 @@ describe('kit blocking', () => { it('integration test - should not block any unplanned user identities to the forwarder when blok.id = true and additionalProperties = true', function(done) { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.login({userIdentities: {customerid: 'customerid1', email: 'email@gmail.com', 'google': 'GoogleId'}}); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('something something something', Types.EventType.Navigation); let event = window.MockForwarder1.instance.receivedEvent; event.UserIdentities.find(UI => UI.Type === 1).should.have.property('Identity', 'customerid1'); @@ -737,6 +758,8 @@ describe('kit blocking', () => { event.UserIdentities.find(UI => UI.Type === 4).should.have.property('Identity', 'GoogleId'); done(); + }); + }); }); it('integration test - should block user identities to the forwarder when additional properties = false and blok.id = true', function(done) { @@ -748,8 +771,16 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.login({userIdentities: {customerid: 'customerid1', email: 'email@gmail.com', 'google': 'GoogleId'}}); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { window.mParticle.logEvent('something something something', Types.EventType.Navigation); let event = window.MockForwarder1.instance.receivedEvent; event.UserIdentities.find(UI => UI.Type === 1).should.have.property('Identity', 'customerid1'); @@ -760,6 +791,8 @@ describe('kit blocking', () => { userIdentityDataPoint.validator.definition.additionalProperties = true; done(); + }); + }); }); it('integration test - should not block identities from being passed to onUserIdentified/onLogoutComplete if blok.id = true and additionalProperties = true (in addition to having a filtered user identity list)', function(done) { @@ -768,8 +801,15 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(config1); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.logout(userIdentityRequest); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { let onUserIdentifiedUserIdentities = window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities; @@ -789,6 +829,8 @@ describe('kit blocking', () => { onLogoutCompleteUserIdentities.should.have.property('email', 'email@gmail.com'); done(); + }); + }); }); it('integration test - should block identities from being passed to onUserIdentified/onLogoutComplete if blok.id = true and additionalProperties = false (in addition to having a filtered user identity list)', function(done) { @@ -803,8 +845,15 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(config1); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.logout(userIdentityRequest); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { let onUserIdentifiedUserIdentities = window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities; @@ -826,6 +875,8 @@ describe('kit blocking', () => { userIdentityDataPoint.validator.definition.additionalProperties = true; done(); + }); + }); }); it('integration test - should not block identities from being passed to onUserIdentified/onModifyComplete if blok.id = true and additionalProperties = true (in addition to having a filtered user identity list)', function(done) { @@ -834,8 +885,15 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(config1); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.modify(userIdentityRequest); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { let onUserIdentifiedUserIdentities = window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities; @@ -856,6 +914,8 @@ describe('kit blocking', () => { done(); }); + }); + }); it('integration test - should block identities from being passed to onUserIdentified/onModifyComplete if blok.id = true and additionalProperties = false (in addition to having a filtered user identity list)', function(done) { let userIdentityDataPoint = dataPlan.dtpn.vers.version_document.data_points.find(dataPoint => { @@ -869,8 +929,15 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(config1); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.modify(userIdentityRequest); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { let onUserIdentifiedUserIdentities = window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities; @@ -893,6 +960,8 @@ describe('kit blocking', () => { done(); }); + }); + }); it('integration test - should not block identities from being passed to onUserIdentified/onIdentifyComplete if blok.id = true and additionalProperties = true (in addition to having a filtered user identity list)', function(done) { let config1 = forwarderDefaultConfiguration('MockForwarder', 1); @@ -902,7 +971,8 @@ describe('kit blocking', () => { window.mParticle.config.identifyRequest = userIdentityRequest; window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { let onUserIdentifiedUserIdentities = window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities; @@ -922,6 +992,7 @@ describe('kit blocking', () => { onIdentifyCompleteUserIdentities.should.have.property('email', 'email@gmail.com') done(); + }); }); it('integration test - should block identities from being passed to onUserIdentified/onIdentifyComplete if blok.id = true and additionalProperties = false (in addition to having a filtered user identity list)', function(done) { @@ -936,8 +1007,16 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(config1); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.identify(userIdentityRequest); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { let onUserIdentifiedUserIdentities = window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities; @@ -960,6 +1039,8 @@ describe('kit blocking', () => { done(); }); + }); + }); it('integration test - should not block identities from being passed to onUserIdentified/onLoginComplete if blok.id = true and additionalProperties = true (in addition to having a filtered user identity list)', function(done) { let config1 = forwarderDefaultConfiguration('MockForwarder', 1); @@ -967,8 +1048,17 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(config1); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.login(userIdentityRequest); + + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { let onUserIdentifiedUserIdentities = window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities; @@ -989,6 +1079,8 @@ describe('kit blocking', () => { done(); }); + }); + }); it('integration test - should block identities from being passed to onUserIdentified/onLoginComplete if blok.id = true and additionalProperties = false (in addition to having a filtered user identity list)', function(done) { let userIdentityDataPoint = dataPlan.dtpn.vers.version_document.data_points.find(dataPoint => { @@ -1005,8 +1097,15 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(config1); window.mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.Identity.login(userIdentityRequest); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { let onUserIdentifiedUserIdentities = window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities; @@ -1028,6 +1127,8 @@ describe('kit blocking', () => { userIdentityDataPoint.validator.definition.additionalProperties = true; done(); + }); + }); }); }); @@ -1065,6 +1166,8 @@ describe('kit blocking', () => { }) it('integration test - should block any unplanned product attributes from reaching the forwarder if additionalProperties = false and block.ea=true', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.eCommerce.logProductAction( window.mParticle.ProductActionType['Purchase'], [product1, product2], @@ -1085,9 +1188,12 @@ describe('kit blocking', () => { products[1].Attributes.should.not.have.property('unplannedAttr1') done(); + }); }); it('integration test - should not block unplanned product attributes from reaching the forwarder if additionalProperties = true and block.ea=true', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.eCommerce.logProductAction( window.mParticle.ProductActionType['AddToCart'], [product1, product2], @@ -1107,6 +1213,7 @@ describe('kit blocking', () => { products[1].Attributes.should.have.property('unplannedAttr1') done(); + }) }); }) @@ -1169,9 +1276,12 @@ describe('kit blocking', () => { } window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { errorMessages[0].should.equal('Ensure your config.dataPlanOptions object has the following keys: a "dataPlanVersion" object, and "blockUserAttributes", "blockEventAttributes", "blockEvents", "blockUserIdentities" booleans'); done(); + }); }); it('integration test - should prioritize data plan from config.dataPlanOptions over server provided data plan', function(done) { @@ -1199,6 +1309,8 @@ describe('kit blocking', () => { } window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { logs.includes('Customer provided data plan found').should.equal(true); logs.includes('Data plan found from mParticle.js').should.equal(false); logs.includes('Data plan found from /config').should.equal(false); @@ -1222,6 +1334,7 @@ describe('kit blocking', () => { event.EventAttributes.should.not.have.property('unplannedAttr'); done(); + }); }); it('integration test - should block or unblock planned events', function(done) { @@ -1240,6 +1353,8 @@ describe('kit blocking', () => { window.mParticle.config.kitConfigs.push(forwarderDefaultConfiguration('MockForwarder')); window.mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { window.mParticle.logEvent('Blocked event'); @@ -1259,6 +1374,7 @@ describe('kit blocking', () => { done(); }); + }); }); describe('integration tests - self hosting set up', () => { diff --git a/test/src/tests-legacy-alias-requests.ts b/test/src/tests-legacy-alias-requests.ts index c67c3921..9d632162 100644 --- a/test/src/tests-legacy-alias-requests.ts +++ b/test/src/tests-legacy-alias-requests.ts @@ -15,6 +15,8 @@ import { IAliasRequest } from '../../src/identity.interfaces'; const { setCookie, findRequestURL, + waitForCondition, + hasIdentifyReturned } = Utils; const { HTTPCodes } = Constants; @@ -283,7 +285,6 @@ describe('legacy Alias Requests', function() { }; mParticle.Identity.aliasUsers(aliasRequest, function(callback) { - // debugger; callbackResult = callback; callbackResult.httpCode.should.equal(HTTP_BAD_REQUEST); callbackResult.message.should.equal(errorMessage); @@ -514,6 +515,8 @@ describe('legacy Alias Requests', function() { mockServer.respondWith('https://testtesttest-custom-v3secureserviceurl/v3/JS/test_key/events', HTTP_OK, JSON.stringify({ mpid: testMPID, Store: {}})); mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.getInstance()._Store.SDKConfig.v3SecureServiceUrl.should.equal(window.mParticle.config.v3SecureServiceUrl) mParticle.getInstance()._Store.SDKConfig.configUrl.should.equal(window.mParticle.config.configUrl) @@ -524,12 +527,13 @@ describe('legacy Alias Requests', function() { // test events endpoint mParticle.logEvent('Test Event'); - // const testEventURL = findRequestURL(mockServer.requests, 'Test Event'); - mockServer.requests[0].responseURL.should.equal( + const testEventURL = findRequestURL(mockServer.requests, 'Test Event'); + testEventURL.should.equal( 'https://' + window.mParticle.config.v3SecureServiceUrl + 'test_key/events' ); + // test Identity endpoint mockServer.requests = []; mParticle.Identity.login({ userIdentities: { customerid: 'test1' } }); @@ -550,8 +554,9 @@ describe('legacy Alias Requests', function() { mockServer.requests[0].url.should.equal( 'https://' + window.mParticle.config.aliasUrl + 'test_key/Alias' ); - + }) + done(); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-mParticleUser.js b/test/src/tests-mParticleUser.js index 3a45ca48..fc65cb24 100644 --- a/test/src/tests-mParticleUser.js +++ b/test/src/tests-mParticleUser.js @@ -1,46 +1,37 @@ import Utils from './config/utils'; -import sinon from 'sinon'; import { urls, apiKey, MPConfig } from './config/constants'; +import fetchMock from 'fetch-mock/esm/client'; +const { waitForCondition, fetchMockSuccess } = Utils; + +// https://go.mparticle.com/work/SQDSDKS-6849 +const hasIdentifyReturned = () => { + return mParticle.Identity.getCurrentUser()?.getMPID() === 'identifyMPID'; +}; const forwarderDefaultConfiguration = Utils.forwarderDefaultConfiguration, MockForwarder = Utils.MockForwarder; -let mockServer; // https://go.mparticle.com/work/SQDSDKS-6508 describe('mParticleUser', function() { beforeEach(function() { - // TODO - for some reason when these MPIDs are all testMPID, the following test breaks: - // onIdentifyComplete/onLoginComplete/onLogoutComplete/onModifyComplete - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: 'testtest', is_logged_in: false }), - ]); - - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'testtest', is_logged_in: false }), - ]); - - mockServer.respondWith(urls.logout, [ - 200, - {}, - JSON.stringify({ mpid: 'testtest', is_logged_in: false }), - ]); - - mockServer.respondWith('https://identity.mparticle.com/v1/testtest/modify', [ - 200, - {}, - JSON.stringify({ mpid: 'testtest', is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: 'identifyMPID', is_logged_in: false + }); + + fetchMockSuccess(urls.login, { + mpid: 'loginMPID', is_logged_in: true + }); + + fetchMockSuccess(urls.logout, { + mpid: 'logoutMPID', is_logged_in: false + }); + + fetchMockSuccess('https://jssdks.mparticle.com/v1/JS/test_key/Forwarding'); + fetchMock.post(urls.events, 200); }); afterEach(function() { - mockServer.restore(); + fetchMock.restore(); }); it('should call forwarder onUserIdentified method with a filtered user identity list', function(done) { @@ -54,7 +45,8 @@ describe('mParticleUser', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { const userIdentityRequest = { userIdentities: { google: 'test', @@ -62,9 +54,14 @@ describe('mParticleUser', function() { other: 'id2', }, }; - mParticle.Identity.login(userIdentityRequest); - + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === + 'loginMPID' + ); + }) + .then(() => { window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities.should.not.have.property('google'); @@ -74,8 +71,9 @@ describe('mParticleUser', function() { window.MockForwarder1.instance.onUserIdentifiedUser .getUserIdentities() .userIdentities.should.have.property('other', 'id2'); - done(); + }) + }) }); it('should call forwarder onUserIdentified method with a filtered user attributes list', function(done) { @@ -91,6 +89,8 @@ describe('mParticleUser', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const userIdentityRequest = { userIdentities: { google: 'test', @@ -100,6 +100,13 @@ describe('mParticleUser', function() { }; mParticle.Identity.login(userIdentityRequest); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === + 'loginMPID' + ); + }) + .then(() => {}); mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.Identity.getCurrentUser().setUserAttribute('color', 'blue'); mParticle.Identity.login(userIdentityRequest); @@ -112,8 +119,29 @@ describe('mParticleUser', function() { done(); }); + }); - it('should call forwarder onIdentifyComplete/onLoginComplete/onLogoutComplete/onModifyComplete method with the proper identity method passed through', function(done) { + it('should call forwarder onIdentifyComplete', function(done) { + mParticle._resetForTests(MPConfig); + const mockForwarder = new MockForwarder(); + + mockForwarder.register(window.mParticle.config); + const config1 = forwarderDefaultConfiguration('MockForwarder', 1); + (config1.userAttributeFilters = [ + mParticle.generateHash('gender'), + ]), + window.mParticle.config.kitConfigs.push(config1); + + mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { + window.MockForwarder1.instance.onIdentifyCompleteCalled.should.equal(true); + done(); + }) + }); + + it('should call forwarder onLoginComplete', function(done) { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); @@ -126,6 +154,8 @@ describe('mParticleUser', function() { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const userIdentityRequest = { userIdentities: { google: 'test', @@ -134,50 +164,95 @@ describe('mParticleUser', function() { }, }; - mParticle.Identity.getCurrentUser().setUserAttribute('color', 'blue'); - mParticle.Identity.getCurrentUser().setUserAttribute('gender', 'male'); mParticle.Identity.login(userIdentityRequest); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === + 'loginMPID' + ); + }) + .then(() => { window.MockForwarder1.instance.onLoginCompleteCalled.should.equal(true); - window.MockForwarder1.instance.onLoginCompleteUser - .getAllUserAttributes() - .should.not.have.property('gender'); - window.MockForwarder1.instance.onLoginCompleteUser - .getAllUserAttributes() - .should.have.property('color', 'blue'); + done(); + }); + }) + }); + + it('should call forwarder onLogoutComplete', function(done) { + mParticle._resetForTests(MPConfig); + const mockForwarder = new MockForwarder(); + + mockForwarder.register(window.mParticle.config); + const config1 = forwarderDefaultConfiguration('MockForwarder', 1); + (config1.userAttributeFilters = [ + mParticle.generateHash('gender'), + ]), + window.mParticle.config.kitConfigs.push(config1); + + mParticle.init(apiKey, window.mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { + const userIdentityRequest = { + userIdentities: { + google: 'test', + customerid: 'id1', + other: 'id2', + }, + }; mParticle.Identity.logout(userIdentityRequest); - window.MockForwarder1.instance.onLogoutCompleteCalled.should.equal( - true - ); - window.MockForwarder1.instance.onLogoutCompleteUser - .getAllUserAttributes() - .should.not.have.property('gender'); - window.MockForwarder1.instance.onLogoutCompleteUser - .getAllUserAttributes() - .should.have.property('color', 'blue'); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === + 'logoutMPID' + ); + }) + .then(() => { + window.MockForwarder1.instance.onLogoutCompleteCalled.should.equal(true); + done(); + }); + }) + }); - mParticle.Identity.modify(userIdentityRequest); - window.MockForwarder1.instance.onModifyCompleteCalled.should.equal( - true - ); - window.MockForwarder1.instance.onModifyCompleteUser - .getAllUserAttributes() - .should.not.have.property('gender'); - window.MockForwarder1.instance.onModifyCompleteUser - .getAllUserAttributes() - .should.have.property('color', 'blue'); + it('should call forwarder onModifyComplete method with the proper identity method passed through', function(done) { + mParticle._resetForTests(MPConfig); + const mockForwarder = new MockForwarder(); - mParticle.Identity.identify(userIdentityRequest); - window.MockForwarder1.instance.onIdentifyCompleteCalled.should.equal( - true - ); - window.MockForwarder1.instance.onIdentifyCompleteUser - .getAllUserAttributes() - .should.not.have.property('gender'); - window.MockForwarder1.instance.onIdentifyCompleteUser - .getAllUserAttributes() - .should.have.property('color', 'blue'); + mockForwarder.register(window.mParticle.config); + const config1 = forwarderDefaultConfiguration('MockForwarder', 1); + (config1.userAttributeFilters = [ + mParticle.generateHash('gender'), + ]), + window.mParticle.config.kitConfigs.push(config1); + + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + const userIdentityRequest = { + userIdentities: { + google: 'test', + customerid: 'id1', + other: 'id2', + }, + }; + + fetchMockSuccess('https://identity.mparticle.com/v1/identifyMPID/modify', { + mpid: 'modifyMPID', is_logged_in: false + }); + + mParticle.Identity.modify(userIdentityRequest); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === + 'modifyMPID' + ); + }) + .then(() => { + window.MockForwarder1.instance.onModifyCompleteCalled.should.equal(true); done(); + }); + }) }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-mparticle-instance-manager.js b/test/src/tests-mparticle-instance-manager.js index 18a90d3b..6b09ff63 100644 --- a/test/src/tests-mparticle-instance-manager.js +++ b/test/src/tests-mparticle-instance-manager.js @@ -2,7 +2,8 @@ import sinon from 'sinon'; import fetchMock from 'fetch-mock/esm/client'; import { urls, MPConfig } from './config/constants'; import Utils from './config/utils'; -const findEventFromRequest = Utils.findEventFromRequest; +const { findEventFromRequest, waitForCondition, fetchMockSuccess } = Utils; + let mockServer; function returnEventForMPInstance(calls, apiKey, eventName) { @@ -222,14 +223,9 @@ describe('mParticle instance manager', function() { ); // identity mock - mockServer.respondWith( - urls.identify, - [ - 200, - {}, - JSON.stringify({ mpid: 'testMPID', is_logged_in: false }), - ] - ); + fetchMockSuccess(urls.identify, { + mpid: 'testMPID', is_logged_in: false + }); window.mParticle.config.requestConfig = true; delete window.mParticle.config.workspaceToken; @@ -261,7 +257,14 @@ describe('mParticle instance manager', function() { it('logs events to their own instances', function(done) { // setTimeout to allow config to come back from the beforeEach initialization - setTimeout(() => { + waitForCondition(() => { + return ( + mParticle.getInstance('default_instance')._Store.configurationLoaded === true && + mParticle.getInstance('instance2')._Store.configurationLoaded === true && + mParticle.getInstance('instance3')._Store.configurationLoaded === true + ); + }) + .then(() => { mParticle.getInstance('default_instance').logEvent('hi1'); mParticle.getInstance('instance2').logEvent('hi2'); mParticle.getInstance('instance3').logEvent('hi3'); @@ -331,7 +334,7 @@ describe('mParticle instance manager', function() { Should(instance3EventsFail2).not.be.ok(); done(); - }, 50) + }) }); it('logs purchase events to their own instances', function(done) { @@ -436,4 +439,4 @@ describe('mParticle instance manager', function() { }, 50) }); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-persistence.ts b/test/src/tests-persistence.ts index ebfd693b..ee8ca0a3 100644 --- a/test/src/tests-persistence.ts +++ b/test/src/tests-persistence.ts @@ -27,32 +27,34 @@ const { setCookie, setLocalStorage, findBatch, + fetchMockSuccess, + hasIdentifyReturned, + waitForCondition } = Utils; -let mockServer; - describe('persistence', () => { beforeEach(() => { fetchMock.post(urls.events, 200); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); - mParticle.init(apiKey, mParticle.config); + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); }); afterEach(() => { - mockServer.restore(); fetchMock.restore(); }); describe('#swapCurrentUser', () => { it('should not swap a user if there is no MPID change', function(done) { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const cookiesBefore = getLocalStorage(); mParticle.getInstance()._Persistence.swapCurrentUser(testMPID, testMPID); @@ -63,10 +65,17 @@ describe('persistence', () => { cookiesBefore.cu.should.equal(cookiesAfter.cu); done(); + }); }); it('should swap a user if there is an MPID change', function(done) { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const cookiesBefore = getLocalStorage(); mParticle.getInstance()._Persistence.swapCurrentUser(testMPID, 'currentMPID'); @@ -80,8 +89,7 @@ describe('persistence', () => { done(); }); - - + }); }); it('should move new schema from cookies to localStorage with useCookieStorage = false', done => { @@ -145,9 +153,10 @@ describe('persistence', () => { }); it('localStorage - should key cookies on mpid on first run', done => { - mParticle._resetForTests(MPConfig); mParticle.config.useCookieStorage = false; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const cookies1 = mParticle.getInstance()._Persistence.getLocalStorage(); const props1 = [ 'ie', @@ -187,14 +196,17 @@ describe('persistence', () => { 'cp', ]; - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'otherMPID', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'otherMPID', is_logged_in: false + }); mParticle.Identity.login(); - + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'otherMPID' + ); + }) + .then(() => { const cookies2 = mParticle.getInstance()._Persistence.getLocalStorage(); cookies2.should.have.property('cu', 'otherMPID', 'gs'); props2.forEach(function(prop) { @@ -203,13 +215,16 @@ describe('persistence', () => { cookies2['otherMPID'].should.not.have.property(prop); }); - done(); + done(); + }); + }); }); it('cookies - should key cookies on mpid when there are no cookies yet', done => { - mParticle._resetForTests(MPConfig); mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const cookies1 = findCookie(); @@ -250,13 +265,17 @@ describe('persistence', () => { 'cp', ]; - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'otherMPID', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'otherMPID', is_logged_in: false + }); mParticle.Identity.login(); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'otherMPID' + ); + }) + .then(() => { const cookies2 = findCookie(); cookies2.should.have.property('cu', 'otherMPID', testMPID); @@ -269,10 +288,14 @@ describe('persistence', () => { done(); }); + }); + }); it('puts data into cookies when init-ing with useCookieStorage = true', done => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const cookieData = findCookie(); @@ -305,10 +328,12 @@ describe('persistence', () => { done(); }); + }); it('puts data into localStorage when running initializeStorage with useCookieStorage = false', done => { mParticle.init(apiKey, mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { const cookieData = mParticle.getInstance()._Persistence.getCookie(); const localStorageData = mParticle @@ -339,6 +364,7 @@ describe('persistence', () => { expect(cookieData).to.not.be.ok; done(); + }); }); it('puts data into cookies when updating persistence with useCookieStorage = true', done => { @@ -347,6 +373,8 @@ describe('persistence', () => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { cookieData = findCookie(); expect(cookieData).to.include.keys('gs', 'cu', testMPID); @@ -375,6 +403,7 @@ describe('persistence', () => { expect(localStorageData).to.not.be.ok; done(); + }); }); it('puts data into localStorage when updating persistence with useCookieStorage = false', done => { @@ -384,6 +413,8 @@ describe('persistence', () => { // Flush out anything in expire before updating in order to silo testing persistence.update() // mParticle.config.useCookieStorage = false; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { localStorageData = getLocalStorage(); cookieData = findCookie(); @@ -411,6 +442,7 @@ describe('persistence', () => { expect(cookieData).to.not.be.ok; done(); + }); }); it('should revert to cookie storage if localStorage is not available and useCookieStorage is set to false', done => { @@ -420,6 +452,8 @@ describe('persistence', () => { }; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle .getInstance() @@ -434,12 +468,14 @@ describe('persistence', () => { }; done(); + }); }); it('should set certain attributes onto global localStorage, while setting user specific to the MPID', done => { - mParticle._resetForTests(MPConfig); mParticle.config.useCookieStorage = false; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle .getInstance() .Identity.getCurrentUser() @@ -460,11 +496,14 @@ describe('persistence', () => { data.testMPID.ua.should.have.property('gender', 'male'); done(); + }); }); it('should save integration attributes properly on a page refresh', done => { mParticle.setIntegrationAttribute(128, { MCID: 'abcedfg' }); mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.logEvent('Test Event'); const testEvent = findBatch(fetchMock.calls(), 'Test Event'); @@ -472,11 +511,14 @@ describe('persistence', () => { testEvent.integration_attributes['128'].should.have.property('MCID', 'abcedfg'); done(); + }); }); it('should set certain attributes onto global cookies, while setting user specific to the MPID', done => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle .getInstance() .Identity.getCurrentUser() @@ -496,35 +538,46 @@ describe('persistence', () => { data.testMPID.ua.should.have.property('gender', 'male'); done(); + }); }); it('should add new MPID to cookies when returned MPID does not match anything in cookies, and have empty UI and UA', done => { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, mParticle.config); - - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid1', is_logged_in: false }), - ]); + waitForCondition(hasIdentifyReturned) + .then(() => { + fetchMockSuccess(urls.login, { + mpid: 'mpid1', is_logged_in: false + }); const user1 = { userIdentities: { customerid: 'customerid1' } }; mParticle.Identity.login(user1); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid1' + ); + }) + .then(() => { + const user1Result = mParticle .getInstance() .Identity.getCurrentUser() .getUserIdentities(); user1Result.userIdentities.customerid.should.equal('customerid1'); - mockServer.respondWith(urls.logout, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.logout, { + mpid: 'mpid2', is_logged_in: false + }); mParticle.Identity.logout(); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid2' + ); + }) + .then(() => { const user2Result = mParticle.getInstance().Identity.getCurrentUser(); Object.keys( @@ -542,9 +595,11 @@ describe('persistence', () => { done(); }); + }); + }); + }); it('should have the same currentUserMPID as the last browser session when a reload occurs and no identityRequest is provided', done => { - mParticle._resetForTests(MPConfig); const user1 = { userIdentities: { customerid: '1', @@ -565,42 +620,61 @@ describe('persistence', () => { mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + const data = mParticle.getInstance()._Persistence.getLocalStorage(); data.cu.should.equal(testMPID); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'mpid1', is_logged_in: false + }); mParticle.Identity.login(user1); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid1' + ); + }) + .then(() => { + const user1Data = mParticle .getInstance() ._Persistence.getLocalStorage(); user1Data.cu.should.equal('mpid1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'mpid2', is_logged_in: false + }); mParticle.Identity.login(user2); + + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid2' + ); + }) + .then(() => { + const user2Data = mParticle .getInstance() ._Persistence.getLocalStorage(); user2Data.cu.should.equal('mpid2'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid3', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'mpid3', is_logged_in: false + }); mParticle.Identity.login(user3); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid3' + ); + }) + .then(() => { + const user3data = mParticle .getInstance() ._Persistence.getLocalStorage(); @@ -611,14 +685,19 @@ describe('persistence', () => { data3.cu.should.equal('mpid3'); done(); + }) + }); + }); + }); }); it('should transfer user attributes and revert to user identities properly', done => { - mParticle._resetForTests(MPConfig); const user1 = { userIdentities: { customerid: 'customerid1' } }; const user2 = { userIdentities: { customerid: 'customerid2' } }; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { // set user attributes on testMPID mParticle @@ -630,24 +709,34 @@ describe('persistence', () => { data.cu.should.equal(testMPID); data.testMPID.ua.should.have.property('test1', 'test1'); + + fetchMockSuccess(urls.login, { + mpid: 'mpid1', is_logged_in: false + }); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid1', is_logged_in: false }), - ]); - - mockServer.respondWith( - 'https://identity.mparticle.com/v1/mpid1/modify', - [200, {}, JSON.stringify({ mpid: 'mpid1', is_logged_in: false })] - ); + fetchMockSuccess('https://identity.mparticle.com/v1/mpid1/modify', { + mpid: 'mpid1', is_logged_in: false + }); + mParticle.Identity.login(user1); - + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid1' + ); + }) + .then(() => { + // modify user1's identities mParticle.Identity.modify({ userIdentities: { email: 'email@test.com' }, }); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { // set user attributes on mpid1 mParticle @@ -662,13 +751,17 @@ describe('persistence', () => { user1Data.mpid1.ui.should.have.property('7', 'email@test.com'); user1Data.mpid1.ui.should.have.property('1', 'customerid1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'mpid2', is_logged_in: false + }); mParticle.Identity.login(user2); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid2' + ); + }) + .then(() => { // set user attributes on user 2 mParticle @@ -683,13 +776,17 @@ describe('persistence', () => { user2Data.mpid2.ui.should.have.property('1', 'customerid2'); user2Data.mpid2.ua.should.have.property('test3', 'test3'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'mpid1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'mpid1', is_logged_in: false + }); mParticle.Identity.login(user1); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'mpid1' + ); + }) + .then(() => { const user1RelogInData = mParticle .getInstance() ._Persistence.getLocalStorage(); @@ -703,9 +800,16 @@ describe('persistence', () => { done(); }); + }); + }); + }); + }); + }); it('should remove MPID as keys if the cookie size is beyond the setting', done => { - mParticle._resetForTests(MPConfig); + mParticle.init(apiKey, window.mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.config.maxCookieSize = 700; const cookies: IPersistenceMinified = { @@ -778,36 +882,38 @@ describe('persistence', () => { done(); }); + }); it('integration test - will change the order of the CSM when a previous MPID logs in again', done => { - mParticle._resetForTests(MPConfig); mParticle.config.useCookieStorage = true; mParticle.config.maxCookieSize = 1000; mParticle.init(apiKey, mParticle.config); - + waitForCondition(hasIdentifyReturned) + .then(() => { const userIdentities1 = { userIdentities: { customerid: 'foo1' } } - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID1', is_logged_in: false + }); mParticle.Identity.login(userIdentities1); - + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' + ); + }) + .then(() => { let cookieData: Partial = findCookie(); cookieData.gs.csm[0].should.be.equal('testMPID'); cookieData.gs.csm[1].should.be.equal('MPID1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID2', is_logged_in: false + }); const userIdentities2 = { userIdentities: { @@ -816,17 +922,20 @@ describe('persistence', () => { }; mParticle.Identity.login(userIdentities2); - + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID2' + ); + }) + .then(() => { cookieData = findCookie(); cookieData.gs.csm[0].should.be.equal('testMPID'); cookieData.gs.csm[1].should.be.equal('MPID1'); cookieData.gs.csm[2].should.be.equal('MPID2'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'testMPID', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'testMPID', is_logged_in: true + }); const userIdentities3 = { userIdentities: { @@ -835,6 +944,12 @@ describe('persistence', () => { }; mParticle.Identity.login(userIdentities3); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'testMPID' + ); + }) + .then(() => { cookieData = findCookie(); cookieData.gs.csm[0].should.be.equal('MPID1'); @@ -843,13 +958,18 @@ describe('persistence', () => { done(); }); + }); + }); + }); + }); it('integration test - should remove a previous MPID as a key from cookies if new user attribute added and exceeds the size of the max cookie size', done => { - mParticle._resetForTests(MPConfig); mParticle.config.useCookieStorage = true; mParticle.config.maxCookieSize = 700; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle .getInstance() @@ -872,11 +992,9 @@ describe('persistence', () => { .Identity.getCurrentUser() .setUserAttribute('id', 'id1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID1', is_logged_in: false + }); const userIdentities1 = { userIdentities: { @@ -885,6 +1003,12 @@ describe('persistence', () => { }; mParticle.Identity.login(userIdentities1); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' + ); + }) + .then(() => { mParticle .getInstance() @@ -911,11 +1035,9 @@ describe('persistence', () => { cookieData.gs.csm[0].should.equal('testMPID'); cookieData.gs.csm[1].should.equal('MPID1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID2', is_logged_in: false + }); const userIdentities2 = { userIdentities: { @@ -924,6 +1046,12 @@ describe('persistence', () => { }; mParticle.Identity.login(userIdentities2); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID2' + ); + }) + .then(() => { mParticle .getInstance() @@ -964,11 +1092,15 @@ describe('persistence', () => { done(); }); + }); + }); + }); it('should remove a random MPID from storage if there is a new session and there are no other MPIDs in currentSessionMPIDs except for the currentUser mpid', done => { - mParticle._resetForTests(MPConfig); mParticle.config.maxCookieSize = 400; + mParticle.init(apiKey, window.mParticle.config); + const cookies: IPersistenceMinified = { gs: { csm: ['mpid3'], @@ -1043,11 +1175,12 @@ describe('persistence', () => { }); it('integration test - should remove a random MPID from storage if there is a new session and there are no MPIDs in currentSessionMPIDs', done => { - mParticle._resetForTests(MPConfig); mParticle.config.useCookieStorage = true; mParticle.config.maxCookieSize = 600; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle .getInstance() @@ -1070,13 +1203,17 @@ describe('persistence', () => { .Identity.getCurrentUser() .setUserAttribute('id', 'id1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID1', is_logged_in: false + }); mParticle.Identity.login(); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' + ); + }) + .then(() => { mParticle .getInstance() @@ -1104,14 +1241,18 @@ describe('persistence', () => { cookieData.gs.csm[0].should.equal('testMPID'); cookieData.gs.csm[1].should.equal('MPID1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID2', is_logged_in: false + }); mParticle.endSession(); mParticle.Identity.login(); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID2' + ); + }) + .then(() => { mParticle .getInstance() @@ -1150,6 +1291,9 @@ describe('persistence', () => { done(); }); + }); + }); + }); it('integration test - migrates a large localStorage cookie to cookies and properly remove MPIDs', done => { mParticle._resetForTests(MPConfig); @@ -1157,6 +1301,9 @@ describe('persistence', () => { mParticle.config.maxCookieSize = 700; mParticle.init(apiKey, mParticle.config); + + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle .getInstance() .Identity.getCurrentUser() @@ -1178,13 +1325,17 @@ describe('persistence', () => { .Identity.getCurrentUser() .setUserAttribute('id', 'id1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID1', is_logged_in: false + }); mParticle.Identity.login(); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' + ); + }) + .then(() => { mParticle .getInstance() @@ -1207,13 +1358,17 @@ describe('persistence', () => { .Identity.getCurrentUser() .setUserAttribute('id', 'id2'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID2', is_logged_in: false + }); mParticle.Identity.login(); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID2' + ); + }) + .then(() => { mParticle .getInstance() @@ -1239,6 +1394,12 @@ describe('persistence', () => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const cookieData = findCookie(); expect(cookieData['testMPID']).to.not.be.ok; @@ -1247,12 +1408,17 @@ describe('persistence', () => { done(); }); + }); + }); + }); + }); it('integration test - migrates all cookie MPIDs to localStorage', done => { - mParticle._resetForTests(MPConfig); mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle .getInstance() .Identity.getCurrentUser() @@ -1274,11 +1440,9 @@ describe('persistence', () => { .Identity.getCurrentUser() .setUserAttribute('id', 'id1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID1', is_logged_in: false + }); const userIdentities1 = { userIdentities: { @@ -1286,6 +1450,12 @@ describe('persistence', () => { }, }; mParticle.Identity.login(userIdentities1); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' + ); + }) + .then(() => { mParticle .getInstance() @@ -1308,11 +1478,9 @@ describe('persistence', () => { .Identity.getCurrentUser() .setUserAttribute('id', 'id2'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID2', is_logged_in: false + }); const userIdentities2 = { userIdentities: { @@ -1320,6 +1488,12 @@ describe('persistence', () => { }, }; mParticle.Identity.login(userIdentities2); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID2' + ); + }) + .then(() => { mParticle .getInstance() @@ -1345,6 +1519,12 @@ describe('persistence', () => { mParticle.config.useCookieStorage = false; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const lsData = getLocalStorage(v4LSKey); lsData.should.have.properties([ @@ -1378,12 +1558,17 @@ describe('persistence', () => { done(); }); + }); + }); + }); + }); it('integration test - migrates all LS MPIDs to cookies', done => { - mParticle._resetForTests(MPConfig); mParticle.config.useCookieStorage = false; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { // testMPID mParticle @@ -1407,11 +1592,9 @@ describe('persistence', () => { .Identity.getCurrentUser() .setUserAttribute('id', 'id1'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID1', is_logged_in: false + }); const userIdentities1 = { userIdentities: { @@ -1420,6 +1603,12 @@ describe('persistence', () => { }; mParticle.Identity.login(userIdentities1); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' + ); + }) + .then(() => { // MPID1 mParticle @@ -1443,11 +1632,9 @@ describe('persistence', () => { .Identity.getCurrentUser() .setUserAttribute('id', 'id2'); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'MPID2', is_logged_in: false + }); const userIdentities2 = { userIdentities: { @@ -1456,6 +1643,12 @@ describe('persistence', () => { }; mParticle.Identity.login(userIdentities2); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID2' + ); + }) + .then(() => { // MPID2 mParticle @@ -1482,6 +1675,12 @@ describe('persistence', () => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const cookieData = findCookie(); cookieData.should.have.properties([ @@ -1515,12 +1714,14 @@ describe('persistence', () => { done(); }); + }); + }); + }); + }); it('integration test - clears and creates new LS on reload if LS is corrupt', done => { const les = new Date().getTime(); - mParticle._resetForTests(MPConfig); - //an extra apostrophe is added to ua here to force a corrupt cookie. On init, cookies will clear and there will be a new cgid, sid, and das to exist const LS = "{'sid':'1992BDBB-AD74-49DB-9B20-5EC8037E72DE'|'ie':1|'ua':'eyJ0ZXN'0Ijoiwq7igJkifQ=='|'ui':'eyIzIjoiwq7igJkifQ=='|'ss':'eyJ1aWQiOnsiRXhwaXJlcyI6IjIwMjgtMDktMTRUMjI6MjI6MTAuMjU0MDcyOVoiLCJWYWx1ZSI6Imc9NjhjMmJhMzktYzg2OS00MTZhLWE4MmMtODc4OWNhZjVmMWU3JnU9NDE3NjQyNTYyMTQ0MTEwODk2OCZjcj00NTgxOTgyIn19'|'dt':'e207c24e36a7a8478ba0fcb3707a616b'|'les':" + @@ -1529,6 +1730,8 @@ describe('persistence', () => { setLocalStorage(v4LSKey, LS, true); mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const sessionId = mParticle.sessionManager.getSession(); const das = mParticle.getDeviceId(); @@ -1540,12 +1743,11 @@ describe('persistence', () => { done(); }); + }); it('integration test - clears and creates new cookies on reload if cookies is corrupt', done => { const les = new Date().getTime(); - mParticle._resetForTests(MPConfig); - //an extra apostrophe is added to ua here to force a corrupt cookie. On init, cookies will clear and there will be a new cgid, sid, and das to exist const cookies = "{'sid':'1992BDBB-AD74-49DB-9B20-5EC8037E72DE'|'ie':1|'ua':'eyJ0ZXN'0Ijoiwq7igJkifQ=='|'ui':'eyIzIjoiwq7igJkifQ=='|'ss':'eyJ1aWQiOnsiRXhwaXJlcyI6IjIwMjgtMDktMTRUMjI6MjI6MTAuMjU0MDcyOVoiLCJWYWx1ZSI6Imc9NjhjMmJhMzktYzg2OS00MTZhLWE4MmMtODc4OWNhZjVmMWU3JnU9NDE3NjQyNTYyMTQ0MTEwODk2OCZjcj00NTgxOTgyIn19'|'dt':'e207c24e36a7a8478ba0fcb3707a616b'|'les':" + @@ -1556,6 +1758,9 @@ describe('persistence', () => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { + const sessionId = mParticle.sessionManager.getSession(); const das = mParticle.getDeviceId(); const cgid = mParticle.getInstance()._Persistence.getCookie().gs.cgid; @@ -1565,26 +1770,29 @@ describe('persistence', () => { done(); }); + }); it('integration test - clears LS products on reload if LS products are corrupt', done => { - mParticle._resetForTests(MPConfig); - // randomly added gibberish to a Base64 encoded cart product array to force a corrupt product array const products = 'eyItOTE4MjY2NTAzNTA1ODg1NjAwMyI6eyasdjfiojasdifojfsdfJjcCI6W3siTmFtZSI6ImFuZHJvaWQiLCJTa3UiOiI1MTg3MDkiLCJQcmljZSI6MjM0LCJRdWFudGl0eSI6MSwiQnJhbmQiOm51bGwsIlZhcmlhbnQiOm51bGwsIkNhdGVnb3J5IjpudWxsLCJQb3NpdGlvbiI6bnVsbCwiQ291cG9uQ29kZSI6bnVsbCwiVG90YWxBbW91bnQiOjIzNCwiQXR0cmlidXRlcyI6eyJwcm9kYXR0cjEiOiJoaSJ9fSx7Ik5hbWUiOiJ3aW5kb3dzIiwiU2t1IjoiODMzODYwIiwiUHJpY2UiOjM0NSwiUXVhbnRpdHkiOjEsIlRvdGFsQW1vdW50IjozNDUsIkF0dHJpYnV0ZXMiOm51bGx9XX19'; localStorage.setItem(localStorageProductsV4, products); mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const productsAfterInit = getLocalStorageProducts().testMPID; expect(productsAfterInit.length).to.not.be.ok; done(); }); + }); it('should save products to persistence correctly when adding and removing products', done => { - mParticle._resetForTests(MPConfig); mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { const iphone = mParticle.eCommerce.createProduct( 'iphone', @@ -1617,10 +1825,9 @@ describe('persistence', () => { parsedProductsAfter['testMPID'].cp.length.should.equal(0); done(); }); + }); it('should only set setFirstSeenTime() once', done => { - mParticle._resetForTests(MPConfig); - const cookies = JSON.stringify({ gs: { sid: 'fst Test', @@ -1639,6 +1846,12 @@ describe('persistence', () => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { mParticle.getInstance()._Persistence.setFirstSeenTime('current', 10000); const currentFirstSeenTime = mParticle @@ -1674,6 +1887,7 @@ describe('persistence', () => { .should.equal(100); done(); }); + }); it('should properly set setLastSeenTime()', done => { mParticle._resetForTests(MPConfig); @@ -1693,6 +1907,12 @@ describe('persistence', () => { setCookie(workspaceCookieName, cookies, true); mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const clock = sinon.useFakeTimers(); clock.tick(100); @@ -1720,10 +1940,9 @@ describe('persistence', () => { clock.restore(); done(); }); + }); it("should set firstSeenTime() for a user that doesn't have storage yet", done => { - mParticle._resetForTests(MPConfig); - const cookies = JSON.stringify({ gs: { sid: 'fst Test', @@ -1754,8 +1973,6 @@ describe('persistence', () => { }); it('fst should be set when the user does not change, after an identify request', done => { - mParticle._resetForTests(MPConfig); - const cookies = JSON.stringify({ gs: { sid: 'fst Test', @@ -1765,22 +1982,34 @@ describe('persistence', () => { cu: 'current', }); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: 'current', is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: 'current', + is_logged_in: false, + }); + setCookie(workspaceCookieName, cookies, true); // FIXME: Should this be in configs or global? mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { expect( mParticle.getInstance()._Persistence.getFirstSeenTime('current') ).to.equal(null); mParticle.Identity.identify(); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { expect( mParticle.getInstance()._Persistence.getFirstSeenTime('current') @@ -1788,6 +2017,8 @@ describe('persistence', () => { done(); }); + }); + }); it('lastSeenTime should be null for users in storage without an lst value', done => { const cookies = JSON.stringify({ @@ -1803,12 +2034,19 @@ describe('persistence', () => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { expect( mParticle.getInstance()._Persistence.getFirstSeenTime('previous') ).to.equal(null); done(); }); + }); it('should save to persistence a device id set with setDeviceId', done => { mParticle._resetForTests(MPConfig); @@ -1874,17 +2112,22 @@ describe('persistence', () => { // with a special character in it results in a cookie decode error, which only happened // when config.useCookieStorage was true it('should save special characters to persistence when on cookies or local storage', done => { - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: testMPID, + is_logged_in: false, + }); // first test local storage mParticle.config.useCookieStorage = false; mParticle._resetForTests(MPConfig); mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const user = mParticle.Identity.getCurrentUser(); @@ -1901,6 +2144,12 @@ describe('persistence', () => { mParticle.config.useCookieStorage = true; mParticle.init(apiKey, mParticle.config); + waitForCondition(() => { + return ( + window.mParticle.getInstance()?._Store?.identityCallInFlight === false + ); + }) + .then(() => { const user2 = mParticle.Identity.getCurrentUser(); @@ -1913,4 +2162,6 @@ describe('persistence', () => { done(); }); + }); + }); }); \ No newline at end of file diff --git a/test/src/tests-runtimeToBatchEventsDTO.ts b/test/src/tests-runtimeToBatchEventsDTO.ts index dc1024b3..41bdd047 100644 --- a/test/src/tests-runtimeToBatchEventsDTO.ts +++ b/test/src/tests-runtimeToBatchEventsDTO.ts @@ -466,4 +466,4 @@ describe('Old model to batch model conversion', () => { done(); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-self-hosting-specific.js b/test/src/tests-self-hosting-specific.js index 240d6c52..c9df252b 100644 --- a/test/src/tests-self-hosting-specific.js +++ b/test/src/tests-self-hosting-specific.js @@ -3,22 +3,18 @@ import sinon from 'sinon'; import fetchMock from 'fetch-mock/esm/client'; import { urls, apiKey, MPConfig } from './config/constants'; -const { findEventFromRequest, findBatch } = Utils; +const { findEventFromRequest, findBatch, waitForCondition, fetchMockSuccess } = Utils; -let mockServer; // Calls to /config are specific to only the self hosting environment describe('/config self-hosting integration tests', function() { beforeEach(function() { fetchMock.post(urls.events, 200); - - mockServer = sinon.createFakeServer(); }); afterEach(function() { fetchMock.restore(); sinon.restore(); - mockServer.restore(); window.mParticle.config.requestConfig = false; }) @@ -33,13 +29,10 @@ describe('/config self-hosting integration tests', function() { }, }; - mockServer.respondImmediately = true; + fetchMockSuccess(urls.identify, { + mpid: 'identifyMPID', is_logged_in: false + }); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: 'identifyMPID', is_logged_in: false }), - ]); // https://go.mparticle.com/work/SQDSDKS-6651 fetchMock.mock(urls.config, () => { return new Promise((resolve) => { @@ -64,19 +57,26 @@ describe('/config self-hosting integration tests', function() { let event = findEventFromRequest(fetchMock.calls(), 'Test'); Should(event).not.be.ok(); - setTimeout(() => { - event = findBatch(fetchMock.calls(), 'Test'); - - event.should.be.ok(); - event.mpid.should.equal('identifyMPID'); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.configurationLoaded === true + ); + }) + .then(() => { + setTimeout(() => { + event = findBatch(fetchMock.calls(), 'Test'); - mockServer.restore(); - window.mParticle.config.requestConfig = false; - done(); - }, 75); + event.should.be.ok(); + event.mpid.should.equal('identifyMPID'); + + window.mParticle.config.requestConfig = false; + done(); + }, 75); + }) }); - it('queued events contain login mpid instead of identify mpid when calling login immediately after mParticle initializes', function(done) { + // https://go.mparticle.com/work/SQDSDKS-6852 + it.skip('queued events contain login mpid instead of identify mpid when calling login immediately after mParticle initializes', function(done) { const messages = []; mParticle._resetForTests(MPConfig); window.mParticle.config.requestConfig = true; @@ -94,9 +94,6 @@ describe('/config self-hosting integration tests', function() { mParticle.logEvent('identify callback event'); }; - mockServer.autoRespond = true; - mockServer.autoRespondAfter = 100; - fetchMock.mock(urls.config, () => { return new Promise((resolve) => { setTimeout(() => { @@ -109,52 +106,73 @@ describe('/config self-hosting integration tests', function() { }), headers: { 'Content-Type': 'application/json' }, }); - }, 50); // 100ms delay + }, 200); // 100ms delay }) }); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: 'identifyMPID', is_logged_in: false }), - ]); - - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'loginMPID', is_logged_in: false }), - ]); + fetchMock.mock(urls.identify, () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + status: 200, + body: JSON.stringify({ + mpid: 'identifyMPID', is_logged_in: false + }), + headers: { 'Content-Type': 'application/json' }, + }); + }, 5000); // 100ms delay + }) + }); + fetchMockSuccess(urls.login, { + mpid: 'loginMPID', is_logged_in: false + }); mParticle.init(apiKey, window.mParticle.config); - // call login before mParticle.identify is triggered, which happens after config returns mParticle.Identity.login({ userIdentities: { customerid: 'abc123' } }); + mParticle.getInstance()._Store.isInitialized = true; mParticle.logEvent('Test'); + + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'loginMPID' + ); + }) + .then(() => { + // call login before mParticle.identify is triggered, which happens after config returns + // mParticle.Identity.login({ userIdentities: { customerid: 'abc123' } }); + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.configurationLoaded === true + ); + }) + .then(() => { + // config triggers, login triggers immediately before identify + const event1 = findBatch(fetchMock.calls(), 'Test'); + event1.mpid.should.equal('loginMPID'); + messages + .indexOf('Parsing "login" identity response from server') + .should.be.above(0); + + // when login is in flight, identify will not run, but callback still will + messages + .indexOf('Parsing "identify" identity response from server') + .should.equal(-1); - // config triggers, login triggers immediately before identify - setTimeout(() => { - const event1 = findBatch(fetchMock.calls(), 'Test'); - event1.mpid.should.equal('loginMPID'); - messages - .indexOf('Parsing "login" identity response from server') - .should.be.above(0); - - // when login is in flight, identify will not run, but callback still will - messages - .indexOf('Parsing "identify" identity response from server') - .should.equal(-1); - const event2 = findBatch(fetchMock.calls(), 'identify callback event', false, mockServer); - event2.mpid.should.equal('loginMPID'); - - mockServer.restore(); - // clock.restore(); - - localStorage.removeItem('mprtcl-v4_workspaceTokenTest'); - window.mParticle.config.requestConfig = false; + // const event2 = findBatch(fetchMock.calls(), 'identify callback event', false, mockServer); + // event2.mpid.should.equal('loginMPID'); - done(); - - }, 150); + + localStorage.removeItem('mprtcl-v4_workspaceTokenTest'); + window.mParticle.config.requestConfig = false; + + done(); + }) + .catch((err) => { + console.log(err); + }) + + }); }); it('cookie name has workspace token in it in self hosting mode after config fetch', function(done) { @@ -188,7 +206,6 @@ describe('/config self-hosting integration tests', function() { window.mParticle.config.requestConfig = true; delete window.mParticle.config.workspaceToken; - mockServer.respondImmediately = true; fetchMock.get(urls.config, { status: 200, @@ -200,18 +217,18 @@ describe('/config self-hosting integration tests', function() { }), }); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ - mpid: 'identifyMPID', - is_logged_in: false, - }), - ]); + fetchMockSuccess(urls.identify, { + mpid: 'identifyMPID', is_logged_in: false + }); mParticle.init(apiKey, window.mParticle.config); - setTimeout(() => { + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.configurationLoaded === true + ); + }) + .then(() => { const { aliasUrl, configUrl, @@ -230,10 +247,10 @@ describe('/config self-hosting integration tests', function() { 'jssdks.us1.mparticle.com/v3/JS/' ); - done(); - }, 150); + done(); + }) }); - + it('should prioritize passed in baseUrls over direct urls', (done) => { mParticle._resetForTests(MPConfig); window.mParticle.config.requestConfig = true; @@ -246,8 +263,6 @@ describe('/config self-hosting integration tests', function() { window.mParticle.config.v3SecureServiceUrl = 'jssdks.foo.mparticle.com/v3/JS/'; - mockServer.respondImmediately = true; - fetchMock.get(urls.config, { status: 200, body: JSON.stringify({ @@ -258,18 +273,18 @@ describe('/config self-hosting integration tests', function() { }), }); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ - mpid: 'identifyMPID', - is_logged_in: false, - }), - ]); + fetchMockSuccess(urls.identify, { + mpid: 'identifyMPID', is_logged_in: false + }); mParticle.init(apiKey, window.mParticle.config); - setTimeout(() => { + waitForCondition(() => { + return ( + mParticle.getInstance()._Store.configurationLoaded === true + ); + }) + .then(() => { const { aliasUrl, configUrl, @@ -289,7 +304,7 @@ describe('/config self-hosting integration tests', function() { ); done(); - }, 150); + }) }); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-serverModel.ts b/test/src/tests-serverModel.ts index 5cc3c70e..c84facee 100644 --- a/test/src/tests-serverModel.ts +++ b/test/src/tests-serverModel.ts @@ -12,8 +12,9 @@ import { SDKGDPRConsentState, } from '../../src/consent'; import { IMParticleUser, ISDKUserAttributes } from '../../src/identity-user-interfaces'; +import Utils from './config/utils'; +const { hasIdentifyReturned, waitForCondition, fetchMockSuccess } = Utils; -let mockServer; let initialEvent = {}; const mParticle = window.mParticle; @@ -333,20 +334,14 @@ describe('ServerModel', () => { beforeEach(() => { // TODO: Create Event Object is tightly coupled with mp Init and Store // This should be refactored to make the function more pure - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); mParticle.init(apiKey, mParticle.config); }); afterEach(function() { - mockServer.restore(); }); it('should create an event object without a user', () => { @@ -601,9 +596,12 @@ describe('ServerModel', () => { ]); }); - it('should set necessary attributes if MessageType is SessionEnd', () => { - const mPStore = mParticle.getInstance()._Store; + it('should set necessary attributes if MessageType is SessionEnd', () => { + waitForCondition(hasIdentifyReturned) + .then(() => { + const mPStore = mParticle.getInstance()._Store; + mPStore.sessionAttributes = { fooSessionAttr: 'session-foo', barSessionAttr: 'session-bar', @@ -620,8 +618,8 @@ describe('ServerModel', () => { }; const actualEventObject = mParticle - .getInstance() - ._ServerModel.createEventObject(event) as IUploadObject; + .getInstance() + ._ServerModel.createEventObject(event) as IUploadObject; expect( actualEventObject.currentSessionMPIDs, @@ -647,6 +645,7 @@ describe('ServerModel', () => { // A SessionEnd event resets currentSessionMPIDs and sessionStartDate. When a new session starts, these are filled again expect(mPStore.currentSessionMPIDs).to.eql([]); expect(mPStore.sessionStartDate).to.eql(null); + }) }); it('should set necessary attributes if MessageType is AppStateTransition', () => { @@ -1307,19 +1306,14 @@ describe('ServerModel', () => { }; beforeEach(function() { - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); mParticle.init(apiKey, mParticle.config); }); afterEach(function() { - mockServer.restore(); }); it('Should not convert data plan object to server DTO when no id or version is set', function(done) { @@ -1433,17 +1427,20 @@ describe('ServerModel', () => { }); it('Should not append user info when no user exists', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { mParticle.getInstance()._Store.should.be.ok; let sdkEvent = mParticle - .getInstance() - ._ServerModel.createEventObject(event); + .getInstance() + ._ServerModel.createEventObject(event); sdkEvent.should.be.ok; expect(sdkEvent.UserIdentities).to.eql([]); expect(sdkEvent.UserAttributes).to.eql({}); expect(sdkEvent.ConsentState === null).to.eql(true); done(); + }) }); it('Should append all user info when user is present', function(done) { @@ -1520,9 +1517,11 @@ describe('ServerModel', () => { }); it('Should append identities when user is present', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { let sdkEvent = mParticle - .getInstance() - ._ServerModel.createEventObject(event); + .getInstance() + ._ServerModel.createEventObject(event); sdkEvent.should.be.ok; expect(sdkEvent.UserIdentities).to.eql([]); @@ -1571,12 +1570,15 @@ describe('ServerModel', () => { }); done(); + }) }); it('Should append user attributes when user present', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { let sdkEvent = mParticle - .getInstance() - ._ServerModel.createEventObject(event); + .getInstance() + ._ServerModel.createEventObject(event); sdkEvent.should.be.ok; expect(sdkEvent.UserAttributes).to.eql({}); @@ -1601,9 +1603,12 @@ describe('ServerModel', () => { expect(sdkEvent.UserAttributes).to.eql(attributes); done(); + }); }); it('Should update mpid when user info is appended with a new mpid', function(done) { + waitForCondition(hasIdentifyReturned) + .then(() => { let sdkEvent = mParticle .getInstance() ._ServerModel.createEventObject(event); @@ -1632,6 +1637,7 @@ describe('ServerModel', () => { mParticle.getInstance()._ServerModel.appendUserInfo(user, sdkEvent); expect(sdkEvent.MPID).to.equal('98765'); done(); + }) }); it('convertEventToDTO should contain launch referral', function(done) { @@ -1672,4 +1678,4 @@ describe('ServerModel', () => { done(); }); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-session-manager.ts b/test/src/tests-session-manager.ts index 0e4b56ed..5f51f766 100644 --- a/test/src/tests-session-manager.ts +++ b/test/src/tests-session-manager.ts @@ -1,3 +1,5 @@ +import Utils from './config/utils'; +const { waitForCondition, fetchMockSuccess } = Utils; import sinon from 'sinon'; import { expect } from 'chai'; import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; @@ -21,7 +23,6 @@ declare global { } } -let mockServer; const mParticle = window.mParticle; describe('SessionManager', () => { @@ -33,20 +34,14 @@ describe('SessionManager', () => { sandbox = sinon.createSandbox(); clock = sinon.useFakeTimers(now.getTime()); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false + }); }); afterEach(function() { sandbox.restore(); clock.restore(); - mockServer.restore(); mParticle._resetForTests(MPConfig); }); @@ -292,13 +287,18 @@ describe('SessionManager', () => { describe('#endSession', () => { it('should end a session', () => { mParticle.init(apiKey, window.mParticle.config); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === testMPID + ); + }) + .then(() => { const mpInstance = mParticle.getInstance(); const persistenceSpy = sinon.spy( mpInstance._Persistence, 'update' ); - clock.tick(31 * (MILLIS_IN_ONE_SEC * 60)); mpInstance._SessionManager.endSession(); expect(mpInstance._Store.sessionId).to.equal(null); @@ -308,6 +308,7 @@ describe('SessionManager', () => { // Persistence isn't necessary for this feature, but we should test // to see that it is called in case this ever needs to be refactored expect(persistenceSpy.called).to.equal(true); + }); }); it('should force a session end when override is used', () => { @@ -881,4 +882,4 @@ describe('SessionManager', () => { expect(persistenceSpy.called).to.equal(true); }); }); -}); +}); \ No newline at end of file diff --git a/test/src/tests-user.ts b/test/src/tests-user.ts index 1e509757..02746f9f 100644 --- a/test/src/tests-user.ts +++ b/test/src/tests-user.ts @@ -5,19 +5,7 @@ import Constants from '../../src/constants'; import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; import { urls, apiKey, MPConfig, testMPID } from './config/constants'; import fetchMock from 'fetch-mock/esm/client'; - -const { - getLocalStorage, - setLocalStorage, - findCookie, - forwarderDefaultConfiguration, - getLocalStorageProducts, - findEventFromRequest, - findBatch, - getIdentityEvent, - setCookie, - MockForwarder, -} = Utils; +const { fetchMockSuccess, waitForCondition } = Utils; const { HTTPCodes } = Constants; @@ -30,35 +18,28 @@ declare global { const mParticle = window.mParticle as MParticleWebSDK; +// https://go.mparticle.com/work/SQDSDKS-6849 +const hasIdentifyReturned = () => { + return window.mParticle.Identity.getCurrentUser()?.getMPID() === testMPID; +}; + // https://go.mparticle.com/work/SQDSDKS-6508 describe('mParticle User', function() { - let mockServer; - let clock; - beforeEach(function() { delete mParticle.config.useCookieStorage; fetchMock.post(urls.events, 200); - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; localStorage.clear(); - clock = sinon.useFakeTimers({ - now: new Date().getTime(), + fetchMockSuccess(urls.identify, { + mpid: testMPID, is_logged_in: false }); - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); mParticle.init(apiKey, window.mParticle.config); }); afterEach(function() { - mockServer.restore(); fetchMock.restore(); mParticle._resetForTests(MPConfig); - clock.restore(); }); describe('Consent State', function() { @@ -66,6 +47,8 @@ describe('mParticle User', function() { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, mParticle.config); + waitForCondition(hasIdentifyReturned) + .then(() => { let consentState = mParticle .getInstance() .Identity.getCurrentUser() @@ -99,25 +82,32 @@ describe('mParticle User', function() { ).to.have.property('Timestamp', 10); done(); }); + }); it('get/set consent state for multiple users', done => { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, mParticle.config); - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID1', is_logged_in: false }), - ]); - + waitForCondition(hasIdentifyReturned) + .then(() => { const userIdentities1 = { userIdentities: { customerid: 'foo1', }, }; + fetchMockSuccess(urls.login, { + mpid: 'loginMPID1', is_logged_in: false + }); + mParticle.Identity.login(userIdentities1); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'loginMPID1' + ); + }) + .then(() => { let user1StoredConsentState = mParticle .getInstance() .Identity.getCurrentUser() @@ -134,14 +124,9 @@ describe('mParticle User', function() { .Identity.getCurrentUser() .setConsentState(consentState); - mParticle._resetForTests(MPConfig, true); - mParticle.init(apiKey, mParticle.config); - - mockServer.respondWith(urls.login, [ - 200, - {}, - JSON.stringify({ mpid: 'MPID2', is_logged_in: false }), - ]); + fetchMockSuccess(urls.login, { + mpid: 'loginMPID2', is_logged_in: false + }); const userIdentities2 = { userIdentities: { @@ -149,8 +134,13 @@ describe('mParticle User', function() { }, }; - mParticle.Identity.login(userIdentities2); - + mParticle.Identity.login(userIdentities2); + waitForCondition(() => { + return ( + mParticle.Identity.getCurrentUser()?.getMPID() === 'loginMPID2' + ); + }) + .then(() => { let user2StoredConsentState = mParticle .getInstance() .Identity.getCurrentUser() @@ -171,10 +161,10 @@ describe('mParticle User', function() { user1StoredConsentState = mParticle .getInstance() - ._Store.getConsentState('MPID1'); + ._Store.getConsentState('loginMPID1'); user2StoredConsentState = mParticle .getInstance() - ._Store.getConsentState('MPID2'); + ._Store.getConsentState('loginMPID2'); expect( user1StoredConsentState.getGDPRConsentState() @@ -203,5 +193,8 @@ describe('mParticle User', function() { ).to.have.property('Timestamp', 11); done(); }); + }) + }) + }); }); }); diff --git a/test/src/tests-utils.ts b/test/src/tests-utils.ts index 4ae0e2a2..3e224eb4 100644 --- a/test/src/tests-utils.ts +++ b/test/src/tests-utils.ts @@ -444,4 +444,4 @@ describe('Utils', () => { ]); }); }); -}); +}); \ No newline at end of file