diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableBackgroundInitializer.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableBackgroundInitializer.java index dbfb432d4..20e91f944 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableBackgroundInitializer.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableBackgroundInitializer.java @@ -338,24 +338,25 @@ static void shutdownBackgroundExecutor() { * Used internally after initialization completes */ private static void shutdownBackgroundExecutorAsync() { - // Schedule shutdown on a separate thread to avoid blocking the executor thread + // Capture the current executor reference so the shutdown thread only shuts down + // THIS executor, not a replacement created by resetBackgroundInitializationState(). + final ExecutorService executorToShutdown = backgroundExecutor; + if (executorToShutdown == null || executorToShutdown.isShutdown()) { + return; + } new Thread(() -> { - synchronized (initLock) { - if (backgroundExecutor != null && !backgroundExecutor.isShutdown()) { - backgroundExecutor.shutdown(); - try { - if (!backgroundExecutor.awaitTermination(5, TimeUnit.SECONDS)) { - IterableLogger.w(TAG, "Background executor did not terminate gracefully, forcing shutdown"); - backgroundExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - IterableLogger.w(TAG, "Interrupted while waiting for executor termination"); - backgroundExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - IterableLogger.d(TAG, "Background executor shutdown completed"); + try { + executorToShutdown.shutdown(); + if (!executorToShutdown.awaitTermination(5, TimeUnit.SECONDS)) { + IterableLogger.w(TAG, "Background executor did not terminate gracefully, forcing shutdown"); + executorToShutdown.shutdownNow(); } + } catch (InterruptedException e) { + IterableLogger.w(TAG, "Interrupted while waiting for executor termination"); + executorToShutdown.shutdownNow(); + Thread.currentThread().interrupt(); } + IterableLogger.d(TAG, "Background executor shutdown completed"); }, "IterableExecutorShutdown").start(); } @@ -413,10 +414,13 @@ static void resetBackgroundInitializationState() { pendingCallbacks.clear(); callbackManager.reset(); - // Recreate executor if it was shut down - if (backgroundExecutor == null || backgroundExecutor.isShutdown()) { - backgroundExecutor = createExecutor(); + // Always create a fresh executor. The old one may have a pending + // shutdownBackgroundExecutorAsync that hasn't run yet — if we kept it, + // the async shutdown would kill the executor under the next test. + if (backgroundExecutor != null && !backgroundExecutor.isShutdown()) { + backgroundExecutor.shutdownNow(); } + backgroundExecutor = createExecutor(); } } diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java index 12d1b473b..ebc879508 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java @@ -67,7 +67,7 @@ private void reInitIterableApi() { authHandler = mock(IterableAuthHandler.class); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testRefreshToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -95,7 +95,7 @@ public void testRefreshToken() throws Exception { timer = IterableApi.getInstance().getAuthManager().timer; } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSetEmailWithToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -119,7 +119,7 @@ public void testSetEmailWithToken() throws Exception { shadowOf(getMainLooper()).runToEndOfTasks(); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSetEmailWithTokenExpired() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -133,7 +133,7 @@ public void testSetEmailWithTokenExpired() throws Exception { assertEquals(IterableApi.getInstance().getAuthToken(), expiredJWT); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSetUserIdWithToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -157,7 +157,7 @@ public void testSetUserIdWithToken() throws Exception { assertEquals(expiredJWT, IterableApi.getInstance().getAuthToken()); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSameEmailWithNewToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -181,7 +181,7 @@ public void testSameEmailWithNewToken() throws Exception { assertEquals(IterableApi.getInstance().getAuthToken(), newJWT); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSameUserIdWithNewToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -200,7 +200,7 @@ public void testSameUserIdWithNewToken() throws Exception { assertEquals(IterableApi.getInstance().getAuthToken(), newJWT); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSetSameEmailAndRemoveToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -219,7 +219,7 @@ public void testSetSameEmailAndRemoveToken() throws Exception { assertNull(IterableApi.getInstance().getAuthToken()); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSetSameUserIdAndRemoveToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -277,7 +277,7 @@ public void testSetSameUserId() throws Exception { assertNull(IterableApi.getInstance().getAuthToken()); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSetSameEmailWithSameToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -297,7 +297,7 @@ public void testSetSameEmailWithSameToken() throws Exception { assertEquals(IterableApi.getInstance().getAuthToken(), token); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testSetSameUserIdWithSameToken() throws Exception { IterableApi.initialize(getContext(), "apiKey"); @@ -352,7 +352,7 @@ public void testUserIdLogOut() throws Exception { assertNull(IterableApi.getInstance().getAuthToken()); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testAuthTokenPresentInRequest() throws Exception { // server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); @@ -392,7 +392,7 @@ public void testAuthTokenPresentInRequest() throws Exception { assertEquals(HEADER_SDK_AUTH_FORMAT + newJWT, getMessagesSet2Request.getHeader("Authorization")); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testAuthFailureReturns401() throws InterruptedException { doReturn(expiredJWT).when(authHandler).onAuthTokenRequested(); @@ -418,7 +418,7 @@ public void testAuthFailureReturns401() throws InterruptedException { assertEquals(IterableApi.getInstance().getAuthToken(), expiredJWT); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testAuthRequestedOnSetEmail() throws InterruptedException { doReturn(expiredJWT).when(authHandler).onAuthTokenRequested(); @@ -433,7 +433,7 @@ public void testAuthRequestedOnSetEmail() throws InterruptedException { } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testAuthRequestedOnUpdateEmail() throws InterruptedException { doReturn(expiredJWT).when(authHandler).onAuthTokenRequested(); @@ -447,7 +447,7 @@ public void testAuthRequestedOnUpdateEmail() throws InterruptedException { //TODO: Shouldn't the update call also update the authToken in IterableAPI class? } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testAuthRequestedOnSetUserId() throws InterruptedException { doReturn(expiredJWT).when(authHandler).onAuthTokenRequested(); @@ -456,7 +456,7 @@ public void testAuthRequestedOnSetUserId() throws InterruptedException { assertEquals(IterableApi.getInstance().getAuthToken(), expiredJWT); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testAuthSetToNullOnLogOut() throws InterruptedException { doReturn(expiredJWT).when(authHandler).onAuthTokenRequested(); @@ -469,7 +469,7 @@ public void testAuthSetToNullOnLogOut() throws InterruptedException { assertNull(IterableApi.getInstance().getAuthToken()); } - @Ignore ("Ignoring the JWT Tests") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testRegisterForPushInvokedAfterTokenRefresh() throws InterruptedException { doReturn(expiredJWT).when(authHandler).onAuthTokenRequested(); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiMergeUserEmailTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiMergeUserEmailTests.java index 8c37c6bdf..fba70269f 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiMergeUserEmailTests.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiMergeUserEmailTests.java @@ -181,6 +181,21 @@ private void addResponse(String endPoint) { dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}")); } + /** + * Takes the next request matching the expected endpoint, skipping any spurious + * in-app sync requests caused by cross-test state leakage. + */ + private RecordedRequest takeRequestWithPath(String expectedEndpoint) throws InterruptedException { + String expectedPath = "/" + expectedEndpoint; + RecordedRequest request; + do { + request = server.takeRequest(1, TimeUnit.SECONDS); + if (request == null) return null; + } while (request.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES) + && !expectedPath.startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES)); + return request; + } + // all userId tests @Test public void testCriteriaNotMetUserIdDefault() throws Exception { @@ -844,18 +859,18 @@ public void testCriteriaMetEmailMergeTrue() throws Exception { triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3); shadowOf(getMainLooper()).idle(); - // check if request was sent to unknown user session endpoint - RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS); + // check if request was sent to unknown user session endpoint (skip any spurious in-app syncs) + RecordedRequest unknownSessionRequest = takeRequestWithPath(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION); assertNotNull("Unknown user session request should not be null", unknownSessionRequest); assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath()); // check if request was sent to track purchase endpoint - RecordedRequest purchaseRequest = server.takeRequest(1, TimeUnit.SECONDS); + RecordedRequest purchaseRequest = takeRequestWithPath(IterableConstants.ENDPOINT_TRACK_PURCHASE); assertNotNull("Purchase request should not be null", purchaseRequest); assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, purchaseRequest.getPath()); // check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin) - RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS); + RecordedRequest inAppRequest = takeRequestWithPath(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES); assertNotNull("InApp messages request should be sent", inAppRequest); assertTrue("InApp messages request path should start with correct endpoint", inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES)); @@ -872,7 +887,7 @@ public void testCriteriaMetEmailMergeTrue() throws Exception { IterableApi.getInstance().setEmail(email, identityResolution); // check if request was sent to merge endpoint - RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS); + RecordedRequest mergeRequest = takeRequestWithPath(IterableConstants.ENDPOINT_MERGE_USER); assertNotNull(mergeRequest); assertEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath()); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java index bb781b9ad..166f1bcbb 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java @@ -225,7 +225,7 @@ public void testPostRequestHeaders() throws Exception { Assert.assertEquals("fake_key", request.getHeader(IterableConstants.HEADER_API_KEY)); } - @Ignore("Ignoring the JWT related test error") + @Ignore("Blocked: IterableAuthManager.executor is not injectable - auth token requests run on uncontrollable background thread") @Test public void testUpdateEmailRequest() throws Exception { server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java index 8d873993a..04303edd8 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java @@ -238,7 +238,7 @@ public void testUpdateEmailWithUserId() throws Exception { assertEquals("testUserId", IterableApi.getInstance().getUserId()); } - @Ignore + @Ignore("handleAppLink performs real HTTP redirect - needs MockWebServer to stub the redirect endpoint") @Test public void testHandleUniversalLinkRewrite() throws Exception { IterableUrlHandler urlHandlerMock = mock(IterableUrlHandler.class); @@ -262,6 +262,9 @@ public void testHandleUniversalLinkRewrite() throws Exception { @Test public void testSetEmailWithAutomaticPushRegistration() throws Exception { IterableApi.initialize(getContext(), "fake_key", new IterableConfig.Builder().setPushIntegrationName("pushIntegration").setAutoPushRegistration(true).build()); + // Flush any pending looper callbacks from initialize, then reset mock + shadowOf(getMainLooper()).idle(); + Mockito.reset(IterablePushRegistration.instance); // Check that setEmail calls registerForPush IterableApi.getInstance().setEmail("test@email.com"); @@ -290,6 +293,8 @@ public void testSetEmailWithoutAutomaticPushRegistration() throws Exception { @Test public void testSetUserIdWithAutomaticPushRegistration() throws Exception { IterableApi.initialize(getContext(), "fake_key", new IterableConfig.Builder().setPushIntegrationName("pushIntegration").setAutoPushRegistration(true).build()); + // Reset after initialize since it may trigger push registration via background init + Mockito.reset(IterablePushRegistration.instance); // Check that setUserId calls registerForPush IterableApi.getInstance().setUserId("userId"); @@ -423,7 +428,7 @@ public void testInAppResetOnLogout() throws Exception { verify(IterableApi.sharedInstance.getInAppManager(), times(2)).reset(); } - @Ignore("Ignoring this test as it fails on CI for some reason") + @Ignore("Fails on CI: likely IterableTaskStorage singleton state leakage between tests - needs investigation") @Test public void databaseClearOnLogout() throws Exception { IterableTaskStorage taskStorage = IterableTaskStorage.sharedInstance(getContext()); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableAsyncInitializationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableAsyncInitializationTest.java index 15f214ded..64cc3bf0c 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableAsyncInitializationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableAsyncInitializationTest.java @@ -148,7 +148,7 @@ public void onSDKInitialized() { }); assertTrue("Initialization with config should complete", - waitForAsyncInitialization(initLatch, 3)); + waitForAsyncInitialization(initLatch, 5)); } // ======================================== @@ -179,7 +179,7 @@ public void onSDKInitialized() { assertTrue("Operations should be queued", IterableBackgroundInitializer.getQueuedOperationCount() > 0); // Wait for initialization to complete - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); // Process queue ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); @@ -216,7 +216,7 @@ public void onSDKInitialized() { // These SHOULD be queued assertTrue("Operations during init should be queued", IterableBackgroundInitializer.getQueuedOperationCount() > 0); - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); } @Test @@ -241,7 +241,7 @@ public void onSDKInitialized() { numOperations, IterableBackgroundInitializer.getQueuedOperationCount()); // Wait for completion and processing - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); // Process queue ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); @@ -291,7 +291,7 @@ public void onSDKInitialized() { }); - assertTrue("Success callback should be called", waitForAsyncInitialization(successLatch, 3)); + assertTrue("Success callback should be called", waitForAsyncInitialization(successLatch, 5)); assertTrue("Callback should execute on main thread", callbackExecutedOnMainThread.get()); } @@ -326,7 +326,7 @@ public void onSDKInitialized() { ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); assertTrue("Callback should be called despite exception", - waitForAsyncInitialization(callbackLatch, 3)); + waitForAsyncInitialization(callbackLatch, 5)); // System should still be in a valid state assertFalse("Should not be initializing after completion despite callback exception", @@ -366,7 +366,7 @@ public void onSDKInitialized() { startLatch.countDown(); - assertTrue("All threads should complete", waitForAsyncInitialization(completeLatch, 3)); + assertTrue("All threads should complete", waitForAsyncInitialization(completeLatch, 5)); // All threads should get success callbacks (concurrent calls should all be notified when init completes) assertEquals("All threads should get success callbacks", numThreads, successCount.get()); @@ -448,7 +448,7 @@ public void onSDKInitialized() { // During initialization - should not be considered fully initialized yet assertFalse("Should not be fully initialized during background init", IterableApi.isSDKInitialized()); - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); // After initialization completes but before setting user - still not fully initialized assertFalse("Should not be fully initialized without user identification", IterableApi.isSDKInitialized()); @@ -489,7 +489,7 @@ public void onSDKInitialized() { }); - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); assertFalse("Should not be initializing after completion", IterableApi.isSDKInitializing()); } @@ -518,7 +518,7 @@ public void onSDKInitialized() { }); // First should complete - assertTrue("First initialization should complete", waitForAsyncInitialization(firstInitLatch, 3)); + assertTrue("First initialization should complete", waitForAsyncInitialization(firstInitLatch, 5)); // Second should also complete (called immediately since first is done) assertTrue("Second initialization should also complete", waitForAsyncInitialization(secondInitLatch, 5)); @@ -842,7 +842,7 @@ public void onSDKInitialized() { } }); - assertTrue("Success callback should be called even with null context", waitForAsyncInitialization(completionLatch, 3)); + assertTrue("Success callback should be called even with null context", waitForAsyncInitialization(completionLatch, 5)); assertEquals("Queue should remain empty", 0, IterableBackgroundInitializer.getQueuedOperationCount()); } @@ -858,7 +858,7 @@ public void onSDKInitialized() { }); - assertTrue("Should handle empty API key", waitForAsyncInitialization(completionLatch, 3)); + assertTrue("Should handle empty API key", waitForAsyncInitialization(completionLatch, 5)); } @Test @@ -874,7 +874,7 @@ public void onSDKInitialized() { }); - assertTrue("Should handle very long API key", waitForAsyncInitialization(completionLatch, 3)); + assertTrue("Should handle very long API key", waitForAsyncInitialization(completionLatch, 5)); } @Test @@ -892,7 +892,7 @@ public void testOnSDKInitialized_CallbackExecutedOnMainThread() throws Interrupt IterableApi.initialize(context, TEST_API_KEY); // Wait for callback - boolean callbackCalled = waitForAsyncInitialization(callbackLatch, 3); + boolean callbackCalled = waitForAsyncInitialization(callbackLatch, 5); assertTrue("onSDKInitialized callback should be called", callbackCalled); assertTrue("onSDKInitialized callback should be executed on main thread", callbackExecutedOnMainThread.get()); @@ -940,7 +940,7 @@ public void testOnSDKInitialized_MultipleCallbacks() throws InterruptedException IterableApi.initialize(context, TEST_API_KEY); // Wait for all callbacks - boolean allCallbacksCalled = waitForAsyncInitialization(callbackLatch, 3); + boolean allCallbacksCalled = waitForAsyncInitialization(callbackLatch, 5); assertTrue("All onSDKInitialized callbacks should be called", allCallbacksCalled); assertEquals("All callbacks should be executed on main thread", 3, mainThreadCallbackCount.get()); @@ -1002,8 +1002,8 @@ public void testOnSDKInitialized_ExceptionInCallback() throws InterruptedExcepti IterableApi.initialize(context, TEST_API_KEY); // Wait for both callbacks - boolean callback1CalledResult = waitForAsyncInitialization(callback1Latch, 3); - boolean callback2CalledResult = waitForAsyncInitialization(callback2Latch, 3); + boolean callback1CalledResult = waitForAsyncInitialization(callback1Latch, 5); + boolean callback2CalledResult = waitForAsyncInitialization(callback2Latch, 5); assertTrue("First callback should be called even though it throws", callback1CalledResult); assertTrue("Second callback should be called despite first callback throwing", callback2CalledResult); @@ -1036,15 +1036,9 @@ public void onSDKInitialized() { @Test public void testPIIMasking_EmailMasked() throws InterruptedException { - CountDownLatch initLatch = new CountDownLatch(1); - - // Start background initialization - IterableApi.initializeInBackground(context, TEST_API_KEY, new IterableInitializationCallback() { - @Override - public void onSDKInitialized() { - initLatch.countDown(); - } - }); + // Hold initialization in progress so operations queue instead of executing + IterableBackgroundInitializer.simulateInitializingState(); + IterableApi.initialize(context, TEST_API_KEY); // Use sensitive PII data that should be masked String sensitiveEmail = "sensitive.user@company.com"; @@ -1085,8 +1079,9 @@ public void onSDKInitialized() { } } - // Wait for initialization to complete - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + // No need to call simulateInitializationComplete() here — tearDown resets state. + // Calling it would trigger processAll() -> shutdownBackgroundExecutorAsync() which + // can race with the next test's executor setup. } @Test @@ -1130,21 +1125,15 @@ public void testPIIMasking_SingleCharacterHandled() { @Test public void testPIIMasking_AuthTokenMasked() throws InterruptedException { - CountDownLatch initLatch = new CountDownLatch(1); - - // Start background initialization - IterableApi.initializeInBackground(context, TEST_API_KEY, new IterableInitializationCallback() { - @Override - public void onSDKInitialized() { - initLatch.countDown(); - } - }); + // Hold initialization in progress so setEmail/setUserId queue instead of execute + IterableBackgroundInitializer.simulateInitializingState(); + IterableApi.initialize(context, TEST_API_KEY); // Use sensitive auth token String sensitiveEmail = "test@example.com"; String sensitiveAuthToken = "SecretAuthToken12345"; - // Make API calls with auth tokens + // Make API calls with auth tokens — these should queue since init is "in progress" IterableApi.getInstance().setEmail(sensitiveEmail, sensitiveAuthToken); IterableApi.getInstance().setUserId("testuser", sensitiveAuthToken); @@ -1165,21 +1154,14 @@ public void onSDKInitialized() { } } - // Wait for initialization - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + // No need to call simulateInitializationComplete() — tearDown resets state. } @Test public void testPIIMasking_VerifyExactFormat() throws InterruptedException { - CountDownLatch initLatch = new CountDownLatch(1); - - // Start background initialization - IterableApi.initializeInBackground(context, TEST_API_KEY, new IterableInitializationCallback() { - @Override - public void onSDKInitialized() { - initLatch.countDown(); - } - }); + // Hold initialization in progress so operations queue instead of executing + IterableBackgroundInitializer.simulateInitializingState(); + IterableApi.initialize(context, TEST_API_KEY); // Test various PII formats String email1 = "john.doe@example.com"; // Should mask to "j***" @@ -1225,8 +1207,7 @@ public void onSDKInitialized() { assertTrue("UserId 'user_123_abc' should be masked to 'u***'", foundUserId1Masked); assertTrue("Email 'a@b.com' should be masked to 'a***'", foundEmail2Masked); - // Wait for initialization - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + // No need to call simulateInitializationComplete() — tearDown resets state. } // ======================================== @@ -1261,7 +1242,7 @@ public void onSDKInitialized() { assertTrue("Should have queued operations during init", queuedOps > 0); // Wait for initialization to complete - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); // After init, queue should be processed and inner method should have been called Thread.sleep(200); @@ -1290,7 +1271,7 @@ public void onSDKInitialized() { } }); - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); // Now call overloaded methods after initialization // setEmail(email) internally calls setEmail(email, null, null, null, null) @@ -1362,7 +1343,7 @@ public void onSDKInitialized() { assertEquals("Should only queue outer operation, not nested calls", 1, queuedOps); // Wait for initialization - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); // Wait for queue processing Thread.sleep(200); @@ -1425,7 +1406,7 @@ public void onSDKInitialized() { assertEquals("Full overload should not be called during queuing", 0, fullOverloadCallCount.get()); // Wait for initialization - assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Initialization should complete", waitForAsyncInitialization(initLatch, 5)); // Wait for queue to process Thread.sleep(300); @@ -1477,7 +1458,7 @@ public void onSDKInitialized() { assertTrue("Operations should be queued during background init", IterableBackgroundInitializer.getQueuedOperationCount() > 0); // Wait for init to complete - assertTrue("Background init should complete", waitForAsyncInitialization(initLatch, 3)); + assertTrue("Background init should complete", waitForAsyncInitialization(initLatch, 5)); // After init completes, new operations should execute immediately Thread.sleep(200); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerSyncTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerSyncTest.java index 3dd8a191c..6d03f73cf 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerSyncTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerSyncTest.java @@ -77,6 +77,8 @@ public void testSyncOnLogin() throws Exception { IterableInAppManager inAppManagerMock = mock(IterableInAppManager.class); IterableApi.sharedInstance = new IterableApi(inAppManagerMock); IterableApi.initialize(getApplicationContext(), "apiKey"); + // Reset after initialize since it may also trigger syncInApp via background init + reset(inAppManagerMock); IterableApi.getInstance().setEmail("test@email.com"); verify(inAppManagerMock).syncInApp(); } diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerTest.java index 7dcabe729..2a5902978 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppManagerTest.java @@ -27,6 +27,7 @@ import org.robolectric.shadows.ShadowDialog; import java.io.IOException; +import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -44,7 +45,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; @@ -61,7 +61,7 @@ public class IterableInAppManagerTest extends BaseTest { private PausedExecutorService backgroundExecutor; @Before - public void setUp() throws IOException { + public void setUp() throws Exception { backgroundExecutor = new PausedExecutorService(); server = new MockWebServer(); dispatcher = new PathBasedQueueDispatcher(); @@ -81,6 +81,10 @@ public IterableConfig.Builder run(IterableConfig.Builder builder) { .setUrlHandler(urlHandler); } }); + // Drain init sync HTTP requests from MockWebServer (constructor sync + setEmail sync) + // and flush their callbacks, so they don't consume responses enqueued by tests + while (server.takeRequest(200, TimeUnit.MILLISECONDS) != null) { } + shadowOf(getMainLooper()).idle(); IterableInAppFragmentHTMLNotification.notification = null; } @@ -90,7 +94,7 @@ public void tearDown() throws IOException { server = null; } - @Ignore("Ignoring due to stalling") + @Ignore("Stalls under Robolectric: showIterableFragmentNotificationHTML requires real Activity lifecycle - candidate for androidTest with Espresso") @Test public void testDoNotShowMultipleTimes() throws Exception { ActivityController controller = Robolectric.buildActivity(FragmentActivity.class).create().start().resume(); @@ -104,7 +108,7 @@ public void testDoNotShowMultipleTimes() throws Exception { controller.pause().stop().destroy(); } - @Ignore("Ignoring due to stalling") + @Ignore("Stalls under Robolectric: showIterableFragmentNotificationHTML requires real Activity lifecycle - candidate for androidTest with Espresso") @Test public void testIfDialogDoesNotDestroysAfterConfigurationChange() throws Exception { ActivityController controller = Robolectric.buildActivity(FragmentActivity.class).create().start().resume(); @@ -118,7 +122,7 @@ public void testIfDialogDoesNotDestroysAfterConfigurationChange() throws Excepti controller.pause().stop().destroy(); } - @Ignore("Ignoring due to stalling") + @Ignore("Stalls under Robolectric: showIterableFragmentNotificationHTML requires real Activity lifecycle - candidate for androidTest with Espresso") @Test public void testIfDialogFragmentExistAfterRotation() throws Exception { ActivityController controller = Robolectric.buildActivity(FragmentActivity.class).create().start().resume(); @@ -234,6 +238,7 @@ public void testListenerCalledOnMainThread() throws Exception { JSONObject payload = new JSONObject(IterableTestUtils.getResourceString("inapp_payload_single.json")); dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(payload.toString())); final IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.syncInApp(); shadowOf(getMainLooper()).idle(); IterableInAppManager.Listener listener = mock(IterableInAppManager.Listener.class); @@ -256,7 +261,7 @@ public void run() { backgroundExecutor.runAll(); shadowOf(getMainLooper()).idle(); - verify(listener, timeout(100)).onInboxUpdated(); + verify(listener).onInboxUpdated(); } @Test @@ -278,9 +283,12 @@ public IterableConfig.Builder run(IterableConfig.Builder builder) { }); doReturn(true).when(urlHandler).handleIterableURL(any(Uri.class), any(IterableActionContext.class)); + // Flush init sync callbacks so messages are loaded before foreground transition + shadowOf(getMainLooper()).idle(); + // Bring the app into foreground to trigger in-app display Robolectric.buildActivity(Activity.class).create().start().resume(); - Robolectric.flushForegroundThreadScheduler(); + shadowOf(getMainLooper()).idle(); ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(IterableHelper.IterableUrlCallback.class); verify(inAppDisplayerMock).showMessage(any(IterableInAppMessage.class), eq(IterableInAppLocation.IN_APP), callbackCaptor.capture()); IterableInAppMessage message = inAppManager.getMessages().get(0); @@ -342,9 +350,12 @@ public IterableConfig.Builder run(IterableConfig.Builder builder) { }); doReturn(true).when(urlHandler).handleIterableURL(any(Uri.class), any(IterableActionContext.class)); + // Flush init sync callbacks so messages are loaded before foreground transition + shadowOf(getMainLooper()).idle(); + // Bring the app into foreground Robolectric.buildActivity(Activity.class).create().start().resume(); - Robolectric.flushForegroundThreadScheduler(); + shadowOf(getMainLooper()).idle(); IterableInAppMessage message = inAppManager.getMessages().get(0); // Verify that message is not consumed by default if consume = false and iterable://dismiss is clicked @@ -379,7 +390,7 @@ public void testInAppAutoDisplayPause() throws Exception { inAppManager.setAutoDisplayPaused(true); ActivityController activityController = Robolectric.buildActivity(Activity.class).create().start().resume(); - Robolectric.flushForegroundThreadScheduler(); + shadowOf(getMainLooper()).idle(); ArgumentCaptor inAppMessageCaptor = ArgumentCaptor.forClass(IterableInAppMessage.class); verify(inAppHandler, times(0)).onNewInApp(inAppMessageCaptor.capture()); @@ -389,6 +400,7 @@ public void testInAppAutoDisplayPause() throws Exception { @Test public void testMessagePersistentReadStateFromServer() throws Exception { + // load the in-app that has not been synchronized with the server yet (read state is set to false) dispatcher.enqueueResponse("/inApp/getMessages", new MockResponse().setBody(IterableTestUtils.getResourceString("inapp_payload_inbox_read_state_1.json"))); IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); @@ -591,8 +603,7 @@ public void testJsonOnlyInAppMessageDelegateCallbacks() throws Exception { mock(IterableInAppDisplayer.class))); IterableApi.sharedInstance = new IterableApi(inAppManager); - // First sync to get messages - inAppManager.syncInApp(); + // Flush constructor sync callback so messages are loaded shadowOf(getMainLooper()).idle(); // Process messages by bringing app to foreground diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java index a66d6002d..817547693 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.io.InputStreamReader; +import static android.os.Looper.getMainLooper; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -50,11 +51,11 @@ private Context getContext() { return getApplicationContext(); } - private IterableNotificationBuilder postNotification(Bundle notificationData) throws InterruptedException { + private IterableNotificationBuilder postNotification(Bundle notificationData) { getContext().getApplicationInfo().icon = android.R.drawable.sym_def_app_icon; IterableNotificationBuilder iterableNotification = IterableNotificationHelper.createNotification(getContext(), notificationData); IterableNotificationHelper.postNotificationOnDevice(appContext, iterableNotification); - Thread.sleep(1000); + shadowOf(getMainLooper()).idle(); return iterableNotification; } diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterablePushActionReceiverTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterablePushActionReceiverTest.java index b738cf69a..02f914164 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterablePushActionReceiverTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterablePushActionReceiverTest.java @@ -90,9 +90,13 @@ public void testTrackPushOpenWithCustomAction() throws Exception { assertNotNull(activityIntent); assertEquals(Intent.ACTION_MAIN, activityIntent.getAction()); - // Verify trackPushOpen HTTP request - RecordedRequest recordedRequest = server.takeRequest(1, TimeUnit.SECONDS); - assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PUSH_OPEN, recordedRequest.getPath()); + // Verify trackPushOpen HTTP request (skip any in-app sync requests from cross-test state) + String expectedPath = "/" + IterableConstants.ENDPOINT_TRACK_PUSH_OPEN; + RecordedRequest recordedRequest; + do { + recordedRequest = server.takeRequest(1, TimeUnit.SECONDS); + assertNotNull("Expected trackPushOpen request but no more requests arrived", recordedRequest); + } while (!recordedRequest.getPath().startsWith(expectedPath)); JSONObject jsonBody = new JSONObject(recordedRequest.getBody().readUtf8()); assertEquals(1234, jsonBody.getInt(IterableConstants.KEY_CAMPAIGN_ID)); assertEquals(4321, jsonBody.getInt(IterableConstants.KEY_TEMPLATE_ID)); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterablePushRegistrationTaskTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterablePushRegistrationTaskTest.java index 214b667ae..194ca8546 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterablePushRegistrationTaskTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterablePushRegistrationTaskTest.java @@ -19,7 +19,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; @@ -73,7 +72,8 @@ public void testEnableDevice() throws Exception { new IterablePushRegistrationTask().execute(data); deviceAttributes.put(DEVICE_ATTRIBUTES_KEY, DEVICE_ATTRIBUTES_VALUE); - verify(apiMock, timeout(100)).registerDeviceToken(eq(IterableTestUtils.userEmail), nullable(String.class), isNull(), eq(INTEGRATION_NAME), eq(TEST_TOKEN), eq(deviceAttributes)); + shadowOf(getMainLooper()).idle(); + verify(apiMock).registerDeviceToken(eq(IterableTestUtils.userEmail), nullable(String.class), isNull(), eq(INTEGRATION_NAME), eq(TEST_TOKEN), eq(deviceAttributes)); verify(apiMock, never()).disableToken(eq(IterableTestUtils.userEmail), nullable(String.class), nullable(String.class), any(String.class), nullable(IterableHelper.SuccessHandler.class), nullable(IterableHelper.FailureHandler.class)); } @@ -87,6 +87,6 @@ public void testDisableDevice() throws Exception { new IterablePushRegistrationTask().execute(data); shadowOf(getMainLooper()).idle(); - verify(apiMock, timeout(100)).disableToken(eq(IterableTestUtils.userEmail), isNull(), isNull(), eq(TEST_TOKEN), nullable(IterableHelper.SuccessHandler.class), nullable(IterableHelper.FailureHandler.class)); + verify(apiMock).disableToken(eq(IterableTestUtils.userEmail), isNull(), isNull(), eq(TEST_TOKEN), nullable(IterableHelper.SuccessHandler.class), nullable(IterableHelper.FailureHandler.class)); } } \ No newline at end of file diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationComplexCriteriaCheckerTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/CombinationComplexCriteriaCheckerTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationComplexCriteriaCheckerTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/CombinationComplexCriteriaCheckerTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationLogicEventTypeCriteriaTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/CombinationLogicEventTypeCriteriaTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationLogicEventTypeCriteriaTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/CombinationLogicEventTypeCriteriaTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/ComplexCriteriaCheckerTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/ComplexCriteriaCheckerTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/ComplexCriteriaCheckerTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/ComplexCriteriaCheckerTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionComparatorTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/CriteriaCompletionComparatorTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionComparatorTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/CriteriaCompletionComparatorTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorArrayInputCriteriaTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/DataTypeComparatorArrayInputCriteriaTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorArrayInputCriteriaTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/DataTypeComparatorArrayInputCriteriaTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorSearchQueryCriteriaTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/DataTypeComparatorSearchQueryCriteriaTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorSearchQueryCriteriaTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/DataTypeComparatorSearchQueryCriteriaTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DoesNotEqualCriteriaMatchTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/DoesNotEqualCriteriaMatchTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DoesNotEqualCriteriaMatchTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/DoesNotEqualCriteriaMatchTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/IsOneOfAndIsNotOneOfCriteriaMatchTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/IsOneOfAndIsNotOneOfCriteriaMatchTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/IsOneOfAndIsNotOneOfCriteriaMatchTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/IsOneOfAndIsNotOneOfCriteriaMatchTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaMatchTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaMatchTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaMatchTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaMatchTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaWithArrayMatchTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaWithArrayMatchTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaWithArrayMatchTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaWithArrayMatchTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/NestedCriteriaMatchTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/NestedCriteriaMatchTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/NestedCriteriaMatchTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/NestedCriteriaMatchTest.java diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/SinglePrimitiveArrayNestedCriteriaMatchTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/util/SinglePrimitiveArrayNestedCriteriaMatchTest.java similarity index 100% rename from iterableapi/src/androidTest/java/com/iterable/iterableapi/util/SinglePrimitiveArrayNestedCriteriaMatchTest.java rename to iterableapi/src/test/java/com/iterable/iterableapi/util/SinglePrimitiveArrayNestedCriteriaMatchTest.java