From 3ebe19f59c16bdf0366122865f5ccd05da69af57 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot <86431345+LaunchDarklyReleaseBot@users.noreply.github.com> Date: Fri, 25 Aug 2023 13:50:56 -0400 Subject: [PATCH] prepare 5.0.1 release (#227) ## [5.0.1] - 2023-08-25 ### Fixed: - Updated how Auto Environment Attributes sanitizes and validates provided values to provide a more user friendly experience. --------- Co-authored-by: Ember Stevens Co-authored-by: Ember Stevens <79482775+ember-stevens@users.noreply.github.com> Co-authored-by: Todd Anderson Co-authored-by: tanderson-ld <127344469+tanderson-ld@users.noreply.github.com> Co-authored-by: ld-repository-standards[bot] <113625520+ld-repository-standards[bot]@users.noreply.github.com> Co-authored-by: Kane Parkinson <93555788+kparkinson-ld@users.noreply.github.com> Co-authored-by: LaunchDarklyReleaseBot Co-authored-by: Matthew M. Keeler --- .../launchdarkly/sdktest/Representations.java | 2 + .../launchdarkly/sdktest/SdkClientEntity.java | 6 +++ launchdarkly-android-client-sdk/build.gradle | 2 + .../EnvironmentReporterBuilderTest.java | 11 ++++ .../sdk/android/AutoEnvContextModifier.java | 5 +- .../launchdarkly/sdk/android/LDConfig.java | 5 +- .../sdk/android/LDPackageConsts.java | 1 + .../com/launchdarkly/sdk/android/LDUtil.java | 50 +++++++++++++---- .../env/AndroidEnvironmentReporter.java | 39 ++++++++------ .../ApplicationInfoEnvironmentReporter.java | 4 ++ .../integrations/ApplicationInfoBuilder.java | 45 ++++++++++++++-- .../android/subsystems/ApplicationInfo.java | 10 ++++ .../android/AutoEnvContextModifierTest.java | 47 ++++++++++++++++ .../android/HttpConfigurationBuilderTest.java | 7 +-- .../sdk/android/LDConfigTest.java | 4 +- .../launchdarkly/sdk/android/LDUtilTest.java | 15 ++++++ .../ApplicationInfoBuilderTest.java | 53 +++++++++++++++++++ 17 files changed, 267 insertions(+), 39 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java index 559dd558..f67b6589 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java @@ -56,7 +56,9 @@ public static class SdkConfigEventParams { public static class SdkConfigTagParams { String applicationId; + String applicationName; String applicationVersion; + String applicationVersionName; } public static class SdkConfigServiceEndpointParams { diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index 7044409b..f5c28cff 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -313,9 +313,15 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, if (params.tags.applicationId != null) { ab.applicationId(params.tags.applicationId); } + if (params.tags.applicationName != null) { + ab.applicationName(params.tags.applicationName); + } if (params.tags.applicationVersion != null) { ab.applicationVersion(params.tags.applicationVersion); } + if (params.tags.applicationVersionName != null) { + ab.applicationVersionName(params.tags.applicationVersionName); + } builder.applicationInfo(ab); } diff --git a/launchdarkly-android-client-sdk/build.gradle b/launchdarkly-android-client-sdk/build.gradle index 6bb8a18b..ecdd607e 100644 --- a/launchdarkly-android-client-sdk/build.gradle +++ b/launchdarkly-android-client-sdk/build.gradle @@ -59,6 +59,7 @@ configurations.all { ext {} ext.versions = [ "androidAnnotation": "1.2.0", + "androidAppcompat": "1.1.0", "eventsource": "3.0.0", "gson": "2.8.9", "jacksonCore": "2.10.5", @@ -82,6 +83,7 @@ dependencies { implementation("com.google.code.gson:gson:${versions.gson}") implementation("androidx.annotation:annotation:${versions.androidAnnotation}") + implementation("androidx.appcompat:appcompat:${versions.androidAppcompat}") implementation("com.launchdarkly:launchdarkly-java-sdk-internal:${versions.launchdarklyJavaSdkInternal}") implementation("com.launchdarkly:okhttp-eventsource:${versions.eventsource}") implementation("com.squareup.okhttp3:okhttp:${versions.okhttp}") diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java index f88acf8d..8f682fd1 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java @@ -47,6 +47,17 @@ public void defaultsToSDKValues() { Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationName()); Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersion()); Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersionName()); + } + @Test + public void fallBackWhenIDMissing() { + EnvironmentReporterBuilder builder = new EnvironmentReporterBuilder(); + ApplicationInfo manualInfoInput = new ApplicationInfoBuilder().applicationName("imNotAnID!").createApplicationInfo(); + builder.setApplicationInfo(manualInfoInput); + IEnvironmentReporter reporter = builder.build(); + Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationId()); + Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationName()); + Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersion()); + Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersionName()); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 984d0f5d..13cc8816 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Callable; /** @@ -139,8 +140,8 @@ private List makeRecipeList() { new ContextRecipe( ldApplicationKind, () -> LDUtil.urlSafeBase64Hash( - environmentReporter.getApplicationInfo().getApplicationId() + ":" - + environmentReporter.getApplicationInfo().getApplicationVersion() + Objects.toString(environmentReporter.getApplicationInfo().getApplicationId(), "") + ":" + + Objects.toString(environmentReporter.getApplicationInfo().getApplicationVersion(), "") ), applicationCallables ), diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index a91fd3af..be5f1f96 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -44,7 +44,6 @@ public class LDConfig { */ public static final int MIN_BACKGROUND_POLL_INTERVAL_MILLIS = 900_000; - static final String DEFAULT_LOGGER_NAME = "LaunchDarklySdk"; static final LDLogLevel DEFAULT_LOG_LEVEL = LDLogLevel.INFO; static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); @@ -205,7 +204,7 @@ public enum AutoEnvAttributes { private PersistentDataStore persistentDataStore; private LDLogAdapter logAdapter = defaultLogAdapter(); - private String loggerName = DEFAULT_LOGGER_NAME; + private String loggerName = LDPackageConsts.DEFAULT_LOGGER_NAME; private LDLogLevel logLevel = null; /** @@ -607,7 +606,7 @@ public Builder logLevel(LDLogLevel logLevel) { * @see #logLevel(LDLogLevel) */ public Builder loggerName(String loggerName) { - this.loggerName = loggerName == null ? DEFAULT_LOGGER_NAME : loggerName; + this.loggerName = loggerName == null ? LDPackageConsts.DEFAULT_LOGGER_NAME : loggerName; return this; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java index f309292e..6210fe9e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java @@ -4,4 +4,5 @@ public class LDPackageConsts { public static final String SDK_NAME = "android-client-sdk"; public static final String SDK_PLATFORM_NAME = "Android"; public static final String SDK_CLIENT_NAME = "AndroidClient"; + public static final String DEFAULT_LOGGER_NAME = "LaunchDarklySdk"; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java index 0d8f4476..1ae9fdf3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java @@ -3,6 +3,7 @@ import android.util.Base64; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.LogValues; @@ -28,7 +29,7 @@ import okhttp3.Headers; -class LDUtil { +public class LDUtil { static final String AUTH_SCHEME = "api_key "; static final String USER_AGENT_HEADER_VALUE = LDPackageConsts.SDK_CLIENT_NAME + "/" + BuildConfig.VERSION_NAME; @@ -44,8 +45,41 @@ public void onError(Throwable error) { }; } - // Tag values must not be empty, and only contain letters, numbers, `.`, `_`, or `-`. - private static Pattern TAG_VALUE_REGEX = Pattern.compile("^[-a-zA-Z0-9._]+$"); + // Key, kind, tag, and several other system values must not be empty, contain only letters, + // numbers, `.`, `_`, or `-`. + private static final Pattern VALID_CHARS_REGEX = Pattern.compile("^[-a-zA-Z0-9._]+$"); + + /** + * @param s the string to validate + * @return null if valid, otherwise string describing issue + */ + @Nullable + public static String validateStringValue(@NonNull String s) { + if (s.isEmpty()) { + return "Empty string."; + } + + if (s.length() > 64) { + return "Longer than 64 characters."; + } + + if (!VALID_CHARS_REGEX.matcher(s).matches()) { + return "Contains invalid characters."; + } + return null; + } + + /** + * Replaces spaces with hyphens. In the future, if this function is made more generic, + * understand that customer data may already exist and changing this sanitization may + * have adverse consequences. + * + * @param s the string to sanitize + * @return the sanitized string + */ + public static String sanitizeSpaces(String s) { + return s.replace(' ', '-'); + } /** * Builds the "X-LaunchDarkly-Tags" HTTP header out of the configured application info. @@ -56,6 +90,7 @@ public void onError(Throwable error) { static String applicationTagHeader(ApplicationInfo applicationInfo, LDLogger logger) { String[][] tags = { {"applicationId", "application-id", applicationInfo.getApplicationId()}, + {"applicationName", "application-name", applicationInfo.getApplicationName()}, {"applicationVersion", "application-version", applicationInfo.getApplicationVersion()}, {"applicationVersionName", "application-version-name", applicationInfo.getApplicationVersionName()} }; @@ -67,12 +102,9 @@ static String applicationTagHeader(ApplicationInfo applicationInfo, LDLogger log if (tagVal == null) { continue; } - if (!TAG_VALUE_REGEX.matcher(tagVal).matches()) { - logger.warn("Value of ApplicationInfo.{} contained invalid characters and was discarded", javaKey); - continue; - } - if (tagVal.length() > 64) { - logger.warn("Value of ApplicationInfo.{} was longer than 64 characters and was discarded", javaKey); + String error = validateStringValue(tagVal); + if (error != null) { + logger.warn("Value of ApplicationInfo.{} was invalid. {}", javaKey, error); continue; } parts.add(tagKey + "/" + tagVal); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java index e28f2684..698ab64f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import com.launchdarkly.sdk.android.LDPackageConsts; +import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; import java.util.Locale; @@ -30,12 +31,20 @@ public AndroidEnvironmentReporter(Application application) { @Override @NonNull public ApplicationInfo getApplicationInfo() { - return new ApplicationInfo( - getApplicationID(), - getApplicationVersion(), - getApplicationName(), - getApplicationVersionName() - ); + // use a builder here since it has sanitization and validation built in + ApplicationInfoBuilder builder = new ApplicationInfoBuilder(); + builder.applicationId(getApplicationID()); + builder.applicationVersion(getApplicationVersion()); + builder.applicationName(getApplicationName()); + builder.applicationVersionName(getApplicationVersionName()); + ApplicationInfo info = builder.createApplicationInfo(); + + // defer to super if required property applicationID is missing + if (info.getApplicationId() == null) { + info = super.getApplicationInfo(); + } + + return info; } @NonNull @@ -80,12 +89,10 @@ public String getOSVersion() { return Build.VERSION.RELEASE; } - @NonNull private String getApplicationID() { return application.getPackageName(); } - @NonNull private String getApplicationName() { try { PackageManager pm = application.getPackageManager(); @@ -99,7 +106,6 @@ private String getApplicationName() { } } - @NonNull private String getApplicationVersion() { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -109,22 +115,23 @@ private String getApplicationVersion() { } } catch (PackageManager.NameNotFoundException e) { // We don't really expect this to ever happen since we just queried the platform - // for the application name and then immediately used it. Since the code has - // this logical path, the best we can do is defer to the next in the chain. - return super.getApplicationInfo().getApplicationVersion(); + // for the application name and then immediately used it. It is best to set + // this to null, if enough properties are missing, we will fallback to the + // next source in the chain. + return null; } } - @NonNull private String getApplicationVersionName() { try { PackageManager pm = application.getPackageManager(); return pm.getPackageInfo(application.getPackageName(), 0).versionName; } catch (PackageManager.NameNotFoundException e) { // We don't really expect this to ever happen since we just queried the platform - // for the application name and then immediately used it. Since the code has - // this logical path, the best we can do is defer to the next in the chain. - return super.getApplicationInfo().getApplicationVersionName(); + // for the application name and then immediately used it. It is best to set + // this to null, if enough properties are missing, we will fallback to the + // next source in the chain. + return null; } } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java index 7ce24d2c..5314a13c 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java @@ -19,6 +19,10 @@ public ApplicationInfoEnvironmentReporter(ApplicationInfo applicationInfo) { @NonNull @Override public ApplicationInfo getApplicationInfo() { + // defer to super if required property applicationID is missing + if (applicationInfo.getApplicationId() == null) { + return super.getApplicationInfo(); + } return applicationInfo; } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java index c5d98b04..be7e3fc1 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java @@ -1,6 +1,13 @@ package com.launchdarkly.sdk.android.integrations; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.util.Consumer; + +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.android.Components; +import com.launchdarkly.sdk.android.LDAndroidLogging; +import com.launchdarkly.sdk.android.LDUtil; import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; /** @@ -25,11 +32,18 @@ * @since 4.1.0 */ public final class ApplicationInfoBuilder { + @Nullable private String applicationId; + @Nullable private String applicationName; + @Nullable private String applicationVersion; + @Nullable private String applicationVersionName; + @VisibleForTesting + LDLogger logger = LDLogger.withAdapter(LDAndroidLogging.adapter(), ApplicationInfoBuilder.class.getSimpleName()); + /** * Create an empty ApplicationInfoBuilder. * @@ -48,7 +62,7 @@ public ApplicationInfoBuilder() {} * @return the builder */ public ApplicationInfoBuilder applicationId(String applicationId) { - this.applicationId = applicationId; + validatedThenSet("applicationId", s -> this.applicationId = s, applicationId, this.logger); return this; } @@ -63,7 +77,7 @@ public ApplicationInfoBuilder applicationId(String applicationId) { * @return the builder */ public ApplicationInfoBuilder applicationName(String applicationName) { - this.applicationName = applicationName; + validatedThenSet("applicationName", s -> this.applicationName = s, applicationName, this.logger); return this; } @@ -79,7 +93,7 @@ public ApplicationInfoBuilder applicationName(String applicationName) { * @return the builder */ public ApplicationInfoBuilder applicationVersion(String version) { - this.applicationVersion = version; + validatedThenSet("applicationVersion", s -> this.applicationVersion = s, version, this.logger); return this; } @@ -94,7 +108,7 @@ public ApplicationInfoBuilder applicationVersion(String version) { * @return the builder */ public ApplicationInfoBuilder applicationVersionName(String versionName) { - this.applicationVersionName = versionName; + validatedThenSet("applicationVersionName", s -> this.applicationVersionName = s, versionName, this.logger); return this; } @@ -106,4 +120,27 @@ public ApplicationInfoBuilder applicationVersionName(String versionName) { public ApplicationInfo createApplicationInfo() { return new ApplicationInfo(applicationId, applicationVersion, applicationName, applicationVersionName); } + + /** + * @param propertyName the name of the property being set. Used for logging. + * @param propertySetter lambda for setting the property. Java is fun and has predefined + * functional interfaces. + * @param input the string that will be sanitized and validated then applied + * @param logger use for logging. Can you believe that!? + */ + private void validatedThenSet(String propertyName, Consumer propertySetter, String input, LDLogger logger) { + if (input == null) { + propertySetter.accept(input); + return; + } + + String sanitized = LDUtil.sanitizeSpaces(input); + String error = LDUtil.validateStringValue(sanitized); + if (error != null) { + logger.warn("Issue setting {} value '{}'. {}", propertyName, sanitized, error); + return; + } + + propertySetter.accept(sanitized); + } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java index 3363e954..99d43141 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java @@ -1,5 +1,6 @@ package com.launchdarkly.sdk.android.subsystems; +import androidx.annotation.Nullable; import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; /** @@ -10,9 +11,14 @@ * @since 4.1.0 */ public final class ApplicationInfo { + + @Nullable private final String applicationId; + @Nullable private final String applicationName; + @Nullable private final String applicationVersion; + @Nullable private final String applicationVersionName; /** @@ -37,6 +43,7 @@ public ApplicationInfo(String applicationId, String applicationVersion, * * @return the application identifier, or null */ + @Nullable public String getApplicationId() { return applicationId; } @@ -47,6 +54,7 @@ public String getApplicationId() { * * @return the application version, or null */ + @Nullable public String getApplicationVersion() { return applicationVersion; } @@ -56,6 +64,7 @@ public String getApplicationVersion() { * * @return the friendly name of the application, or null */ + @Nullable public String getApplicationName() { return applicationName; } @@ -65,6 +74,7 @@ public String getApplicationName() { * * @return the friendly name of the version, or null */ + @Nullable public String getApplicationVersionName() { return applicationVersionName; } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java index eed7bc27..aab51c58 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -6,6 +6,7 @@ import com.launchdarkly.sdk.ObjectBuilder; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.env.IEnvironmentReporter; +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; import org.junit.Assert; import org.junit.Rule; @@ -156,4 +157,50 @@ public void generatesConsistentKeysAcrossMultipleCalls() { Assert.assertEquals(key1, key2); } + + /** + * Test that when only myID is supplied, hash is hash(myID:) and not hash(myId:null) + */ + @Test + public void generatedApplicationKeyWithVersionMissing() { + PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); + ApplicationInfo info = new ApplicationInfo("myID", null, null, null); + EnvironmentReporterBuilder b = new EnvironmentReporterBuilder(); + b.setApplicationInfo(info); + IEnvironmentReporter reporter = b.build(); + AutoEnvContextModifier underTest = new AutoEnvContextModifier( + wrapper, + reporter, + LDLogger.none() + ); + + LDContext input = LDContext.builder(ContextKind.of("aKind"), "aKey").build(); + LDContext output = underTest.modifyContext(input); + + // it is important that we create this expected context after the code runs because there + // will be persistence side effects + ContextKind applicationKind = ContextKind.of(AutoEnvContextModifier.LD_APPLICATION_KIND); + String expectedApplicationKey = LDUtil.urlSafeBase64Hash(reporter.getApplicationInfo().getApplicationId() + ":"); + + LDContext expectedAppContext = LDContext.builder(applicationKind, expectedApplicationKey) + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION) + .set(AutoEnvContextModifier.ATTR_ID, "myID") + .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") + .build(); + + ContextKind deviceKind = ContextKind.of(AutoEnvContextModifier.LD_DEVICE_KIND); + LDContext expectedDeviceContext = LDContext.builder(deviceKind, wrapper.getOrGenerateContextKey(deviceKind)) + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION) + .set(AutoEnvContextModifier.ATTR_MANUFACTURER, "unknown") + .set(AutoEnvContextModifier.ATTR_MODEL, "unknown") + .set(AutoEnvContextModifier.ATTR_OS, new ObjectBuilder() + .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") + .put(AutoEnvContextModifier.ATTR_NAME, "unknown") + .put(AutoEnvContextModifier.ATTR_VERSION, "unknown") + .build()) + .build(); + + LDContext expectedOutput = LDContext.multiBuilder().add(input).add(expectedAppContext).add(expectedDeviceContext).build(); + Assert.assertEquals(expectedOutput, output); + } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java index 8b78441a..b143c302 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java @@ -21,8 +21,8 @@ private static Map buildBasicHeaders() { Map ret = new HashMap<>(); ret.put("Authorization", LDUtil.AUTH_SCHEME + MOBILE_KEY); ret.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); - ret.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + - BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); + ret.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-name/" + LDPackageConsts.SDK_NAME + + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); return ret; } @@ -71,7 +71,8 @@ public void testApplicationTags() { null, null, null, "", false, null, null, false, null, null, false); HttpConfiguration hc = Components.httpConfiguration() .build(contextWithTags); - assertEquals("application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME , + assertEquals("application-id/" + LDPackageConsts.SDK_NAME + " application-name/" + LDPackageConsts.SDK_NAME + + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME , toMap(hc.getDefaultHeaders()).get("X-LaunchDarkly-Tags")); } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java index 92d99d8f..f5328cd3 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java @@ -100,8 +100,8 @@ public void headersForEnvironmentWithTransform() { expected.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); expected.put("Authorization", "api_key test-key"); - expected.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + - BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); + expected.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-name/" + LDPackageConsts.SDK_NAME + + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); Map headers = headersToMap( LDUtil.makeHttpProperties(clientContext).toHeadersBuilder().build() ); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java index 53662e58..01b1d411 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java @@ -12,4 +12,19 @@ public void testUrlSafeBase64Hash() { String output = LDUtil.urlSafeBase64Hash(input); Assert.assertEquals(expectedOutput, output); } + + @Test + public void testValidateStringValue() { + Assert.assertNotNull(LDUtil.validateStringValue("")); + Assert.assertNotNull(LDUtil.validateStringValue("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEFwhoops")); + Assert.assertNotNull(LDUtil.validateStringValue("#@$%^&")); + Assert.assertNull(LDUtil.validateStringValue("a-Az-Z0-9._-")); + } + + @Test + public void testSanitizeSpaces() { + Assert.assertEquals("", LDUtil.sanitizeSpaces("")); + Assert.assertEquals("--hello--", LDUtil.sanitizeSpaces(" hello ")); + Assert.assertEquals("world", LDUtil.sanitizeSpaces("world")); + } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java new file mode 100644 index 00000000..e6a2aa16 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java @@ -0,0 +1,53 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +import org.junit.Assert; +import org.junit.Test; + +public class ApplicationInfoBuilderTest { + + @Test + public void ignoresInvalidValues() { + ApplicationInfoBuilder b = new ApplicationInfoBuilder(); + b.logger = LDLogger.none(); + b.applicationId("im#invalid"); + b.applicationName("im#invalid"); + b.applicationVersion("im#invalid"); + b.applicationVersionName("im#invalid"); + ApplicationInfo info = b.createApplicationInfo(); + Assert.assertNull(info.getApplicationId()); + Assert.assertNull(info.getApplicationName()); + Assert.assertNull(info.getApplicationVersion()); + Assert.assertNull(info.getApplicationVersionName()); + } + + @Test + public void sanitizesValues() { + ApplicationInfoBuilder b = new ApplicationInfoBuilder(); + b.logger = LDLogger.none(); + b.applicationId("id has spaces"); + b.applicationName("name has spaces"); + b.applicationVersion("version has spaces"); + b.applicationVersionName("version name has spaces"); + ApplicationInfo info = b.createApplicationInfo(); + Assert.assertEquals("id-has-spaces", info.getApplicationId()); + Assert.assertEquals("name-has-spaces", info.getApplicationName()); + Assert.assertEquals("version-has-spaces", info.getApplicationVersion()); + Assert.assertEquals("version-name-has-spaces", info.getApplicationVersionName()); + } + + @Test + public void nullValueIsValid() { + ApplicationInfoBuilder b = new ApplicationInfoBuilder(); + b.logger = LDLogger.none(); + b.applicationId("myID"); // first non-null + ApplicationInfo info = b.createApplicationInfo(); + Assert.assertEquals("myID", info.getApplicationId()); + + b.applicationId(null); // now back to null + ApplicationInfo info2 = b.createApplicationInfo(); + Assert.assertNull(info2.getApplicationId()); + } +}