From cf68865006bc5a4c32b7606c699ddac40d3c36ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B3th=20D=C3=A1niel?= Date: Tue, 13 Feb 2024 15:27:00 +0100 Subject: [PATCH] More v6 test (#47) * Small fixes and extra v6 tests - segment and prerequisite evaluation tests - comparison attribute conversion to canonical String test - optimize getKeyAndValue logic - fix error message - fix cache deserialization * Update version to 10.0.1 * Add test case for skipped sdkKey validation in case of LOCAL_ONLY OverrideBehavior. * Update double format. Add missing required variation ids to json overrides. * Run code format * Fixes based on code review. * Move configSalt validation from deserialization. Added platform based line ending. Moved LogHelper into EvaluateLogger. * Fix configSalt checks --- gradle.properties | 2 +- src/main/AndroidManifest.xml | 4 +- .../java/com/configcat/ConfigCatClient.java | 14 +- .../com/configcat/ConfigCatLogMessages.java | 6 +- .../java/com/configcat/ConfigService.java | 2 +- src/main/java/com/configcat/Entry.java | 2 +- .../java/com/configcat/EvaluateLogger.java | 175 +++- src/main/java/com/configcat/LogHelper.java | 171 ---- .../java/com/configcat/RolloutEvaluator.java | 28 +- .../com/configcat/UserAttributeConverter.java | 33 +- src/main/java/com/configcat/Utils.java | 18 +- .../com/configcat/ConfigCatClientTest.java | 8 + .../com/configcat/ConfigV2EvaluationTest.java | 86 +- .../com/configcat/EntrySerializationTest.java | 4 +- .../java/com/configcat/EvaluationTest.java | 5 +- .../java/com/configcat/EvaluatorTrimTest.java | 13 +- src/test/java/com/configcat/Helpers.java | 8 +- src/test/java/com/configcat/LocalTest.java | 2 +- .../java/com/configcat/ManualPollingTest.java | 6 +- .../java/com/configcat/VariationIdTests.java | 2 +- .../comparison_attribute_conversion.json | 804 ++++++++++++++++++ .../list_truncation/test_list_truncation.json | 56 +- .../evaluation/prerequisite_flag.json | 6 + .../prerequisite_flag_multilevel.txt | 24 + src/test/resources/evaluation/segment.json | 10 +- .../segment_no_user_multi_conditions.txt | 7 + .../resources/test_circulardependency.json | 56 +- 27 files changed, 1283 insertions(+), 269 deletions(-) delete mode 100644 src/main/java/com/configcat/LogHelper.java create mode 100644 src/test/resources/comparison_attribute_conversion.json create mode 100644 src/test/resources/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt create mode 100644 src/test/resources/evaluation/segment/segment_no_user_multi_conditions.txt diff --git a/gradle.properties b/gradle.properties index b136898..185e800 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=10.0.0 +version=10.0.1 org.gradle.jvmargs=-Xmx2g \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f792fe0..bd5df81 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="com.configcat"> - + \ No newline at end of file diff --git a/src/main/java/com/configcat/ConfigCatClient.java b/src/main/java/com/configcat/ConfigCatClient.java index 15a0ce8..dd5bda1 100644 --- a/src/main/java/com/configcat/ConfigCatClient.java +++ b/src/main/java/com/configcat/ConfigCatClient.java @@ -468,10 +468,12 @@ private Map.Entry getKeyAndValueFromSettingsMap(Class classOfT } for (TargetingRule targetingRule : setting.getTargetingRules()) { - if (targetingRule.getSimpleValue() != null && variationId.equals(targetingRule.getSimpleValue().getVariationId())) { - return new AbstractMap.SimpleEntry<>(settingKey, (T) this.parseObject(classOfT, targetingRule.getSimpleValue().getValue(), setting.getType())); - } - if (targetingRule.getPercentageOptions() != null) { + if (targetingRule.getSimpleValue() != null) { + if (variationId.equals(targetingRule.getSimpleValue().getVariationId())) { + return new AbstractMap.SimpleEntry<>(settingKey, (T) this.parseObject(classOfT, targetingRule.getSimpleValue().getValue(), setting.getType())); + + } + } else if (targetingRule.getPercentageOptions() != null) { for (PercentageOption percentageRule : targetingRule.getPercentageOptions()) { if (variationId.equals(percentageRule.getVariationId())) { return new AbstractMap.SimpleEntry<>(settingKey, (T) this.parseObject(classOfT, percentageRule.getValue(), setting.getType())); @@ -522,10 +524,10 @@ else if ((classOfT == Double.class || classOfT == double.class) && settingsValue else if ((classOfT == Boolean.class || classOfT == boolean.class) && settingsValue.getBooleanValue() != null && SettingType.BOOLEAN.equals(settingType)) return settingsValue.getBooleanValue(); - throw new IllegalArgumentException("The type of a setting must match the type of the setting's default value. " + throw new IllegalArgumentException("The type of a setting must match the type of the specified default value. " + "Setting's type was {" + settingType + "} but the default value's type was {" + classOfT + "}. " + "Please use a default value which corresponds to the setting type {" + settingType + "}." - + "Learn more: https://configcat.com/docs/sdk-reference/dotnet/#setting-type-mapping"); + + "Learn more: https://configcat.com/docs/sdk-reference/android/#setting-type-mapping"); } private Class classBySettingType(SettingType settingType) { diff --git a/src/main/java/com/configcat/ConfigCatLogMessages.java b/src/main/java/com/configcat/ConfigCatLogMessages.java index ba7cab7..17a2ee5 100644 --- a/src/main/java/com/configcat/ConfigCatLogMessages.java +++ b/src/main/java/com/configcat/ConfigCatLogMessages.java @@ -200,7 +200,7 @@ public static String getUserObjectMissing(final String key) { * @return The formatted warn message. */ public static String getUserAttributeMissing(final String key, final UserCondition userCondition, final String attributeName) { - return "Cannot evaluate condition (" + LogHelper.formatUserCondition(userCondition) + ") for setting '" + key + "' (the User." + attributeName + " attribute is missing). You should set the User." + attributeName + " attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/"; + return "Cannot evaluate condition (" + EvaluateLogger.formatUserCondition(userCondition) + ") for setting '" + key + "' (the User." + attributeName + " attribute is missing). You should set the User." + attributeName + " attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/"; } /** @@ -224,7 +224,7 @@ public static String getUserAttributeMissing(final String key, final String attr * @return The formatted warn message. */ public static String getUserAttributeInvalid(final String key, final UserCondition userCondition, final String reason, final String attributeName) { - return "Cannot evaluate condition (" + LogHelper.formatUserCondition(userCondition) + ") for setting '" + key + "' (" + reason + "). Please check the User." + attributeName + " attribute and make sure that its value corresponds to the comparison operator."; + return "Cannot evaluate condition (" + EvaluateLogger.formatUserCondition(userCondition) + ") for setting '" + key + "' (" + reason + "). Please check the User." + attributeName + " attribute and make sure that its value corresponds to the comparison operator."; } /** @@ -237,7 +237,7 @@ public static String getUserAttributeInvalid(final String key, final UserConditi * @return The formatted warn message. */ public static String getUserObjectAttributeIsAutoConverted(String key, UserCondition userCondition, String attributeName, String attributeValue) { - return "Evaluation of condition (" + LogHelper.formatUserCondition(userCondition) + ") for setting '" + key + "' may not produce the expected result (the User." + attributeName + " attribute is not a string value, thus it was automatically converted to the string value '" + attributeValue + "'). Please make sure that using a non-string value was intended."; + return "Evaluation of condition (" + EvaluateLogger.formatUserCondition(userCondition) + ") for setting '" + key + "' may not produce the expected result (the User." + attributeName + " attribute is not a string value, thus it was automatically converted to the string value '" + attributeValue + "'). Please make sure that using a non-string value was intended."; } /** diff --git a/src/main/java/com/configcat/ConfigService.java b/src/main/java/com/configcat/ConfigService.java index e6105ac..7c88024 100644 --- a/src/main/java/com/configcat/ConfigService.java +++ b/src/main/java/com/configcat/ConfigService.java @@ -45,7 +45,7 @@ class ConfigService implements Closeable { private String cachedEntryString = ""; private Entry cachedEntry = Entry.EMPTY; private CompletableFuture> runningTask; - private final AtomicBoolean initialized = new AtomicBoolean(false); + private final AtomicBoolean initialized = new AtomicBoolean(false); private final AtomicBoolean offline; private final AtomicBoolean closed = new AtomicBoolean(false); private final String cacheKey; diff --git a/src/main/java/com/configcat/Entry.java b/src/main/java/com/configcat/Entry.java index 048e657..3f5b452 100644 --- a/src/main/java/com/configcat/Entry.java +++ b/src/main/java/com/configcat/Entry.java @@ -67,7 +67,7 @@ public static Entry fromString(String cacheValue) throws IllegalArgumentExceptio throw new IllegalArgumentException("Empty config jsom value."); } try { - Config config = Utils.gson.fromJson(configJson, Config.class); + Config config = Utils.deserializeConfig(configJson); return new Entry(config, eTag, configJson, fetchTimeUnixMillis); } catch (Exception e) { throw new IllegalArgumentException("Invalid config JSON content: " + configJson); diff --git a/src/main/java/com/configcat/EvaluateLogger.java b/src/main/java/com/configcat/EvaluateLogger.java index e5256df..89cd3c1 100644 --- a/src/main/java/com/configcat/EvaluateLogger.java +++ b/src/main/java/com/configcat/EvaluateLogger.java @@ -1,7 +1,20 @@ package com.configcat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + public class EvaluateLogger { + private static final String HASHED_VALUE = ""; + public static final String INVALID_VALUE = ""; + public static final String INVALID_NAME = ""; + public static final String INVALID_REFERENCE = ""; + + private static final int MAX_LIST_ELEMENT = 10; private final StringBuilder stringBuilder = new StringBuilder(); public EvaluateLogger(LogLevel logLevel) { @@ -90,7 +103,7 @@ public final void newLine() { if (!isLoggable) { return; } - stringBuilder.append("\n"); + stringBuilder.append(System.lineSeparator()); for (int i = 0; i < indentLevel; i++) { stringBuilder.append(" "); } @@ -170,7 +183,7 @@ public void logPercentageEvaluationReturnValue(int hashValue, int i, int percent if (!isLoggable) { return; } - String percentageOptionValue = settingsValue != null ? settingsValue.toString() : LogHelper.INVALID_VALUE; + String percentageOptionValue = settingsValue != null ? settingsValue.toString() : INVALID_VALUE; newLine(); append("- Hash value " + hashValue + " selects % option " + (i + 1) + " (" + percentage + "%), '" + percentageOptionValue + "'."); } @@ -194,7 +207,7 @@ public void logSegmentEvaluationResult(SegmentCondition segmentCondition, Segmen String segmentResultComparator = segmentResult ? SegmentComparator.IS_IN_SEGMENT.getName() : SegmentComparator.IS_NOT_IN_SEGMENT.getName(); append("Segment evaluation result: User " + segmentResultComparator + "."); newLine(); - append("Condition (" + LogHelper.formatSegmentFlagCondition(segmentCondition, segment) + ") evaluates to " + result + "."); + append("Condition (" + formatSegmentFlagCondition(segmentCondition, segment) + ") evaluates to " + result + "."); decreaseIndentLevel(); newLine(); append(")"); @@ -208,7 +221,7 @@ public void logSegmentEvaluationError(SegmentCondition segmentCondition, Segment append("Segment evaluation result: " + error + "."); newLine(); - append("Condition (" + LogHelper.formatSegmentFlagCondition(segmentCondition, segment) + ") failed to evaluate."); + append("Condition (" + formatSegmentFlagCondition(segmentCondition, segment) + ") failed to evaluate."); decreaseIndentLevel(); newLine(); append(")"); @@ -230,12 +243,162 @@ public void logPrerequisiteFlagEvaluationResult(PrerequisiteFlagCondition prereq return; } newLine(); - String prerequisiteFlagValueFormat = prerequisiteFlagValue != null ? prerequisiteFlagValue.toString() : LogHelper.INVALID_VALUE; + String prerequisiteFlagValueFormat = prerequisiteFlagValue != null ? prerequisiteFlagValue.toString() : INVALID_VALUE; append("Prerequisite flag evaluation result: '" + prerequisiteFlagValueFormat + "'."); newLine(); - append("Condition (" + LogHelper.formatPrerequisiteFlagCondition(prerequisiteFlagCondition) + ") evaluates to " + result + "."); + append("Condition (" + formatPrerequisiteFlagCondition(prerequisiteFlagCondition) + ") evaluates to " + result + "."); decreaseIndentLevel(); newLine(); append(")"); } + + private static String formatStringListComparisonValue(String[] comparisonValue, boolean isSensitive) { + if (comparisonValue == null) { + return INVALID_VALUE; + } + List comparisonValues = new ArrayList<>(Arrays.asList(comparisonValue)); + if (comparisonValues.isEmpty()) { + return INVALID_VALUE; + } + String formattedList; + if (isSensitive) { + String sensitivePostFix = comparisonValues.size() == 1 ? "value" : "values"; + formattedList = "<" + comparisonValues.size() + " hashed " + sensitivePostFix + ">"; + } else { + String listPostFix = ""; + if (comparisonValues.size() > MAX_LIST_ELEMENT) { + int count = comparisonValues.size() - MAX_LIST_ELEMENT; + String countPostFix = count == 1 ? "value" : "values"; + listPostFix = ", ... <" + count + " more " + countPostFix + ">"; + } + List subList = comparisonValues.subList(0, Math.min(MAX_LIST_ELEMENT, comparisonValues.size())); + StringBuilder formatListBuilder = new StringBuilder(); + int subListSize = subList.size(); + for (int i = 0; i < subListSize; i++) { + formatListBuilder.append("'").append(subList.get(i)).append("'"); + if (i != subListSize - 1) { + formatListBuilder.append(", "); + } + } + formatListBuilder.append(listPostFix); + formattedList = formatListBuilder.toString(); + } + + return "[" + formattedList + "]"; + } + + private static String formatStringComparisonValue(String comparisonValue, boolean isSensitive) { + return "'" + (isSensitive ? HASHED_VALUE : comparisonValue) + "'"; + } + + private static String formatDoubleComparisonValue(Double comparisonValue, boolean isDate) { + if (comparisonValue == null) { + return INVALID_VALUE; + } + DecimalFormat decimalFormat = new DecimalFormat("0.#####"); + decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.UK)); + if (isDate) { + return "'" + decimalFormat.format(comparisonValue) + "' (" + DateTimeUtils.doubleToFormattedUTC(comparisonValue) + " UTC)"; + } + return "'" + decimalFormat.format(comparisonValue) + "'"; + } + public static String formatUserCondition(UserCondition userCondition) { + UserComparator userComparator = UserComparator.fromId(userCondition.getComparator()); + if (userComparator == null) { + throw new IllegalArgumentException("Comparison operator is invalid."); + } + String comparisonValue; + switch (userComparator) { + case IS_ONE_OF: + case IS_NOT_ONE_OF: + case CONTAINS_ANY_OF: + case NOT_CONTAINS_ANY_OF: + case SEMVER_IS_ONE_OF: + case SEMVER_IS_NOT_ONE_OF: + case TEXT_STARTS_WITH: + case TEXT_NOT_STARTS_WITH: + case TEXT_ENDS_WITH: + case TEXT_NOT_ENDS_WITH: + case TEXT_ARRAY_CONTAINS: + case TEXT_ARRAY_NOT_CONTAINS: + comparisonValue = formatStringListComparisonValue(userCondition.getStringArrayValue(), false); + break; + case SEMVER_LESS: + case SEMVER_LESS_EQUALS: + case SEMVER_GREATER: + case SEMVER_GREATER_EQUALS: + case TEXT_EQUALS: + case TEXT_NOT_EQUALS: + comparisonValue = formatStringComparisonValue(userCondition.getStringValue(), false); + break; + case NUMBER_EQUALS: + case NUMBER_NOT_EQUALS: + case NUMBER_LESS: + case NUMBER_LESS_EQUALS: + case NUMBER_GREATER: + case NUMBER_GREATER_EQUALS: + comparisonValue = formatDoubleComparisonValue(userCondition.getDoubleValue(), false); + break; + case SENSITIVE_IS_ONE_OF: + case SENSITIVE_IS_NOT_ONE_OF: + case HASHED_STARTS_WITH: + case HASHED_NOT_STARTS_WITH: + case HASHED_ENDS_WITH: + case HASHED_NOT_ENDS_WITH: + case HASHED_ARRAY_CONTAINS: + case HASHED_ARRAY_NOT_CONTAINS: + comparisonValue = formatStringListComparisonValue(userCondition.getStringArrayValue(), true); + break; + case DATE_BEFORE: + case DATE_AFTER: + comparisonValue = formatDoubleComparisonValue(userCondition.getDoubleValue(), true); + break; + case HASHED_EQUALS: + case HASHED_NOT_EQUALS: + comparisonValue = formatStringComparisonValue(userCondition.getStringValue(), true); + break; + default: + comparisonValue = INVALID_VALUE; + } + + return "User." + userCondition.getComparisonAttribute() + " " + userComparator.getName() + " " + comparisonValue; + } + + public static String formatSegmentFlagCondition(SegmentCondition segmentCondition, Segment segment) { + String segmentName; + if (segment != null) { + segmentName = segment.getName(); + if (segmentName == null || segmentName.isEmpty()) { + segmentName = INVALID_NAME; + } + } else { + segmentName = INVALID_REFERENCE; + } + SegmentComparator segmentComparator = SegmentComparator.fromId(segmentCondition.getSegmentComparator()); + if (segmentComparator == null) { + throw new IllegalArgumentException("Segment comparison operator is invalid."); + } + return "User " + segmentComparator.getName() + " '" + segmentName + "'"; + } + + public static String formatPrerequisiteFlagCondition(PrerequisiteFlagCondition prerequisiteFlagCondition) { + String prerequisiteFlagKey = prerequisiteFlagCondition.getPrerequisiteFlagKey(); + PrerequisiteComparator prerequisiteComparator = PrerequisiteComparator.fromId(prerequisiteFlagCondition.getPrerequisiteComparator()); + if (prerequisiteComparator == null) { + throw new IllegalArgumentException("Prerequisite Flag comparison operator is invalid."); + } + SettingsValue prerequisiteValue = prerequisiteFlagCondition.getValue(); + String comparisonValue = prerequisiteValue == null ? INVALID_VALUE : prerequisiteValue.toString(); + return "Flag '" + prerequisiteFlagKey + "' " + prerequisiteComparator.getName() + " '" + comparisonValue + "'"; + } + + + public static String formatCircularDependencyList(List visitedKeys, String key) { + StringBuilder builder = new StringBuilder(); + for (String visitedKey : visitedKeys) { + builder.append("'").append(visitedKey).append("' -> "); + } + builder.append("'").append(key).append("'"); + return builder.toString(); + } } diff --git a/src/main/java/com/configcat/LogHelper.java b/src/main/java/com/configcat/LogHelper.java deleted file mode 100644 index e6ed106..0000000 --- a/src/main/java/com/configcat/LogHelper.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.configcat; - -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -final class LogHelper { - - private static final String HASHED_VALUE = ""; - public static final String INVALID_VALUE = ""; - public static final String INVALID_NAME = ""; - public static final String INVALID_REFERENCE = ""; - - private static final int MAX_LIST_ELEMENT = 10; - - private LogHelper() {/* prevent from instantiation*/} - - private static String formatStringListComparisonValue(String[] comparisonValue, boolean isSensitive) { - if (comparisonValue == null) { - return INVALID_VALUE; - } - List comparisonValues = new ArrayList<>(Arrays.asList(comparisonValue)); - if (comparisonValues.isEmpty()) { - return INVALID_VALUE; - } - String formattedList; - if (isSensitive) { - String sensitivePostFix = comparisonValues.size() == 1 ? "value" : "values"; - formattedList = "<" + comparisonValues.size() + " hashed " + sensitivePostFix + ">"; - } else { - String listPostFix = ""; - if (comparisonValues.size() > MAX_LIST_ELEMENT) { - int count = comparisonValues.size() - MAX_LIST_ELEMENT; - String countPostFix = count == 1 ? "value" : "values"; - listPostFix = ", ... <" + count + " more " + countPostFix + ">"; - } - List subList = comparisonValues.subList(0, Math.min(MAX_LIST_ELEMENT, comparisonValues.size())); - StringBuilder formatListBuilder = new StringBuilder(); - int subListSize = subList.size(); - for (int i = 0; i < subListSize; i++) { - formatListBuilder.append("'").append(subList.get(i)).append("'"); - if (i != subListSize - 1) { - formatListBuilder.append(", "); - } - } - formatListBuilder.append(listPostFix); - formattedList = formatListBuilder.toString(); - } - - return "[" + formattedList + "]"; - } - - private static String formatStringComparisonValue(String comparisonValue, boolean isSensitive) { - return "'" + (isSensitive ? HASHED_VALUE : comparisonValue) + "'"; - } - - private static String formatDoubleComparisonValue(Double comparisonValue, boolean isDate) { - if (comparisonValue == null) { - return INVALID_VALUE; - } - DecimalFormat decimalFormat = new DecimalFormat("0.#####"); - decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.UK)); - if (isDate) { - return "'" + decimalFormat.format(comparisonValue) + "' (" + DateTimeUtils.doubleToFormattedUTC(comparisonValue) + " UTC)"; - } - return "'" + decimalFormat.format(comparisonValue) + "'"; - } - - public static String formatUserCondition(UserCondition userCondition) { - UserComparator userComparator = UserComparator.fromId(userCondition.getComparator()); - if (userComparator == null) { - throw new IllegalArgumentException("Comparison operator is invalid."); - } - String comparisonValue; - switch (userComparator) { - case IS_ONE_OF: - case IS_NOT_ONE_OF: - case CONTAINS_ANY_OF: - case NOT_CONTAINS_ANY_OF: - case SEMVER_IS_ONE_OF: - case SEMVER_IS_NOT_ONE_OF: - case TEXT_STARTS_WITH: - case TEXT_NOT_STARTS_WITH: - case TEXT_ENDS_WITH: - case TEXT_NOT_ENDS_WITH: - case TEXT_ARRAY_CONTAINS: - case TEXT_ARRAY_NOT_CONTAINS: - comparisonValue = formatStringListComparisonValue(userCondition.getStringArrayValue(), false); - break; - case SEMVER_LESS: - case SEMVER_LESS_EQUALS: - case SEMVER_GREATER: - case SEMVER_GREATER_EQUALS: - case TEXT_EQUALS: - case TEXT_NOT_EQUALS: - comparisonValue = formatStringComparisonValue(userCondition.getStringValue(), false); - break; - case NUMBER_EQUALS: - case NUMBER_NOT_EQUALS: - case NUMBER_LESS: - case NUMBER_LESS_EQUALS: - case NUMBER_GREATER: - case NUMBER_GREATER_EQUALS: - comparisonValue = formatDoubleComparisonValue(userCondition.getDoubleValue(), false); - break; - case SENSITIVE_IS_ONE_OF: - case SENSITIVE_IS_NOT_ONE_OF: - case HASHED_STARTS_WITH: - case HASHED_NOT_STARTS_WITH: - case HASHED_ENDS_WITH: - case HASHED_NOT_ENDS_WITH: - case HASHED_ARRAY_CONTAINS: - case HASHED_ARRAY_NOT_CONTAINS: - comparisonValue = formatStringListComparisonValue(userCondition.getStringArrayValue(), true); - break; - case DATE_BEFORE: - case DATE_AFTER: - comparisonValue = formatDoubleComparisonValue(userCondition.getDoubleValue(), true); - break; - case HASHED_EQUALS: - case HASHED_NOT_EQUALS: - comparisonValue = formatStringComparisonValue(userCondition.getStringValue(), true); - break; - default: - comparisonValue = INVALID_VALUE; - } - - return "User." + userCondition.getComparisonAttribute() + " " + userComparator.getName() + " " + comparisonValue; - } - - public static String formatSegmentFlagCondition(SegmentCondition segmentCondition, Segment segment) { - String segmentName; - if (segment != null) { - segmentName = segment.getName(); - if (segmentName == null || segmentName.isEmpty()) { - segmentName = INVALID_NAME; - } - } else { - segmentName = INVALID_REFERENCE; - } - SegmentComparator segmentComparator = SegmentComparator.fromId(segmentCondition.getSegmentComparator()); - if (segmentComparator == null) { - throw new IllegalArgumentException("Segment comparison operator is invalid."); - } - return "User " + segmentComparator.getName() + " '" + segmentName + "'"; - } - - public static String formatPrerequisiteFlagCondition(PrerequisiteFlagCondition prerequisiteFlagCondition) { - String prerequisiteFlagKey = prerequisiteFlagCondition.getPrerequisiteFlagKey(); - PrerequisiteComparator prerequisiteComparator = PrerequisiteComparator.fromId(prerequisiteFlagCondition.getPrerequisiteComparator()); - if (prerequisiteComparator == null) { - throw new IllegalArgumentException("Prerequisite Flag comparison operator is invalid."); - } - SettingsValue prerequisiteValue = prerequisiteFlagCondition.getValue(); - String comparisonValue = prerequisiteValue == null ? INVALID_VALUE : prerequisiteValue.toString(); - return "Flag '" + prerequisiteFlagKey + "' " + prerequisiteComparator.getName() + " '" + comparisonValue + "'"; - } - - - public static String formatCircularDependencyList(List visitedKeys, String key) { - StringBuilder builder = new StringBuilder(); - for (String visitedKey : visitedKeys) { - builder.append("'").append(visitedKey).append("' -> "); - } - builder.append("'").append(key).append("'"); - return builder.toString(); - } -} diff --git a/src/main/java/com/configcat/RolloutEvaluator.java b/src/main/java/com/configcat/RolloutEvaluator.java index 2b484e9..b83a50e 100644 --- a/src/main/java/com/configcat/RolloutEvaluator.java +++ b/src/main/java/com/configcat/RolloutEvaluator.java @@ -1,7 +1,6 @@ package com.configcat; import de.skuzzle.semantic.Version; -import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; import java.nio.charset.StandardCharsets; @@ -172,7 +171,7 @@ private boolean evaluateConditions(ConditionAccessor[] conditions, TargetingRule } private boolean evaluateUserCondition(UserCondition userCondition, EvaluationContext context, String configSalt, String contextSalt, EvaluateLogger evaluateLogger) { - evaluateLogger.append(LogHelper.formatUserCondition(userCondition)); + evaluateLogger.append(EvaluateLogger.formatUserCondition(userCondition)); if (context.getUser() == null) { if (!context.isUserMissing()) { @@ -246,7 +245,7 @@ private boolean evaluateUserCondition(UserCondition userCondition, EvaluationCon case HASHED_NOT_STARTS_WITH: case HASHED_NOT_ENDS_WITH: String userAttributeForHashedStartEnd = getUserAttributeAsString(context.getKey(), userCondition, comparisonAttribute, userAttributeValue); - return evaluateHashedStartOrEndsWith(userCondition, configSalt, contextSalt, comparator, userAttributeForHashedStartEnd); + return evaluateHashedStartOrEndsWith(userCondition, ensureConfigSalt(configSalt), contextSalt, comparator, userAttributeForHashedStartEnd); case TEXT_STARTS_WITH: case TEXT_NOT_STARTS_WITH: boolean negateTextStartWith = UserComparator.TEXT_NOT_STARTS_WITH.equals(comparator); @@ -360,7 +359,7 @@ private boolean evaluateArrayContains(UserCondition userCondition, String config return false; } for (String userContainsValue : userContainsValues) { - String userContainsValueConverted = hashedArrayContains ? getSaltedUserValue(userContainsValue, configSalt, contextSalt) : userContainsValue; + String userContainsValueConverted = hashedArrayContains ? getSaltedUserValue(userContainsValue, ensureConfigSalt(configSalt), contextSalt) : userContainsValue; for (String inValuesElement : comparisonValues) { if (ensureComparisonValue(inValuesElement).equals(userContainsValueConverted)) { return !negateArrayContains; @@ -438,7 +437,7 @@ private boolean evaluateHashedStartOrEndsWith(UserCondition userCondition, Strin private boolean evaluateEquals(UserCondition userCondition, String configSalt, String contextSalt, String userValue, boolean negateEquals, boolean hashedEquals) { String comparisonValue = ensureComparisonValue(userCondition.getStringValue()); - String valueEquals = hashedEquals ? getSaltedUserValue(userValue, configSalt, contextSalt) : userValue; + String valueEquals = hashedEquals ? getSaltedUserValue(userValue, ensureConfigSalt(configSalt), contextSalt) : userValue; return negateEquals != valueEquals.equals(comparisonValue); } @@ -451,7 +450,7 @@ private boolean evaluateDate(UserCondition userCondition, UserComparator compara private boolean evaluateIsOneOf(UserCondition userCondition, String configSalt, String contextSalt, String userValue, boolean negateIsOneOf, boolean sensitiveIsOneOf) { String[] comparisonValues = ensureComparisonValue(userCondition.getStringArrayValue()); - String userIsOneOfValue = sensitiveIsOneOf ? getSaltedUserValue(userValue, configSalt, contextSalt) : userValue; + String userIsOneOfValue = sensitiveIsOneOf ? getSaltedUserValue(userValue, ensureConfigSalt(configSalt), contextSalt) : userValue; for (String inValuesElement : comparisonValues) { if (ensureComparisonValue(inValuesElement).equals(userIsOneOfValue)) { @@ -540,7 +539,7 @@ private boolean evaluateSegmentCondition(SegmentCondition segmentCondition, Eval if (segmentIndex < segments.length) { segment = segments[segmentIndex]; } - evaluateLogger.append(LogHelper.formatSegmentFlagCondition(segmentCondition, segment)); + evaluateLogger.append(EvaluateLogger.formatSegmentFlagCondition(segmentCondition, segment)); if (context.getUser() == null) { if (!context.isUserMissing()) { @@ -589,7 +588,7 @@ private boolean evaluateSegmentCondition(SegmentCondition segmentCondition, Eval private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prerequisiteFlagCondition, EvaluationContext context, EvaluateLogger evaluateLogger) { - evaluateLogger.append(LogHelper.formatPrerequisiteFlagCondition(prerequisiteFlagCondition)); + evaluateLogger.append(EvaluateLogger.formatPrerequisiteFlagCondition(prerequisiteFlagCondition)); String prerequisiteFlagKey = prerequisiteFlagCondition.getPrerequisiteFlagKey(); Setting prerequsiteFlagSetting = context.getSettings().get(prerequisiteFlagKey); @@ -611,7 +610,7 @@ private boolean evaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition prer } visitedKeys.add(context.getKey()); if (visitedKeys.contains(prerequisiteFlagKey)) { - String dependencyCycle = LogHelper.formatCircularDependencyList(visitedKeys, prerequisiteFlagKey); + String dependencyCycle = EvaluateLogger.formatCircularDependencyList(visitedKeys, prerequisiteFlagKey); throw new IllegalArgumentException("Circular dependency detected between the following depending flags: " + dependencyCycle + "."); } @@ -662,7 +661,7 @@ private EvaluationResult evaluatePercentageOptions(PercentageOption[] percentage } String percentageOptionAttributeValue; String percentageOptionAttributeName = percentageOptionAttribute; - if (percentageOptionAttributeName == null || percentageOptionAttributeName.isEmpty()) { + if (percentageOptionAttributeName == null) { percentageOptionAttributeName = "Identifier"; percentageOptionAttributeValue = context.getUser().getIdentifier(); } else { @@ -680,7 +679,7 @@ private EvaluationResult evaluatePercentageOptions(PercentageOption[] percentage evaluateLogger.logPercentageOptionEvaluation(percentageOptionAttributeName); String hashCandidate = context.getKey() + percentageOptionAttributeValue; int scale = 100; - String hexHash = new String(Hex.encodeHex(DigestUtils.sha1(hashCandidate))).substring(0, 7); + String hexHash = DigestUtils.sha1Hex(hashCandidate.getBytes(StandardCharsets.UTF_8)).substring(0, 7); int longHash = Integer.parseInt(hexHash, 16); int scaled = longHash % scale; evaluateLogger.logPercentageOptionEvaluationHash(percentageOptionAttributeName, scaled); @@ -704,6 +703,13 @@ private static T ensureComparisonValue(T value) { } return value; } + + private static String ensureConfigSalt(String configSalt){ + if(configSalt == null){ + throw new IllegalArgumentException("Config JSON salt is missing."); + } + return configSalt; + } } class RolloutEvaluatorException extends RuntimeException { diff --git a/src/main/java/com/configcat/UserAttributeConverter.java b/src/main/java/com/configcat/UserAttributeConverter.java index cb2f5f5..ab62cac 100644 --- a/src/main/java/com/configcat/UserAttributeConverter.java +++ b/src/main/java/com/configcat/UserAttributeConverter.java @@ -1,7 +1,10 @@ package com.configcat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.Date; import java.util.List; +import java.util.Locale; final class UserAttributeConverter { @@ -20,11 +23,39 @@ public static String userAttributeToString(Object userAttribute) { } if (userAttribute instanceof Date) { Date userAttributeDate = (Date) userAttribute; - return String.valueOf(DateTimeUtils.getUnixSeconds(userAttributeDate)); + return doubleToString(DateTimeUtils.getUnixSeconds(userAttributeDate)); + } + if (userAttribute instanceof Double) { + return doubleToString((Double) userAttribute); } return userAttribute.toString(); } + private static String doubleToString(Double doubleToString) { + + // Handle Double.NaN, Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY + if (doubleToString.isNaN() || doubleToString.isInfinite()) { + return doubleToString.toString(); + } + + // To get similar result between different SDKs the Double value format is modified. + // Between 1e-6 and 1e21 we don't use scientific-notation. Over these limits scientific-notation used but the + // ExponentSeparator replaced with "e" and "e+". + // "." used as decimal separator in all cases. + double abs = Math.abs(doubleToString); + DecimalFormat fmt = 1e-6 <= abs && abs < 1e21 + ? new DecimalFormat("#.#################") + : new DecimalFormat("#.#################E0"); + DecimalFormatSymbols SYMBOLS = DecimalFormatSymbols.getInstance(Locale.UK); + if (abs > 1) { + SYMBOLS.setExponentSeparator("e+"); + } else { + SYMBOLS.setExponentSeparator("e"); + } + fmt.setDecimalFormatSymbols(SYMBOLS); + return fmt.format(doubleToString); + } + public static Double userAttributeToDouble(Object userAttribute) { if (userAttribute == null) { return null; diff --git a/src/main/java/com/configcat/Utils.java b/src/main/java/com/configcat/Utils.java index 4af9789..9029e14 100644 --- a/src/main/java/com/configcat/Utils.java +++ b/src/main/java/com/configcat/Utils.java @@ -3,28 +3,14 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.util.Locale; - - final class Utils { private Utils() { /* prevent from instantiation*/ } - static final Gson gson = new GsonBuilder().create(); - - public static DecimalFormat getDecimalFormat() { - DecimalFormat decimalFormat = new DecimalFormat("0.#####"); - decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.UK)); - return decimalFormat; - } + static final Gson gson = new GsonBuilder().disableHtmlEscaping().create(); public static Config deserializeConfig(String json) { Config config = Utils.gson.fromJson(json, Config.class); String salt = config.getPreferences().getSalt(); - if (salt == null || salt.isEmpty()) { - throw new IllegalArgumentException("Config JSON salt is missing."); - } Segment[] segments = config.getSegments(); if (segments == null) { segments = new Segment[]{}; @@ -44,7 +30,7 @@ private Constants() { /* prevent from instantiation*/ } static final long DISTANT_PAST = 0; static final String CONFIG_JSON_NAME = "config_v6.json"; static final String SERIALIZATION_FORMAT_VERSION = "v2"; - static final String VERSION = "10.0.0"; + static final String VERSION = "10.0.1"; static final String SDK_KEY_PROXY_PREFIX = "configcat-proxy/"; static final String SDK_KEY_PREFIX = "configcat-sdk-1"; diff --git a/src/test/java/com/configcat/ConfigCatClientTest.java b/src/test/java/com/configcat/ConfigCatClientTest.java index 26dbb73..241d179 100644 --- a/src/test/java/com/configcat/ConfigCatClientTest.java +++ b/src/test/java/com/configcat/ConfigCatClientTest.java @@ -80,6 +80,14 @@ void testSDKKeyValidation() throws IOException { IllegalArgumentException builderException = assertThrows( IllegalArgumentException.class, () -> ConfigCatClient.get("configcat-proxy/", options -> options.baseUrl("https://my-configcat-proxy"))); assertEquals("SDK Key 'configcat-proxy/' is invalid.", builderException.getMessage()); + + //TEST OverrideBehaviour.LOCAL_ONLY skip sdkKey validation + ConfigCatClient clientLocalOnly = ConfigCatClient.get("sdk-key-90123456789012", options -> { + options.flagOverrides(OverrideDataSource.map(new HashMap<>()), OverrideBehaviour.LOCAL_ONLY); + }); + assertNotNull(clientLocalOnly); + + ConfigCatClient.closeAll(); } @Test diff --git a/src/test/java/com/configcat/ConfigV2EvaluationTest.java b/src/test/java/com/configcat/ConfigV2EvaluationTest.java index 5082f99..27fa7be 100644 --- a/src/test/java/com/configcat/ConfigV2EvaluationTest.java +++ b/src/test/java/com/configcat/ConfigV2EvaluationTest.java @@ -12,13 +12,16 @@ import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.junit.Assert.fail; + public class ConfigV2EvaluationTest { private static Stream testDataForRuleAndPercentageOptionTest() { @@ -87,6 +90,37 @@ private static Stream testDataForPrerequisiteFlagOverrideTest() { ); } + private static Stream testDataComparisonAttributeConversionToCanonicalStringRepresentationTest() { + return Stream.of( + Arguments.of("numberToStringConversion", .12345, "1"), + Arguments.of("numberToStringConversion", .12345f, "1"), + Arguments.of("numberToStringConversion", 0.12345f, "1"), + Arguments.of("numberToStringConversionInt", (byte) 125, "4"), + Arguments.of("numberToStringConversionInt", (short) 125, "4"), + Arguments.of("numberToStringConversionInt", 125, "4"), + Arguments.of("numberToStringConversionInt", 125L, "4"), + Arguments.of("numberToStringConversionPositiveExp", -1.23456789e96, "2"), + Arguments.of("numberToStringConversionNegativeExp", -12345.6789E-100, "4"), + Arguments.of("numberToStringConversionNaN", Double.NaN, "3"), + Arguments.of("numberToStringConversionPositiveInf", Double.POSITIVE_INFINITY, "4"), + Arguments.of("numberToStringConversionNegativeInf", Double.NEGATIVE_INFINITY, "3"), + Arguments.of("numberToStringConversionPositiveExp", -1.23456789e96d, "2"), + Arguments.of("numberToStringConversionNegativeExp", -12345.6789E-100d, "4"), + Arguments.of("numberToStringConversionNaN", Float.NaN, "3"), + Arguments.of("numberToStringConversionPositiveInf", Float.POSITIVE_INFINITY, "4"), + Arguments.of("numberToStringConversionNegativeInf", Float.NEGATIVE_INFINITY, "3"), + Arguments.of("dateToStringConversion", "date:2023-03-31T23:59:59.999Z", "3"), + Arguments.of("dateToStringConversion", 1680307199.999, "3"), + Arguments.of("dateToStringConversionNaN", Double.NaN, "3"), + Arguments.of("dateToStringConversionPositiveInf", Double.POSITIVE_INFINITY, "1"), + Arguments.of("dateToStringConversionNegativeInf", Double.NEGATIVE_INFINITY, "5"), + Arguments.of("stringArrayToStringConversion", new String[]{"read", "Write", " eXecute "}, "4"), + Arguments.of("stringArrayToStringConversionEmpty", new String[0], "5"), + Arguments.of("stringArrayToStringConversionSpecialChars", new String[]{"+<>%\"'\\/\t\r\n"}, "3"), + Arguments.of("stringArrayToStringConversionUnicode", "specialCharacter:specialCharacters.txt", "2") + ); + } + @ParameterizedTest @MethodSource("testDataForRuleAndPercentageOptionTest") public void matchedEvaluationRuleAndPercentageOption(String userId, String email, String percentageBaseCustom, String expectedValue, boolean expectedTargetingRule, boolean expectedPercentageOption) throws IOException { @@ -210,4 +244,50 @@ public void prerequisiteFlagOverrideTest(String key, String userId, String email Assert.assertEquals(expectedValue, value); ConfigCatClient.closeAll(); } + + @ParameterizedTest + @MethodSource("testDataComparisonAttributeConversionToCanonicalStringRepresentationTest") + public void comparisonAttributeConversionToCanonicalStringRepresentationTest(String key, Object userAttribute, String expectedValue) throws IOException, ParseException { + MockWebServer server = new MockWebServer(); + server.start(); + + server.enqueue(new MockResponse().setResponseCode(200).setBody(Helpers.readFile("comparison_attribute_conversion.json"))); + + ConfigCatClient client = ConfigCatClient.get(Helpers.SDK_KEY, options -> { + options.pollingMode(PollingModes.lazyLoad(2)); + options.baseUrl(server.url("/").toString()); + }); + + if (userAttribute instanceof String) { + String userAttributeString = (String) userAttribute; + if (userAttributeString.startsWith("date:")) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSSSSSS'Z'"); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + userAttribute = sdf.parse(userAttributeString.substring(5)); + } + if (userAttributeString.startsWith("specialCharacter:")) { + ClassLoader classLoader = getClass().getClassLoader(); + + Scanner scanner = new Scanner(new File(Objects.requireNonNull(classLoader.getResource(userAttributeString.substring(17))).getFile()), "UTF-8"); + if (!scanner.hasNext()) { + fail(); + } + String specialCharacters = scanner.nextLine(); + userAttribute = new String[]{specialCharacters}; + } + + } + Map custom = new HashMap<>(); + custom.put("Custom1", userAttribute); + + User user = User.newBuilder() + .custom(custom) + .build("12345"); + + String value = client.getValue(String.class, key, user, "default"); + + Assert.assertEquals(expectedValue, value); + + ConfigCatClient.closeAll(); + } } diff --git a/src/test/java/com/configcat/EntrySerializationTest.java b/src/test/java/com/configcat/EntrySerializationTest.java index a91c44c..a3bc3a1 100644 --- a/src/test/java/com/configcat/EntrySerializationTest.java +++ b/src/test/java/com/configcat/EntrySerializationTest.java @@ -6,7 +6,7 @@ public class EntrySerializationTest { - private static final String TEST_JSON = "{ f: { fakeKey: { v: { s: %s }, t: %s, p: [], r: [] } } }"; + private static final String TEST_JSON = "{ p: { s: 'test-salt'}, f: { fakeKey: { v: { s: %s }, t: %s, p: [], r: [] } } }"; private static final String SERIALIZED_DATA = "%s\n%s\n%s"; @@ -24,7 +24,7 @@ void serialize() { @Test void payloadSerializationPlatformIndependent() { - String payloadTestConfigJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0,\"s\": \"test-slat\"},\"f\":{\"testKey\":{\"v\":{\"s\":\"testValue\"},\"t\":1,\"p\":[],\"r\":[]}}}"; + String payloadTestConfigJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0,\"s\": \"test-salt\"},\"f\":{\"testKey\":{\"v\":{\"s\":\"testValue\"},\"t\":1,\"p\":[],\"r\":[]}}}"; Config config = Utils.gson.fromJson(payloadTestConfigJson, Config.class); Entry entry = new Entry(config, "test-etag", payloadTestConfigJson, 1686756435844L); diff --git a/src/test/java/com/configcat/EvaluationTest.java b/src/test/java/com/configcat/EvaluationTest.java index 896ebc2..cd64db8 100644 --- a/src/test/java/com/configcat/EvaluationTest.java +++ b/src/test/java/com/configcat/EvaluationTest.java @@ -98,7 +98,7 @@ void testEvaluation(String testDescriptorName) throws IOException { typeOfExpectedResult = Integer.class; } else if (settingKey.startsWith("double") || settingKey.startsWith("decimal") || settingKey.startsWith("mainDouble")) { typeOfExpectedResult = Double.class; - } else if (settingKey.startsWith("boolean") || settingKey.startsWith("bool") || settingKey.startsWith("mainBool") || settingKey.equals("developerAndBetaUserSegment") || settingKey.equals("featureWithSegmentTargeting") || settingKey.equals("featureWithNegatedSegmentTargeting") || settingKey.equals("featureWithNegatedSegmentTargetingCleartext")) { + } else if (settingKey.startsWith("boolean") || settingKey.startsWith("bool") || settingKey.startsWith("mainBool") || settingKey.equals("developerAndBetaUserSegment") || settingKey.equals("featureWithSegmentTargeting") || settingKey.equals("featureWithNegatedSegmentTargeting") || settingKey.equals("featureWithNegatedSegmentTargetingCleartext") || settingKey.equals("featureWithSegmentTargetingMultipleConditions")) { typeOfExpectedResult = Boolean.class; } else { //handle as String in any other case @@ -114,11 +114,12 @@ void testEvaluation(String testDescriptorName) throws IOException { errors.add(String.format("Return value mismatch for test: %s Test Key: %s Expected: %s, Result: %s \n", testDescriptorName, settingKey, returnValue, result)); } String expectedLog = Helpers.readFile(EVALUATION_FOLDER + testDescriptorName + "/" + test.getExpectedLog()); + expectedLog = expectedLog.replaceAll("\r\n", "\n"); StringBuilder logResultBuilder = new StringBuilder(); List logsList = listAppender.list; for (ILoggingEvent logEvent : logsList) { - logResultBuilder.append(formatLogLevel(logEvent.getLevel())).append(" ").append(logEvent.getFormattedMessage()).append("\n"); + logResultBuilder.append(formatLogLevel(logEvent.getLevel())).append(" ").append(logEvent.getFormattedMessage().replaceAll("\r\n", "\n")).append("\n"); } String logResult = logResultBuilder.toString(); if (!expectedLog.equals(logResult)) { diff --git a/src/test/java/com/configcat/EvaluatorTrimTest.java b/src/test/java/com/configcat/EvaluatorTrimTest.java index 53a926e..a267752 100644 --- a/src/test/java/com/configcat/EvaluatorTrimTest.java +++ b/src/test/java/com/configcat/EvaluatorTrimTest.java @@ -9,12 +9,9 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.io.File; import java.io.IOException; -import java.nio.file.Files; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -63,12 +60,6 @@ void tearDown() throws IOException { this.server.shutdown(); } - private String loadJsonFileAsString(String fileName) throws IOException { - ClassLoader classLoader = getClass().getClassLoader(); - File file = new File(Objects.requireNonNull(classLoader.getResource(fileName)).getFile()); - byte[] byteArray = Files.readAllBytes(file.toPath()); - return new String(byteArray); - } private User createTestUser(String identifier, String country, String version, String number, String date) { Map customAttributes = new HashMap<>(); @@ -120,7 +111,7 @@ private static Stream testComparatorValueTrimsData() { @ParameterizedTest @MethodSource("testComparatorValueTrimsData") void testComparatorValueTrims(String key, String expectedValue) throws IOException { - server.enqueue(new MockResponse().setResponseCode(200).setBody(loadJsonFileAsString(TRIM_COMPARATOR_VALUES_JSON))); + server.enqueue(new MockResponse().setResponseCode(200).setBody(Helpers.readFile(TRIM_COMPARATOR_VALUES_JSON))); User user = createTestUser(TEST_IDENTIFIER, TEST_COUNTRY, TEST_VERSION, TEST_NUMBER, TEST_DATE); String result = this.client.getValue(String.class, key, user, "default"); assertEquals(expectedValue, result); @@ -173,7 +164,7 @@ private static Stream testUserValueTrimsData() { @ParameterizedTest @MethodSource("testUserValueTrimsData") void testUserValueTrims(String key, String expectedValue) throws IOException { - server.enqueue(new MockResponse().setResponseCode(200).setBody(loadJsonFileAsString(TRIM_USER_VALUES_JSON))); + server.enqueue(new MockResponse().setResponseCode(200).setBody(Helpers.readFile(TRIM_USER_VALUES_JSON))); User user = createTestUser(addWhiteSpaces(TEST_IDENTIFIER), TEST_COUNTRY_WITH_WHITESPACES, addWhiteSpaces(TEST_VERSION), addWhiteSpaces(TEST_NUMBER), addWhiteSpaces(TEST_DATE)); String result = this.client.getValue(String.class, key, user, "default"); assertEquals(expectedValue, result); diff --git a/src/test/java/com/configcat/Helpers.java b/src/test/java/com/configcat/Helpers.java index 2f30ab2..d64914a 100644 --- a/src/test/java/com/configcat/Helpers.java +++ b/src/test/java/com/configcat/Helpers.java @@ -3,7 +3,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.function.Supplier; final class Helpers { @@ -30,10 +30,6 @@ static String cacheValueFromConfigJsonWithEtag(String json, String etag) { return entry.serialize(); } - static String entryToJson(Entry entry) { - return Utils.gson.toJson(entry); - } - static void waitFor(Supplier predicate) throws InterruptedException { waitFor(2000, predicate); } @@ -60,7 +56,7 @@ public static String readFile(String filePath) throws IOException { while ((temp = stream.read(buffer)) != -1) { outputStream.write(buffer, 0, temp); } - return new String(outputStream.toByteArray(), Charset.defaultCharset()); + return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); } } diff --git a/src/test/java/com/configcat/LocalTest.java b/src/test/java/com/configcat/LocalTest.java index c8440e0..3b24964 100644 --- a/src/test/java/com/configcat/LocalTest.java +++ b/src/test/java/com/configcat/LocalTest.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; class LocalTest { - private static final String TEST_JSON = "{ p: { s: 'test-slat'}, f: { fakeKey: { t: 1, v: { s: %s }, p: [], r: [] } } }"; + private static final String TEST_JSON = "{ p: { s: 'test-salt'}, f: { fakeKey: { t: 1, v: { s: %s }, p: [], r: [] } } }"; @Test void invalidArguments() throws IOException { diff --git a/src/test/java/com/configcat/ManualPollingTest.java b/src/test/java/com/configcat/ManualPollingTest.java index 608ca1a..79b47d4 100644 --- a/src/test/java/com/configcat/ManualPollingTest.java +++ b/src/test/java/com/configcat/ManualPollingTest.java @@ -6,11 +6,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - -import java.io.IOException; - import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -20,7 +18,7 @@ class ManualPollingTest { private ConfigService policy; private MockWebServer server; private final ConfigCatLogger logger = new ConfigCatLogger(LoggerFactory.getLogger(ManualPollingTest.class)); - private static final String TEST_JSON = "{ p: { s: 'test-slat'}, f: { fakeKey: { v: { s: %s }, p: [], r: [] } } }"; + private static final String TEST_JSON = "{ p: { s: 'test-salt'}, f: { fakeKey: { v: { s: %s }, p: [], r: [] } } }"; @BeforeEach public void setUp() throws IOException { diff --git a/src/test/java/com/configcat/VariationIdTests.java b/src/test/java/com/configcat/VariationIdTests.java index c88e1b0..26bfaf4 100644 --- a/src/test/java/com/configcat/VariationIdTests.java +++ b/src/test/java/com/configcat/VariationIdTests.java @@ -16,7 +16,7 @@ class VariationIdTests { - private static final String TEST_JSON = "{ p: { s: 'test-slat' }, f: { key1: { v: { b: true }, i: 'fakeId1', p: [] ,r: [] }, key2: { v: { b: false }, i: 'fakeId2', p: [] ,r: [] } } }"; + private static final String TEST_JSON = "{ p: { s: 'test-salt' }, f: { key1: { v: { b: true }, i: 'fakeId1', p: [] ,r: [] }, key2: { v: { b: false }, i: 'fakeId2', p: [] ,r: [] } } }"; private ConfigCatClient client; private MockWebServer server; diff --git a/src/test/resources/comparison_attribute_conversion.json b/src/test/resources/comparison_attribute_conversion.json new file mode 100644 index 0000000..c89f393 --- /dev/null +++ b/src/test/resources/comparison_attribute_conversion.json @@ -0,0 +1,804 @@ +{ + "p": { + "u": "https://test-cdn-global.configcat.com", + "r": 0, + "s": "uM29sy1rjx71ze3ehr\u002BqCnoIpx8NZgL8V//MN7OL1aM=" + }, + "f": { + "numberToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "0.12345" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "numberToStringConversionInt": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "125" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "numberToStringConversionPositiveExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e+96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "numberToStringConversionNegativeExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e-96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "numberToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "numberToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "numberToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "dateToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "1680307199.999" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "dateToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "dateToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "dateToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "stringArrayToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"read\",\"Write\",\" eXecute \"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "stringArrayToStringConversionEmpty": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "stringArrayToStringConversionSpecialChars": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"+<>%\\\"'\\\\/\\t\\r\\n\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + }, + "stringArrayToStringConversionUnicode": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"äöüÄÖÜçéèñışğ⢙✓😀\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + }, + "i": "test-variation-id" + } + } +} \ No newline at end of file diff --git a/src/test/resources/evaluation/list_truncation/test_list_truncation.json b/src/test/resources/evaluation/list_truncation/test_list_truncation.json index b6d3fd9..157e249 100644 --- a/src/test/resources/evaluation/list_truncation/test_list_truncation.json +++ b/src/test/resources/evaluation/list_truncation/test_list_truncation.json @@ -7,7 +7,9 @@ "f": { "booleanKey1": { "t": 0, - "v": { "b": false }, + "v": { + "b": false + }, "r": [ { "c": [ @@ -15,27 +17,69 @@ "u": { "a": "Identifier", "c": 2, - "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ] + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] } }, { "u": { "a": "Identifier", "c": 2, - "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11" ] + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11" + ] } }, { "u": { "a": "Identifier", "c": 2, - "l": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" ] + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12" + ] } } ], - "s": { "v": { "b": true } } + "s": { + "v": { + "b": true + }, + "i": "test-variation-id" + } } - ] + ], + "i": "test-variation-id" } } } diff --git a/src/test/resources/evaluation/prerequisite_flag.json b/src/test/resources/evaluation/prerequisite_flag.json index e39f94c..5f0ecde 100644 --- a/src/test/resources/evaluation/prerequisite_flag.json +++ b/src/test/resources/evaluation/prerequisite_flag.json @@ -29,6 +29,12 @@ }, "returnValue": "Horse", "expectedLog": "prerequisite_flag.txt" + }, + { + "key": "dependentFeatureMultipleLevels", + "defaultValue": "default", + "returnValue": "Dog", + "expectedLog": "prerequisite_flag_multilevel.txt" } ] } diff --git a/src/test/resources/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt b/src/test/resources/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt new file mode 100644 index 0000000..e9b9da6 --- /dev/null +++ b/src/test/resources/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt @@ -0,0 +1,24 @@ +INFO [5000] Evaluating 'dependentFeatureMultipleLevels' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'intermediateFeature' EQUALS 'true' + ( + Evaluating prerequisite flag 'intermediateFeature': + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + AND Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + THEN 'true' => MATCH, applying rule + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'intermediateFeature' EQUALS 'true') evaluates to true. + ) + THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/src/test/resources/evaluation/segment.json b/src/test/resources/evaluation/segment.json index 685b0b6..4bb4f31 100644 --- a/src/test/resources/evaluation/segment.json +++ b/src/test/resources/evaluation/segment.json @@ -1,5 +1,5 @@ { - "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA", + "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", "tests": [ { "key": "featureWithSegmentTargeting", @@ -26,7 +26,7 @@ "returnValue": true, "expectedLog": "segment_matching.txt" }, - { + { "key": "featureWithNegatedSegmentTargeting", "defaultValue": false, "user": { @@ -35,6 +35,12 @@ }, "returnValue": false, "expectedLog": "segment_no_matching.txt" + }, + { + "key": "featureWithSegmentTargetingMultipleConditions", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user_multi_conditions.txt" } ] } diff --git a/src/test/resources/evaluation/segment/segment_no_user_multi_conditions.txt b/src/test/resources/evaluation/segment/segment_no_user_multi_conditions.txt new file mode 100644 index 0000000..6ef50ce --- /dev/null +++ b/src/test/resources/evaluation/segment/segment_no_user_multi_conditions.txt @@ -0,0 +1,7 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargetingMultipleConditions' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()`/`getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargetingMultipleConditions' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users (cleartext)' => false, skipping the remaining AND conditions + THEN 'true' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/src/test/resources/test_circulardependency.json b/src/test/resources/test_circulardependency.json index d2f8bec..ffbd013 100644 --- a/src/test/resources/test_circulardependency.json +++ b/src/test/resources/test_circulardependency.json @@ -7,7 +7,9 @@ "f": { "key1": { "t": 1, - "v": { "s": "key1-value" }, + "v": { + "s": "key1-value" + }, "r": [ { "c": [ @@ -15,17 +17,25 @@ "p": { "f": "key1", "c": 0, - "v": { "s": "key1-prereq" } + "v": { + "s": "key1-prereq" + } } } ], - "s": { "v": { "s": "key1-prereq" } } + "s": { + "v": { + "s": "key1-prereq" + } + } } ] }, "key2": { "t": 1, - "v": { "s": "key2-value" }, + "v": { + "s": "key2-value" + }, "r": [ { "c": [ @@ -33,17 +43,25 @@ "p": { "f": "key3", "c": 0, - "v": { "s": "key3-prereq" } + "v": { + "s": "key3-prereq" + } } } ], - "s": { "v": { "s": "key2-prereq" } } + "s": { + "v": { + "s": "key2-prereq" + } + } } ] }, "key3": { "t": 1, - "v": { "s": "key3-value" }, + "v": { + "s": "key3-value" + }, "r": [ { "c": [ @@ -51,17 +69,25 @@ "p": { "f": "key2", "c": 0, - "v": { "s": "key2-prereq" } + "v": { + "s": "key2-prereq" + } } } ], - "s": { "v": { "s": "key3-prereq" } } + "s": { + "v": { + "s": "key3-prereq" + } + } } ] }, "key4": { "t": 1, - "v": { "s": "key4-value" }, + "v": { + "s": "key4-value" + }, "r": [ { "c": [ @@ -69,11 +95,17 @@ "p": { "f": "key3", "c": 0, - "v": { "s": "key3-prereq" } + "v": { + "s": "key3-prereq" + } } } ], - "s": { "v": { "s": "key4-prereq" } } + "s": { + "v": { + "s": "key4-prereq" + } + } } ] }