diff --git a/parsely/build.gradle b/parsely/build.gradle index e6ed109f..b0767cf6 100644 --- a/parsely/build.gradle +++ b/parsely/build.gradle @@ -57,6 +57,7 @@ dependencies { testImplementation 'androidx.test:core:1.5.0' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'junit:junit:4.13.2' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' } apply from: "${rootProject.projectDir}/publication.gradle" diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.java b/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.java index beb225f3..992fb973 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.java +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.java @@ -18,16 +18,24 @@ import android.os.AsyncTask; +import androidx.annotation.NonNull; + import java.io.OutputStream; import java.net.URL; import java.net.HttpURLConnection; -public class ParselyAPIConnection extends AsyncTask { +class ParselyAPIConnection extends AsyncTask { + + @NonNull + private final ParselyTracker tracker; + private Exception exception; - public Exception exception; + ParselyAPIConnection(@NonNull ParselyTracker tracker) { + this.tracker = tracker; + } @Override - protected HttpURLConnection doInBackground(String... data) { + protected Void doInBackground(String... data) { HttpURLConnection connection = null; try { if (data.length == 1) { // non-batched (since no post data is included) @@ -46,35 +54,23 @@ protected HttpURLConnection doInBackground(String... data) { } catch (Exception ex) { this.exception = ex; - return null; } - return connection; + return null; } - protected void onPostExecute(HttpURLConnection conn) { + @Override + protected void onPostExecute(Void result) { if (this.exception != null) { ParselyTracker.PLog("Pixel request exception"); ParselyTracker.PLog(this.exception.toString()); } else { ParselyTracker.PLog("Pixel request success"); - ParselyTracker instance = null; - try { - instance = ParselyTracker.sharedInstance(); - } catch (NullPointerException ex) { - ParselyTracker.PLog("ParselyTracker is null"); - } - - if (instance != null) { - // only purge the queue if the request was successful - instance.eventQueue.clear(); - instance.purgeStoredQueue(); + // only purge the queue if the request was successful + tracker.purgeEventsQueue(); - if (instance.queueSize() == 0 && instance.storedEventsCount() == 0) { - ParselyTracker.PLog("Event queue empty, flush timer cleared."); - instance.stopFlushTimer(); - } - } + ParselyTracker.PLog("Event queue empty, flush timer cleared."); + tracker.stopFlushTimer(); } } } diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java index 5887a77e..47e848c2 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java @@ -455,10 +455,9 @@ private void sendBatchRequest(ArrayList> events) { if (isDebug) { PLog("Debug mode on. Not sending to Parse.ly"); - eventQueue.clear(); - purgeStoredQueue(); + purgeEventsQueue(); } else { - new ParselyAPIConnection().execute(ROOT_URL + "mobileproxy", JsonEncode(batchMap)); + new ParselyAPIConnection(this).execute(ROOT_URL + "mobileproxy", JsonEncode(batchMap)); PLog("Requested %s", ROOT_URL); } PLog("POST Data %s", JsonEncode(batchMap)); @@ -518,10 +517,15 @@ private ArrayList> getStoredQueue() { return storedQueue; } + void purgeEventsQueue() { + eventQueue.clear(); + purgeStoredQueue(); + } + /** * Delete the stored queue from persistent storage. */ - protected void purgeStoredQueue() { + private void purgeStoredQueue() { persistObject(new ArrayList>()); } diff --git a/parsely/src/test/java/com/parsely/parselyandroid/ParselyAPIConnectionTest.kt b/parsely/src/test/java/com/parsely/parselyandroid/ParselyAPIConnectionTest.kt new file mode 100644 index 00000000..815abc93 --- /dev/null +++ b/parsely/src/test/java/com/parsely/parselyandroid/ParselyAPIConnectionTest.kt @@ -0,0 +1,119 @@ +package com.parsely.parselyandroid + +import androidx.test.core.app.ApplicationProvider +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.LooperMode +import org.robolectric.shadows.ShadowLooper.shadowMainLooper + +@RunWith(RobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ParselyAPIConnectionTest { + + private lateinit var sut: ParselyAPIConnection + private val mockServer = MockWebServer() + private val url = mockServer.url("").toString() + private val tracker = FakeTracker() + + @Before + fun setUp() { + sut = ParselyAPIConnection(tracker) + } + + @After + fun tearDown() { + mockServer.shutdown() + } + + @Test + fun `given successful response, when making connection without any events, then make GET request`() { + // given + mockServer.enqueue(MockResponse().setResponseCode(200)) + + // when + sut.execute(url).get() + shadowMainLooper().idle(); + + // then + val request = mockServer.takeRequest() + assertThat(request).satisfies({ + assertThat(it.method).isEqualTo("GET") + assertThat(it.failure).isNull() + }) + } + + @Test + fun `given successful response, when making connection with events, then make POST request with JSON Content-Type header`() { + // given + mockServer.enqueue(MockResponse().setResponseCode(200)) + + // when + sut.execute(url, pixelPayload).get() + shadowMainLooper().idle(); + + // then + assertThat(mockServer.takeRequest()).satisfies({ + assertThat(it.method).isEqualTo("POST") + assertThat(it.headers["Content-Type"]).isEqualTo("application/json") + assertThat(it.body.readUtf8()).isEqualTo(pixelPayload) + }) + } + + @Test + fun `given successful response, when request is made, then purge events queue and stop flush timer`() { + // given + mockServer.enqueue(MockResponse().setResponseCode(200)) + tracker.events.add(mapOf("idsite" to "example.com")) + + // when + sut.execute(url).get() + shadowMainLooper().idle(); + + // then + assertThat(tracker.events).isEmpty() + assertThat(tracker.flushTimerStopped).isTrue + } + + @Test + fun `given unsuccessful response, when request is made, then do not purge events queue and do not stop flush timer`() { + // given + mockServer.enqueue(MockResponse().setResponseCode(400)) + val sampleEvents = mapOf("idsite" to "example.com") + tracker.events.add(sampleEvents) + + // when + sut.execute(url).get() + shadowMainLooper().idle(); + + // then + assertThat(tracker.events).containsExactly(sampleEvents) + assertThat(tracker.flushTimerStopped).isFalse + } + + companion object { + val pixelPayload: String = + this::class.java.getResource("pixel_payload.json")?.readText().orEmpty() + } + + private class FakeTracker : ParselyTracker( + "siteId", 10, ApplicationProvider.getApplicationContext() + ) { + + var flushTimerStopped = false + val events = mutableListOf>() + + override fun purgeEventsQueue() { + events.clear() + } + + override fun stopFlushTimer() { + flushTimerStopped = true + } + } +} diff --git a/parsely/src/test/resources/pixel_payload.json b/parsely/src/test/resources/pixel_payload.json new file mode 100644 index 00000000..803b4930 --- /dev/null +++ b/parsely/src/test/resources/pixel_payload.json @@ -0,0 +1,10 @@ +{ + "events": [ + { + "idsite": "example.com" + }, + { + "idsite": "example2.com" + } + ] +}