Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.By
import com.iterable.iterableapi.IterableApi
import com.iterable.iterableapi.IterableEmbeddedMessage
import com.iterable.iterableapi.IterableEmbeddedUpdateHandler
import com.iterable.integration.tests.activities.EmbeddedMessageTestActivity
import com.iterable.integration.tests.utils.maskEmail
import com.iterable.iterableapi.ui.embedded.IterableEmbeddedView
Expand All @@ -25,6 +26,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger

@RunWith(AndroidJUnit4::class)
class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {
Expand All @@ -36,6 +38,20 @@ class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {

private lateinit var uiDevice: UiDevice
private lateinit var mainActivityScenario: ActivityScenario<MainActivity>
private var syncHandlerRegistered = false
private val syncSuccesses = AtomicInteger(0)
private val syncFailures = AtomicInteger(0)
private val syncHandler = object : IterableEmbeddedUpdateHandler {
override fun onMessagesUpdated() {}
override fun onEmbeddedMessagingDisabled() {}
override fun onEmbeddedMessagingSyncSucceeded() {
syncSuccesses.incrementAndGet()
}
override fun onEmbeddedMessagingSyncFailed(reason: String?) {
Log.d(TAG, "Embedded sync failed: $reason")
syncFailures.incrementAndGet()
}
}

@Before
override fun setUp() {
Expand Down Expand Up @@ -70,6 +86,10 @@ class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {

@After
override fun tearDown() {
if (syncHandlerRegistered) {
IterableApi.getInstance().embeddedManager.removeUpdateListener(syncHandler)
syncHandlerRegistered = false
}
super.tearDown()
}

Expand Down Expand Up @@ -138,15 +158,13 @@ class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {
// Drive a clean standard→premium membership transition. Mirrors the iOS BCIT
// embedded test β€” the BCIT campaign's audience predicate is on
// `membershipLevel == "premium"`.
setMembershipLevel("standard")
syncMessagesAndWait()
val standardPlacementIds = waitForMembershipLevel("standard", present = false)
Assert.assertFalse(
"User should not be eligible for placement $TEST_PLACEMENT_ID with membershipLevel=standard",
IterableApi.getInstance().embeddedManager.getPlacementIds().contains(TEST_PLACEMENT_ID)
"User should not be eligible for placement $TEST_PLACEMENT_ID with membershipLevel=standard, but found: $standardPlacementIds",
standardPlacementIds.contains(TEST_PLACEMENT_ID)
)

setMembershipLevel("premium")
val placementIds = syncAndWaitForPlacement(TEST_PLACEMENT_ID, timeoutSeconds = 30)
val placementIds = waitForMembershipLevel("premium", present = true)
Assert.assertTrue(
"Placement ID $TEST_PLACEMENT_ID should exist, but found: $placementIds",
placementIds.contains(TEST_PLACEMENT_ID)
Expand Down Expand Up @@ -255,24 +273,71 @@ class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {
}

private fun setMembershipLevel(level: String) {
Log.d(TAG, "Updating membershipLevel=$level")
IterableApi.getInstance().updateUser(JSONObject().put("membershipLevel", level))
Thread.sleep(3000)
}

private fun syncMessagesAndWait() {
IterableApi.getInstance().embeddedManager.syncMessages()
Thread.sleep(2000)
private fun waitForMembershipLevel(
level: String,
present: Boolean,
timeoutSeconds: Long = 30
): List<Long> {
val deadline = System.currentTimeMillis() + timeoutSeconds * 1000
var lastIds = IterableApi.getInstance().embeddedManager.getPlacementIds()
var updateSent = false
var consecutiveMatches = 0
while (System.currentTimeMillis() < deadline) {
val synced = awaitSuccessfulSync()
if (synced == null) {
consecutiveMatches = 0
Thread.sleep(1000)
continue
}
lastIds = synced
Log.d(TAG, "Placement IDs while waiting for membershipLevel=$level present=$present: $lastIds")
val matches = lastIds.contains(TEST_PLACEMENT_ID) == present
if (!matches && !updateSent) {
setMembershipLevel(level)
Comment on lines +299 to +300

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setMembershipLevel(level) only runs after a successful sync disagrees with present; if the standard placement is already absent, the helper returns after two matching syncs without ever setting membershipLevel=standard.
This can pass while testing an unset or stale profile.

The fix would be to establish the requested membership before accepting the placement state.
Suggest sending the update unconditionally, or forcing an observable transition, before polling for its resulting placement state.

updateSent = true
consecutiveMatches = 0
Thread.sleep(1000)
continue
}
if (matches) {
consecutiveMatches += 1
if (consecutiveMatches >= 2) {
return lastIds
}
} else {
consecutiveMatches = 0
}
Thread.sleep(1000)
}
return lastIds
}

private fun syncAndWaitForPlacement(placementId: Long, timeoutSeconds: Long = 30): List<Long> {
val deadline = System.currentTimeMillis() + timeoutSeconds * 1000
var placementIds = IterableApi.getInstance().embeddedManager.getPlacementIds()
while (System.currentTimeMillis() < deadline && !placementIds.contains(placementId)) {
IterableApi.getInstance().embeddedManager.syncMessages()
Thread.sleep(2000)
placementIds = IterableApi.getInstance().embeddedManager.getPlacementIds()
// removeUpdateListener() ends the embedded session, so the handler stays on until tearDown.
// A failed sync does not update placement ids, so only a successful sync counts.
private fun awaitSuccessfulSync(): List<Long>? {
if (!syncHandlerRegistered) {
IterableApi.getInstance().embeddedManager.addUpdateListener(syncHandler)
syncHandlerRegistered = true
}
val successesBefore = syncSuccesses.get()
val failuresBefore = syncFailures.get()
IterableApi.getInstance().embeddedManager.syncMessages()
val deadline = System.currentTimeMillis() + 10_000
while (System.currentTimeMillis() < deadline) {
if (syncSuccesses.get() > successesBefore) {
return IterableApi.getInstance().embeddedManager.getPlacementIds()
}
if (syncFailures.get() > failuresBefore) {
return null
}
Thread.sleep(100)
}
return placementIds
Log.d(TAG, "Embedded sync did not finish within 10s")
return null
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.junit.runner.RunWith;

import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

Expand All @@ -28,6 +29,7 @@
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertThat;

@RunWith(AndroidJUnit4.class)
Expand Down Expand Up @@ -130,21 +132,52 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {

@Test
public void testResponseCode400WithoutMessage() throws Exception {
final CountDownLatch signal = new CountDownLatch(1);

stubAnyRequestReturningStatusCode(400, "{}");
String lastReason = null;
boolean serverReceivedRequest = false;
for (int attempt = 1; attempt <= 3; attempt++) {
final CountDownLatch signal = new CountDownLatch(1);
final String[] reason = new String[1];

stubAnyRequestReturningStatusCode(400, "{}");

IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, new IterableHelper.FailureHandler() {
@Override
public void onFailure(@NonNull String failureReason, @Nullable JSONObject data) {
reason[0] = failureReason;
signal.countDown();
}
});
new IterableRequestTask().execute(request);

RecordedRequest recorded = server.takeRequest(5, TimeUnit.SECONDS);
boolean callbackFinished = signal.await(5, TimeUnit.SECONDS);
lastReason = reason[0];
if (recorded != null) {
serverReceivedRequest = true;
}

IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, new IterableHelper.FailureHandler() {
@Override
public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
assertEquals("Invalid Request", reason);
signal.countDown();
if (callbackFinished && "Invalid Request".equals(lastReason)) {
return;
}
});
new IterableRequestTask().execute(request);
if (recorded != null && callbackFinished && !isEmulatorTransportTimeout(lastReason)) {
assertEquals("Invalid Request", lastReason);
}
Comment on lines +162 to +164

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest failing any completed callback with an unrecognized non-transport reason even when takeRequest() timed out.

The request can reach MockWebServer after that five-second wait, so recorded == null can suppress an unexpected SDK result and a later attempt can make the test pass, masking the product assertion.

}

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(5, TimeUnit.SECONDS));
if (isEmulatorTransportTimeout(lastReason)) {
fail("Emulator transport timeout: stubbed HTTP 400 was not applied before the HTTP timeout. "
+ "MockWebServer received the request: " + serverReceivedRequest
+ ". Last failure reason: " + lastReason);
}
fail("MockWebServer received the request but the SDK did not report Invalid Request. Last failure reason: " + lastReason);
}

private static boolean isEmulatorTransportTimeout(String reason) {
if (reason == null) {
return true;
}
String lower = reason.toLowerCase(Locale.US);
return lower.contains("timeout") || lower.contains("timed out") || lower.contains("failed to connect");
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;

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;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;

Expand All @@ -51,7 +53,6 @@ public class IterableNotificationTest {
String itbl_ghost = "{\"templateId\":1,\"campaignId\":1,\"messageId\":\"11111111111111111111111111111111\",\"isGhostPush\":true}";
String itbl1 = "{\"templateId\":1,\"campaignId\":1,\"messageId\":\"11111111111111111111111111111111\",\"isGhostPush\":false}";
String itbl2 = "{\"templateId\":2,\"campaignId\":2,\"messageId\":\"22222222222222222222222222222222\",\"isGhostPush\":false}}";
String itbl_image = "{\"templateId\":1,\"campaignId\":1,\"messageId\":\"11111111111111111111111111111111\",\"isGhostPush\":false,\"attachment-url\":\"https://assets.iterable.com/assets/images/logos/itbl-logo-full-gray-800x300.png\"}";

private Context getContext() {
return getApplicationContext();
Expand Down Expand Up @@ -108,13 +109,19 @@ public void testGhostPush() throws Exception {
}

/**
* Tests loading a notification with an image.
* Loads the notification image from a test resource so the assertion does not hit the network.
* @throws Exception
*/
@Test
public void testNotificationImage() throws Exception {
URL imageFixture = getClass().getClassLoader().getResource("notification_image.png");
assertNotNull("notification_image.png fixture missing from test resources", imageFixture);

String itblImage = "{\"templateId\":1,\"campaignId\":1,\"messageId\":\"11111111111111111111111111111111\",\"isGhostPush\":false,\"attachment-url\":\""
+ imageFixture.toExternalForm() + "\"}";

Bundle notif = new Bundle();
notif.putString(IterableConstants.ITERABLE_DATA_KEY, itbl_image);
notif.putString(IterableConstants.ITERABLE_DATA_KEY, itblImage);
notif.putString(IterableConstants.ITERABLE_DATA_BODY, body);

IterableNotificationBuilder iterableNotification = postNotification(notif);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading