diff --git a/src/androidTest/assets/split_changes_flag_set-0.json b/src/androidTest/assets/split_changes_flag_set-0.json new file mode 100644 index 000000000..93be5fda4 --- /dev/null +++ b/src/androidTest/assets/split_changes_flag_set-0.json @@ -0,0 +1 @@ +{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602798638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602797638344,"till":1602798638344} diff --git a/src/androidTest/assets/split_changes_flag_set-1.json b/src/androidTest/assets/split_changes_flag_set-1.json new file mode 100644 index 000000000..67f617712 --- /dev/null +++ b/src/androidTest/assets/split_changes_flag_set-1.json @@ -0,0 +1 @@ +{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602797638344,"algo":2,"configurations":{},"sets":["set_1"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602796638344,"till":1602797638344} diff --git a/src/androidTest/assets/split_changes_flag_set-2.json b/src/androidTest/assets/split_changes_flag_set-2.json new file mode 100644 index 000000000..a96e3e209 --- /dev/null +++ b/src/androidTest/assets/split_changes_flag_set-2.json @@ -0,0 +1 @@ +{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_1","set_2"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]},{"trafficTypeName":"client","name":"workm_set_3","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":-1,"till":1602796638344} diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 243b46dbe..92fcce547 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -2,6 +2,7 @@ import android.content.Context; +import androidx.annotation.Nullable; import androidx.core.util.Pair; import com.google.common.base.Strings; @@ -16,6 +17,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.BlockingQueue; import fake.HttpClientMock; import fake.HttpResponseMock; @@ -257,12 +259,18 @@ public static String splitChangeV2CompressionType0() { "eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0="); } - private static String splitChangeV2(String changeNumber, String previousChangeNumber, String compressionType, String compressedPayload) { + public static String splitChangeV2(String changeNumber, String previousChangeNumber, String compressionType, String compressedPayload) { return "id: vQQ61wzBRO:0:0\n" + "event: message\n" + "data: {\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":"+System.currentTimeMillis()+",\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":"+changeNumber+",\\\"pcn\\\":"+previousChangeNumber+",\\\"c\\\":"+compressionType+",\\\"d\\\":\\\""+compressedPayload+"\\\"}\"}\n"; } + public static String splitKill(String changeNumber, String splitName) { + return "id:cf74eb42-f687-48e4-ad18-af2125110aac\n" + + "event:message\n" + + "data:{\"id\":\"-OT-rGuSwz:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:NDIxNjU0NTUyNw==\",\"timestamp\":"+System.currentTimeMillis()+",\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_KILL\\\",\\\"changeNumber\\\":" + changeNumber + ",\\\"defaultTreatment\\\":\\\"off\\\",\\\"splitName\\\":\\\"" + splitName + "\\\"}\"}\n"; + } + /** * Builds a dispatcher with the given responses. * @@ -270,17 +278,17 @@ private static String splitChangeV2(String changeNumber, String previousChangeNu * @return The dispatcher to be used in {@link HttpClientMock} */ public static HttpResponseMockDispatcher buildDispatcher(Map responses) { - return buildDispatcher(responses, Collections.emptyMap()); + return buildDispatcher(responses, null); } /** * Builds a dispatcher with the given responses. * * @param responses The responses to be returned by the dispatcher. The keys are url paths. - * @param streamingResponses The streaming responses to be returned by the dispatcher. The keys are url paths. + * @param streamingQueue The streaming responses to be returned by the dispatcher. * @return The dispatcher to be used in {@link HttpClientMock} */ - public static HttpResponseMockDispatcher buildDispatcher(Map responses, Map streamingResponses) { + public static HttpResponseMockDispatcher buildDispatcher(Map responses, @Nullable BlockingQueue streamingQueue) { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { @@ -300,17 +308,11 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { @Override public HttpStreamResponseMock getStreamResponse(URI uri) { try { - String path = uri.getPath().replace("/api/", ""); - if (streamingResponses.containsKey(path)) { - return streamingResponses.get(path).onResponse(uri); - } else { - return new HttpStreamResponseMock(200, null); - } + return new HttpStreamResponseMock(200, streamingQueue); } catch (IOException e) { e.printStackTrace(); + return null; } - - return null; } }; } @@ -322,6 +324,10 @@ public interface ResponseClosure { HttpResponseMock onResponse(URI uri, HttpMethod httpMethod, String body); + + static String getSinceFromUri(URI uri) { + return uri.getQuery().split("&")[0].split("=")[1]; + } } /** diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java new file mode 100644 index 000000000..1458a4016 --- /dev/null +++ b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java @@ -0,0 +1,218 @@ +package tests.integration.sets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFilter; +import io.split.android.client.SyncConfig; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.SplitEventTask; +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.Logger; + +public class FlagSetsPollingTest { + + private final FileHelper fileHelper = new FileHelper(); + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private CountDownLatch hitsLatch; + private CountDownLatch firstChangeLatch; + private CountDownLatch secondChangeLatch; + private CountDownLatch thirdChangeLatch; + private SplitRoomDatabase mRoomDb; + + @Before + public void setUp() throws Exception { + mRoomDb = DatabaseHelper.getTestDatabase(mContext); + mRoomDb.clearAllTables(); + + hitsLatch = new CountDownLatch(3); + firstChangeLatch = new CountDownLatch(1); + secondChangeLatch = new CountDownLatch(1); + thirdChangeLatch = new CountDownLatch(1); + } + + @Test + public void featureFlagIsUpdatedAccordingToSetsWhenTheyAreConfigured() throws IOException, InterruptedException { + // 1. Initialize a factory with polling and sets set_1 & set_2 configured. + createFactory(mContext, mRoomDb, false, "set_1", "set_2"); + + // 2. Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + // -> only one feature flag should be added + boolean awaitFirst = firstChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(200); + int firstSize = mRoomDb.splitDao().getAll().size(); + boolean firstSetsCorrect = mRoomDb.splitDao().getAll().get(0).getBody().contains("[\"set_1\",\"set_2\"]"); + + // 3. Receive split change with 1 split belonging to set_1 only + // -> the feature flag should be updated + boolean awaitSecond = secondChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(200); + int secondSize = mRoomDb.splitDao().getAll().size(); + boolean secondSetsCorrect = mRoomDb.splitDao().getAll().get(0).getBody().contains("[\"set_1\"]"); + + // 4. Receive split change with 1 split belonging to set_3 only + // -> the feature flag should be removed + boolean awaitThird = thirdChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(200); + int thirdSize = mRoomDb.splitDao().getAll().size(); + + boolean awaitHits = hitsLatch.await(120, TimeUnit.SECONDS); + + assertEquals(1, firstSize); + assertEquals(1, secondSize); + assertEquals(0, thirdSize); + assertTrue(awaitFirst); + assertTrue(awaitSecond); + assertTrue(awaitThird); + assertTrue(firstSetsCorrect); + assertTrue(secondSetsCorrect); + + assertTrue(awaitHits); + } + + @Test + public void featureFlagSetsAreIgnoredWhenSetsAreNotConfigured() throws IOException, InterruptedException { + // 1. Initialize a factory with polling and sets set_1 & set_2 configured. + createFactory(mContext, mRoomDb, false); + + // 2. Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 + // -> only one feature flag should be added + boolean awaitFirst = firstChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(500); + int firstSize = mRoomDb.splitDao().getAll().size(); + List firstEntities = mRoomDb.splitDao().getAll(); + boolean firstSetsCorrect = firstEntities.get(0).getBody().contains("[\"set_1\",\"set_2\"]") && + firstEntities.get(1).getBody().contains("[\"set_3\"]"); + + // 3. Receive split change with 1 split belonging to set_1 only + // -> the feature flag should be updated + boolean awaitSecond = secondChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(500); + int secondSize = mRoomDb.splitDao().getAll().size(); + List secondEntities = mRoomDb.splitDao().getAll(); + String body0 = secondEntities.get(0).getBody(); + String body1 = secondEntities.get(1).getBody(); + boolean secondSetsCorrect = body1.contains("[\"set_1\"]") && + body1.contains("\"name\":\"workm\",") && + body0.contains("\"name\":\"workm_set_3\",") && + body0.contains("[\"set_3\"]"); + + Logger.w("body0: " + body0); + Logger.w("body1: " + body1); + + // 4. Receive split change with 1 split belonging to set_3 only + // -> the feature flag should be removed + boolean awaitThird = thirdChangeLatch.await(5, TimeUnit.SECONDS); + Thread.sleep(500); + List thirdEntities = mRoomDb.splitDao().getAll(); + int thirdSize = thirdEntities.size(); + String body30 = thirdEntities.get(0).getBody(); + String body31 = thirdEntities.get(1).getBody(); + boolean thirdSetsCorrect = body31.contains("[\"set_3\"]") && + body31.contains("\"name\":\"workm\",") && + body30.contains("\"name\":\"workm_set_3\",") && + body30.contains("[\"set_3\"]"); + + boolean awaitHits = hitsLatch.await(120, TimeUnit.SECONDS); + + assertEquals(2, firstSize); + assertEquals(2, secondSize); + assertEquals(2, thirdSize); + assertTrue(awaitFirst); + assertTrue(awaitSecond); + assertTrue(awaitThird); + assertTrue(firstSetsCorrect); + assertTrue(secondSetsCorrect); + assertTrue(thirdSetsCorrect); + + assertTrue(awaitHits); + } + + private SplitFactory createFactory( + Context mContext, + SplitRoomDatabase splitRoomDatabase, + boolean streamingEnabled, + String... sets) throws IOException { + SplitClientConfig config = new TestableSplitConfigBuilder() + .ready(30000) + .trafficType("client") + .enableDebug() + .impressionsRefreshRate(1000) + .impressionsCountersRefreshRate(1000) + .syncConfig(SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Arrays.asList(sets))) + .build()) + .featuresRefreshRate(2) + .streamingEnabled(streamingEnabled) + .eventFlushInterval(1000) + .build(); + + Map responses = new HashMap<>(); + responses.put("splitChanges", (uri, httpMethod, body) -> { + String since = getSinceFromUri(uri); + + hitsLatch.countDown(); + if (since.equals("-1")) { + firstChangeLatch.countDown(); + return new HttpResponseMock(200, loadSplitChangeWithSet(2)); + } else if (since.equals("1602796638344")) { + secondChangeLatch.countDown(); + return new HttpResponseMock(200, loadSplitChangeWithSet(1)); + } else { + thirdChangeLatch.countDown(); + return new HttpResponseMock(200, loadSplitChangeWithSet(0)); + } + }); + + responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + + HttpResponseMockDispatcher httpResponseMockDispatcher = IntegrationHelper.buildDispatcher(responses); + + return IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + config, + mContext, + new HttpClientMock(httpResponseMockDispatcher), + splitRoomDatabase, null, null, null); + } + + private String loadSplitChangeWithSet(int setsCount) { + String change = fileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = parsedChange.till; + + return Json.toJson(parsedChange); + } +} diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java new file mode 100644 index 000000000..d61a46e06 --- /dev/null +++ b/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java @@ -0,0 +1,289 @@ +package tests.integration.sets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static helper.IntegrationHelper.splitChangeV2; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import helper.DatabaseHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFilter; +import io.split.android.client.SyncConfig; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.SplitEventTask; +import io.split.android.client.storage.db.SplitEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.logger.Logger; +import tests.integration.shared.TestingHelper; + +public class FlagSetsStreamingTest { + + // workm with set_3, set_4 + private static final String splitChange5 = splitChangeV2("5", "4", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjUsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMyIsInNldF80Il0sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0"); + // workm with no sets + private static final String splitChange4None = splitChangeV2("4", "3", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjUsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6W10sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0="); + // workm with set_3 + private static final String splitChange4 = splitChangeV2("4", "3", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjQsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMyJdLCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibmV3X3NlZ21lbnQifSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjB9LHsidHJlYXRtZW50IjoiZnJlZSIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG5ld19zZWdtZW50In0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6bnVsbCwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjoxMDB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19"); + // workm with set_1 + private static final String splitChange3 = splitChangeV2("3", "2", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjMsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMSJdLCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibmV3X3NlZ21lbnQifSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjB9LHsidHJlYXRtZW50IjoiZnJlZSIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG5ld19zZWdtZW50In0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6ImNsaWVudCIsImF0dHJpYnV0ZSI6bnVsbH0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6bnVsbCwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOm51bGwsInVuYXJ5TnVtZXJpY01hdGNoZXJEYXRhIjpudWxsLCJiZXR3ZWVuTWF0Y2hlckRhdGEiOm51bGwsImJvb2xlYW5NYXRjaGVyRGF0YSI6bnVsbCwiZGVwZW5kZW5jeU1hdGNoZXJEYXRhIjpudWxsLCJzdHJpbmdNYXRjaGVyRGF0YSI6bnVsbH1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjoxMDB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJjb250YSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19"); + // workm with set_1, set_2 + private static final String splitChange2 = splitChangeV2("2", "1", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjIsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMSIsInNldF8yIl0sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJJTl9TRUdNRU5UIiwibmVnYXRlIjpmYWxzZSwidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOnsic2VnbWVudE5hbWUiOiJuZXdfc2VnbWVudCJ9LCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJmcmVlIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbmV3X3NlZ21lbnQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoiY2xpZW50IiwiYXR0cmlidXRlIjpudWxsfSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjpudWxsLCJ3aGl0ZWxpc3RNYXRjaGVyRGF0YSI6bnVsbCwidW5hcnlOdW1lcmljTWF0Y2hlckRhdGEiOm51bGwsImJldHdlZW5NYXRjaGVyRGF0YSI6bnVsbCwiYm9vbGVhbk1hdGNoZXJEYXRhIjpudWxsLCJkZXBlbmRlbmN5TWF0Y2hlckRhdGEiOm51bGwsInN0cmluZ01hdGNoZXJEYXRhIjpudWxsfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImZyZWUiLCJzaXplIjowfSx7InRyZWF0bWVudCI6ImNvbnRhIiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0"); + // mauro_java with no sets + private static final String noSetsSplitChange = splitChangeV2("2", "1", "0", "eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTYwMjc5OTYzODM0NCwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJzZXRzIjpbXSwiY29uZGl0aW9ucyI6W3siY29uZGl0aW9uVHlwZSI6IldISVRFTElTVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJtYXRjaGVyVHlwZSI6IldISVRFTElTVCIsIm5lZ2F0ZSI6ZmFsc2UsIndoaXRlbGlzdE1hdGNoZXJEYXRhIjp7IndoaXRlbGlzdCI6WyJhZG1pbiIsIm1hdXJvIiwibmljbyJdfX1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfV0sImxhYmVsIjoid2hpdGVsaXN0ZWQifSx7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoidXNlciJ9LCJtYXRjaGVyVHlwZSI6IklOX1NFR01FTlQiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6eyJzZWdtZW50TmFtZSI6Im1hdXItMiJ9fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6IlY0Iiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJ2NSIsInNpemUiOjB9XSwibGFiZWwiOiJpbiBzZWdtZW50IG1hdXItMiJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfSx7InRyZWF0bWVudCI6IlY0Iiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJ2NSIsInNpemUiOjB9XSwibGFiZWwiOiJkZWZhdWx0IHJ1bGUifV19"); + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private final AtomicInteger mSplitChangesHits = new AtomicInteger(0); + private SplitRoomDatabase mRoomDb; + + @Before + public void setUp() { + mSplitChangesHits.set(0); + mRoomDb = DatabaseHelper.getTestDatabase(mContext); + mRoomDb.clearAllTables(); + } + + @Test + public void sdkWithoutSetsConfiguredDoesNotExcludeUpdates() throws IOException, InterruptedException { + // 1. Initialize a factory with streaming enabled and no sets. + LinkedBlockingDeque mStreamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, mStreamingData); + + int initialSplitsSize = mRoomDb.splitDao().getAll().size(); + CountDownLatch updateLatch = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + + // 2. Receive notification with new feature flag with no sets. + // 3. Assert that the update is processed and the split is stored. + pushToStreaming(mStreamingData, noSetsSplitChange); + boolean updateAwait = updateLatch.await(5, TimeUnit.SECONDS); + + assertTrue(updateAwait); + assertEquals(0, initialSplitsSize); + assertEquals(1, mRoomDb.splitDao().getAll().size()); + } + + @Test + public void sdkWithSetsConfiguredDeletedDueToEmptySets() throws IOException, InterruptedException { + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, "set_1", "set_2"); + + // 1. Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + // -> flag is added to the storage + boolean firstChange = processUpdate(readyClient, streamingData, splitChange2, "\"sets\":[\"set_1\",\"set_2\"]", "\"name\":\"workm\""); + + // 2. Receive a SPLIT_UPDATE with "sets":["set_1"] + // -> flag is updated in storage + boolean secondChange = processUpdate(readyClient, streamingData, splitChange3, "\"sets\":[\"set_1\"]", "\"name\":\"workm\""); + + // 3. Receive a SPLIT_UPDATE with "sets":[] + // -> flag is removed from storage + boolean thirdChange = processUpdate(readyClient, streamingData, splitChange4None); + + assertTrue(firstChange); + assertTrue(secondChange); + assertTrue(thirdChange); + } + + @Test + public void sdkWithSetsConfiguredDeletedDueToNonMatchingSets() throws IOException, InterruptedException { + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, "set_1", "set_2"); + + // 1. Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + // -> workm is added to the storage + boolean firstChange = processUpdate(readyClient, streamingData, splitChange2, "\"sets\":[\"set_1\",\"set_2\"]", "\"name\":\"workm\""); + + // 2. Receive a SPLIT_UPDATE with "sets":["set_1"] + // -> workm sets are updated to set_1 only + boolean secondChange = processUpdate(readyClient, streamingData, splitChange3, "\"sets\":[\"set_1\"]", "\"name\":\"workm\""); + + // 3. Receive a SPLIT_UPDATE with "sets":["set_3"] + // -> workm is removed from the storage + boolean thirdChange = processUpdate(readyClient, streamingData, splitChange4); + + // 4. Receive a SPLIT_UPDATE with "sets":["set_3", "set_4"] + // -> workm is not added to the storage + boolean fourthChange = processUpdate(readyClient, streamingData, splitChange5); + + assertTrue(firstChange); + assertTrue(secondChange); + assertTrue(thirdChange); + assertTrue(fourthChange); + } + + @Test + public void sdkWithSetsReceivesSplitKill() throws IOException, InterruptedException { + + // 1. Initialize a factory with set_1 & set_2 sets configured. + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, "set_1", "set_2"); + + // 2. Receive a SPLIT_UPDATE with "sets":["set_1", "set_2"] + // -> flag is added to the storage + CountDownLatch firstUpdate = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(firstUpdate)); + pushToStreaming(streamingData, splitChange2); + boolean firstUpdateAwait = firstUpdate.await(5, TimeUnit.SECONDS); + List entities = mRoomDb.splitDao().getAll(); + boolean firstUpdateStored = entities.size() == 1 && entities.get(0).getBody().contains("\"sets\":[\"set_1\",\"set_2\"]") && + entities.get(0).getBody().contains("\"killed\":false") && + entities.get(0).getBody().contains("\"name\":\"workm\""); + + // 3. Receive a SPLIT_KILL for flag + // -> flag is updated in storage + CountDownLatch secondUpdate = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(secondUpdate)); + pushToStreaming(streamingData, IntegrationHelper.splitKill("5", "workm")); + boolean secondUpdateAwait = secondUpdate.await(5, TimeUnit.SECONDS); + entities = mRoomDb.splitDao().getAll(); + boolean secondUpdateStored = entities.size() == 1 && entities.get(0).getBody().contains("\"killed\":true") && + entities.get(0).getBody().contains("\"name\":\"workm\""); + + // 4. A fetch is triggered due to the SPLIT_KILL + boolean correctAmountOfChanges = mSplitChangesHits.get() == 3; + + assertTrue(firstUpdateAwait); + assertTrue(firstUpdateStored); + assertTrue(secondUpdateAwait); + assertTrue(secondUpdateStored); + assertTrue(correctAmountOfChanges); + } + + @Test + public void sdkWithSetsReceivesSplitKillForNonExistingFeatureFlag() throws IOException, InterruptedException { + + // 1. Initialize a factory with set_1 & set_2 sets configured. + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, "set_1", "set_2"); + + int initialEntities = mRoomDb.splitDao().getAll().size(); + + // 2. Receive a SPLIT_KILL; storage is not modified since flag is not present. + CountDownLatch firstUpdate = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(firstUpdate)); + int initialChangesHits = mSplitChangesHits.get(); + pushToStreaming(streamingData, IntegrationHelper.splitKill("2", "workm")); + boolean firstUpdateAwait = firstUpdate.await(5, TimeUnit.SECONDS); + List entities = mRoomDb.splitDao().getAll(); + boolean firstUpdateStored = entities.isEmpty(); + + // 3. A fetch is triggered due to the SPLIT_KILL + int finalChangesHits = mSplitChangesHits.get(); + + assertFalse(firstUpdateAwait); + assertTrue(firstUpdateStored); + assertEquals(initialEntities, 0); + assertTrue(finalChangesHits > initialChangesHits); + } + + @Nullable + private SplitClient getReadyClient( + Context mContext, + SplitRoomDatabase splitRoomDatabase, + BlockingQueue streamingData, + String... sets) throws IOException, InterruptedException { + SplitClientConfig config = new TestableSplitConfigBuilder() + .ready(30000) + .trafficType("client") + .enableDebug() + .impressionsRefreshRate(1000) + .impressionsCountersRefreshRate(1000) + .syncConfig(SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Arrays.asList(sets))) + .build()) + .featuresRefreshRate(2) + .streamingEnabled(true) + .eventFlushInterval(1000) + .build(); + CountDownLatch authLatch = new CountDownLatch(1); + Map responses = new HashMap<>(); + responses.put("splitChanges", (uri, httpMethod, body) -> { + mSplitChangesHits.incrementAndGet(); + return new HttpResponseMock(200, IntegrationHelper.emptySplitChanges(-1, 1)); + }); + responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put("v2/auth", (uri, httpMethod, body) -> { + authLatch.countDown(); + return new HttpResponseMock(200, IntegrationHelper.streamingEnabledToken()); + }); + + HttpResponseMockDispatcher httpResponseMockDispatcher = IntegrationHelper.buildDispatcher(responses, streamingData); + + SplitFactory splitFactory = IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + config, + mContext, + new HttpClientMock(httpResponseMockDispatcher), + splitRoomDatabase, null, null, null); + + CountDownLatch readyLatch = new CountDownLatch(1); + SplitClient client = splitFactory.client(); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + readyLatch.countDown(); + } + }); + + boolean await = readyLatch.await(5, TimeUnit.SECONDS); + boolean authAwait = authLatch.await(5, TimeUnit.SECONDS); + TestingHelper.pushKeepAlive(streamingData); + + return (await && authAwait) ? client : null; + } + + private static void pushToStreaming(LinkedBlockingDeque streamingData, String message) throws InterruptedException { + try { + streamingData.put(message + "" + "\n"); + + Logger.d("Pushed message: " + message); + } catch (InterruptedException ignored) { + } + } + + private boolean processUpdate(SplitClient client, LinkedBlockingDeque streamingData, String splitChange, String... expectedContents) throws InterruptedException { + CountDownLatch updateLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + pushToStreaming(streamingData, splitChange); + boolean updateAwaited = updateLatch.await(5, TimeUnit.SECONDS); + List entities = mRoomDb.splitDao().getAll(); + + if (expectedContents == null || expectedContents.length == 0) { + return updateAwaited && entities.isEmpty(); + } + + boolean contentMatches = true; + for (String expected : expectedContents) { + contentMatches = contentMatches && entities.size() == 1 && entities.get(0).getBody().contains(expected); + } + + return updateAwaited && contentMatches; + } +} diff --git a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java b/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java index e9d072cf6..c34dd6539 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java @@ -49,7 +49,7 @@ public SplitTaskExecutionInfo execute() { mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); - Logger.d("Updated feature flag: " + mSplit.name); + Logger.v("Updated feature flag"); return SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC); } catch (Exception ex) { Logger.e("Could not update feature flag"); diff --git a/src/main/java/io/split/android/client/service/splits/SplitKillTask.java b/src/main/java/io/split/android/client/service/splits/SplitKillTask.java index bdf91da04..06b8be469 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitKillTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitKillTask.java @@ -31,18 +31,23 @@ public SplitKillTask(@NonNull SplitsStorage splitsStorage, Split split, public SplitTaskExecutionInfo execute() { try { if (mKilledSplit == null) { - logError("Split name to kill could not be null."); + logError("Feature flag name to kill could not be null."); return SplitTaskExecutionInfo.error(SplitTaskType.SPLIT_KILL); } long changeNumber = mSplitsStorage.getTill(); if (mKilledSplit.changeNumber <= changeNumber) { - Logger.d("Skipping killed split notification for old change number: " + Logger.d("Skipping killed feature flag notification for old change number: " + mKilledSplit.changeNumber); return SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL); } Split splitToKill = mSplitsStorage.get(mKilledSplit.name); + if (splitToKill == null) { + Logger.d("Skipping " + mKilledSplit.name + " since not in storage"); + return SplitTaskExecutionInfo.error(SplitTaskType.SPLIT_KILL); + } + splitToKill.killed = true; splitToKill.defaultTreatment = mKilledSplit.defaultTreatment; splitToKill.changeNumber = mKilledSplit.changeNumber; @@ -50,14 +55,14 @@ public SplitTaskExecutionInfo execute() { mSplitsStorage.updateWithoutChecks(splitToKill); mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLIT_KILLED_NOTIFICATION); } catch (Exception e) { - logError("Unknown error while updating killed split: " + e.getLocalizedMessage()); + logError("Unknown error while updating killed feature flag: " + e.getLocalizedMessage()); return SplitTaskExecutionInfo.error(SplitTaskType.SPLIT_KILL); } - Logger.d("Killed split has been updated"); + Logger.d("Killed feature flag has been updated"); return SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL); } private void logError(String message) { - Logger.e("Error while executing Split kill task: " + message); + Logger.e("Error while executing feature flag kill task: " + message); } } diff --git a/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java b/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java index 1bddee8c2..d0944a07b 100644 --- a/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java +++ b/src/test/java/io/split/android/client/service/splits/SplitChangeProcessorTest.java @@ -226,9 +226,9 @@ public void creatingWithNullFilterProcessesEverything() { @Test public void creatingWithFilterWithEmptyConfiguredValuesProcessesEverything() { - Map filterList = Collections.singletonMap(SplitFilter.Type.BY_SET, SplitFilter.bySet(Collections.emptyList())); + Map filterMap = Collections.singletonMap(SplitFilter.Type.BY_SET, SplitFilter.bySet(Collections.emptyList())); - mProcessor = new SplitChangeProcessor(filterList); + mProcessor = new SplitChangeProcessor(filterMap); Split split1 = newSplit("split_1", Status.ACTIVE); Split split2 = newSplit("split_2", Status.ARCHIVED);