From 8a2e225a3e69d97b437f21d03c9a156c8561fb32 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 8 Nov 2024 09:54:11 -0500 Subject: [PATCH] refactor: Migrate IdentityAPIClient to TypeScript --- src/aliasRequestApiClient.ts | 90 ----- src/identityApiClient.interfaces.ts | 44 --- src/identityApiClient.js | 131 ------- src/identityApiClient.ts | 257 +++++++++++++ test/src/_test.index.ts | 2 +- test/src/tests-aliasRequestApiClient.ts | 123 ------ test/src/tests-identityApiClient.ts | 480 ++++++++++++++++++++++++ 7 files changed, 738 insertions(+), 389 deletions(-) delete mode 100644 src/aliasRequestApiClient.ts delete mode 100644 src/identityApiClient.interfaces.ts delete mode 100644 src/identityApiClient.js create mode 100644 src/identityApiClient.ts delete mode 100644 test/src/tests-aliasRequestApiClient.ts create mode 100644 test/src/tests-identityApiClient.ts diff --git a/src/aliasRequestApiClient.ts b/src/aliasRequestApiClient.ts deleted file mode 100644 index 011ac542..00000000 --- a/src/aliasRequestApiClient.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { IAliasRequest, IAliasCallback } from "./identity.interfaces"; -import { MParticleWebSDK } from "./sdkRuntimeModels"; -import Constants from './constants'; -import { FetchUploader, XHRUploader } from './uploaders'; -import { HTTP_ACCEPTED, HTTP_OK } from "./constants"; -import { IIdentityApiClientSendAliasRequest } from "./identityApiClient.interfaces"; - - -const { HTTPCodes, Messages } = Constants; - -interface IAliasResponseBody { - message?: string -} - -export const sendAliasRequest: IIdentityApiClientSendAliasRequest = async function (mpInstance: MParticleWebSDK, aliasRequest: IAliasRequest, aliasCallback: IAliasCallback): Promise { - const { verbose, error } = mpInstance.Logger; - const { invokeAliasCallback } = mpInstance._Helpers; - const { aliasUrl } = mpInstance._Store.SDKConfig; - const { devToken: apiKey } = mpInstance._Store; - - verbose(Messages.InformationMessages.SendAliasHttp); - - // https://go.mparticle.com/work/SQDSDKS-6750 - const uploadUrl = `https://${aliasUrl}${apiKey}/Alias`; - const uploader = window.fetch - ? new FetchUploader(uploadUrl) - : new XHRUploader(uploadUrl); - - - // https://go.mparticle.com/work/SQDSDKS-6568 - const uploadPayload = { - method: 'post', - headers: { - Accept: 'text/plain;charset=UTF-8', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(aliasRequest), - }; - - try { - const response = await uploader.upload(uploadPayload); - - let message: string; - let aliasResponseBody: IAliasResponseBody; - - // FetchUploader returns the response as a JSON object that we have to await - if (response.json) { - // HTTP responses of 202, 200, and 403 do not have a response. response.json will always exist on a fetch, but can only be await-ed when the response is not empty, otherwise it will throw an error. - try { - aliasResponseBody = await response.json(); - } catch (e) { - verbose('The request has no response body'); - } - } else { - // 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; - - aliasResponseBody = xhrResponse.responseText - ? JSON.parse(xhrResponse.responseText) - : ''; - } - - let errorMessage: string; - - switch (response.status) { - case HTTP_OK: - case HTTP_ACCEPTED: - // https://go.mparticle.com/work/SQDSDKS-6670 - message = - 'Successfully sent forwarding stats to mParticle Servers'; - break; - default: - // 400 has an error message, but 403 doesn't - if (aliasResponseBody?.message) { - errorMessage = aliasResponseBody.message; - } - message = - 'Issue with sending Alias Request to mParticle Servers, received HTTP Code of ' + - response.status; - } - - verbose(message); - invokeAliasCallback(aliasCallback, response.status, errorMessage); - } catch (e) { - const err = e as Error; - error('Error sending alias request to mParticle servers. ' + err); - invokeAliasCallback(aliasCallback, HTTPCodes.noHttpCoverage, (err.message)); - } - }; \ No newline at end of file diff --git a/src/identityApiClient.interfaces.ts b/src/identityApiClient.interfaces.ts deleted file mode 100644 index bbea6585..00000000 --- a/src/identityApiClient.interfaces.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { IdentityApiData, MPID, UserIdentities } from '@mparticle/web-sdk'; -import { - IdentityCallback, - IIdentityResponse, -} from './identity-user-interfaces'; -import { - IAliasRequest, - IAliasCallback, - IIdentityRequest, - IdentityAPIMethod, - IIdentity, -} from './identity.interfaces'; -import { MParticleWebSDK } from './sdkRuntimeModels'; - -export interface IIdentityApiClient { - sendAliasRequest: ( - aliasRequest: IAliasRequest, - aliasCallback: IAliasCallback - ) => Promise; - sendIdentityRequest: ( - identityApiRequest: IIdentityRequest, - method: IdentityAPIMethod, - callback: IdentityCallback, - originalIdentityApiData: IdentityApiData, - parseIdentityResponse: IIdentity['parseIdentityResponse'], - mpid: MPID, - knownIdentities: UserIdentities - ) => Promise; - getUploadUrl: (method: IdentityAPIMethod, mpid: MPID) => string; - getIdentityResponseFromFetch: ( - response: Response, - responseBody: string - ) => IIdentityResponse; - getIdentityResponseFromXHR: (response: Response) => IIdentityResponse; -} - -// https://go.mparticle.com/work/SQDSDKS-6568 -// https://go.mparticle.com/work/SQDSDKS-6679 -// Combine with `sendIdentityRequest` above once module is fully migrated -export type IIdentityApiClientSendAliasRequest = ( - mpInstance: MParticleWebSDK, - aliasRequest: IAliasRequest, - aliasCallback: IAliasCallback -) => Promise; diff --git a/src/identityApiClient.js b/src/identityApiClient.js deleted file mode 100644 index a169050e..00000000 --- a/src/identityApiClient.js +++ /dev/null @@ -1,131 +0,0 @@ -import Constants from './constants'; -import { sendAliasRequest } from './aliasRequestApiClient'; -import { FetchUploader, XHRUploader } from './uploaders'; -import { CACHE_HEADER } from './identity-utils'; -import { parseNumber } from './utils'; - -var HTTPCodes = Constants.HTTPCodes, - Messages = Constants.Messages; - -const { Modify } = Constants.IdentityMethods; - -export default function IdentityAPIClient(mpInstance) { - this.sendAliasRequest = async function(aliasRequest, callback) { - await sendAliasRequest(mpInstance, aliasRequest, callback); - }; - - this.sendIdentityRequest = async function( - identityApiRequest, - method, - callback, - originalIdentityApiData, - parseIdentityResponse, - mpid, - knownIdentities - ) { - const { verbose, error } = mpInstance.Logger; - const { invokeCallback } = mpInstance._Helpers; - - verbose(Messages.InformationMessages.SendIdentityBegin); - if (!identityApiRequest) { - error(Messages.ErrorMessages.APIRequestEmpty); - return; - } - verbose(Messages.InformationMessages.SendIdentityHttp); - - if (mpInstance._Store.identityCallInFlight) { - invokeCallback( - callback, - HTTPCodes.activeIdentityRequest, - 'There is currently an Identity request processing. Please wait for this to return before requesting again' - ); - return; - } - - const previousMPID = mpid || null; - const uploadUrl = this.getUploadUrl(method, mpid); - - const uploader = window.fetch - ? new FetchUploader(uploadUrl) - : new XHRUploader(uploadUrl); - - const fetchPayload = { - method: 'post', - headers: { - Accept: 'text/plain;charset=UTF-8', - 'Content-Type': 'application/json', - 'x-mp-key': mpInstance._Store.devToken, - }, - body: JSON.stringify(identityApiRequest), - }; - - try { - mpInstance._Store.identityCallInFlight = true; - const response = await uploader.upload(fetchPayload); - - let identityResponse; - - if (response.json) { - // https://go.mparticle.com/work/SQDSDKS-6568 - // FetchUploader returns the response as a JSON object that we have to await - const responseBody = await response.json(); - identityResponse = this.getIdentityResponseFromFetch( - response, - responseBody - ); - } else { - identityResponse = this.getIdentityResponseFromXHR(response); - } - - verbose( - 'Received Identity Response from server: ' + - JSON.stringify(identityResponse.responseText) - ); - - parseIdentityResponse( - identityResponse, - previousMPID, - callback, - originalIdentityApiData, - method, - knownIdentities, - false - ); - } catch (err) { - mpInstance._Store.identityCallInFlight = false; - invokeCallback(callback, HTTPCodes.noHttpCoverage, err); - error('Error sending identity request to servers' + ' - ' + err); - } - }; - - this.getUploadUrl = (method, mpid) => { - const uploadServiceUrl = mpInstance._Helpers.createServiceUrl( - mpInstance._Store.SDKConfig.identityUrl - ); - - const uploadUrl = - method === Modify - ? uploadServiceUrl + mpid + '/' + method - : uploadServiceUrl + method; - - return uploadUrl; - }; - - this.getIdentityResponseFromFetch = (response, responseBody) => ({ - status: response.status, - responseText: responseBody, - cacheMaxAge: parseInt(response.headers.get(CACHE_HEADER)) || 0, - expireTimestamp: 0, - }); - - this.getIdentityResponseFromXHR = response => ({ - status: response.status, - responseText: response.responseText - ? JSON.parse(response.responseText) - : {}, - cacheMaxAge: parseNumber( - response.getResponseHeader(CACHE_HEADER) || '' - ), - expireTimestamp: 0, - }); -} diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts new file mode 100644 index 00000000..ccd12ac0 --- /dev/null +++ b/src/identityApiClient.ts @@ -0,0 +1,257 @@ +import Constants, { HTTP_ACCEPTED, HTTP_OK } from './constants'; +import { FetchUploader, XHRUploader } from './uploaders'; +import { CACHE_HEADER } from './identity-utils'; +import { parseNumber } from './utils'; +import { IAliasCallback, IAliasRequest, IdentityAPIMethod, IIdentity, IIdentityAPIRequestData } from './identity.interfaces'; +import { IdentityApiData, MPID, UserIdentities } from '@mparticle/web-sdk'; +import { IdentityCallback, IdentityResultBody, IIdentityResponse } from './identity-user-interfaces'; + +const { + HTTPCodes, + Messages, + IdentityMethods, +} = Constants; + +const { Modify } = IdentityMethods; + +export interface IIdentityApiClient { + sendAliasRequest: ( + aliasRequest: IAliasRequest, + aliasCallback: IAliasCallback + ) => Promise; + sendIdentityRequest: ( + identityApiRequest: IIdentityAPIRequestData, + method: IdentityAPIMethod, + callback: IdentityCallback, + originalIdentityApiData: IdentityApiData, + parseIdentityResponse: IIdentity['parseIdentityResponse'], + mpid: MPID, + knownIdentities: UserIdentities + ) => Promise; + getUploadUrl: (method: IdentityAPIMethod, mpid: MPID) => string; + getIdentityResponseFromFetch: ( + response: Response, + responseBody: IdentityResultBody + ) => IIdentityResponse; + getIdentityResponseFromXHR: (response: XMLHttpRequest) => IIdentityResponse; +} + + +export interface IAliasResponseBody { + message?: string +} + +export default function IdentityAPIClient(this: IIdentityApiClient, mpInstance) { + this.sendAliasRequest = async function(aliasRequest, aliasCallback) { + const { verbose, error } = mpInstance.Logger; + const { invokeAliasCallback } = mpInstance._Helpers; + const { aliasUrl } = mpInstance._Store.SDKConfig; + const { devToken: apiKey } = mpInstance._Store; + + verbose(Messages.InformationMessages.SendAliasHttp); + + // https://go.mparticle.com/work/SQDSDKS-6750 + const uploadUrl = `https://${aliasUrl}${apiKey}/Alias`; + const uploader = window.fetch + ? new FetchUploader(uploadUrl) + : new XHRUploader(uploadUrl); + + + // https://go.mparticle.com/work/SQDSDKS-6568 + const uploadPayload = { + method: 'post', + headers: { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(aliasRequest), + }; + + try { + const response = await uploader.upload(uploadPayload); + + let message: string; + let aliasResponseBody: IAliasResponseBody; + + // FetchUploader returns the response as a JSON object that we have to await + if (response.json) { + // HTTP responses of 202, 200, and 403 do not have a response. response.json will always exist on a fetch, but can only be await-ed when the response is not empty, otherwise it will throw an error. + try { + aliasResponseBody = await response.json(); + } catch (e) { + verbose('The request has no response body'); + } + } else { + // 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; + + aliasResponseBody = xhrResponse.responseText + ? JSON.parse(xhrResponse.responseText) + : ''; + } + + let errorMessage: string; + + switch (response.status) { + case HTTP_OK: + case HTTP_ACCEPTED: + // https://go.mparticle.com/work/SQDSDKS-6670 + message = + 'Successfully sent forwarding stats to mParticle Servers'; + break; + default: + // 400 has an error message, but 403 doesn't + if (aliasResponseBody?.message) { + errorMessage = aliasResponseBody.message; + } + message = + 'Issue with sending Alias Request to mParticle Servers, received HTTP Code of ' + + response.status; + } + + verbose(message); + invokeAliasCallback(aliasCallback, response.status, errorMessage); + } catch (e) { + const err = e as Error; + error('Error sending alias request to mParticle servers. ' + err); + invokeAliasCallback(aliasCallback, HTTPCodes.noHttpCoverage, (err.message)); + } + }; + + this.sendIdentityRequest = async function( + identityApiRequest: IIdentityAPIRequestData, + method: IdentityAPIMethod, + callback, + originalIdentityApiData, + parseIdentityResponse, + mpid: MPID, + knownIdentities: UserIdentities + ) { + const { verbose, error } = mpInstance.Logger; + const { invokeCallback } = mpInstance._Helpers; + + verbose(Messages.InformationMessages.SendIdentityBegin); + if (!identityApiRequest) { + error(Messages.ErrorMessages.APIRequestEmpty); + return; + } + verbose(Messages.InformationMessages.SendIdentityHttp); + + if (mpInstance._Store.identityCallInFlight) { + invokeCallback( + callback, + HTTPCodes.activeIdentityRequest, + 'There is currently an Identity request processing. Please wait for this to return before requesting again' + ); + return; + } + + const previousMPID = mpid || null; + const uploadUrl = this.getUploadUrl(method, mpid); + + const uploader = window.fetch + ? new FetchUploader(uploadUrl) + : new XHRUploader(uploadUrl); + + // TODO: Write test to verify fetch payload + + // https://go.mparticle.com/work/SQDSDKS-6568 + const fetchPayload = { + method: 'post', + headers: { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'application/json', + 'x-mp-key': mpInstance._Store.devToken, + }, + body: JSON.stringify(identityApiRequest), + }; + + console.log('fetchPayload', fetchPayload) + + try { + console.log('uploader', uploader); + + mpInstance._Store.identityCallInFlight = true; + const response = await uploader.upload(fetchPayload); + + console.log('response', response); + + let identityResponse: IIdentityResponse; + + if (response.json) { + + console.log('fetch response') + + // https://go.mparticle.com/work/SQDSDKS-6568 + // FetchUploader returns the response as a JSON object that we have to await + const responseBody = await response.json(); + + console.log('responseBody', responseBody); + + identityResponse = this.getIdentityResponseFromFetch( + response, + responseBody + ); + } else { + console.log('xhr response', response) + + identityResponse = this.getIdentityResponseFromXHR(response as unknown as XMLHttpRequest); + } + + verbose( + 'Received Identity Response from server: ' + + JSON.stringify(identityResponse.responseText) + ); + + console.log('parseIdentityResponse', parseIdentityResponse) + + parseIdentityResponse( + identityResponse, + previousMPID, + callback, + originalIdentityApiData, + method, + knownIdentities, + false + ); + } catch (err) { + console.log('did i error?'); + console.log('callback', callback); + mpInstance._Store.identityCallInFlight = false; + invokeCallback(callback, HTTPCodes.noHttpCoverage, err); + error('Error sending identity request to servers' + ' - ' + err); + } + }; + + this.getUploadUrl = (method, mpid) => { + const uploadServiceUrl = mpInstance._Helpers.createServiceUrl( + mpInstance._Store.SDKConfig.identityUrl + ); + + const uploadUrl = + method === Modify + ? uploadServiceUrl + mpid + '/' + method + : uploadServiceUrl + method; + + return uploadUrl; + }; + + this.getIdentityResponseFromFetch = (response, responseBody) => ({ + status: response.status, + responseText: responseBody, + cacheMaxAge: parseInt(response.headers.get(CACHE_HEADER)) || 0, + expireTimestamp: 0, + }); + + this.getIdentityResponseFromXHR = response => ({ + status: response.status, + responseText: response.responseText + ? JSON.parse(response.responseText) + : {}, + cacheMaxAge: parseNumber( + response.getResponseHeader(CACHE_HEADER) || '' + ), + expireTimestamp: 0, + }); +} diff --git a/test/src/_test.index.ts b/test/src/_test.index.ts index 7cb1e5b0..ddcec169 100644 --- a/test/src/_test.index.ts +++ b/test/src/_test.index.ts @@ -36,7 +36,7 @@ import './tests-audience-manager'; import './tests-feature-flags'; import './tests-user'; import './tests-legacy-alias-requests'; -import './tests-aliasRequestApiClient'; +import './tests-identityApiClient'; import './tests-integration-capture'; import './tests-batchUploader_4'; diff --git a/test/src/tests-aliasRequestApiClient.ts b/test/src/tests-aliasRequestApiClient.ts deleted file mode 100644 index 10e47e6e..00000000 --- a/test/src/tests-aliasRequestApiClient.ts +++ /dev/null @@ -1,123 +0,0 @@ -import sinon from 'sinon'; -import fetchMock from 'fetch-mock/esm/client'; -import { urls, apiKey, MPConfig, testMPID } from './config/constants'; -import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; -import { expect } from 'chai'; -import { sendAliasRequest } from '../../src/aliasRequestApiClient'; -import { IAliasRequest } from '../../src/identity.interfaces'; -import { HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_OK } from '../../src/constants'; - -declare global { - interface Window { - mParticle: MParticleWebSDK; - fetchMock: any; - } -} - -let mockServer; -const mParticle = window.mParticle; - -declare global { - interface Window { - mParticle: MParticleWebSDK; - fetchMock: any; - } -} - -const aliasUrl = 'https://jssdks.mparticle.com/v1/identity/test_key/Alias'; - -describe('Alias Request Api Client', 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 }), - ]); - mParticle.init(apiKey, window.mParticle.config); - }); - - afterEach(function() { - mockServer.restore(); - fetchMock.restore(); - mParticle._resetForTests(MPConfig); - }); - - it('should have just an httpCode on the result passed to the callback on a 200', async () => { - const mpInstance: MParticleWebSDK = mParticle.getInstance(); - const aliasRequest: IAliasRequest = { - destinationMpid: '123', - sourceMpid: '456', - startTime: 10001230123, - endTime: 10001231123 - }; - - const aliasCallback = sinon.spy() - fetchMock.post(aliasUrl, HTTP_OK); - - await sendAliasRequest(mpInstance, aliasRequest, aliasCallback); - expect(aliasCallback.calledOnce).to.eq(true); - const callbackArgs = aliasCallback.getCall(0).args - expect(callbackArgs[0]).to.deep.equal({httpCode: HTTP_OK}); - }); - - it('should have just an httpCode on the result passed to the callback on a 202', async () => { - const mpInstance: MParticleWebSDK = mParticle.getInstance(); - const aliasRequest: IAliasRequest = { - destinationMpid: '123', - sourceMpid: '456', - startTime: 10001230123, - endTime: 10001231123 - }; - - const aliasCallback = sinon.spy() - fetchMock.post(aliasUrl, HTTP_ACCEPTED); - - await sendAliasRequest(mpInstance, aliasRequest, aliasCallback); - expect(aliasCallback.calledOnce).to.eq(true); - const callbackArgs = aliasCallback.getCall(0).args - expect(callbackArgs[0]).to.deep.equal({httpCode: HTTP_ACCEPTED}); - }); - - it('should have just an httpCode on the result passed to the callback on a 400', async () => { - const mpInstance: MParticleWebSDK = mParticle.getInstance(); - const aliasRequest: IAliasRequest = { - destinationMpid: '123', - sourceMpid: '456', - startTime: 10001230123, - endTime: 10001231123 - }; - - const aliasCallback = sinon.spy() - fetchMock.post(aliasUrl, HTTP_BAD_REQUEST); - - await sendAliasRequest(mpInstance, aliasRequest, aliasCallback); - expect(aliasCallback.calledOnce).to.eq(true); - const callbackArgs = aliasCallback.getCall(0).args - expect(callbackArgs[0]).to.deep.equal({httpCode: HTTP_BAD_REQUEST}); - }); - - it('should have an httpCode and an error message passed to the callback on a 403', async () => { - const mpInstance: MParticleWebSDK = mParticle.getInstance(); - const aliasRequest: IAliasRequest = { - destinationMpid: '123', - sourceMpid: '456', - startTime: 10001230123, - endTime: 10001231123 - }; - - const aliasCallback = sinon.spy() - fetchMock.post(aliasUrl, { - status: HTTP_FORBIDDEN, - body: JSON.stringify({message: 'error'}), - }); - - await sendAliasRequest(mpInstance, aliasRequest, aliasCallback); - expect(aliasCallback.calledOnce).to.eq(true); - const callbackArgs = aliasCallback.getCall(0).args - expect(callbackArgs[0]).to.deep.equal({httpCode: HTTP_FORBIDDEN, message: 'error'}); - }); -}); \ No newline at end of file diff --git a/test/src/tests-identityApiClient.ts b/test/src/tests-identityApiClient.ts new file mode 100644 index 00000000..058bbf44 --- /dev/null +++ b/test/src/tests-identityApiClient.ts @@ -0,0 +1,480 @@ +import sinon from 'sinon'; +import fetchMock from 'fetch-mock/esm/client'; +import { urls, apiKey, MPConfig, testMPID } from './config/constants'; +import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; +import { expect } from 'chai'; +import { + IAliasRequest, + IIdentityAPIRequestData, +} from '../../src/identity.interfaces'; +import { + HTTP_ACCEPTED, + HTTP_BAD_REQUEST, + HTTP_FORBIDDEN, + HTTP_OK, +} from '../../src/constants'; +import IdentityAPIClient, { IIdentityApiClient } from '../../src/identityApiClient'; +import { IIdentityResponse } from '../../src/identity-user-interfaces'; +import Utils from './config/utils'; +const { fetchMockSuccess } = Utils; + +declare global { + interface Window { + mParticle: MParticleWebSDK; + fetchMock: any; + } +} + +let mockServer; +const mParticle = window.mParticle; + +declare global { + interface Window { + mParticle: MParticleWebSDK; + fetchMock: any; + } +} + + +describe('Identity Api Client', () => { + describe('#sendIdentityRequest', () => { + const identityRequest: IIdentityAPIRequestData = { + client_sdk: { + platform: 'web', + sdk_vendor: 'mparticle', + sdk_version: '1.0.0', + }, + context: 'test-context', + environment: 'development', + request_id: '123', + request_timestamp_unixtime_ms: Date.now(), + previous_mpid: null, + known_identities: { + email: 'user@mparticle.com', + }, + }; + + const originalIdentityApiData = { + userIdentities: { + other: '123456', + }, + }; + + const apiSuccessResponseBody = { + mpid: testMPID, + is_logged_in: false, + context: 'test-context', + is_ephemeral: false, + matched_identities: {}, + } + + const expectedIdentityResponse: IIdentityResponse = { + status: 200, + responseText: apiSuccessResponseBody, + cacheMaxAge: 0, + expireTimestamp: 0, + }; + + + it('should call parseIdentityResponse with the correct arguments', async () => { + fetchMockSuccess(urls.identify, apiSuccessResponseBody); + + const callbackSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: () => {}, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + expect(parseIdentityResponseSpy.calledOnce, 'Call parseIdentityResponse').to.eq(true); + expect(parseIdentityResponseSpy.args[0][0]).to.deep.equal(expectedIdentityResponse); + expect(parseIdentityResponseSpy.args[0][1]).to.equal(testMPID); + expect(parseIdentityResponseSpy.args[0][2]).to.be.a('function'); + expect(parseIdentityResponseSpy.args[0][3]).to.deep.equal(originalIdentityApiData); + expect(parseIdentityResponseSpy.args[0][4]).to.equal('identify'); + expect(parseIdentityResponseSpy.args[0][5]).to.deep.equal(identityRequest.known_identities); + expect(parseIdentityResponseSpy.args[0][6]).to.equal(false); + }); + + it('should return early without calling parseIdentityResponse if the identity call is in flight', async () => { + fetchMockSuccess(urls.identify, apiSuccessResponseBody); + + const invokeCallbackSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: invokeCallbackSpy, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + identityCallInFlight: true, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + sinon.spy(), + null, + parseIdentityResponseSpy, + testMPID, + null, + ); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy called').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.be.a('function'); + expect(invokeCallbackSpy.args[0][1]).to.equal(-2); + expect(invokeCallbackSpy.args[0][2]).to.equal('There is currently an Identity request processing. Please wait for this to return before requesting again'); + + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy NOT called').to.eq(false); + + }); + + it('should call invokeCallback with an error if the fetch fails', async () => { + fetchMock.post(urls.identify, { + status: 500, + throws: 'server error', + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: invokeCallbackSpy, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + identityCallInFlight: false, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + null, + parseIdentityResponseSpy, + testMPID, + null, + ); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy called').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.be.a('function'); + expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); + expect(invokeCallbackSpy.args[0][1]).to.equal(-1); + expect(invokeCallbackSpy.args[0][2]).to.equal('server error'); + }); + + it('should use XHR if fetch is not available', async () => { + const mockServer = sinon.createFakeServer(); + mockServer.respondImmediately = true; + + mockServer.respondWith(urls.identify, [ + 200, + {}, + JSON.stringify(apiSuccessResponseBody), + ]); + + const fetch = window.fetch; + delete window.fetch; + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: sinon.spy(), + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + identityCallInFlight: false, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + const originalIdentityApiData = { + userIdentities: { + other: '123456', + }, + }; + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + sinon.spy(), + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + expect(parseIdentityResponseSpy.calledOnce, 'Call parseIdentityResponse').to.eq(true); + expect(parseIdentityResponseSpy.args[0][0]).to.deep.equal(expectedIdentityResponse); + expect(parseIdentityResponseSpy.args[0][1]).to.equal(testMPID); + expect(parseIdentityResponseSpy.args[0][2]).to.be.a('function'); + expect(parseIdentityResponseSpy.args[0][3]).to.deep.equal(originalIdentityApiData); + expect(parseIdentityResponseSpy.args[0][4]).to.equal('identify'); + expect(parseIdentityResponseSpy.args[0][5]).to.deep.equal(identityRequest.known_identities); + expect(parseIdentityResponseSpy.args[0][6]).to.equal(false); + + window.fetch = fetch; + }); + + it('should construct the correct fetch payload', async () => { + fetchMockSuccess(urls.identify, apiSuccessResponseBody); + + const callbackSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: () => {}, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + const expectedFetchPayload = { + method: 'post', + headers: { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'application/json', + 'x-mp-key': 'test_key', + }, + body: JSON.stringify(identityRequest), + }; + + expect(fetchMock.calls()[0][1].method).to.deep.equal(expectedFetchPayload.method); + expect(fetchMock.calls()[0][1].body).to.deep.equal(expectedFetchPayload.body); + expect(fetchMock.calls()[0][1].headers).to.deep.equal(expectedFetchPayload.headers); + }); + }); + + describe('#sendAliasRequest', () => { + const aliasUrl = 'https://jssdks.mparticle.com/v1/identity/test_key/Alias'; + + 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 }), + ]); + mParticle.init(apiKey, window.mParticle.config); + }); + + afterEach(function() { + mockServer.restore(); + fetchMock.restore(); + mParticle._resetForTests(MPConfig); + }); + + it('should have just an httpCode on the result passed to the callback on a 200', async () => { + const mpInstance: MParticleWebSDK = mParticle.getInstance(); + const identityApiClient = new IdentityAPIClient(mpInstance); + + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, HTTP_OK); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0]).to.deep.equal({ httpCode: HTTP_OK }); + }); + + it('should have just an httpCode on the result passed to the callback on a 202', async () => { + const mpInstance: MParticleWebSDK = mParticle.getInstance(); + const identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, HTTP_ACCEPTED); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0]).to.deep.equal({ httpCode: HTTP_ACCEPTED }); + }); + + it('should have just an httpCode on the result passed to the callback on a 400', async () => { + const mpInstance: MParticleWebSDK = mParticle.getInstance(); + const identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, HTTP_BAD_REQUEST); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0]).to.deep.equal({ + httpCode: HTTP_BAD_REQUEST, + }); + }); + + it('should have an httpCode and an error message passed to the callback on a 403', async () => { + const mpInstance: MParticleWebSDK = mParticle.getInstance(); + const identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, { + status: HTTP_FORBIDDEN, + body: JSON.stringify({ message: 'error' }), + }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0]).to.deep.equal({ + httpCode: HTTP_FORBIDDEN, + message: 'error', + }); + }); + }); +});