From 49f637c64ccd6b020c475d8f683759204e761e72 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Wed, 23 Sep 2026 10:48:07 +0100 Subject: [PATCH 1/2] Serve push notification test image from MockWebServer testNotificationImage pointed attachment-url at a live logo on assets.iterable.com. That asset now returns 404, so getInputStream() throws, no BigPictureStyle is attached, and the EXTRA_PICTURE assertion fails on every branch including master. Serve the existing 350x150.png test resource locally instead so the test no longer depends on a CDN. Co-Authored-By: Claude Opus 5 --- .../iterableapi/IterableNotificationTest.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java index a8a65c9f5..a239b50fb 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java @@ -38,11 +38,18 @@ import org.json.JSONArray; import org.json.JSONObject; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import okio.Buffer; + @RunWith(RobolectricTestRunner.class) @Config(sdk = {Build.VERSION_CODES.P}) public class IterableNotificationTest { Context appContext; NotificationManager mNotificationManager; + private MockWebServer imageServer; //EX: itbl_notif = "{\"itbl\":{\"templateId\":28,\"campaignId\":17,\"messageId\":\"85fbb5663ada4f40b5ae0d437e040fa6\",\"isGhostPush\":false},\"body\":\"Pushy\",\"badge\":2,\"sound\":\"me.wav\"}}"; @@ -51,7 +58,7 @@ 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\"}"; + String itbl_image; private Context getContext() { return getApplicationContext(); @@ -80,17 +87,42 @@ private String getResourceString(String fileName) throws IOException { return stringBuilder.toString(); } + private byte[] getResourceBytes(String fileName) throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName); + Buffer buffer = new Buffer(); + buffer.readFrom(inputStream); + inputStream.close(); + return buffer.readByteArray(); + } + @Before public void setUp() throws Exception { appContext = getContext().getApplicationContext(); mNotificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.cancelAll(); + + final byte[] imageBytes = getResourceBytes("350x150.png"); + imageServer = new MockWebServer(); + // Every notification build() re-downloads the attachment, so serve the image on + // every request instead of enqueuing a fixed number of responses. + imageServer.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + return new MockResponse() + .setHeader("Content-Type", "image/png") + .setBody(new Buffer().write(imageBytes)); + } + }); + imageServer.start(); + + itbl_image = "{\"templateId\":1,\"campaignId\":1,\"messageId\":\"11111111111111111111111111111111\",\"isGhostPush\":false,\"attachment-url\":\"" + imageServer.url("/push-image.png") + "\"}"; } @After - public void tearDown() { + public void tearDown() throws Exception { mNotificationManager.cancelAll(); + imageServer.shutdown(); } @Test From 080825af5ddc13c52186d6714f96952ead1d3aaa Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Wed, 23 Sep 2026 13:19:13 +0100 Subject: [PATCH 2/2] Stop IterableApiResponseTest retries leaking across tests IterableRequestTask resolves the endpoint from a static override on every attempt and schedules retries on a static main-looper Handler up to RETRY_DELAY_MS * MAX_RETRY_COUNT ahead. testResponseCode500 asserted that retry #2 happened and returned, leaving retries 3-5 pending; those fired after tearDown and re-resolved to the next test's MockWebServer, consuming the response it had enqueued. The default queue dispatcher blocks on an empty queue, so the victim hung until its latch timed out - usually testResponseCode400WithoutMessage - and a stray response reaching a finished test's assertion in a callback crashed the instrumentation process. Serve responses from a dispatcher that repeats the last scripted response so a stray request is harmless, and end the 500 retry chain with a success so it cannot outlive the test. Co-Authored-By: Claude Opus 5 --- .../iterableapi/IterableApiResponseTest.java | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java index afab7676b..abefab182 100644 --- a/iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java +++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java @@ -15,9 +15,12 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -36,21 +39,45 @@ public class IterableApiResponseTest { private MockWebServer server; + private final Object scriptLock = new Object(); + private final Deque scriptedResponses = new ArrayDeque<>(); + private MockResponse lastServedResponse; + @Before public void setUp() throws IOException { server = new MockWebServer(); - // Explicitly start the server to ensure it's ready - try { - server.start(); - } catch (IllegalStateException e) { - // Server may already be started by url() call below, which is fine - } + // IterableRequestTask resolves the endpoint from a static override on every attempt, + // and schedules retries on a static main-looper Handler up to + // RETRY_DELAY_MS * MAX_RETRY_COUNT in the future. A retry left pending when a test + // finishes therefore re-resolves to the *next* test's server. With the default queue + // dispatcher that stray request consumed the response the next test was waiting for, + // leaving it blocked until its latch timed out. Serving from a dispatcher that repeats + // the last scripted response makes a stray request harmless instead of destructive. + server.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + synchronized (scriptLock) { + MockResponse next = scriptedResponses.poll(); + if (next != null) { + lastServedResponse = next; + } else if (lastServedResponse == null) { + lastServedResponse = new MockResponse().setResponseCode(200).setBody("{}"); + } + return lastServedResponse; + } + } + }); + server.start(); IterableApi.overrideURLEndpointPath(server.url("").toString()); createIterableApi(); } @After public void tearDown() throws IOException { + synchronized (scriptLock) { + scriptedResponses.clear(); + lastServedResponse = null; + } server.shutdown(); server = null; } @@ -67,7 +94,13 @@ private void stubAnyRequestReturningStatusCode(int statusCode, String body) { if (body != null) { response.setBody(body); } - server.enqueue(response); + stubResponse(response); + } + + private void stubResponse(MockResponse response) { + synchronized (scriptLock) { + scriptedResponses.add(response); + } } @Test @@ -274,9 +307,11 @@ public void testMaxRetriesOnMultipleInvalidJwtPayloads() throws Exception { @Test public void testResponseCode500() throws Exception { - for (int i = 0; i < 5; i++) { - stubAnyRequestReturningStatusCode(500, "{}"); - } + // Ends the retry chain with a success so it cannot outlive the test and fire against + // the next test's server. + stubAnyRequestReturningStatusCode(500, "{}"); + stubAnyRequestReturningStatusCode(500, "{}"); + stubAnyRequestReturningStatusCode(200, "{}"); IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, null); IterableRequestTask task = new IterableRequestTask(); @@ -310,8 +345,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) { public void testConnectionError() throws Exception { final CountDownLatch signal = new CountDownLatch(1); - MockResponse response = new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY); - server.enqueue(response); + stubResponse(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)); IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, new IterableHelper.FailureHandler() { @Override