diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 09a17e8b0..528f730fa 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -27,6 +27,7 @@ import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import fake.HttpClientMock; import fake.HttpResponseMock; @@ -390,40 +391,45 @@ public static Set asSet(T... elements) { return result; } - /** - * A simple interface to allow us to define the response for a given path - */ - public interface ResponseClosure { - HttpResponseMock onResponse(URI uri, - HttpMethod httpMethod, - String body); + public static long getTimestampDaysAgo(int days) { + return System.currentTimeMillis() - TimeUnit.DAYS.toMillis(days); + } - static String getSinceFromUri(URI uri) { - try { - return parse(uri.getQuery()).get("since"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + + public static String getSinceFromUri(URI uri) { + try { + return parse(uri.getQuery()).get("since"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); } + } - static Map parse(String query) throws UnsupportedEncodingException { - Map queryPairs = new HashMap<>(); - String[] pairs = query.split("&"); + static Map parse(String query) throws UnsupportedEncodingException { + Map queryPairs = new HashMap<>(); + String[] pairs = query.split("&"); - for (String pair : pairs) { - int idx = pair.indexOf("="); - try { - String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"); - String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + try { + String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"); + String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); - queryPairs.put(key, value); - } catch (Exception e) { - e.printStackTrace(); - } + queryPairs.put(key, value); + } catch (Exception e) { + e.printStackTrace(); } - - return queryPairs; } + + return queryPairs; + } + + /** + * A simple interface to allow us to define the response for a given path + */ + public interface ResponseClosure { + HttpResponseMock onResponse(URI uri, + HttpMethod httpMethod, + String body); } /** @@ -435,5 +441,6 @@ public interface StreamingResponseClosure { public static class ServicePath { public static final String MEMBERSHIPS = "memberships"; + public static final String SPLIT_CHANGES = "splitChanges"; } } diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java index 105d47d3d..d3f9152f4 100644 --- a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java +++ b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java @@ -3,7 +3,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; + +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java b/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java index 12ecce898..c289ef64e 100644 --- a/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java +++ b/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java @@ -3,7 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java b/src/androidTest/java/tests/integration/InitialChangeNumberTest.java index 434fd63f9..a8b64a13e 100644 --- a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java +++ b/src/androidTest/java/tests/integration/InitialChangeNumberTest.java @@ -1,6 +1,6 @@ package tests.integration; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java b/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java index 1183f7d7a..548daad4d 100644 --- a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java +++ b/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java @@ -2,7 +2,7 @@ import static java.lang.Thread.sleep; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java b/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java index d0e63c4e8..14fa837ae 100644 --- a/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java +++ b/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java b/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java index 928d1d93a..e11af1561 100644 --- a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java +++ b/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java @@ -4,7 +4,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java new file mode 100644 index 000000000..a1c0a2e2a --- /dev/null +++ b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java @@ -0,0 +1,194 @@ +package tests.integration.rollout; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static helper.IntegrationHelper.buildFactory; +import static helper.IntegrationHelper.dummyApiKey; +import static helper.IntegrationHelper.dummyUserKey; +import static helper.IntegrationHelper.emptySplitChanges; +import static helper.IntegrationHelper.getTimestampDaysAgo; +import static helper.IntegrationHelper.randomizedAllSegments; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import io.split.android.client.RolloutCacheConfiguration; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.dtos.Split; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.storage.db.GeneralInfoEntity; +import io.split.android.client.storage.db.MyLargeSegmentEntity; +import io.split.android.client.storage.db.MySegmentEntity; +import io.split.android.client.storage.db.SplitEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.SplitLogLevel; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import tests.integration.shared.TestingHelper; + +public class RolloutCacheManagerIntegrationTest { + + private final AtomicReference mSinceFromUri = new AtomicReference<>(null); + private MockWebServer mWebServer; + private SplitRoomDatabase mRoomDb; + private Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + @Before + public void setUp() { + mSinceFromUri.set(null); + setupServer(); + mRoomDb = DatabaseHelper.getTestDatabase(mContext); + mRoomDb.clearAllTables(); + } + + @Test + public void expirationPeriodIsUsed() throws InterruptedException { + + +// +// Verify persistent storage of flags & segments is cleared + + // Preload DB with update timestamp of 1 day ago + long oldTimestamp = getTimestampDaysAgo(1); + preloadDb(oldTimestamp, 0L, 18000L); + + // Track initial values + List initialFlags = mRoomDb.splitDao().getAll(); + List initialSegments = mRoomDb.mySegmentDao().getAll(); + List initialLargeSegments = mRoomDb.myLargeSegmentDao().getAll(); + long initialChangeNumber = mRoomDb.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO).getLongValue(); + + // Initialize SDK with an expiration of 1 day + CountDownLatch readyLatch = new CountDownLatch(1); + SplitFactory factory = getSplitFactory(RolloutCacheConfiguration.builder().expiration(1).build()); + + // Track final values + List finalFlags = mRoomDb.splitDao().getAll(); + List finalSegments = mRoomDb.mySegmentDao().getAll(); + List finalLargeSegments = mRoomDb.myLargeSegmentDao().getAll(); + long finalChangeNumber = mRoomDb.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO).getLongValue(); + + // Wait for ready + factory.client().on(SplitEvent.SDK_READY, TestingHelper.testTask(readyLatch)); + boolean readyAwait = readyLatch.await(10, TimeUnit.SECONDS); + + assertTrue(readyAwait); + assertEquals(2, initialFlags.size()); + assertEquals(1, initialSegments.size()); + assertEquals(1, initialLargeSegments.size()); + assertEquals(18000L, initialChangeNumber); + assertEquals(0, finalFlags.size()); + assertEquals(0, finalSegments.size()); + assertEquals(0, finalLargeSegments.size()); + assertEquals(-1, finalChangeNumber); + assertTrue(0L < mRoomDb.generalInfoDao() + .getByName("rolloutCacheLastClearTimestamp").getLongValue()); + assertEquals("-1", mSinceFromUri.get()); + } + + private SplitFactory getSplitFactory(RolloutCacheConfiguration rolloutCacheConfiguration) { + final String url = mWebServer.url("/").url().toString(); + ServiceEndpoints endpoints = ServiceEndpoints.builder() + .apiEndpoint(url).eventsEndpoint(url).build(); + SplitClientConfig.Builder builder = new SplitClientConfig.Builder() + .serviceEndpoints(endpoints) + .streamingEnabled(false) + .featuresRefreshRate(9999) + .segmentsRefreshRate(9999) + .impressionsRefreshRate(9999) + .logLevel(SplitLogLevel.VERBOSE) + .streamingEnabled(false); + + if (rolloutCacheConfiguration != null) { + builder.rolloutCacheConfiguration(rolloutCacheConfiguration); + } + + SplitClientConfig config = builder + .build(); + + return buildFactory( + dummyApiKey(), dummyUserKey(), + config, mContext, null, mRoomDb); + } + + private void setupServer() { + mWebServer = new MockWebServer(); + + final Dispatcher dispatcher = new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + Thread.sleep(1000); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(randomizedAllSegments()); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.SPLIT_CHANGES)) { + mSinceFromUri.compareAndSet(null, IntegrationHelper.getSinceFromUri(request.getRequestUrl().uri())); + return new MockResponse().setResponseCode(200) + .setBody(emptySplitChanges(-1, 10000)); + } else { + return new MockResponse().setResponseCode(404); + } + } + }; + mWebServer.setDispatcher(dispatcher); + } + + private void preloadDb(long updateTimestamp, long lastClearTimestamp, long changeNumber) { + List splitListFromJson = getSplitListFromJson(); + List entities = splitListFromJson.stream() + .filter(split -> split.name != null) + .map(split -> { + SplitEntity result = new SplitEntity(); + result.setName(split.name); + result.setBody(Json.toJson(split)); + + return result; + }).collect(Collectors.toList()); + + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 1)); + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, updateTimestamp)); + mRoomDb.generalInfoDao().update(new GeneralInfoEntity("rolloutCacheLastClearTimestamp", lastClearTimestamp)); + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, changeNumber)); + + MyLargeSegmentEntity largeSegment = new MyLargeSegmentEntity(); + largeSegment.setSegmentList("{\"m1\":1,\"m2\":1}"); + largeSegment.setUserKey(dummyUserKey().matchingKey()); + largeSegment.setUpdatedAt(System.currentTimeMillis()); + mRoomDb.myLargeSegmentDao().update(largeSegment); + + MySegmentEntity segment = new MySegmentEntity(); + segment.setSegmentList("m1,m2"); + segment.setUserKey(dummyUserKey().matchingKey()); + segment.setUpdatedAt(System.currentTimeMillis()); + mRoomDb.mySegmentDao().update(segment); + mRoomDb.splitDao().insert(entities); + } + + private List getSplitListFromJson() { + FileHelper fileHelper = new FileHelper(); + String s = fileHelper.loadFileContent(mContext, "attributes_test_split_change.json"); + + SplitChange changes = Json.fromJson(s, SplitChange.class); + + return changes.splits; + } +} diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java index 86f42034f..5507e432b 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java index 059d7c97d..da3d18250 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; import android.database.Cursor; diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java index 6faaab513..06146259a 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java @@ -3,7 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java b/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java index bf7fea927..45b0369d5 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java @@ -1,7 +1,7 @@ package tests.integration.streaming; import static java.lang.Thread.sleep; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java b/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java index 536a9c7d4..3675b4612 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java @@ -1,7 +1,7 @@ package tests.integration.streaming; import static java.lang.Thread.sleep; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java index 2659ef8c4..c852e4a85 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java @@ -101,7 +101,7 @@ private boolean validateExpiration() { // calculate elapsed time since last update long daysSinceLastUpdate = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastUpdateTimestamp); - if (lastUpdateTimestamp > 0 && daysSinceLastUpdate > mConfig.getExpiration()) { + if (lastUpdateTimestamp > 0 && daysSinceLastUpdate >= mConfig.getExpiration()) { Logger.v("Clearing rollout definitions cache due to expiration"); return true; } else if (mConfig.clearOnInit()) { @@ -112,7 +112,7 @@ private boolean validateExpiration() { long daysSinceCacheClear = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastCacheClearTimestamp); // don't clear too soon - if (daysSinceCacheClear > MIN_CACHE_CLEAR_DAYS) { + if (daysSinceCacheClear >= MIN_CACHE_CLEAR_DAYS) { Logger.v("Forcing rollout definitions cache clear"); return true; }