UNPKG

@iterable/react-native-sdk

Version:
1,216 lines (1,126 loc) 46.9 kB
import { NativeEventEmitter, Platform } from 'react-native'; import { MockLinking } from '../../__mocks__/MockLinking'; import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; // import from the same location that consumers import from import { Iterable, IterableAction, IterableActionContext, IterableActionSource, IterableAttributionInfo, IterableAuthResponse, IterableCommerceItem, IterableConfig, IterableDataRegion, IterableEventName, IterableInAppCloseSource, IterableInAppDeleteSource, IterableInAppLocation, IterableInAppMessage, IterableInAppShowResponse, IterableInAppTrigger, IterableInAppTriggerType, IterableLogLevel, } from '../..'; import { TestHelper } from '../../__tests__/TestHelper'; describe('Iterable', () => { beforeEach(() => { jest.clearAllMocks(); }); afterEach(() => { // Clean up all event listeners to prevent Jest worker process hanging const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled); nativeEmitter.removeAllListeners(IterableEventName.handleInAppCalled); nativeEmitter.removeAllListeners( IterableEventName.handleCustomActionCalled ); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); nativeEmitter.removeAllListeners(IterableEventName.handleAuthSuccessCalled); nativeEmitter.removeAllListeners(IterableEventName.handleAuthFailureCalled); // Clear any pending timers jest.clearAllTimers(); }); describe('setEmail', () => { it('should set the email', async () => { const result = 'user@example.com'; // GIVEN an email const email = 'user@example.com'; // WHEN Iterable.setEmail is called with the given email Iterable.setEmail(email); // THEN Iterable.getEmail returns the given email return await Iterable.getEmail().then((mail) => { expect(mail).toBe(result); }); }); }); describe('setUserId', () => { it('should set the userId', async () => { const result = 'user1'; // GIVEN an userId const userId = 'user1'; // WHEN Iterable.setUserId is called with the given userId Iterable.setUserId(userId); // THEN Iterable.getUserId returns the given userId return await Iterable.getUserId().then((id) => { expect(id).toBe(result); }); }); }); describe('logout', () => { it('should call setEmail with null', () => { // GIVEN no parameters // WHEN Iterable.logout is called const setEmailSpy = jest.spyOn(Iterable, 'setEmail'); Iterable.logout(); // THEN Iterable.setEmail is called with null expect(setEmailSpy).toBeCalledWith(null); setEmailSpy.mockRestore(); }); it('should call setUserId with null', () => { // GIVEN no parameters // WHEN Iterable.logout is called const setUserIdSpy = jest.spyOn(Iterable, 'setUserId'); Iterable.logout(); // THEN Iterable.setUserId is called with null expect(setUserIdSpy).toBeCalledWith(null); setUserIdSpy.mockRestore(); }); it('should clear email and userId', async () => { // GIVEN a user is logged in // This is just for testing purposed. // Usually you'd either call `setEmail` or `setUserId`, but not both. Iterable.setEmail('user@example.com'); Iterable.setUserId('user123'); // WHEN Iterable.logout is called Iterable.logout(); // THEN email and userId are set to null const email = await Iterable.getEmail(); const userId = await Iterable.getUserId(); expect(email).toBeNull(); expect(userId).toBeNull(); }); it('should call setEmail and setUserId with null', () => { // GIVEN no parameters const setEmailSpy = jest.spyOn(Iterable, 'setEmail'); const setUserIdSpy = jest.spyOn(Iterable, 'setUserId'); // WHEN Iterable.logout is called Iterable.logout(); // THEN both methods are called with null expect(setEmailSpy).toBeCalledWith(null); expect(setUserIdSpy).toBeCalledWith(null); // Clean up setEmailSpy.mockRestore(); setUserIdSpy.mockRestore(); }); }); describe('disableDeviceForCurrentUser', () => { it('should disable the device for the current user', () => { // GIVEN no parameters // WHEN Iterable.disableDeviceForCurrentUser is called Iterable.disableDeviceForCurrentUser(); // THEN corresponding method is called on RNITerableAPI expect(MockRNIterableAPI.disableDeviceForCurrentUser).toBeCalled(); }); }); describe('getLastPushPayload', () => { it('should return the last push payload', async () => { const result = { var1: 'val1', var2: true }; // GIVEN no parameters // WHEN the lastPushPayload is set MockRNIterableAPI.lastPushPayload = { var1: 'val1', var2: true }; // THEN the lastPushPayload is returned when getLastPushPayload is called return await Iterable.getLastPushPayload().then((payload) => { expect(payload).toEqual(result); }); }); }); describe('trackPushOpenWithCampaignId', () => { it('should track the push open with the campaign id', () => { // GIVEN the following parameters const campaignId = 123; const templateId = 234; const messageId = 'someMessageId'; const appAlreadyRunning = false; const dataFields = { dataFieldKey: 'dataFieldValue' }; // WHEN Iterable.trackPushOpenWithCampaignId is called Iterable.trackPushOpenWithCampaignId( campaignId, templateId, messageId, appAlreadyRunning, dataFields ); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( campaignId, templateId, messageId, appAlreadyRunning, dataFields ); }); }); describe('updateCart', () => { it('should call IterableAPI.updateCart with the correct items', () => { // GIVEN list of items const items = [new IterableCommerceItem('id1', 'Boba Tea', 18, 26)]; // WHEN Iterable.updateCart is called Iterable.updateCart(items); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.updateCart).toBeCalledWith(items); }); }); describe('trackPurchase', () => { it('should track the purchase', () => { // GIVEN the following parameters const total = 10; const items = [new IterableCommerceItem('id1', 'Boba Tea', 18, 26)]; const dataFields = { dataFieldKey: 'dataFieldValue' }; // WHEN Iterable.trackPurchase is called Iterable.trackPurchase(total, items, dataFields); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( total, items, dataFields ); }); it('should track the purchase when called with optional fields', () => { // GIVEN the following parameters const total = 5; const items = [ new IterableCommerceItem( 'id', 'swordfish', 64, 1, 'SKU', 'description', 'url', 'imageUrl', ['sword', 'shield'] ), ]; const dataFields = { key: 'value' }; // WHEN Iterable.trackPurchase is called Iterable.trackPurchase(total, items, dataFields); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( total, items, dataFields ); }); }); describe('trackEvent', () => { it('should call IterableAPI.trackEvent with the correct name and dataFields', () => { // GIVEN the following parameters const name = 'EventName'; const dataFields = { DatafieldKey: 'DatafieldValue' }; // WHEN Iterable.trackEvent is called Iterable.trackEvent(name, dataFields); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.trackEvent).toBeCalledWith(name, dataFields); }); }); describe('setAttributionInfo', () => { it('should set the attribution info', async () => { // GIVEN attribution info const campaignId = 1234; const templateId = 5678; const messageId = 'qwer'; // WHEN Iterable.setAttributionInfo is called with the given attribution info Iterable.setAttributionInfo( new IterableAttributionInfo(campaignId, templateId, messageId) ); // THEN Iterable.getAttrbutionInfo returns the given attribution info return await Iterable.getAttributionInfo().then((attributionInfo) => { expect(attributionInfo?.campaignId).toBe(campaignId); expect(attributionInfo?.templateId).toBe(templateId); expect(attributionInfo?.messageId).toBe(messageId); }); }); }); describe('updateUser', () => { it('should update the user', () => { // GIVEN the following parameters const dataFields = { field: 'value1' }; // WHEN Iterable.updateUser is called Iterable.updateUser(dataFields, false); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.updateUser).toBeCalledWith(dataFields, false); }); }); describe('updateEmail', () => { it('should call IterableAPI.updateEmail with the correct email', () => { // GIVEN the new email const newEmail = 'woo@newemail.com'; // WHEN Iterable.updateEmail is called Iterable.updateEmail(newEmail); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.updateEmail).toBeCalledWith(newEmail, undefined); }); it('should call IterableAPI.updateEmail with the correct email and token', () => { // GIVEN the new email and a token const newEmail = 'woo@newemail.com'; const newToken = 'token2'; // WHEN Iterable.updateEmail is called Iterable.updateEmail(newEmail, newToken); // THEN corresponding function is called on RNITerableAPI expect(MockRNIterableAPI.updateEmail).toBeCalledWith(newEmail, newToken); }); }); describe('iterableConfig', () => { it('should have default values', () => { // GIVEN no parameters // WHEN config is initialized const config = new IterableConfig(); // THEN config has default values expect(config.pushIntegrationName).toBe(undefined); expect(config.autoPushRegistration).toBe(true); expect(config.checkForDeferredDeeplink).toBe(false); expect(config.inAppDisplayInterval).toBe(30.0); expect(config.urlHandler).toBe(undefined); expect(config.customActionHandler).toBe(undefined); expect(config.inAppHandler).toBe(undefined); expect(config.authHandler).toBe(undefined); expect(config.logLevel).toBe(IterableLogLevel.debug); expect(config.logReactNativeSdkCalls).toBe(true); expect(config.expiringAuthTokenRefreshPeriod).toBe(60.0); expect(config.allowedProtocols).toEqual([]); expect(config.androidSdkUseInMemoryStorageForInApps).toBe(false); expect(config.useInMemoryStorageForInApps).toBe(false); expect(config.dataRegion).toBe(IterableDataRegion.US); expect(config.encryptionEnforced).toBe(false); const configDict = config.toDict(); expect(configDict.pushIntegrationName).toBe(undefined); expect(configDict.autoPushRegistration).toBe(true); expect(configDict.inAppDisplayInterval).toBe(30.0); expect(configDict.urlHandlerPresent).toBe(false); expect(configDict.customActionHandlerPresent).toBe(false); expect(configDict.inAppHandlerPresent).toBe(false); expect(configDict.authHandlerPresent).toBe(false); expect(configDict.logLevel).toBe(IterableLogLevel.debug); expect(configDict.expiringAuthTokenRefreshPeriod).toBe(60.0); expect(configDict.allowedProtocols).toEqual([]); expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false); expect(configDict.useInMemoryStorageForInApps).toBe(false); expect(configDict.dataRegion).toBe(IterableDataRegion.US); expect(configDict.encryptionEnforced).toBe(false); }); }); describe('urlHandler', () => { it('should open the url when canOpenURL returns true and urlHandler returns false', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled); // sets up config file and urlHandler function // urlHandler set to return false const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { return false; }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN canOpenUrl set to return a promise that resolves to true MockLinking.canOpenURL = jest.fn(async () => { return await new Promise((resolve) => { resolve(true); }); }); MockLinking.openURL.mockReset(); const expectedUrl = 'https://somewhere.com'; const actionDict = { type: 'openUrl' }; const dict = { url: expectedUrl, context: { action: actionDict, source: 'inApp' }, }; // WHEN handleUrlCalled event is emitted nativeEmitter.emit(IterableEventName.handleUrlCalled, dict); // THEN urlHandler and MockLinking is called with expected url return await TestHelper.delayed(0, () => { expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); expect(MockLinking.openURL).toBeCalledWith(expectedUrl); }); }); it('should not open the url when canOpenURL returns false and urlHandler returns false', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled); // sets up config file and urlHandler function // urlHandler set to return false const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { return false; }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN canOpenUrl set to return a promise that resolves to false MockLinking.canOpenURL = jest.fn(async () => { return await new Promise((resolve) => { resolve(false); }); }); MockLinking.openURL.mockReset(); const expectedUrl = 'https://somewhere.com'; const actionDict = { type: 'openUrl' }; const dict = { url: expectedUrl, context: { action: actionDict, source: 'inApp' }, }; // WHEN handleUrlCalled event is emitted nativeEmitter.emit(IterableEventName.handleUrlCalled, dict); // THEN urlHandler is called and MockLinking.openURL is not called return await TestHelper.delayed(0, () => { expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); expect(MockLinking.openURL).not.toBeCalled(); }); }); it('should not open the url when canOpenURL returns true and urlHandler returns true', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled); // sets up config file and urlHandler function // urlHandler set to return true const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.urlHandler = jest.fn((_url: string, _: IterableActionContext) => { return true; }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN canOpenUrl set to return a promise that resolves to true MockLinking.canOpenURL = jest.fn(async () => { return await new Promise((resolve) => { resolve(true); }); }); MockLinking.openURL.mockReset(); const expectedUrl = 'https://somewhere.com'; const actionDict = { type: 'openUrl' }; const dict = { url: expectedUrl, context: { action: actionDict, source: 'inApp' }, }; // WHEN handleUrlCalled event is emitted nativeEmitter.emit(IterableEventName.handleUrlCalled, dict); // THEN urlHandler is called and MockLinking.openURL is not called return await TestHelper.delayed(0, () => { expect(config.urlHandler).toBeCalledWith(expectedUrl, dict.context); expect(MockLinking.openURL).not.toBeCalled(); }); }); }); describe('customActionHandler', () => { it('should be called with the correct action and context', () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners( IterableEventName.handleCustomActionCalled ); // sets up config file and customActionHandler function // customActionHandler set to return true const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.customActionHandler = jest.fn( (_action: IterableAction, _context: IterableActionContext) => { return true; } ); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN custom action name and custom action data const actionName = 'zeeActionName'; const actionData = 'zeeActionData'; const actionDict = { type: actionName, data: actionData }; const actionSource = IterableActionSource.inApp; const dict = { action: actionDict, context: { action: actionDict, source: IterableActionSource.inApp }, }; // WHEN handleCustomActionCalled event is emitted nativeEmitter.emit(IterableEventName.handleCustomActionCalled, dict); // THEN customActionHandler is called with expected action and expected context const expectedAction = new IterableAction(actionName, actionData); const expectedContext = new IterableActionContext( expectedAction, actionSource ); expect(config.customActionHandler).toBeCalledWith( expectedAction, expectedContext ); }); }); describe('handleAppLink', () => { it('should call IterableAPI.handleAppLink', () => { // GIVEN a link const link = 'https://somewhere.com/link/something'; // WHEN Iterable.handleAppLink is called Iterable.handleAppLink(link); // THEN corresponding function is called on RNITerableAPI expect(MockRNIterableAPI.handleAppLink).toBeCalledWith(link); }); }); describe('updateSubscriptions', () => { it('should call IterableAPI.updateSubscriptions with the correct parameters', () => { // GIVEN the following parameters const emailListIds = [1, 2, 3]; const unsubscribedChannelIds = [4, 5, 6]; const unsubscribedMessageTypeIds = [7, 8]; const subscribedMessageTypeIds = [9]; const campaignId = 10; const templateId = 11; // WHEN Iterable.updateSubscriptions is called Iterable.updateSubscriptions( emailListIds, unsubscribedChannelIds, unsubscribedMessageTypeIds, subscribedMessageTypeIds, campaignId, templateId ); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.updateSubscriptions).toBeCalledWith( emailListIds, unsubscribedChannelIds, unsubscribedMessageTypeIds, subscribedMessageTypeIds, campaignId, templateId ); }); }); describe('initialize', () => { it('should call IterableAPI.initializeWithApiKey and save the config', async () => { // GIVEN an API key and config const apiKey = 'test-api-key'; const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.logLevel = IterableLogLevel.debug; // WHEN Iterable.initialize is called const result = await Iterable.initialize(apiKey, config); // THEN corresponding function is called on RNIterableAPI and config is saved expect(MockRNIterableAPI.initializeWithApiKey).toBeCalledWith( apiKey, config.toDict(), expect.any(String) ); expect(Iterable.savedConfig).toBe(config); expect(result).toBe(true); }); it('should give the default config if no config is provided', async () => { // GIVEN an API key const apiKey = 'test-api-key'; // WHEN Iterable.initialize is called const result = await Iterable.initialize(apiKey); // THEN corresponding function is called on RNIterableAPI and config is saved expect(Iterable.savedConfig).toStrictEqual(new IterableConfig()); expect(result).toBe(true); }); }); describe('initialize2', () => { it('should call IterableAPI.initialize2WithApiKey with an endpoint and save the config', async () => { // GIVEN an API key, config, and endpoint const apiKey = 'test-api-key'; const config = new IterableConfig(); config.logReactNativeSdkCalls = false; const apiEndPoint = 'https://api.staging.iterable.com'; // WHEN Iterable.initialize2 is called const result = await Iterable.initialize2(apiKey, config, apiEndPoint); // THEN corresponding function is called on RNIterableAPI and config is saved expect(MockRNIterableAPI.initialize2WithApiKey).toBeCalledWith( apiKey, config.toDict(), expect.any(String), apiEndPoint ); expect(Iterable.savedConfig).toBe(config); expect(result).toBe(true); }); it('should give the default config if no config is provided', async () => { // GIVEN an API key const apiKey = 'test-api-key'; const apiEndPoint = 'https://api.staging.iterable.com'; // WHEN Iterable.initialize is called const result = await Iterable.initialize2(apiKey, undefined, apiEndPoint); // THEN corresponding function is called on RNIterableAPI and config is saved expect(Iterable.savedConfig).toStrictEqual(new IterableConfig()); expect(result).toBe(true); }); }); describe('wakeApp', () => { it('should call IterableAPI.wakeApp on Android', () => { // GIVEN Android platform const originalPlatform = Platform.OS; Object.defineProperty(Platform, 'OS', { value: 'android', writable: true, }); // WHEN Iterable.wakeApp is called Iterable.wakeApp(); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.wakeApp).toBeCalled(); // Restore original platform Object.defineProperty(Platform, 'OS', { value: originalPlatform, writable: true, }); }); it('should not call IterableAPI.wakeApp on iOS', () => { // GIVEN iOS platform const originalPlatform = Platform.OS; Object.defineProperty(Platform, 'OS', { value: 'ios', writable: true, }); // WHEN Iterable.wakeApp is called Iterable.wakeApp(); // THEN corresponding function is not called on RNIterableAPI expect(MockRNIterableAPI.wakeApp).not.toBeCalled(); // Restore original platform Object.defineProperty(Platform, 'OS', { value: originalPlatform, writable: true, }); }); }); describe('trackInAppOpen', () => { it('should call IterableAPI.trackInAppOpen with the correct parameters', () => { // GIVEN an in-app message and location const message = new IterableInAppMessage( '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), new Date(), false, undefined, undefined, false, 0 ); const location = IterableInAppLocation.inApp; // WHEN Iterable.trackInAppOpen is called Iterable.trackInAppOpen(message, location); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.trackInAppOpen).toBeCalledWith( message.messageId, location ); }); }); describe('trackInAppClick', () => { it('should call IterableAPI.trackInAppClick with the correct parameters', () => { // GIVEN an in-app message, location, and clicked URL const message = new IterableInAppMessage( '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), new Date(), false, undefined, undefined, false, 0 ); const location = IterableInAppLocation.inApp; const clickedUrl = 'https://www.example.com'; // WHEN Iterable.trackInAppClick is called Iterable.trackInAppClick(message, location, clickedUrl); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.trackInAppClick).toBeCalledWith( message.messageId, location, clickedUrl ); }); }); describe('trackInAppClose', () => { it('should call IterableAPI.trackInAppClose with the correct parameters', () => { // GIVEN an in-app message, location, and source (no URL) const message = new IterableInAppMessage( '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), new Date(), false, undefined, undefined, false, 0 ); const location = IterableInAppLocation.inApp; const source = IterableInAppCloseSource.back; // WHEN Iterable.trackInAppClose is called Iterable.trackInAppClose(message, location, source); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.trackInAppClose).toBeCalledWith( message.messageId, location, source, undefined ); }); it('should call IterableAPI.trackInAppClose with a clicked URL when provided', () => { // GIVEN an in-app message, location, source, and clicked URL const message = new IterableInAppMessage( '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), new Date(), false, undefined, undefined, false, 0 ); const location = IterableInAppLocation.inApp; const source = IterableInAppCloseSource.back; const clickedUrl = 'https://www.example.com'; // WHEN Iterable.trackInAppClose is called Iterable.trackInAppClose(message, location, source, clickedUrl); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.trackInAppClose).toBeCalledWith( message.messageId, location, source, clickedUrl ); }); }); describe('inAppConsume', () => { it('should call IterableAPI.inAppConsume with the correct parameters', () => { // GIVEN an in-app message, location, and delete source const message = new IterableInAppMessage( '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), new Date(), false, undefined, undefined, false, 0 ); const location = IterableInAppLocation.inApp; const source = IterableInAppDeleteSource.deleteButton; // WHEN Iterable.inAppConsume is called Iterable.inAppConsume(message, location, source); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.inAppConsume).toBeCalledWith( message.messageId, location, source ); }); }); describe('getVersionFromPackageJson', () => { it('should return the version from the package.json file', () => { // GIVEN no parameters // WHEN Iterable.getVersionFromPackageJson is called const version = Iterable.getVersionFromPackageJson(); // THEN a version string is returned expect(typeof version).toBe('string'); expect(version.length).toBeGreaterThan(0); }); }); describe('setupEventHandlers', () => { it('should call inAppHandler when handleInAppCalled event is emitted', () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleInAppCalled); // sets up config file and inAppHandler function const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.inAppHandler = jest.fn((_message: IterableInAppMessage) => { return IterableInAppShowResponse.show; }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN message dictionary const messageDict = { messageId: '1234', campaignId: 4567, trigger: { type: 0 }, createdAt: new Date().toISOString(), expiresAt: new Date().toISOString(), saveToInbox: false, inboxMetadata: undefined, customPayload: undefined, read: false, priorityLevel: 0, }; // WHEN handleInAppCalled event is emitted nativeEmitter.emit(IterableEventName.handleInAppCalled, messageDict); // THEN inAppHandler is called and setInAppShowResponse is called expect(config.inAppHandler).toBeCalledWith( expect.any(IterableInAppMessage) ); expect(MockRNIterableAPI.setInAppShowResponse).toBeCalledWith( IterableInAppShowResponse.show ); }); describe('authHandler', () => { it('should call authHandler when handleAuthCalled event is emitted', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); nativeEmitter.removeAllListeners( IterableEventName.handleAuthSuccessCalled ); nativeEmitter.removeAllListeners( IterableEventName.handleAuthFailureCalled ); // sets up config file and authHandler function const config = new IterableConfig(); config.logReactNativeSdkCalls = false; const successCallback = jest.fn(); const failureCallback = jest.fn(); const authResponse = new IterableAuthResponse(); authResponse.authToken = 'test-token'; authResponse.successCallback = successCallback; authResponse.failureCallback = failureCallback; config.authHandler = jest.fn(() => { return Promise.resolve(authResponse); }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN auth handler returns AuthResponse // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); // WHEN handleAuthSuccessCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthSuccessCalled); // THEN passAlongAuthToken is called with the token and success callback is called after timeout return await TestHelper.delayed(1100, () => { expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith( 'test-token' ); expect(successCallback).toBeCalled(); expect(failureCallback).not.toBeCalled(); }); }); it('should call authHandler when handleAuthFailureCalled event is emitted', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); nativeEmitter.removeAllListeners( IterableEventName.handleAuthSuccessCalled ); nativeEmitter.removeAllListeners( IterableEventName.handleAuthFailureCalled ); // sets up config file and authHandler function const config = new IterableConfig(); config.logReactNativeSdkCalls = false; const successCallback = jest.fn(); const failureCallback = jest.fn(); const authResponse = new IterableAuthResponse(); authResponse.authToken = 'test-token'; authResponse.successCallback = successCallback; authResponse.failureCallback = failureCallback; config.authHandler = jest.fn(() => { // Why are we resolving when this is a failure? return Promise.resolve(authResponse); }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN auth handler returns AuthResponse // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); // WHEN handleAuthFailureCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthFailureCalled); // THEN passAlongAuthToken is called with the token and failure callback is called after timeout return await TestHelper.delayed(1100, () => { expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith( 'test-token' ); expect(failureCallback).toBeCalled(); expect(successCallback).not.toBeCalled(); }); }); it('should call authHandler when handleAuthCalled event is emitted and returns a string token', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); // sets up config file and authHandler function const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.authHandler = jest.fn(() => { return Promise.resolve('string-token'); }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN auth handler returns string token // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); // THEN passAlongAuthToken is called with the string token return await TestHelper.delayed(100, () => { expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith( 'string-token' ); }); }); it('should call authHandler when handleAuthCalled event is emitted and returns an unexpected response', () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); // sets up config file and authHandler function const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.authHandler = jest.fn(() => { return Promise.resolve({ unexpected: 'object' } as unknown as | string | IterableAuthResponse); }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN auth handler returns unexpected response // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); // THEN error is logged (we can't easily test console.log, but we can verify no crash) expect(MockRNIterableAPI.passAlongAuthToken).not.toBeCalled(); }); it('should call authHandler when handleAuthCalled event is emitted and rejects the promise', () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); // sets up config file and authHandler function const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.authHandler = jest.fn(() => { return Promise.reject(new Error('Auth failed')); }); // initialize Iterable object Iterable.initialize('apiKey', config); // GIVEN auth handler rejects promise // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); // THEN error is logged (we can't easily test console.log, but we can verify no crash) expect(MockRNIterableAPI.passAlongAuthToken).not.toBeCalled(); }); }); }); describe('authManager', () => { describe('pauseAuthRetries', () => { it('should call RNIterableAPI.pauseAuthRetries with true when pauseRetry is true', () => { // GIVEN pauseRetry is true const pauseRetry = true; // WHEN pauseAuthRetries is called Iterable.authManager.pauseAuthRetries(pauseRetry); // THEN RNIterableAPI.pauseAuthRetries is called with true expect(MockRNIterableAPI.pauseAuthRetries).toBeCalledWith(true); }); it('should call RNIterableAPI.pauseAuthRetries with false when pauseRetry is false', () => { // GIVEN pauseRetry is false const pauseRetry = false; // WHEN pauseAuthRetries is called Iterable.authManager.pauseAuthRetries(pauseRetry); // THEN RNIterableAPI.pauseAuthRetries is called with false expect(MockRNIterableAPI.pauseAuthRetries).toBeCalledWith(false); }); it('should return the result from RNIterableAPI.pauseAuthRetries', () => { // GIVEN RNIterableAPI.pauseAuthRetries returns a value const expectedResult = 'pause-result'; MockRNIterableAPI.pauseAuthRetries = jest .fn() .mockReturnValue(expectedResult); // WHEN pauseAuthRetries is called const result = Iterable.authManager.pauseAuthRetries(true); // THEN the result is returned expect(result).toBe(expectedResult); }); }); describe('passAlongAuthToken', () => { it('should call RNIterableAPI.passAlongAuthToken with a valid string token', async () => { // GIVEN a valid auth token const authToken = 'valid-jwt-token'; const expectedResponse = new IterableAuthResponse(); expectedResponse.authToken = 'new-token'; MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValue(expectedResponse); // WHEN passAlongAuthToken is called const result = await Iterable.authManager.passAlongAuthToken(authToken); // THEN RNIterableAPI.passAlongAuthToken is called with the token expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(authToken); expect(result).toBe(expectedResponse); }); it('should call RNIterableAPI.passAlongAuthToken with null token', async () => { // GIVEN a null auth token const authToken = null; const expectedResponse = 'success'; MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValue(expectedResponse); // WHEN passAlongAuthToken is called const result = await Iterable.authManager.passAlongAuthToken(authToken); // THEN RNIterableAPI.passAlongAuthToken is called with null expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(null); expect(result).toBe(expectedResponse); }); it('should call RNIterableAPI.passAlongAuthToken with undefined token', async () => { // GIVEN an undefined auth token const authToken = undefined; const expectedResponse = undefined; MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValue(expectedResponse); // WHEN passAlongAuthToken is called const result = await Iterable.authManager.passAlongAuthToken(authToken); // THEN RNIterableAPI.passAlongAuthToken is called with undefined expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(undefined); expect(result).toBe(expectedResponse); }); it('should call RNIterableAPI.passAlongAuthToken with empty string token', async () => { // GIVEN an empty string auth token const authToken = ''; const expectedResponse = new IterableAuthResponse(); MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValue(expectedResponse); // WHEN passAlongAuthToken is called const result = await Iterable.authManager.passAlongAuthToken(authToken); // THEN RNIterableAPI.passAlongAuthToken is called with empty string expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(''); expect(result).toBe(expectedResponse); }); it('should return IterableAuthResponse when API returns IterableAuthResponse', async () => { // GIVEN API returns IterableAuthResponse const authToken = 'test-token'; const expectedResponse = new IterableAuthResponse(); expectedResponse.authToken = 'new-token'; expectedResponse.successCallback = jest.fn(); expectedResponse.failureCallback = jest.fn(); MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValue(expectedResponse); // WHEN passAlongAuthToken is called const result = await Iterable.authManager.passAlongAuthToken(authToken); // THEN the result is the expected IterableAuthResponse expect(result).toBe(expectedResponse); expect(result).toBeInstanceOf(IterableAuthResponse); }); it('should return string when API returns string', async () => { // GIVEN API returns string const authToken = 'test-token'; const expectedResponse = 'success-string'; MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValue(expectedResponse); // WHEN passAlongAuthToken is called const result = await Iterable.authManager.passAlongAuthToken(authToken); // THEN the result is the expected string expect(result).toBe(expectedResponse); expect(typeof result).toBe('string'); }); it('should return undefined when API returns undefined', async () => { // GIVEN API returns undefined const authToken = 'test-token'; const expectedResponse = undefined; MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValue(expectedResponse); // WHEN passAlongAuthToken is called const result = await Iterable.authManager.passAlongAuthToken(authToken); // THEN the result is undefined expect(result).toBeUndefined(); }); it('should handle API rejection and propagate the error', async () => { // GIVEN API rejects with an error const authToken = 'test-token'; const expectedError = new Error('API Error'); MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockRejectedValue(expectedError); // WHEN passAlongAuthToken is called // THEN the error is propagated await expect( Iterable.authManager.passAlongAuthToken(authToken) ).rejects.toThrow('API Error'); }); it('should handle API rejection with network error', async () => { // GIVEN API rejects with a network error const authToken = 'test-token'; const networkError = new Error('Network request failed'); MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockRejectedValue(networkError); // WHEN passAlongAuthToken is called // THEN the network error is propagated await expect( Iterable.authManager.passAlongAuthToken(authToken) ).rejects.toThrow('Network request failed'); }); it('should handle API rejection with timeout error', async () => { // GIVEN API rejects with a timeout error const authToken = 'test-token'; const timeoutError = new Error('Request timeout'); MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockRejectedValue(timeoutError); // WHEN passAlongAuthToken is called // THEN the timeout error is propagated await expect( Iterable.authManager.passAlongAuthToken(authToken) ).rejects.toThrow('Request timeout'); }); }); describe('integration', () => { it('should work with both methods in sequence', async () => { // GIVEN a sequence of operations const authToken = 'test-token'; const expectedResponse = new IterableAuthResponse(); MockRNIterableAPI.pauseAuthRetries = jest .fn() .mockReturnValue('paused'); MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValue(expectedResponse); // WHEN calling both methods in sequence const pauseResult = Iterable.authManager.pauseAuthRetries(true); const tokenResult = await Iterable.authManager.passAlongAuthToken(authToken); // THEN both operations should work correctly expect(pauseResult).toBe('paused'); expect(tokenResult).toBe(expectedResponse); expect(MockRNIterableAPI.pauseAuthRetries).toBeCalledWith(true); expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(authToken); }); it('should handle rapid successive calls', async () => { // GIVEN rapid successive calls const authToken1 = 'token1'; const authToken2 = 'token2'; const response1 = new IterableAuthResponse(); const response2 = 'success'; MockRNIterableAPI.passAlongAuthToken = jest .fn() .mockResolvedValueOnce(response1) .mockResolvedValueOnce(response2); // WHEN making rapid successive calls const promise1 = Iterable.authManager.passAlongAuthToken(authToken1); const promise2 = Iterable.authManager.passAlongAuthToken(authToken2); const [result1, result2] = await Promise.all([promise1, promise2]); // THEN both calls should work correctly expect(result1).toBe(response1); expect(result2).toBe(response2); expect(MockRNIterableAPI.passAlongAuthToken).toHaveBeenCalledTimes(2); expect(MockRNIterableAPI.passAlongAuthToken).toHaveBeenNthCalledWith( 1, authToken1 ); expect(MockRNIterableAPI.passAlongAuthToken).toHaveBeenNthCalledWith( 2, authToken2 ); }); }); }); });