package com.mixpanel.android.mpmetrics; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.test.AndroidTestCase; import com.mixpanel.android.util.Base64Coder; import com.mixpanel.android.util.RemoteService; import com.mixpanel.android.util.HttpService; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLSocketFactory; public class HttpTest extends AndroidTestCase { private Future<SharedPreferences> mMockPreferences; private List<Object> mFlushResults, mDecideResults; private BlockingQueue<String> mPerformRequestCalls, mDecideCalls; private List<String> mCleanupCalls; private MixpanelAPI mMetrics; private volatile boolean mDisableFallback; private volatile int mFlushInterval; private volatile boolean mForceOverMemThreshold; private static final long POLL_WAIT_MAX_MILLISECONDS = 3500; private static final TimeUnit DEFAULT_TIMEUNIT = TimeUnit.MILLISECONDS; private static final String SUCCEED_TEXT = "Should Succeed"; private static final String FAIL_TEXT = "Should Fail"; public void setUp() { mDisableFallback = true; mFlushInterval = 2 * 1000; mMockPreferences = new TestUtils.EmptyPreferences(getContext()); mFlushResults = new ArrayList<Object>(); mPerformRequestCalls = new LinkedBlockingQueue<String>(); mDecideCalls = new LinkedBlockingQueue<String>(); mCleanupCalls = new ArrayList<String>(); mDecideResults = new ArrayList<Object>(); mForceOverMemThreshold = false; final RemoteService mockPoster = new HttpService() { @Override public byte[] performRequest(String endpointUrl, Map<String, Object> params, SSLSocketFactory socketFactory) throws ServiceUnavailableException, IOException { try { if (null == params) { mDecideCalls.put(endpointUrl); if (mDecideResults.isEmpty()) { return TestUtils.bytes("{}"); } final Object obj = mDecideResults.remove(0); if (obj instanceof IOException) { throw (IOException)obj; } else if (obj instanceof MalformedURLException) { throw (MalformedURLException)obj; } else if (obj instanceof ServiceUnavailableException) { throw (ServiceUnavailableException)obj; } return (byte[])obj; } if (mFlushResults.isEmpty()) { mFlushResults.add(TestUtils.bytes("1\n")); } assertTrue(params.containsKey("data")); final Object obj = mFlushResults.remove(0); if (obj instanceof IOException) { throw (IOException)obj; } else if (obj instanceof MalformedURLException) { throw (MalformedURLException)obj; } else if (obj instanceof ServiceUnavailableException) { throw (ServiceUnavailableException)obj; } else if (obj instanceof SocketTimeoutException) { throw (SocketTimeoutException)obj; } final String jsonData = Base64Coder.decodeString(params.get("data").toString()); JSONArray msg = new JSONArray(jsonData); JSONObject event = msg.getJSONObject(0); mPerformRequestCalls.put(event.getString("event")); return (byte[])obj; } catch (JSONException e) { throw new RuntimeException("Malformed data passed to test mock", e); } catch (InterruptedException e) { throw new RuntimeException("Could not write message to reporting queue for tests.", e); } } }; final MPConfig config = new MPConfig(new Bundle(), getContext()) { @Override public String getDecideEndpoint() { return "DECIDE ENDPOINT"; } @Override public String getDecideFallbackEndpoint() { return "DECIDE FALLBACK"; } @Override public String getEventsEndpoint() { return "EVENTS ENDPOINT"; } @Override public String getEventsFallbackEndpoint() { return "EVENTS FALLBACK"; } @Override public boolean getDisableFallback() { return mDisableFallback; } @Override public int getFlushInterval() { return mFlushInterval; } }; final MPDbAdapter mockAdapter = new MPDbAdapter(getContext()) { @Override public void cleanupEvents(String last_id, Table table) { mCleanupCalls.add("called"); super.cleanupEvents(last_id, table); } @Override protected boolean belowMemThreshold() { if (mForceOverMemThreshold) { return false; } else { return super.belowMemThreshold(); } } }; final AnalyticsMessages listener = new AnalyticsMessages(getContext()) { @Override protected MPDbAdapter makeDbAdapter(Context context) { return mockAdapter; } @Override protected RemoteService getPoster() { return mockPoster; } @Override protected MPConfig getConfig(Context context) { return config; } }; mMetrics = new TestUtils.CleanMixpanelAPI(getContext(), mMockPreferences, "Test Message Queuing") { @Override protected AnalyticsMessages getAnalyticsMessages() { return listener; } }; } public void testHTTPFailures() { try { runBasicSucceed(); runIOException(); runMalformedURLException(); runServiceUnavailableException(null); runServiceUnavailableException("10"); runServiceUnavailableException("40"); runDoubleServiceUnavailableException(); runBasicSucceed(); runMemoryTest(); } catch (InterruptedException e) { throw new RuntimeException("Test was interrupted."); } } public void runBasicSucceed() throws InterruptedException { mCleanupCalls.clear(); mMetrics.track(SUCCEED_TEXT, null); Thread.sleep(mFlushInterval + POLL_WAIT_MAX_MILLISECONDS); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(null, mPerformRequestCalls.poll()); assertEquals(1, mCleanupCalls.size()); } public void runIOException() throws InterruptedException { mCleanupCalls.clear(); mFlushResults.add(new IOException()); mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(0, mCleanupCalls.size()); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); waitForBackOffTimeInterval(); assertEquals(1, mCleanupCalls.size()); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(2, mCleanupCalls.size()); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); } public void runMalformedURLException() throws InterruptedException { mCleanupCalls.clear(); mFlushResults.add(new MalformedURLException()); mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(1, mCleanupCalls.size()); mFlushResults.add(new MalformedURLException()); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(2, mCleanupCalls.size()); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(3, mCleanupCalls.size()); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); } private void runServiceUnavailableException(String retryAfterSeconds) throws InterruptedException { mCleanupCalls.clear(); mFlushResults.add(new RemoteService.ServiceUnavailableException("", retryAfterSeconds)); mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(0, mCleanupCalls.size()); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); waitForBackOffTimeInterval(); assertEquals(1, mCleanupCalls.size()); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); mMetrics.track(SUCCEED_TEXT, null); mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(2, mCleanupCalls.size()); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); } private void runDoubleServiceUnavailableException() throws InterruptedException { mCleanupCalls.clear(); mFlushResults.add(new RemoteService.ServiceUnavailableException("", "")); mFlushResults.add(new RemoteService.ServiceUnavailableException("", "")); mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(0, mCleanupCalls.size()); int numEvents = 2 * 50 + 20; // we send batches of 50 each time for (int i = 0; i <= numEvents; i++) { mMetrics.track(SUCCEED_TEXT, null); } waitForBackOffTimeInterval(); assertEquals(0, mCleanupCalls.size()); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); waitForBackOffTimeInterval(); Thread.sleep(5000); assertEquals(3, mCleanupCalls.size()); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); } private void runMemoryTest() throws InterruptedException { mForceOverMemThreshold = true; mCleanupCalls.clear(); mMetrics.track(FAIL_TEXT, null); waitForFlushInternval(); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(0, mCleanupCalls.size()); mForceOverMemThreshold = false; mMetrics.track(SUCCEED_TEXT, null); waitForFlushInternval(); assertEquals(SUCCEED_TEXT, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(null, mPerformRequestCalls.poll(POLL_WAIT_MAX_MILLISECONDS, DEFAULT_TIMEUNIT)); assertEquals(1, mCleanupCalls.size()); } private void waitForBackOffTimeInterval() throws InterruptedException { long waitForMs = mMetrics.getAnalyticsMessages().getTrackEngageRetryAfter(); Thread.sleep(waitForMs + 500); } private void waitForFlushInternval() throws InterruptedException { Thread.sleep(mFlushInterval + 500); } }