From 68a0fee5655415f960cd1ffd9c1e689e38163d96 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:02:42 -0500 Subject: [PATCH] refactor: Migrate IdentityAPIClient to TypeScript (#948) --- src/aliasRequestApiClient.ts | 90 ----- src/audienceManager.ts | 4 +- src/batchUploader.ts | 4 +- src/configAPIClient.ts | 4 +- src/identityApiClient.interfaces.ts | 44 --- src/identityApiClient.js | 131 ------- src/identityApiClient.ts | 284 ++++++++++++++ src/sdkRuntimeModels.ts | 5 +- src/uploaders.ts | 8 +- test/src/_test.index.ts | 2 +- test/src/tests-aliasRequestApiClient.ts | 123 ------- test/src/tests-identityApiClient.ts | 471 ++++++++++++++++++++++++ 12 files changed, 768 insertions(+), 402 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/audienceManager.ts b/src/audienceManager.ts index aac4b95f..4771bde4 100644 --- a/src/audienceManager.ts +++ b/src/audienceManager.ts @@ -3,7 +3,7 @@ import { FetchUploader, XHRUploader, AsyncUploader, - fetchPayload + IFetchPayload } from './uploaders'; import Audience from './audience'; @@ -38,7 +38,7 @@ export default class AudienceManager { public async sendGetUserAudienceRequest(mpid: string, callback: (userAudiences: IAudienceMemberships) => void) { this.logger.verbose('Fetching user audiences from server'); - const fetchPayload: fetchPayload = { + const fetchPayload: IFetchPayload = { method: 'GET', headers: { Accept: '*/*', diff --git a/src/batchUploader.ts b/src/batchUploader.ts index ea4b6606..0f0bdfe3 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -9,7 +9,7 @@ import { AsyncUploader, FetchUploader, XHRUploader, - fetchPayload, + IFetchPayload, } from './uploaders'; import { IMParticleUser } from './identity-user-interfaces'; @@ -366,7 +366,7 @@ export class BatchUploader { logger.verbose(`Batch count: ${uploads.length}`); for (let i = 0; i < uploads.length; i++) { - const fetchPayload: fetchPayload = { + const fetchPayload: IFetchPayload = { method: 'POST', headers: { Accept: BatchUploader.CONTENT_TYPE, diff --git a/src/configAPIClient.ts b/src/configAPIClient.ts index fda5b2f7..551d70db 100644 --- a/src/configAPIClient.ts +++ b/src/configAPIClient.ts @@ -9,7 +9,7 @@ import { import { Dictionary } from './utils'; import { AsyncUploader, - fetchPayload, + IFetchPayload, FetchUploader, XHRUploader, } from './uploaders'; @@ -125,7 +125,7 @@ export default function ConfigAPIClient( this.getSDKConfiguration = async (): Promise => { let configResponse: IConfigResponse; - const fetchPayload: fetchPayload = { + const fetchPayload: IFetchPayload = { method: 'get', headers: { Accept: 'text/plain;charset=UTF-8', 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..1bf253dd --- /dev/null +++ b/src/identityApiClient.ts @@ -0,0 +1,284 @@ +import Constants, { HTTP_ACCEPTED, HTTP_OK } from './constants'; +import { + AsyncUploader, + FetchUploader, + XHRUploader, + IFetchPayload, +} from './uploaders'; +import { CACHE_HEADER } from './identity-utils'; +import { parseNumber } from './utils'; +import { + IAliasCallback, + IAliasRequest, + IdentityAPIMethod, + IIdentity, + IIdentityAPIRequestData, +} from './identity.interfaces'; +import { + Callback, + IdentityApiData, + MPID, + UserIdentities, +} from '@mparticle/web-sdk'; +import { + IdentityCallback, + IdentityResultBody, + IIdentityResponse, +} from './identity-user-interfaces'; +import { MParticleWebSDK } from './sdkRuntimeModels'; + +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; +} + +interface IdentityApiRequestPayload extends IFetchPayload { + headers: { + Accept: string; + 'Content-Type': string; + 'x-mp-key': string; + }; +} + +export default function IdentityAPIClient( + this: IIdentityApiClient, + mpInstance: MParticleWebSDK +) { + this.sendAliasRequest = async function( + aliasRequest: IAliasRequest, + aliasCallback: IAliasCallback + ) { + 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: AsyncUploader = window.fetch + ? new FetchUploader(uploadUrl) + : new XHRUploader(uploadUrl); + + // https://go.mparticle.com/work/SQDSDKS-6568 + const uploadPayload: IFetchPayload = { + method: 'post', + headers: { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(aliasRequest), + }; + + try { + const response: 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: IdentityCallback, + originalIdentityApiData: IdentityApiData, + parseIdentityResponse: IIdentity['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: AsyncUploader = window.fetch + ? new FetchUploader(uploadUrl) + : new XHRUploader(uploadUrl); + + // https://go.mparticle.com/work/SQDSDKS-6568 + const fetchPayload: IdentityApiRequestPayload = { + 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: Response = await uploader.upload(fetchPayload); + + let identityResponse: IIdentityResponse; + + 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: IdentityResultBody = await response.json(); + + identityResponse = this.getIdentityResponseFromFetch( + response, + responseBody + ); + } else { + identityResponse = this.getIdentityResponseFromXHR( + (response as unknown) as XMLHttpRequest + ); + } + + verbose( + 'Received Identity Response from server: ' + + JSON.stringify(identityResponse.responseText) + ); + + parseIdentityResponse( + identityResponse, + previousMPID, + callback, + originalIdentityApiData, + method, + knownIdentities, + false + ); + } catch (err) { + const errorMessage = (err as Error).message || err.toString(); + + mpInstance._Store.identityCallInFlight = false; + invokeCallback( + callback, + HTTPCodes.noHttpCoverage, + errorMessage, + ); + error('Error sending identity request to servers' + ' - ' + err); + } + }; + + this.getUploadUrl = (method: IdentityAPIMethod, mpid: MPID) => { + const uploadServiceUrl: string = mpInstance._Helpers.createServiceUrl( + mpInstance._Store.SDKConfig.identityUrl + ); + + const uploadUrl: string = + method === Modify + ? uploadServiceUrl + mpid + '/' + method + : uploadServiceUrl + method; + + return uploadUrl; + }; + + this.getIdentityResponseFromFetch = ( + response: Response, + responseBody: IdentityResultBody + ): IIdentityResponse => ({ + status: response.status, + responseText: responseBody, + cacheMaxAge: parseInt(response.headers.get(CACHE_HEADER)) || 0, + expireTimestamp: 0, + }); + + this.getIdentityResponseFromXHR = ( + response: XMLHttpRequest + ): IIdentityResponse => ({ + status: response.status, + responseText: response.responseText + ? JSON.parse(response.responseText) + : {}, + cacheMaxAge: parseNumber( + response.getResponseHeader(CACHE_HEADER) || '' + ), + expireTimestamp: 0, + }); +} diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index ae3e29b5..cb94f7d5 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -3,7 +3,6 @@ import { DataPlanVersion } from '@mparticle/data-planning-models'; import { MPConfiguration, MPID, - Callback, IdentityApiData, } from '@mparticle/web-sdk'; import { IStore } from './store'; @@ -287,8 +286,8 @@ export interface SDKHelpersApi { ): boolean; isObject?(item: any); invokeCallback?( - callback: Callback, - code: string, + callback: IdentityCallback, + code: number, body: string, mParticleUser?: IMParticleUser, previousMpid?: MPID diff --git a/src/uploaders.ts b/src/uploaders.ts index a075596e..a7f01ea6 100644 --- a/src/uploaders.ts +++ b/src/uploaders.ts @@ -1,6 +1,6 @@ type HTTPMethod = 'get' | 'post'; -export interface fetchPayload { +export interface IFetchPayload { method: string; headers: { Accept: string; @@ -12,7 +12,7 @@ export interface fetchPayload { export abstract class AsyncUploader { url: string; public abstract upload( - fetchPayload: fetchPayload, + fetchPayload: IFetchPayload, url?: string ): Promise; @@ -23,7 +23,7 @@ export abstract class AsyncUploader { export class FetchUploader extends AsyncUploader { public async upload( - fetchPayload: fetchPayload, + fetchPayload: IFetchPayload, _url?: string ): Promise { const url = _url || this.url; @@ -32,7 +32,7 @@ export class FetchUploader extends AsyncUploader { } export class XHRUploader extends AsyncUploader { - public async upload(fetchPayload: fetchPayload): Promise { + public async upload(fetchPayload: IFetchPayload): Promise { const response: Response = await this.makeRequest( this.url, fetchPayload.body, 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..4cc7d060 --- /dev/null +++ b/test/src/tests-identityApiClient.ts @@ -0,0 +1,471 @@ +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: { message: '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() { + fetchMockSuccess(urls.events); + mParticle.init(apiKey, window.mParticle.config); + }); + + afterEach(function() { + 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', + }); + }); + }); +});