diff --git a/build.gradle b/build.gradle index 9bfa6ed06..dba8c84c8 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'signing' apply plugin: 'kotlin-android' ext { - splitVersion = '3.4.0' + splitVersion = '3.5.0-alpha-1' } android { diff --git a/gradle.properties b/gradle.properties index 7de624fb0..19a21ffb5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ ossrhUsername= ossrhPassword= -android.enableJetifier=true android.useAndroidX=true + +kotlin.stdlib.default.dependency=false diff --git a/src/androidTest/java/helper/TestableSplitConfigBuilder.java b/src/androidTest/java/helper/TestableSplitConfigBuilder.java index ac73eaaee..2493b7d67 100644 --- a/src/androidTest/java/helper/TestableSplitConfigBuilder.java +++ b/src/androidTest/java/helper/TestableSplitConfigBuilder.java @@ -60,6 +60,8 @@ public class TestableSplitConfigBuilder { private long mDefaultSSEConnectionDelayInSecs = ServiceConstants.DEFAULT_SSE_CONNECTION_DELAY_SECS; private long mSSEDisconnectionDelayInSecs = 60L; + private String mPrefix = ""; + public TestableSplitConfigBuilder() { mServiceEndpoints = ServiceEndpoints.builder().build(); } @@ -249,6 +251,11 @@ public TestableSplitConfigBuilder sseDisconnectionDelayInSecs(long seconds) { return this; } + public TestableSplitConfigBuilder prefix(String prefix) { + this.mPrefix = prefix; + return this; + } + public SplitClientConfig build() { Constructor constructor = SplitClientConfig.class.getDeclaredConstructors()[0]; constructor.setAccessible(true); @@ -300,7 +307,8 @@ public SplitClientConfig build() { mUserConsent, mEncryptionEnabled, mDefaultSSEConnectionDelayInSecs, - mSSEDisconnectionDelayInSecs); + mSSEDisconnectionDelayInSecs, + mPrefix); return config; } catch (Exception e) { Logger.e("Error creating Testable Split client builder: " diff --git a/src/androidTest/java/tests/database/DatabaseInitializationTest.java b/src/androidTest/java/tests/database/DatabaseInitializationTest.java new file mode 100644 index 000000000..5729389dd --- /dev/null +++ b/src/androidTest/java/tests/database/DatabaseInitializationTest.java @@ -0,0 +1,186 @@ +package tests.database; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.concurrent.LinkedBlockingDeque; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import fake.HttpStreamResponseMock; +import helper.IntegrationHelper; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.api.Key; +import io.split.android.client.network.HttpMethod; + +public class DatabaseInitializationTest { + + private HttpClientMock mHttpClientMock; + private Context mContext; + + @Before + public void setUp() throws IOException { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mHttpClientMock = new HttpClientMock(new HttpResponseMockDispatcher() { + @Override + public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { + return new HttpResponseMock(400, ""); + } + + @Override + public HttpStreamResponseMock getStreamResponse(URI uri) { + try { + return new HttpStreamResponseMock(400, new LinkedBlockingDeque<>(0)); + } catch (IOException e) { + return null; + } + } + }); + } + + @Test + public void initializationWithNullApiKeyResultsInNullFactory() throws IOException { + SplitFactory factory = IntegrationHelper.buildFactory(null, + new Key("matchingKey"), + IntegrationHelper.basicConfig(), + mContext, + mHttpClientMock); + + assertNull(factory); + } + + @Test + public void initializationWithoutPrefixCreatesCorrectDatabaseName() { + String[] initialDatabaseList = getDbList(mContext); + SplitFactory factory = IntegrationHelper.buildFactory("abcdefghijkl", + new Key("matchingKey"), + IntegrationHelper.basicConfig(), + mContext, + mHttpClientMock); + + String[] finalDatabaseList = getDbList(mContext); + + assertNotNull(factory); + assertEquals(0, initialDatabaseList.length); + assertEquals("abcdijkl", finalDatabaseList[0]); + } + + @Test + public void initializationWithPrefixCreatesCorrectDatabaseName() { + String[] initialDatabaseList = getDbList(mContext); + SplitFactory factory = IntegrationHelper.buildFactory("abcdefghijkl", + new Key("matchingKey"), + SplitClientConfig.builder().prefix("my_prefix").build(), + mContext, + mHttpClientMock); + + String[] finalDatabaseList = getDbList(mContext); + + assertNotNull(factory); + assertEquals(0, initialDatabaseList.length); + assertEquals("my_prefixabcdijkl", finalDatabaseList[0]); + } + + @Test + public void factoriesWithSameSdkKeyCreateOnlyOneDatabase() { + String[] initialDatabaseList = getDbList(mContext); + SplitFactory factory1 = IntegrationHelper.buildFactory("abcdefghijkl", + new Key("matchingKey"), + IntegrationHelper.basicConfig(), + mContext, + mHttpClientMock); + + SplitFactory factory2 = IntegrationHelper.buildFactory("abcdefghijkl", + new Key("matchingKey2"), + IntegrationHelper.basicConfig(), + mContext, + mHttpClientMock); + + String[] finalDatabaseList = getDbList(mContext); + + assertNotNull(factory1); + assertNotNull(factory2); + assertEquals(0, initialDatabaseList.length); + assertEquals(1, finalDatabaseList.length); + assertEquals("abcdijkl", finalDatabaseList[0]); + } + + @Test + public void oneFactoryWithPrefixCreatesNewDatabase() { + String[] initialDatabaseList = getDbList(mContext); + SplitFactory factory1 = IntegrationHelper.buildFactory("abcdefghijkl", + new Key("matchingKey"), + SplitClientConfig.builder().build(), + mContext, + mHttpClientMock); + + SplitFactory factory2 = IntegrationHelper.buildFactory("abcdefghijkl", + new Key("matchingKey"), + SplitClientConfig.builder().prefix("my_prefix").build(), + mContext, + mHttpClientMock); + + String[] finalDatabaseList = getDbList(mContext); + + assertNotNull(factory1); + assertNotNull(factory2); + assertEquals(0, initialDatabaseList.length); + assertEquals(2, finalDatabaseList.length); + assertEquals("abcdijkl", finalDatabaseList[0]); + assertEquals("my_prefixabcdijkl", finalDatabaseList[1]); + } + + @Test + public void usingInvalidPrefixResultsInIgnoredPrefix() { + String[] initialDatabaseList = getDbList(mContext); + + SplitFactory factory = IntegrationHelper.buildFactory("abcdefghijkl", + new Key("matchingKey"), + SplitClientConfig.builder().prefix(":l").build(), + mContext, + mHttpClientMock); + + String[] finalDatabaseList = getDbList(mContext); + + assertNotNull(factory); + assertEquals(0, initialDatabaseList.length); + assertEquals(1, finalDatabaseList.length); + assertEquals("abcdijkl", finalDatabaseList[0]); + } + + @Test + public void usingNullPrefixResultsInIgnoredPrefix() { + String[] initialDatabaseList = getDbList(mContext); + + SplitFactory factory = IntegrationHelper.buildFactory("abcdefghijkl", + new Key("matchingKey"), + SplitClientConfig.builder().prefix(null).build(), + mContext, + mHttpClientMock); + + String[] finalDatabaseList = getDbList(mContext); + + assertNotNull(factory); + assertEquals(0, initialDatabaseList.length); + assertEquals(1, finalDatabaseList.length); + assertEquals("abcdijkl", finalDatabaseList[0]); + } + + private static String[] getDbList(Context context) { + // remove -journal dbs since we're not interested in them + return Arrays.stream(context.databaseList()).filter(db -> !db.endsWith("-journal")).toArray(String[]::new); + } +} diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java index 55864f291..51fe77397 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java @@ -8,7 +8,6 @@ import androidx.test.platform.app.InstrumentationRegistry; -import org.junit.Before; import org.junit.Test; import java.io.IOException; @@ -39,7 +38,7 @@ public class FlagSetsEvaluationTest { - private final FileHelper fileHelper = new FileHelper(); + private final FileHelper mFileHelper = new FileHelper(); private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); @Test @@ -163,7 +162,7 @@ private SplitClient getClient( } private String loadSplitChangeWithSet(int setsCount) { - String change = fileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); + String change = mFileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); SplitChange parsedChange = Json.fromJson(change, SplitChange.class); parsedChange.since = parsedChange.till; diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java new file mode 100644 index 000000000..932c505b9 --- /dev/null +++ b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java @@ -0,0 +1,149 @@ +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 android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import helper.FileHelper; +import helper.IntegrationHelper; +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.utils.Json; +import tests.integration.shared.TestingHelper; + +public class FlagSetsMultipleFactoryTest { + private final FileHelper mFileHelper = new FileHelper(); + private Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + } + + @Test + public void factoriesContainCorrectSplits() throws IOException, InterruptedException { + CountDownLatch readyLatch = new CountDownLatch(1); + CountDownLatch readyLatch2 = new CountDownLatch(1); + HttpClientMock httpClient = new HttpClientMock(getDispatcher(2)); + + SplitClientConfig.Builder builderWithoutPrefix = SplitClientConfig.builder().syncConfig(SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Arrays.asList("set_1", "set_2"))) + .build()); + SplitClientConfig.Builder builderWithPrefix = SplitClientConfig.builder().prefix("mydb").syncConfig(SyncConfig.builder() + .addSplitFilter(SplitFilter.bySet(Collections.singletonList("set_3"))) + .build()); + + Thread thread = buildFactoryInitializationThread(builderWithoutPrefix, httpClient, readyLatch); + Thread thread2 = buildFactoryInitializationThread(builderWithPrefix, httpClient, readyLatch2); + + thread.start(); + thread2.start(); + + boolean readyAwait = readyLatch.await(5, TimeUnit.SECONDS); + boolean readyAwait2 = readyLatch2.await(5, TimeUnit.SECONDS); + + assertTrue(readyAwait); + assertTrue(readyAwait2); + + List namesInDb = getNamesFromDb("sdk_ey_1"); + List namesInDb2 = getNamesFromDb("mydbsdk_ey_1"); + + assertEquals(1, namesInDb.size()); + assertEquals(1, namesInDb2.size()); + assertEquals("workm", namesInDb.get(0)); + assertEquals("workm_set_3", namesInDb2.get(0)); + } + + @NonNull + private Thread buildFactoryInitializationThread(SplitClientConfig.Builder builder,HttpClientMock httpClient, CountDownLatch readyLatch) { + return new Thread(() -> { + CountDownLatch innerLatch = new CountDownLatch(1); + try { + initSplitFactory(builder, httpClient, innerLatch); + innerLatch.await(5, TimeUnit.SECONDS); + readyLatch.countDown(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + private void initSplitFactory(SplitClientConfig.Builder builder, HttpClientMock httpClient, CountDownLatch innerLatch) { + SplitFactory factory = IntegrationHelper.buildFactory( + "sdk_key_1", + IntegrationHelper.dummyUserKey(), + builder.build(), + mContext, + httpClient); + + SplitClient client = factory.client(); + client.on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(innerLatch)); + } + + @NonNull + private List getNamesFromDb(String dbName) { + List namesInDb = new ArrayList<>(); + try (SQLiteDatabase sqLiteDatabase = mContext.openOrCreateDatabase(dbName, Context.MODE_PRIVATE, null)) { + try (Cursor cursor = sqLiteDatabase.query("splits", null, null, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + do { + namesInDb.add(cursor.getString(cursor.getColumnIndex("name"))); + } while (cursor.moveToNext()); + } + } + } + return namesInDb; + } + + private String loadSplitChangeWithSet(int setsCount) { + String change = mFileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = parsedChange.till; + + return Json.toJson(parsedChange); + } + + private HttpResponseMockDispatcher getDispatcher(int setsCount) { + Map responses = new HashMap<>(); + responses.put("splitChanges", (uri, httpMethod, body) -> { + String since = getSinceFromUri(uri); + if (since.equals("-1")) { + return new HttpResponseMock(200, loadSplitChangeWithSet(setsCount)); + } else { + return new HttpResponseMock(200, IntegrationHelper.emptySplitChanges(1602796638344L, 1602796638344L)); + } + }); + + responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + + return IntegrationHelper.buildDispatcher(responses); + } +} diff --git a/src/main/java/io/split/android/client/FilterBuilder.java b/src/main/java/io/split/android/client/FilterBuilder.java index e562ef75c..c2067d544 100644 --- a/src/main/java/io/split/android/client/FilterBuilder.java +++ b/src/main/java/io/split/android/client/FilterBuilder.java @@ -7,8 +7,10 @@ import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; @@ -72,16 +74,17 @@ private void addFilters(List filters) { return; } + Set presentTypes = new HashSet<>(); boolean containsSetsFilter = false; for (SplitFilter filter : filters) { if (filter == null) { continue; } + presentTypes.add(filter.getType()); if (filter.getType() == SplitFilter.Type.BY_SET) { // BY_SET filter has precedence over other filters, so we remove all other filters // and only add BY_SET filters - Logger.w("SDK Config: The Set filter is exclusive and cannot be used simultaneously with names or prefix filters. Ignoring names and prefixes"); if (!containsSetsFilter) { mFilters.clear(); containsSetsFilter = true; @@ -93,6 +96,10 @@ private void addFilters(List filters) { mFilters.add(filter); } } + + if (presentTypes.contains(SplitFilter.Type.BY_SET) && presentTypes.size() > 1) { + Logger.e("SDK Config: The Set filter is exclusive and cannot be used simultaneously with names or prefix filters. Ignoring names and prefixes"); + } } private void validateFilterSize(SplitFilter.Type type, int size) { diff --git a/src/main/java/io/split/android/client/SplitClientConfig.java b/src/main/java/io/split/android/client/SplitClientConfig.java index 22f79f417..507675326 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/src/main/java/io/split/android/client/SplitClientConfig.java @@ -21,6 +21,8 @@ import io.split.android.client.telemetry.TelemetryHelperImpl; import io.split.android.client.utils.logger.Logger; import io.split.android.client.utils.logger.SplitLogLevel; +import io.split.android.client.validators.PrefixValidatorImpl; +import io.split.android.client.validators.ValidationErrorInfo; import okhttp3.Authenticator; import static com.google.common.base.Preconditions.checkNotNull; @@ -117,6 +119,7 @@ public class SplitClientConfig { private int mLogLevel = SplitLogLevel.NONE; private UserConsent mUserConsent; private boolean mEncryptionEnabled = false; + private final String mPrefix; private final long mDefaultSSEConnectionDelayInSecs; private final long mSSEDisconnectionDelayInSecs; @@ -172,7 +175,8 @@ private SplitClientConfig(String endpoint, UserConsent userConsent, boolean encryptionEnabled, long defaultSSEConnectionDelayInSecs, - long sseDisconnectionDelayInSecs) { + long sseDisconnectionDelayInSecs, + String prefix) { mEndpoint = endpoint; mEventsEndpoint = eventsEndpoint; mTelemetryEndpoint = telemetryEndpoint; @@ -226,8 +230,7 @@ private SplitClientConfig(String endpoint, mEncryptionEnabled = encryptionEnabled; mDefaultSSEConnectionDelayInSecs = defaultSSEConnectionDelayInSecs; mSSEDisconnectionDelayInSecs = sseDisconnectionDelayInSecs; - - Logger.instance().setLevel(mLogLevel); + mPrefix = prefix; } public String trafficType() { @@ -350,6 +353,10 @@ String defaultDataFolder() { return DEFAULT_DATA_FOLDER; } + String prefix() { + return mPrefix; + } + public String ip() { return mIp; } @@ -521,6 +528,8 @@ public static final class Builder { private final long mSSEDisconnectionDelayInSecs = 60L; + private String mPrefix = null; + public Builder() { mServiceEndpoints = ServiceEndpoints.builder().build(); } @@ -1022,8 +1031,18 @@ public Builder encryptionEnabled(boolean enabled) { return this; } - public SplitClientConfig build() { + /** + * Optional prefix for the database name. + * @param prefix Prefix for the database name. + * @return This builder + */ + public Builder prefix(String prefix) { + mPrefix = (prefix == null) ? "" : prefix; + return this; + } + public SplitClientConfig build() { + Logger.instance().setLevel(mLogLevel); if (mFeaturesRefreshRate < MIN_FEATURES_REFRESH_RATE) { Logger.w("Features refresh rate is lower than allowed. " + @@ -1079,6 +1098,16 @@ public SplitClientConfig build() { mTelemetryRefreshRate = DEFAULT_TELEMETRY_REFRESH_RATE; } + if (mPrefix != null) { + ValidationErrorInfo result = new PrefixValidatorImpl().validate(mPrefix); + if (result != null) { + Logger.e(result.getErrorMessage()); + Logger.w("Setting prefix to empty string"); + + mPrefix = ""; + } + } + HttpProxy proxy = parseProxyHost(mProxyHost); return new SplitClientConfig( @@ -1127,7 +1156,8 @@ public SplitClientConfig build() { mUserConsent, mEncryptionEnabled, mDefaultSSEConnectionDelayInSecs, - mSSEDisconnectionDelayInSecs); + mSSEDisconnectionDelayInSecs, + mPrefix); } private HttpProxy parseProxyHost(String proxyUri) { diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index 4550db1ab..386876c3b 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -99,13 +99,20 @@ String getDatabaseName(SplitClientConfig config, String apiToken, Context contex } private String buildDatabaseName(SplitClientConfig config, String apiToken) { - int apiTokenLength = apiToken.length(); + if (apiToken == null) { + throw new IllegalArgumentException("SDK key cannot be null"); + } + + final int apiTokenLength = apiToken.length(); + final String prefix = (config.prefix() == null) ? "" : config.prefix(); + if (apiTokenLength > DB_MAGIC_CHARS_COUNT) { String begin = apiToken.substring(0, DB_MAGIC_CHARS_COUNT); String end = apiToken.substring(apiTokenLength - DB_MAGIC_CHARS_COUNT); - return begin + end; + return prefix + begin + end; } - return config.defaultDataFolder(); + + return prefix + config.defaultDataFolder(); } private String buildLegacyDatabaseName(SplitClientConfig splitClientConfig, String apiToken) { diff --git a/src/main/java/io/split/android/client/utils/Utils.java b/src/main/java/io/split/android/client/utils/Utils.java index b853a42b7..913a731d0 100644 --- a/src/main/java/io/split/android/client/utils/Utils.java +++ b/src/main/java/io/split/android/client/utils/Utils.java @@ -5,13 +5,6 @@ public class Utils { - public static String sanitizeForFileName(String string) { - if(string == null) { - return ""; - } - return string.replaceAll("[^a-zA-Z0-9.\\-]", "_"); - } - private static String sanitizeForFolderName(String string) { if(string == null) { return ""; diff --git a/src/main/java/io/split/android/client/validators/PrefixValidator.java b/src/main/java/io/split/android/client/validators/PrefixValidator.java new file mode 100644 index 000000000..e2d4fd68a --- /dev/null +++ b/src/main/java/io/split/android/client/validators/PrefixValidator.java @@ -0,0 +1,9 @@ +package io.split.android.client.validators; + +import androidx.annotation.Nullable; + +interface PrefixValidator { + + @Nullable + ValidationErrorInfo validate(@Nullable String prefix); +} diff --git a/src/main/java/io/split/android/client/validators/PrefixValidatorImpl.java b/src/main/java/io/split/android/client/validators/PrefixValidatorImpl.java new file mode 100644 index 000000000..0007fabbc --- /dev/null +++ b/src/main/java/io/split/android/client/validators/PrefixValidatorImpl.java @@ -0,0 +1,26 @@ +package io.split.android.client.validators; + +import androidx.annotation.Nullable; + +public class PrefixValidatorImpl implements PrefixValidator { + + private static final String PREFIX_REGEX = "^[a-zA-Z0-9_]{1,80}$"; + + @Nullable + @Override + public ValidationErrorInfo validate(@Nullable String prefix) { + if (prefix == null) { + return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "You passed a null prefix, prefix must be a non-empty string"); + } + + if (prefix.trim().isEmpty()) { + return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "You passed an empty prefix, prefix must be a non-empty string"); + } + + if (!prefix.trim().matches(PREFIX_REGEX)) { + return new ValidationErrorInfo(ValidationErrorInfo.ERROR_SOME, "Prefix can only contain alphanumeric characters and underscore, and must be 80 characters or less"); + } + + return null; + } +} diff --git a/src/test/java/io/split/android/client/SplitClientConfigTest.java b/src/test/java/io/split/android/client/SplitClientConfigTest.java index 5578412dd..567f2b52f 100644 --- a/src/test/java/io/split/android/client/SplitClientConfigTest.java +++ b/src/test/java/io/split/android/client/SplitClientConfigTest.java @@ -2,14 +2,22 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertTrue; + +import androidx.annotation.NonNull; import org.junit.Test; +import java.util.LinkedList; +import java.util.Queue; + +import io.split.android.client.utils.logger.LogPrinter; +import io.split.android.client.utils.logger.Logger; +import io.split.android.client.utils.logger.SplitLogLevel; + public class SplitClientConfigTest { @Test - public void cannot_set_feature_refresh_rate_to_less_than_30() { + public void cannotSetFeatureRefreshRateToLessThan30() { SplitClientConfig build = SplitClientConfig.builder() .featuresRefreshRate(29) .build(); @@ -18,7 +26,7 @@ public void cannot_set_feature_refresh_rate_to_less_than_30() { } @Test - public void cannot_set_segment_refresh_rate_to_less_than_30() { + public void cannotSetSegmentRefreshRateToLessThan30() { SplitClientConfig build = SplitClientConfig.builder() .segmentsRefreshRate(29) .build(); @@ -27,7 +35,7 @@ public void cannot_set_segment_refresh_rate_to_less_than_30() { } @Test - public void cannot_set_impression_refresh_rate_to_less_than_30() { + public void cannotSetImpressionRefreshRateToLessThan30() { SplitClientConfig build = SplitClientConfig.builder() .impressionsRefreshRate(29) .build(); @@ -36,7 +44,7 @@ public void cannot_set_impression_refresh_rate_to_less_than_30() { } @Test - public void can_set_refresh_rates_to__30() { + public void canSetRefreshRatesTo30() { SplitClientConfig build = SplitClientConfig.builder() .featuresRefreshRate(30) .segmentsRefreshRate(30) @@ -47,7 +55,7 @@ public void can_set_refresh_rates_to__30() { } @Test - public void telemetry_refresh_rate_less_than_60_sets_value_to_default() { + public void telemetryRefreshRateLessThan60SetsValueToDefault() { SplitClientConfig config = SplitClientConfig.builder() .telemetryRefreshRate(30) .build(); @@ -56,11 +64,102 @@ public void telemetry_refresh_rate_less_than_60_sets_value_to_default() { } @Test - public void telemetry_refresh_rate_greater_than_60_is_accepted() { + public void telemetryRefreshRateGreaterThan60IsAccepted() { SplitClientConfig config = SplitClientConfig.builder() .telemetryRefreshRate(120) .build(); assertEquals(120, config.telemetryRefreshRate()); } + + @Test + public void logMessageIsDisplayedWhenUsingInvalidPrefix() { + Queue logMessages = getLogMessagesQueue(); + + SplitClientConfig.builder() + .logLevel(SplitLogLevel.WARNING) + .prefix("") + .build(); + + assertEquals(2, logMessages.size()); + assertEquals("You passed an empty prefix, prefix must be a non-empty string", logMessages.poll()); + assertEquals("Setting prefix to empty string", logMessages.poll()); + } + + @Test + public void logMessageIsDisplayedWhenUsingNullPrefix() { + Queue logMessages = getLogMessagesQueue(); + + SplitClientConfig.builder() + .logLevel(SplitLogLevel.WARNING) + .prefix(null) + .build(); + + assertEquals(2, logMessages.size()); + assertEquals("You passed an empty prefix, prefix must be a non-empty string", logMessages.poll()); + assertEquals("Setting prefix to empty string", logMessages.poll()); + } + + @Test + public void logMessageIsDisplayedWhenUsingEmptyPrefix() { + Queue logMessages = getLogMessagesQueue(); + + SplitClientConfig.builder() + .logLevel(SplitLogLevel.WARNING) + .prefix(" ") + .build(); + + assertEquals(2, logMessages.size()); + assertEquals("You passed an empty prefix, prefix must be a non-empty string", logMessages.poll()); + assertEquals("Setting prefix to empty string", logMessages.poll()); + } + + @Test + public void logMessageIsNotDisplayedWhenUsingValidPrefix() { + Queue logMessages = getLogMessagesQueue(); + + SplitClientConfig.builder() + .logLevel(SplitLogLevel.WARNING) + .prefix("valid10_") + .build(); + + assertEquals(0, logMessages.size()); + } + + @NonNull + private static Queue getLogMessagesQueue() { + Queue logMessages = new LinkedList<>(); + Logger.instance().setPrinter(new LogPrinter() { + @Override + public void v(String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + + @Override + public void d(String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + + @Override + public void i(String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + + @Override + public void w(String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + + @Override + public void e(String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + + @Override + public void wtf(String tag, String msg, Throwable tr) { + logMessages.add(msg); + } + }); + return logMessages; + } } diff --git a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt index e79858464..7c123c8dd 100644 --- a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt +++ b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt @@ -1,35 +1,53 @@ package io.split.android.client +import android.content.Context import io.split.android.client.service.executor.SplitTaskExecutionListener import io.split.android.client.service.executor.SplitTaskExecutor import io.split.android.client.storage.cipher.EncryptionMigrationTask import io.split.android.client.storage.db.SplitRoomDatabase +import junit.framework.TestCase.assertEquals +import org.junit.After import org.junit.Before import org.junit.Test import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.any import org.mockito.Mockito.argThat +import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import java.io.File +import java.lang.IllegalArgumentException class SplitFactoryHelperTest { + private lateinit var mocks: AutoCloseable + @Mock private lateinit var splitRoomDatabase: SplitRoomDatabase @Mock private lateinit var splitTaskExecutor: SplitTaskExecutor @Mock private lateinit var taskListener: SplitTaskExecutionListener + @Mock + private lateinit var context: Context private lateinit var helper: SplitFactoryHelper @Before fun setup() { - MockitoAnnotations.openMocks(this) + mocks = MockitoAnnotations.openMocks(this) helper = SplitFactoryHelper() } + @After + fun tearDown() { + mocks.close() + } + @Test - fun testMigrateEncryption() { + fun migrateEncryption() { helper.migrateEncryption( "abcdedfghijklmnopqrstuvwxyz", @@ -43,4 +61,95 @@ class SplitFactoryHelperTest { argThat { it is EncryptionMigrationTask }, argThat { it?.equals(taskListener) == true }) } + + @Test + fun generateDatabaseNameWithoutPrefixAndKeyLongerThan4() { + val path = mock(File::class.java) + `when`(path.exists()).thenReturn(true) + `when`(context.getDatabasePath("abcdwxyz")).thenReturn(path) + val databaseName = helper.getDatabaseName( + SplitClientConfig.builder().build(), + "abcdedfghijklmnopqrstuvwxyz", + context + ) + + assertEquals("abcdwxyz", databaseName) + } + + @Test + fun generateDatabaseNameWithoutPrefixAndKeyShorterThan4() { + val path = mock(File::class.java) + `when`(path.exists()).thenReturn(true) + `when`(context.getDatabasePath("split_data")).thenReturn(path) + val databaseName = helper.getDatabaseName( + SplitClientConfig.builder().build(), + "abc", + context + ) + + assertEquals("split_data", databaseName) + } + + @Test + fun generateDatabaseNameWithPrefixAndKeyLongerThan4() { + val path = mock(File::class.java) + `when`(path.exists()).thenReturn(true) + `when`(context.getDatabasePath("mydbabcdwxyz")).thenReturn(path) + val databaseName = helper.getDatabaseName( + SplitClientConfig.builder().prefix("mydb").build(), + "abcdedfghijklmnopqrstuvwxyz", + context + ) + + assertEquals("mydbabcdwxyz", databaseName) + } + + @Test + fun generateDatabaseNameWithPrefixAndKeyShorterThan4() { + val path = mock(File::class.java) + `when`(path.exists()).thenReturn(true) + `when`(context.getDatabasePath("mydbsplit_data")).thenReturn(path) + val databaseName = helper.getDatabaseName( + SplitClientConfig.builder().prefix("mydb").build(), + "abc", + context + ) + + assertEquals("mydbsplit_data", databaseName) + } + + @Test + fun generateDatabaseNameWithNullKeyThrowsIllegalArgumentException() { + val path = mock(File::class.java) + `when`(path.exists()).thenReturn(true) + `when`(context.getDatabasePath(Mockito.anyString())).thenReturn(path) + + try { + helper.getDatabaseName( + SplitClientConfig.builder().build(), + null, + context + ) + } catch (e: IllegalArgumentException) { + assertEquals("SDK key cannot be null", e.message) + } + } + + @Test + fun legacyDbIsRenamedIfExists() { + val nonExistingPath = mock(File::class.java) + val existingPath = mock(File::class.java) + `when`(nonExistingPath.exists()).thenReturn(false) + `when`(existingPath.exists()).thenReturn(true) + `when`(context.getDatabasePath(any())).thenReturn(existingPath); + `when`(context.getDatabasePath("abcdwxyz")).thenReturn(nonExistingPath) + val databaseName = helper.getDatabaseName( + SplitClientConfig.builder().build(), + "abcdfghijklmnopqrstuvwxyz", + context + ) + + verify(existingPath).renameTo(nonExistingPath) + assertEquals("abcdwxyz", databaseName) + } } diff --git a/src/test/java/io/split/android/client/validators/PrefixValidatorImplTest.java b/src/test/java/io/split/android/client/validators/PrefixValidatorImplTest.java new file mode 100644 index 000000000..939d794b1 --- /dev/null +++ b/src/test/java/io/split/android/client/validators/PrefixValidatorImplTest.java @@ -0,0 +1,77 @@ +package io.split.android.client.validators; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +public class PrefixValidatorImplTest { + + private PrefixValidatorImpl mValidator; + + @Before + public void setUp() { + mValidator = new PrefixValidatorImpl(); + } + + @Test + public void validateNullPrefix() { + ValidationErrorInfo result = mValidator.validate(null); + + assertTrue(result.isError()); + assertEquals("You passed a null prefix, prefix must be a non-empty string", result.getErrorMessage()); + } + + @Test + public void validateEmptyPrefix() { + ValidationErrorInfo result = mValidator.validate(""); + + assertTrue(result.isError()); + assertEquals("You passed an empty prefix, prefix must be a non-empty string", result.getErrorMessage()); + } + + @Test + public void validatePrefixTooLong() { + StringBuilder prefix = new StringBuilder(); + for (int i = 0; i < 81; i++) { + prefix.append("a"); + } + + ValidationErrorInfo result = mValidator.validate(prefix.toString()); + + assertTrue(result.isError()); + assertEquals("Prefix can only contain alphanumeric characters and underscore, and must be 80 characters or less", result.getErrorMessage()); + } + + @Test + public void validateWhitespacePrefix() { + ValidationErrorInfo result = mValidator.validate(" "); + + assertTrue(result.isError()); + assertEquals("You passed an empty prefix, prefix must be a non-empty string", result.getErrorMessage()); + } + + @Test + public void validateSpecialCharPrefix() { + ValidationErrorInfo result = mValidator.validate("a!b"); + + assertTrue(result.isError()); + assertEquals("Prefix can only contain alphanumeric characters and underscore, and must be 80 characters or less", result.getErrorMessage()); + } + + @Test + public void validateWithUnderscores() { + ValidationErrorInfo result = mValidator.validate("a_b____"); + + assertNull(result); + } + + @Test + public void validateValidPrefix() { + ValidationErrorInfo result = mValidator.validate("_ab99_"); + + assertNull(result); + } +}