From bd9a63dca75b744e65d79a58ad8baef7492b94f9 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:21:03 -0500 Subject: [PATCH] refactor: Update Error handling for Identity API Client (#959) --- src/constants.ts | 3 + src/identityApiClient.ts | 179 +++++++---- test/src/config/utils.js | 4 +- test/src/tests-core-sdk.js | 40 ++- test/src/tests-identityApiClient.ts | 449 +++++++++++++++++++++++++++- 5 files changed, 585 insertions(+), 90 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index ebeed00e..96390f5e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -201,4 +201,7 @@ export const MILLIS_IN_ONE_SEC = 1000; export const HTTP_OK = 200 as const; export const HTTP_ACCEPTED = 202 as const; export const HTTP_BAD_REQUEST = 400 as const; +export const HTTP_UNAUTHORIZED = 401 as const; export const HTTP_FORBIDDEN = 403 as const; +export const HTTP_NOT_FOUND = 404 as const; +export const HTTP_SERVER_ERROR = 500 as const; diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts index 1bf253dd..00ec9d07 100644 --- a/src/identityApiClient.ts +++ b/src/identityApiClient.ts @@ -1,4 +1,4 @@ -import Constants, { HTTP_ACCEPTED, HTTP_OK } from './constants'; +import Constants, { HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_OK } from './constants'; import { AsyncUploader, FetchUploader, @@ -6,7 +6,7 @@ import { IFetchPayload, } from './uploaders'; import { CACHE_HEADER } from './identity-utils'; -import { parseNumber } from './utils'; +import { parseNumber, valueof } from './utils'; import { IAliasCallback, IAliasRequest, @@ -15,7 +15,6 @@ import { IIdentityAPIRequestData, } from './identity.interfaces'; import { - Callback, IdentityApiData, MPID, UserIdentities, @@ -53,9 +52,8 @@ export interface IIdentityApiClient { getIdentityResponseFromXHR: (response: XMLHttpRequest) => IIdentityResponse; } -export interface IAliasResponseBody { - message?: string; -} +// A successfull Alias request will return a 202 with no body +export interface IAliasResponseBody {} interface IdentityApiRequestPayload extends IFetchPayload { headers: { @@ -65,6 +63,23 @@ interface IdentityApiRequestPayload extends IFetchPayload { }; } +type HTTP_STATUS_CODES = typeof HTTP_OK | typeof HTTP_ACCEPTED; + +interface IdentityApiError { + code: string; + message: string; +} + +interface IdentityApiErrorResponse { + Errors: IdentityApiError[], + ErrorCode: string, + StatusCode: valueof; + RequestId: string; +} + +// All Identity Api Responses have the same structure, except for Alias +interface IAliasErrorResponse extends IdentityApiError {} + export default function IdentityAPIClient( this: IIdentityApiClient, mpInstance: MParticleWebSDK @@ -99,55 +114,72 @@ export default function IdentityAPIClient( 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 message: string; let errorMessage: string; switch (response.status) { - case HTTP_OK: + // A successfull Alias request will return without a body case HTTP_ACCEPTED: + case HTTP_OK: // https://go.mparticle.com/work/SQDSDKS-6670 - message = - 'Successfully sent forwarding stats to mParticle Servers'; + message = 'Received Alias Response from server: ' + JSON.stringify(response.status); break; - default: - // 400 has an error message, but 403 doesn't - if (aliasResponseBody?.message) { - errorMessage = aliasResponseBody.message; + + // Our Alias Request API will 400 if there is an issue with the request body (ie timestamps are too far + // in the past or MPIDs don't exist). + // A 400 will return an error in the response body and will go through the happy path to report the error + case HTTP_BAD_REQUEST: + // 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. + if (response.json) { + 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) + : ''; } + + const errorResponse: IAliasErrorResponse = aliasResponseBody as unknown as IAliasErrorResponse; + + if (errorResponse?.message) { + errorMessage = errorResponse.message; + } + message = 'Issue with sending Alias Request to mParticle Servers, received HTTP Code of ' + response.status; + + if (errorResponse?.code) { + message += ' - ' + errorResponse.code; + } + + break; + + // Any unhandled errors, such as 500 or 429, will be caught here as well + default: { + throw new Error('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); + const errorMessage = (e as Error).message || e.toString(); + error('Error sending alias request to mParticle servers. ' + errorMessage); invokeAliasCallback( aliasCallback, HTTPCodes.noHttpCoverage, - err.message + errorMessage, ); } }; @@ -197,33 +229,67 @@ export default function IdentityAPIClient( }, body: JSON.stringify(identityApiRequest), }; + mpInstance._Store.identityCallInFlight = true; try { - mpInstance._Store.identityCallInFlight = true; const response: Response = await uploader.upload(fetchPayload); let identityResponse: IIdentityResponse; + let message: string; + + switch (response.status) { + case HTTP_ACCEPTED: + case HTTP_OK: + + // Our Identity API will return a 400 error if there is an issue with the requeest body + // such as if the body is empty or one of the attributes is missing or malformed + // A 400 will return an error in the response body and will go through the happy path to report the error + case HTTP_BAD_REQUEST: + + // FetchUploader returns the response as a JSON object that we have to await + 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 + ); + } - 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 - ); + if (identityResponse.status === HTTP_BAD_REQUEST) { + const errorResponse: IdentityApiErrorResponse = identityResponse.responseText as unknown as IdentityApiErrorResponse; + message = 'Issue with sending Identity Request to mParticle Servers, received HTTP Code of ' + identityResponse.status; + + if (errorResponse?.Errors) { + const errorMessage = errorResponse.Errors.map((error) => error.message).join(', '); + message += ' - ' + errorMessage; + } + + } else { + message = 'Received Identity Response from server: '; + message += JSON.stringify(identityResponse.responseText); + } + + break; + + // Our Identity API will return: + // - 401 if the `x-mp-key` is incorrect or missing + // - 403 if the there is a permission or account issue related to the `x-mp-key` + // 401 and 403 have no response bodies and should be rejected outright + default: { + throw new Error('Received HTTP Code of ' + response.status); + } } - verbose( - 'Received Identity Response from server: ' + - JSON.stringify(identityResponse.responseText) - ); + mpInstance._Store.identityCallInFlight = false; + verbose(message); parseIdentityResponse( identityResponse, previousMPID, @@ -234,15 +300,16 @@ export default function IdentityAPIClient( false ); } catch (err) { + mpInstance._Store.identityCallInFlight = false; + const errorMessage = (err as Error).message || err.toString(); - mpInstance._Store.identityCallInFlight = false; + error('Error sending identity request to servers' + ' - ' + errorMessage); invokeCallback( callback, HTTPCodes.noHttpCoverage, errorMessage, ); - error('Error sending identity request to servers' + ' - ' + err); } }; diff --git a/test/src/config/utils.js b/test/src/config/utils.js index ac1d3fb2..a6582ae8 100644 --- a/test/src/config/utils.js +++ b/test/src/config/utils.js @@ -634,7 +634,8 @@ var pluses = /\+/g, hasIdentifyReturned = () => { return window.mParticle.Identity.getCurrentUser()?.getMPID() === testMPID; }, - hasIdentityCallInflightReturned = () => !mParticle.getInstance()?._Store?.identityCallInFlight; + hasIdentityCallInflightReturned = () => !mParticle.getInstance()?._Store?.identityCallInFlight, + hasConfigLoaded = () => !!mParticle.getInstance()?._Store?.configurationLoaded var TestsCore = { getLocalStorageProducts: getLocalStorageProducts, @@ -663,6 +664,7 @@ var TestsCore = { fetchMockSuccess: fetchMockSuccess, hasIdentifyReturned: hasIdentifyReturned, hasIdentityCallInflightReturned, + hasConfigLoaded, }; export default TestsCore; \ No newline at end of file diff --git a/test/src/tests-core-sdk.js b/test/src/tests-core-sdk.js index d0f91ddd..52d86c95 100644 --- a/test/src/tests-core-sdk.js +++ b/test/src/tests-core-sdk.js @@ -12,7 +12,13 @@ const DefaultConfig = Constants.DefaultConfig, findEventFromRequest = Utils.findEventFromRequest, findBatch = Utils.findBatch; -const { waitForCondition, fetchMockSuccess, hasIdentifyReturned, hasIdentityCallInflightReturned } = Utils; +const { + waitForCondition, + fetchMockSuccess, + hasIdentifyReturned, + hasIdentityCallInflightReturned, + hasConfigLoaded, +} = Utils; describe('core SDK', function() { beforeEach(function() { @@ -1126,7 +1132,7 @@ describe('core SDK', function() { }) }); - it('should initialize and log events even with a failed /config fetch and empty config', function async(done) { + it('should initialize and log events even with a failed /config fetch and empty config', async () => { // this instance occurs when self hosting and the user only passes an object into init mParticle._resetForTests(MPConfig); @@ -1152,12 +1158,7 @@ describe('core SDK', function() { mParticle.init(apiKey, window.mParticle.config); - waitForCondition(() => { - return ( - mParticle.getInstance()._Store.configurationLoaded === true - ); - }) - .then(() => { + await waitForCondition(hasConfigLoaded); // fetching the config is async and we need to wait for it to finish mParticle.getInstance()._Store.isInitialized.should.equal(true); @@ -1170,23 +1171,16 @@ describe('core SDK', function() { 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' - ); - testEvent.should.be.ok(); + await waitForCondition(() => mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1'); - done(); - }); - }); + mParticle.logEvent('Test Event'); + const testEvent = findEventFromRequest( + fetchMock.calls(), + 'Test Event' + ); + + testEvent.should.be.ok(); }); it('should initialize without a config object passed to init', async function() { diff --git a/test/src/tests-identityApiClient.ts b/test/src/tests-identityApiClient.ts index 4cc7d060..80ebb9d2 100644 --- a/test/src/tests-identityApiClient.ts +++ b/test/src/tests-identityApiClient.ts @@ -7,16 +7,20 @@ import { IAliasRequest, IIdentityAPIRequestData, } from '../../src/identity.interfaces'; -import { +import Constants, { HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_FORBIDDEN, + HTTP_NOT_FOUND, HTTP_OK, + HTTP_SERVER_ERROR, + HTTP_UNAUTHORIZED, } from '../../src/constants'; import IdentityAPIClient, { IIdentityApiClient } from '../../src/identityApiClient'; import { IIdentityResponse } from '../../src/identity-user-interfaces'; import Utils from './config/utils'; const { fetchMockSuccess } = Utils; +const { HTTPCodes } = Constants; declare global { interface Window { @@ -25,7 +29,6 @@ declare global { } } -let mockServer; const mParticle = window.mParticle; declare global { @@ -352,9 +355,341 @@ describe('Identity Api Client', () => { 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); + expect(fetchMock.calls()[0][1].method, 'Payload Method').to.deep.equal(expectedFetchPayload.method); + expect(fetchMock.calls()[0][1].body, 'Payload Body').to.deep.equal(expectedFetchPayload.body); + expect(fetchMock.calls()[0][1].headers, 'Payload Headers').to.deep.equal(expectedFetchPayload.headers); + }); + + it('should include a detailed error message if the fetch returns a 400 (Bad Request)', async () => { + const identityRequestError = { + "Errors": [ + { + "code": "LOOKUP_ERROR", + "message": "knownIdentities is empty." + } + ], + "ErrorCode": "LOOKUP_ERROR", + "StatusCode": 400, + "RequestId": "6c6a234f-e171-4588-90a2-d7bc02530ec3" + }; + + fetchMock.post(urls.identify, { + status: HTTP_BAD_REQUEST, + body: identityRequestError, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: (callback, httpCode, errorMessage) => + invokeCallbackSpy(callback, httpCode, errorMessage), + }, + _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 expectedIdentityErrorRequest = { + status: 400, + responseText: identityRequestError, + cacheMAxAge: 0, + expireTimestamp: 0, + } + + expect(verboseSpy.lastCall, 'verboseSpy called').to.be.ok; + expect(verboseSpy.lastCall.firstArg).to.equal("Issue with sending Identity Request to mParticle Servers, received HTTP Code of 400 - knownIdentities is empty."); + + // A 400 will still call parseIdentityResponse + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(true); + expect(parseIdentityResponseSpy.args[0][0].status, 'Identity Error Request Status').to.equal(expectedIdentityErrorRequest.status); + expect(parseIdentityResponseSpy.args[0][0].responseText, 'Identity Error Request responseText').to.deep.equal(expectedIdentityErrorRequest.responseText); + 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 include a detailed error message if the fetch returns a 401 (Unauthorized)', async () => { + fetchMock.post(urls.identify, { + status: HTTP_UNAUTHORIZED, + body: null, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: (callback, httpCode, errorMessage) => + invokeCallbackSpy(callback, httpCode, errorMessage), + }, + _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(errorSpy.lastCall, 'errorSpy called').to.be.ok; + expect(errorSpy.lastCall.firstArg).to.equal("Error sending identity request to servers - Received HTTP Code of 401"); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); + expect(invokeCallbackSpy.args[0][1]).to.equal(-1); + expect(invokeCallbackSpy.args[0][2]).to.equal("Received HTTP Code of 401"); + + // A 401 should not call parseIdentityResponse + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + }); + + it('should include a detailed error message if the fetch returns a 403 (Forbidden)', async () => { + fetchMock.post(urls.identify, { + status: HTTP_FORBIDDEN, + body: null, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: (callback, httpCode, errorMessage) => invokeCallbackSpy(callback, httpCode, errorMessage), + }, + _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(errorSpy.lastCall, 'errorSpy called').to.be.ok; + expect(errorSpy.lastCall.firstArg).to.equal("Error sending identity request to servers - Received HTTP Code of 403"); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); + expect(invokeCallbackSpy.args[0][1]).to.equal(-1); + expect(invokeCallbackSpy.args[0][2]).to.equal("Received HTTP Code of 403"); + + // A 403 should not call parseIdentityResponse + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + + }); + + it('should include a detailed error message if the fetch returns a 404 (Not Found)', async () => { + fetchMock.post(urls.identify, { + status: HTTP_NOT_FOUND, + body: null, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: (callback, httpCode, errorMessage) => invokeCallbackSpy(callback, httpCode, errorMessage), + }, + _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(errorSpy.lastCall, 'errorSpy called').to.be.ok; + expect(errorSpy.lastCall.firstArg).to.equal("Error sending identity request to servers - Received HTTP Code of 404"); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); + expect(invokeCallbackSpy.args[0][1]).to.equal(-1); + expect(invokeCallbackSpy.args[0][2]).to.equal("Received HTTP Code of 404"); + + // A 404 should not call parseIdentityResponse + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + }); + + it('should include a detailed error message if the fetch returns a 500 (Server Error)', async () => { + fetchMock.post(urls.identify, { + status: HTTP_SERVER_ERROR, + body: { + "Errors": [ + { + "code": "INTERNAL_ERROR", + "message": "An unknown error was encountered." + } + ], + "ErrorCode": "INTERNAL_ERROR", + "StatusCode": 500, + "RequestId": null + }, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _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(errorSpy.calledOnce, 'errorSpy called').to.eq(true); + + expect(errorSpy.args[0][0]).to.equal('Error sending identity request to servers - Received HTTP Code of 500'); }); }); @@ -427,7 +762,14 @@ describe('Identity Api Client', () => { }; const aliasCallback = sinon.spy(); - fetchMock.post(aliasUrl, HTTP_BAD_REQUEST); + fetchMock.post(aliasUrl, { + status: HTTP_BAD_REQUEST, + body: { + message:"The payload was malformed JSON or had missing fields.", + code:"INVALID_DATA"} + }, { + overwriteRoutes: true + }); await identityApiClient.sendAliasRequest( aliasRequest, @@ -437,9 +779,36 @@ describe('Identity Api Client', () => { const callbackArgs = aliasCallback.getCall(0).args; expect(callbackArgs[0]).to.deep.equal({ httpCode: HTTP_BAD_REQUEST, + message: 'The payload was malformed JSON or had missing fields.', }); }); + it('should have an httpCode and an error message passed to the callback on a 401', 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_UNAUTHORIZED, + body: null, + }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0].httpCode).to.equal(HTTPCodes.noHttpCoverage); + expect(callbackArgs[0].message).deep.equal('Received HTTP Code of 401'); + }); + 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); @@ -453,7 +822,7 @@ describe('Identity Api Client', () => { const aliasCallback = sinon.spy(); fetchMock.post(aliasUrl, { status: HTTP_FORBIDDEN, - body: JSON.stringify({ message: 'error' }), + body: null, }); await identityApiClient.sendAliasRequest( @@ -462,10 +831,70 @@ describe('Identity Api Client', () => { ); expect(aliasCallback.calledOnce).to.eq(true); const callbackArgs = aliasCallback.getCall(0).args; - expect(callbackArgs[0]).to.deep.equal({ - httpCode: HTTP_FORBIDDEN, - message: 'error', + expect(callbackArgs[0].httpCode).to.equal(HTTPCodes.noHttpCoverage); + expect(callbackArgs[0].message).deep.equal('Received HTTP Code of 403'); + }); + + it('should have an httpCode and an error message passed to the callback on a 404', 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_NOT_FOUND, + body: null, + }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0].httpCode).to.equal(HTTPCodes.noHttpCoverage); + expect(callbackArgs[0].message).deep.equal('Received HTTP Code of 404'); + }); + + it('should have an httpCode and an error message passed to the callback on a 500', 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_SERVER_ERROR, + body: { + "Errors": [ + { + "code": "INTERNAL_ERROR", + "message": "An unknown error was encountered." + } + ], + "ErrorCode": "INTERNAL_ERROR", + "StatusCode": 500, + "RequestId": null + }, }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0].httpCode).to.equal(HTTPCodes.noHttpCoverage); + expect(callbackArgs[0].message).deep.equal('Received HTTP Code of 500'); }); }); });