From 2abf5831efb217ec2e1f1a05b77ed231caf6e451 Mon Sep 17 00:00:00 2001 From: adams85 <31276480+adams85@users.noreply.github.com> Date: Thu, 8 Feb 2024 22:54:56 +0100 Subject: [PATCH] More config v6 tests (#88) * Use valid SDK keys in cache key generation tests * Add test for another special case of segment condition logging * Add test for multi-level flag dependency * Add tests for comparison attribute and comparison value trimming * Add tests for comparison attribute conversion to canonical string representation * Improve number to canonical string formatting * Don't accept string arrays that contain null values in the case of ARRAY (NOT) CONTAINS ANY OF * Simplify EvaluateContext + improve perf. of user attribute retrieval during evaluation + reduce allocations * Bump version --- appveyor.yml | 2 +- .../ConfigCacheTests.cs | 4 +- .../ConfigV2EvaluationTests.cs | 186 ++++ .../data/comparison_attribute_conversion.json | 789 ++++++++++++++ .../data/comparison_attribute_trimming.json | 985 ++++++++++++++++++ .../data/comparison_value_trimming.json | 777 ++++++++++++++ .../data/evaluationlog/prerequisite_flag.json | 6 + .../prerequisite_flag_multilevel.txt | 24 + .../data/evaluationlog/segment.json | 10 +- .../segment_no_user_multi_conditions.txt | 7 + .../Evaluation/EvaluateContext.cs | 18 +- .../Evaluation/EvaluateLogHelper.cs | 2 +- .../Evaluation/RolloutEvaluator.cs | 65 +- .../Extensions/SerializationExtensions.cs | 35 +- src/ConfigCatClient/User.cs | 12 + 15 files changed, 2876 insertions(+), 46 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/data/comparison_attribute_conversion.json create mode 100644 src/ConfigCat.Client.Tests/data/comparison_attribute_trimming.json create mode 100644 src/ConfigCat.Client.Tests/data/comparison_value_trimming.json create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt create mode 100644 src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user_multi_conditions.txt diff --git a/appveyor.yml b/appveyor.yml index c37344e3..06115f61 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ environment: - build_version: 9.0.1 + build_version: 9.0.2 version: $(build_version)-{build} image: Visual Studio 2022 configuration: Release diff --git a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs index 1ce2065e..4aeaa894 100644 --- a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs @@ -228,8 +228,8 @@ public async Task ConfigCache_ShouldHandleWhenExternalCacheFails(bool isAsync) loggerMock.Verify(l => l.Log(LogLevel.Error, 2200, ref It.Ref.IsAny, It.Is(ex => ex is ApplicationException)), Times.Once); } - [DataRow("test1", "7f845c43ecc95e202b91e271435935e6d1391e5d")] - [DataRow("test2", "a78b7e323ef543a272c74540387566a22415148a")] + [DataRow("configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012", "f83ba5d45bceb4bb704410f51b704fb6dfa19942")] + [DataRow("configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012", "da7bfd8662209c8ed3f9db96daed4f8d91ba5876")] [DataTestMethod] public void CacheKeyGeneration_ShouldBePlatformIndependent(string sdkKey, string expectedCacheKey) { diff --git a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs index ddb980ce..ba5b68c9 100644 --- a/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigV2EvaluationTests.cs @@ -586,4 +586,190 @@ public void UserObjectAttributeValueConversion_NonTextComparisons_Test(string sd Assert.AreEqual(expectedReturnValue, evaluationDetails.Value); } + + [DataTestMethod] + [DataRow("numberToStringConversion", .12345, "1")] + [DataRow("numberToStringConversion", "decimal:0.12345", "1")] + [DataRow("numberToStringConversionInt", (sbyte)125, "4")] + [DataRow("numberToStringConversionInt", (byte)125, "4")] + [DataRow("numberToStringConversionInt", (short)125, "4")] + [DataRow("numberToStringConversionInt", (ushort)125, "4")] + [DataRow("numberToStringConversionInt", 125, "4")] + [DataRow("numberToStringConversionInt", 125u, "4")] + [DataRow("numberToStringConversionInt", 125L, "4")] + [DataRow("numberToStringConversionInt", 125ul, "4")] + [DataRow("numberToStringConversionPositiveExp", -1.23456789e96, "2")] + [DataRow("numberToStringConversionNegativeExp", -12345.6789E-100, "4")] + [DataRow("numberToStringConversionNaN", double.NaN, "3")] + [DataRow("numberToStringConversionPositiveInf", double.PositiveInfinity, "4")] + [DataRow("numberToStringConversionNegativeInf", double.NegativeInfinity, "3")] + [DataRow("dateToStringConversion", "datetime:2023-03-31T23:59:59.9990000Z", "3")] + [DataRow("dateToStringConversion", "datetimeoffset:2023-03-31T23:59:59.9990000Z", "3")] + [DataRow("dateToStringConversion", 1680307199.999, "3")] + [DataRow("dateToStringConversion", "decimal:1680307199.999", "3")] + [DataRow("dateToStringConversionNaN", double.NaN, "3")] + [DataRow("dateToStringConversionPositiveInf", double.PositiveInfinity, "1")] + [DataRow("dateToStringConversionNegativeInf", double.NegativeInfinity, "5")] + [DataRow("stringArrayToStringConversion", new[] { "read", "Write", " eXecute " }, "4")] + [DataRow("stringArrayToStringConversionEmpty", new string[0], "5")] + [DataRow("stringArrayToStringConversionSpecialChars", new[] { "+<>%\"'\\/\t\r\n" }, "3")] + [DataRow("stringArrayToStringConversionUnicode", new[] { "äöüÄÖÜçéèñışğ⢙✓😀" }, "2")] + public void ComparisonAttributeConversionToCanonicalStringRepresentation_Test(string key, object customAttributeValue, string expectedReturnValue) + { + var config = new ConfigLocation.LocalFile("data", "comparison_attribute_conversion.json").FetchConfig(); + + var logger = new Mock().Object.AsWrapper(); + var evaluator = new RolloutEvaluator(logger); + + if (customAttributeValue is string s) + { + const string decimalPrefix = "decimal:", dateTimePrefix = "datetime:", dateTimeOffsetPrefix = "datetimeoffset:"; + if (s.StartsWith(decimalPrefix, StringComparison.Ordinal)) + { + customAttributeValue = decimal.Parse(s.Substring(decimalPrefix.Length)); + } + else if (s.StartsWith(dateTimePrefix, StringComparison.Ordinal)) + { + var dateTimeStyle = s.EndsWith("Z", StringComparison.Ordinal) ? DateTimeStyles.AdjustToUniversal : DateTimeStyles.None; + customAttributeValue = DateTime.ParseExact(s.Substring(dateTimePrefix.Length), "o", CultureInfo.InvariantCulture, dateTimeStyle); + } + else if (s.StartsWith(dateTimeOffsetPrefix, StringComparison.Ordinal)) + { + customAttributeValue = DateTimeOffset.ParseExact(s.Substring(dateTimeOffsetPrefix.Length), "o", CultureInfo.InvariantCulture); + } + } + + var user = new User("12345") + { + Custom = + { + ["Custom1"] = customAttributeValue + } + }; + + const string defaultValue = "default"; + var actualReturnValue = evaluator.Evaluate(config!.Settings, key, defaultValue, user, remoteConfig: null, logger).Value; + + Assert.AreEqual(expectedReturnValue, actualReturnValue); + } + + [DataTestMethod] + [DataRow("isoneof", "no trim")] + [DataRow("isnotoneof", "no trim")] + [DataRow("isoneofhashed", "no trim")] + [DataRow("isnotoneofhashed", "no trim")] + [DataRow("equalshashed", "no trim")] + [DataRow("notequalshashed", "no trim")] + [DataRow("arraycontainsanyofhashed", "no trim")] + [DataRow("arraynotcontainsanyofhashed", "no trim")] + [DataRow("equals", "no trim")] + [DataRow("notequals", "no trim")] + [DataRow("startwithanyof", "no trim")] + [DataRow("notstartwithanyof", "no trim")] + [DataRow("endswithanyof", "no trim")] + [DataRow("notendswithanyof", "no trim")] + [DataRow("arraycontainsanyof", "no trim")] + [DataRow("arraynotcontainsanyof", "no trim")] + [DataRow("startwithanyofhashed", "no trim")] + [DataRow("notstartwithanyofhashed", "no trim")] + [DataRow("endswithanyofhashed", "no trim")] + [DataRow("notendswithanyofhashed", "no trim")] + //semver comparators user values trimmed because of backward compatibility + [DataRow("semverisoneof", "4 trim")] + [DataRow("semverisnotoneof", "5 trim")] + [DataRow("semverless", "6 trim")] + [DataRow("semverlessequals", "7 trim")] + [DataRow("semvergreater", "8 trim")] + [DataRow("semvergreaterequals", "9 trim")] + //number and date comparators user values trimmed because of backward compatibility + [DataRow("numberequals", "10 trim")] + [DataRow("numbernotequals", "11 trim")] + [DataRow("numberless", "12 trim")] + [DataRow("numberlessequals", "13 trim")] + [DataRow("numbergreater", "14 trim")] + [DataRow("numbergreaterequals", "15 trim")] + [DataRow("datebefore", "18 trim")] + [DataRow("dateafter", "19 trim")] + //"contains any of" and "not contains any of" is a special case, the not trimmed user attribute checked against not trimmed comparator values. + [DataRow("containsanyof", "no trim")] + [DataRow("notcontainsanyof", "no trim")] + public void ComparisonAttributeTrimming_Test(string key, string expectedReturnValue) + { + var config = new ConfigLocation.LocalFile("data", "comparison_attribute_trimming.json").FetchConfig(); + + var logger = new Mock().Object.AsWrapper(); + var evaluator = new RolloutEvaluator(logger); + + var user = new User(" 12345 ") + { + Country = "[\" USA \"]", + Custom = + { + ["Version"] = " 1.0.0 ", + ["Number"] = " 3 ", + ["Date"] = " 1705253400 " + } + }; + + const string defaultValue = "default"; + var actualReturnValue = evaluator.Evaluate(config!.Settings, key, defaultValue, user, remoteConfig: null, logger).Value; + + Assert.AreEqual(expectedReturnValue, actualReturnValue); + } + + [DataTestMethod] + [DataRow("isoneof", "no trim")] + [DataRow("isnotoneof", "no trim")] + [DataRow("containsanyof", "no trim")] + [DataRow("notcontainsanyof", "no trim")] + [DataRow("isoneofhashed", "no trim")] + [DataRow("isnotoneofhashed", "no trim")] + [DataRow("equalshashed", "no trim")] + [DataRow("notequalshashed", "no trim")] + [DataRow("arraycontainsanyofhashed", "no trim")] + [DataRow("arraynotcontainsanyofhashed", "no trim")] + [DataRow("equals", "no trim")] + [DataRow("notequals", "no trim")] + [DataRow("startwithanyof", "no trim")] + [DataRow("notstartwithanyof", "no trim")] + [DataRow("endswithanyof", "no trim")] + [DataRow("notendswithanyof", "no trim")] + [DataRow("arraycontainsanyof", "no trim")] + [DataRow("arraynotcontainsanyof", "no trim")] + [DataRow("startwithanyofhashed", "default")] + [DataRow("notstartwithanyofhashed", "default")] + [DataRow("endswithanyofhashed", "default")] + [DataRow("notendswithanyofhashed", "default")] + //semver comparator values trimmed because of backward compatibility + [DataRow("semverisoneof", "4 trim")] + [DataRow("semverisnotoneof", "5 trim")] + [DataRow("semverless", "6 trim")] + [DataRow("semverlessequals", "7 trim")] + [DataRow("semvergreater", "8 trim")] + [DataRow("semvergreaterequals", "9 trim")] + public void ComparisonValueTrimming_Test(string key, string expectedReturnValue) + { + var config = new ConfigLocation.LocalFile("data", "comparison_value_trimming.json").FetchConfig(); + + var logger = new Mock().Object.AsWrapper(); + var evaluator = new RolloutEvaluator(logger); + + var user = new User("12345") + { + Country = "[\"USA\"]", + Custom = + { + ["Version"] = "1.0.0", + ["Number"] = "3", + ["Date"] = "1705253400" + } + }; + + const string defaultValue = "default"; + string actualReturnValue; + try { actualReturnValue = evaluator.Evaluate(config!.Settings, key, defaultValue, user, remoteConfig: null, logger).Value; } + catch (InvalidOperationException) { actualReturnValue = defaultValue; } + + Assert.AreEqual(expectedReturnValue, actualReturnValue); + } } diff --git a/src/ConfigCat.Client.Tests/data/comparison_attribute_conversion.json b/src/ConfigCat.Client.Tests/data/comparison_attribute_conversion.json new file mode 100644 index 00000000..5a900ae6 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/comparison_attribute_conversion.json @@ -0,0 +1,789 @@ +{ + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/comparison_attribute_trimming.json b/src/ConfigCat.Client.Tests/data/comparison_attribute_trimming.json new file mode 100644 index 00000000..a42df5f2 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/comparison_attribute_trimming.json @@ -0,0 +1,985 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "VjBfGYcmyHzLBv5EINgSBbX6/rYevYGWQhF3Zk5t8i4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + "09d5761537a8136eb7fc45a53917b51cb9dcd2bb9b62ffa24ace0e8a7600a3c7" + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + "99d06b6b3669b906803c285267f76fe4e2ccc194b00801ab07f2fd49939b6960" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + "5_7eb158c29b48b62cec860dffc459171edbfeef458bcc8e8bb62956d823eef3df" + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": "ea0d05859bb737105eea40bc605f6afd542c8f50f8497cd21ace38e731d7eef0" + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + "1765b470044971bbc19e7bed10112199c5da9c626455f86be109fef96e747911" + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + "5_2a338d3beb8ebe2e711d198420d04e2627e39501c2fcc7d5b3b8d93540691097" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": "650fe0e8e86030b5f73ccd77e6532f307adf82506048a22f02d95386206ecea1" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + "5_586ab2ec61946cb1457d4af170d88e7f14e655d9debf352b4ab6bf5bf77df3f7" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + "1.0.1" + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + "1.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + "5_67a323069ee45fef4ccd8365007d4713f7a3bc87764943b1139e8e50d1aee8fd" + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + }, + "dateafter": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 19, + "d": 1705251600 + } + } + ], + "s": { + "v": { + "s": "19 trim" + }, + "i": "83e580ce" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1c12e0cc" + }, + "datebefore": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 18, + "d": 1705255200 + } + } + ], + "s": { + "v": { + "s": "18 trim" + }, + "i": "34614b07" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "26d4f328" + }, + "numberequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 10, + "d": 3 + } + } + ], + "s": { + "v": { + "s": "10 trim" + }, + "i": "6a8c0a08" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "7b8e49b9" + }, + "numbergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 14, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "14 trim" + }, + "i": "2037a7a4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "902f9bd9" + }, + "numbergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 15, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "15 trim" + }, + "i": "527c49d2" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2280c961" + }, + "numberless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 12, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "12 trim" + }, + "i": "c454f775" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ec935943" + }, + "numberlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 13, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "13 trim" + }, + "i": "1e31aed8" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1d53c679" + }, + "numbernotequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 11, + "d": 6 + } + } + ], + "s": { + "v": { + "s": "11 trim" + }, + "i": "e8d7cf05" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "21c749a7" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "2 trim" + }, + "i": "c3ab37cf" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "3 trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "f91ecf16" + } + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/comparison_value_trimming.json b/src/ConfigCat.Client.Tests/data/comparison_value_trimming.json new file mode 100644 index 00000000..db917032 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/comparison_value_trimming.json @@ -0,0 +1,777 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "zsVN1DQ9Oa2FjFc96MvPfMM5Vs+KKV00NyybJZipyf4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + " 028fdb841bf3b2cc27fce407da08f87acd3a58a08c67d819cdb9351857b14237 " + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + " 60b747c290642863f9a6c68773ed309a9fb02c6c1ae65c77037046918f4c1d3c " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "2 trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "c3ab37cf" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + " 5_a6ce5e2838d4e0c27cd705c90f39e60d79056062983c39951668cf947ec406c2 " + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": " a2868640b1fe24c98e50b168756d83fd03779dd4349d6ddab5d7d6ef8dad13bd " + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + " 55ce90920d20fc0bf8078471062a85f82cc5ea2226012a901a5045775bace0f4 " + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "3 trim" + }, + "i": "f91ecf16" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + " 5_c517fc957907e30b6a790540a20172a3a5d3a7458a85e340a7b1a1ac982be278 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": " 31ceae14b865b0842e93fdc3a42a7e45780ccc41772ca9355db50e09d81e13ef " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + " 5_3643bbdd1bce4021fe4dbd55e6cc2f4902e4f50e592597d1a2d0e944fb7dfb42 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + " 1.0.1 " + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + " 1.0.0 " + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + " 5_3e052709552ca9d5bd6c459cb7ab0389f3210f6aafc3d006a2481635e9614a7c " + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + } + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json index 674e2d33..9c35c00e 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag.json @@ -30,6 +30,12 @@ }, "returnValue": "Horse", "expectedLog": "prerequisite_flag.txt" + }, + { + "key": "dependentFeatureMultipleLevels", + "defaultValue": "default", + "returnValue": "Dog", + "expectedLog": "prerequisite_flag_multilevel.txt" } ] } diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt new file mode 100644 index 00000000..4f26d48e --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/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/ConfigCat.Client.Tests/data/evaluationlog/segment.json b/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json index 41744c22..1bb4df5b 100644 --- a/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/segment.json @@ -1,6 +1,6 @@ { - "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d9f207-6883-43e5-868c-cbf677af3fe6/244cf8b0-f604-11e8-b543-f23c917f9d8d", - "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA", + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", "tests": [ { "key": "featureWithSegmentTargeting", @@ -8,6 +8,12 @@ "returnValue": false, "expectedLog": "segment_no_user.txt" }, + { + "key": "featureWithSegmentTargetingMultipleConditions", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user_multi_conditions.txt" + }, { "key": "featureWithNegatedSegmentTargetingCleartext", "defaultValue": false, diff --git a/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user_multi_conditions.txt b/src/ConfigCat.Client.Tests/data/evaluationlog/segment/segment_no_user_multi_conditions.txt new file mode 100644 index 00000000..a8dcbb48 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/evaluationlog/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/ConfigCatClient/Evaluation/EvaluateContext.cs b/src/ConfigCatClient/Evaluation/EvaluateContext.cs index 8815489c..eb88836f 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateContext.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateContext.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using ConfigCat.Client.Utils; namespace ConfigCat.Client.Evaluation; @@ -8,16 +7,9 @@ internal struct EvaluateContext { public readonly string Key; public readonly Setting Setting; + public readonly User? User; public readonly IReadOnlyDictionary Settings; - private readonly User? user; - - [MemberNotNullWhen(true, nameof(UserAttributes))] - public readonly bool IsUserAvailable => this.user is not null; - - private IReadOnlyDictionary? userAttributes; - public IReadOnlyDictionary? UserAttributes => this.userAttributes ??= this.user?.GetAllAttributes(); - private List? visitedFlags; public List VisitedFlags => this.visitedFlags ??= new List(); @@ -30,19 +22,17 @@ public EvaluateContext(string key, Setting setting, User? user, IReadOnlyDiction { this.Key = key; this.Setting = setting; - this.user = user; + this.User = user; this.Settings = settings; - this.userAttributes = null; this.visitedFlags = null; this.IsMissingUserObjectLogged = this.IsMissingUserObjectAttributeLogged = false; this.LogBuilder = null; // initialized by RolloutEvaluator.Evaluate } - public EvaluateContext(string key, Setting setting, ref EvaluateContext dependentFlagContext) - : this(key, setting, dependentFlagContext.user, dependentFlagContext.Settings) + public EvaluateContext(string key, Setting setting, in EvaluateContext dependentFlagContext) + : this(key, setting, dependentFlagContext.User, dependentFlagContext.Settings) { - this.userAttributes = dependentFlagContext.UserAttributes; this.visitedFlags = dependentFlagContext.VisitedFlags; // crucial to use the property here to make sure the list is created! this.LogBuilder = dependentFlagContext.LogBuilder; } diff --git a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs index a39d2880..d9fc69a2 100644 --- a/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs +++ b/src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs @@ -121,7 +121,7 @@ UserComparator.SensitiveTextEquals or public static IndentedTextBuilder AppendPrerequisiteFlagCondition(this IndentedTextBuilder builder, PrerequisiteFlagCondition condition) { - var prerequisiteFlagKey = condition.PrerequisiteFlagKey; + var prerequisiteFlagKey = condition.PrerequisiteFlagKey ?? InvalidReferencePlaceholder; var comparator = condition.Comparator; var comparisonValue = condition.ComparisonValue.GetValue(throwIfInvalid: false); diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 831c8493..9a80ba7b 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -34,9 +34,9 @@ public EvaluateResult Evaluate(T defaultValue, ref EvaluateContext context, [ logBuilder.Append($"Evaluating '{context.Key}'"); - if (context.IsUserAvailable) + if (context.User is not null) { - logBuilder.Append($" for User '{context.UserAttributes.Serialize()}'"); + logBuilder.Append($" for User '{context.User.GetAllAttributes().Serialize()}'"); } logBuilder.IncreaseIndent(); @@ -111,7 +111,7 @@ private EvaluateResult EvaluateSetting(ref EvaluateContext context) } var percentageOptions = context.Setting.PercentageOptions; - if (percentageOptions.Length > 0 && TryEvaluatePercentageOptions(percentageOptions, targetingRule: null, ref context, out evaluateResult)) + if (percentageOptions.Length > 0 && TryEvaluatePercentageOptions(percentageOptions, matchedTargetingRule: null, ref context, out evaluateResult)) { return evaluateResult; } @@ -173,11 +173,11 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu return false; } - private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, TargetingRule? targetingRule, ref EvaluateContext context, out EvaluateResult result) + private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, TargetingRule? matchedTargetingRule, ref EvaluateContext context, out EvaluateResult result) { var logBuilder = context.LogBuilder; - if (!context.IsUserAvailable) + if (context.User is null) { logBuilder?.NewLine("Skipping % options because the User Object is missing."); @@ -191,9 +191,20 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, return false; } - var percentageOptionsAttributeName = context.Setting.PercentageOptionsAttribute ?? nameof(User.Identifier); + var percentageOptionsAttributeName = context.Setting.PercentageOptionsAttribute; + object? percentageOptionsAttributeValue; - if (!context.UserAttributes.TryGetValue(percentageOptionsAttributeName, out var percentageOptionsAttributeValue)) + if (percentageOptionsAttributeName is null) + { + percentageOptionsAttributeName = nameof(User.Identifier); + percentageOptionsAttributeValue = context.User.Identifier; + } + else + { + percentageOptionsAttributeValue = context.User.GetAttribute(percentageOptionsAttributeName); + } + + if (percentageOptionsAttributeValue is null) { logBuilder?.NewLine().Append($"Skipping % options because the User.{percentageOptionsAttributeName} attribute is missing."); @@ -233,10 +244,13 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions, continue; } - var percentageOptionValue = percentageOption.Value.GetValue(throwIfInvalid: false); - logBuilder?.NewLine().Append($"- Hash value {hashValue} selects % option {i + 1} ({percentageOption.Percentage}%), '{percentageOptionValue ?? EvaluateLogHelper.InvalidValuePlaceholder}'."); + if (logBuilder is not null) + { + var percentageOptionValue = percentageOption.Value.GetValue(throwIfInvalid: false) ?? EvaluateLogHelper.InvalidValuePlaceholder; + logBuilder.NewLine().Append($"- Hash value {hashValue} selects % option {i + 1} ({percentageOption.Percentage}%), '{percentageOptionValue}'."); + } - result = new EvaluateResult(percentageOption, matchedTargetingRule: targetingRule, matchedPercentageOption: percentageOption); + result = new EvaluateResult(percentageOption, matchedTargetingRule, matchedPercentageOption: percentageOption); return true; } @@ -284,7 +298,7 @@ private bool EvaluateConditions(TCondition[] conditions, TargetingRu break; case PrerequisiteFlagCondition prerequisiteFlagCondition: - conditionResult = EvaluatePrerequisiteFlagCondition(prerequisiteFlagCondition, ref context, out error); + conditionResult = EvaluatePrerequisiteFlagCondition(prerequisiteFlagCondition, ref context); newLineBeforeThen = true; break; @@ -333,7 +347,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, var logBuilder = context.LogBuilder; logBuilder?.AppendUserCondition(condition); - if (!context.IsUserAvailable) + if (context.User is null) { if (!context.IsMissingUserObjectLogged) { @@ -346,8 +360,9 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt, } var userAttributeName = condition.ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing."); + var userAttributeValue = context.User.GetAttribute(userAttributeName); - if (!context.UserAttributes.TryGetValue(userAttributeName, out var userAttributeValue) || userAttributeValue is string { Length: 0 }) + if (userAttributeValue is null || userAttributeValue is string { Length: 0 }) { this.logger.UserObjectAttributeIsMissing(condition.ToString(), context.Key, userAttributeName); error = string.Format(CultureInfo.InvariantCulture, MissingUserAttributeError, userAttributeName); @@ -693,10 +708,8 @@ private static bool EvaluateSensitiveArrayContainsAnyOf(string[] array, string[] return negate; } - private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition condition, ref EvaluateContext context, out string? error) + private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition condition, ref EvaluateContext context) { - error = null; - var logBuilder = context.LogBuilder; logBuilder?.AppendPrerequisiteFlagCondition(condition); @@ -722,7 +735,7 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi throw new InvalidOperationException($"Circular dependency detected between the following depending flags: {dependencyCycle}."); } - var prerequisiteFlagContext = new EvaluateContext(prerequisiteFlagKey!, prerequisiteFlag!, ref context); + var prerequisiteFlagContext = new EvaluateContext(prerequisiteFlagKey!, prerequisiteFlag!, context); logBuilder? .NewLine("(") @@ -744,7 +757,7 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi }; logBuilder? - .NewLine().Append($"Prerequisite flag evaluation result: '{prerequisiteFlagValue ?? EvaluateLogHelper.InvalidValuePlaceholder}'.") + .NewLine().Append($"Prerequisite flag evaluation result: '{prerequisiteFlagValue}'.") .NewLine("Condition (") .AppendPrerequisiteFlagCondition(condition) .Append(") evaluates to ").AppendEvaluationResult(result).Append(".") @@ -761,7 +774,7 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo var logBuilder = context.LogBuilder; logBuilder?.AppendSegmentCondition(condition); - if (!context.IsUserAvailable) + if (context.User is null) { if (!context.IsMissingUserObjectLogged) { @@ -864,11 +877,14 @@ private static string UserAttributeValueToString(object attributeValue) } else if (attributeValue is string[] stringArray) { - return stringArray.Serialize(); + return stringArray.Serialize(unescapeAstral: true); } else if (attributeValue.TryConvertNumericToDouble(out var number)) { - return number.ToString(CultureInfo.InvariantCulture); + var format = Math.Abs(number) is >= 1e-6 and < 1e21 + ? "0.#################" + : "0.#################e+0"; + return number.ToString(format, CultureInfo.InvariantCulture); } else if (attributeValue.TryConvertDateTimeToDateTimeOffset(out var dateTimeOffset)) { @@ -939,8 +955,11 @@ private double GetUserAttributeValueAsUnixTimeSeconds(string attributeName, obje if (attributeValue is string[] stringArray || attributeValue is string json && (stringArray = json.DeserializeOrDefault()!) is not null) { - error = null; - return stringArray; + if (!Array.Exists(stringArray, item => item is null)) + { + error = null; + return stringArray; + } } error = HandleInvalidUserAttribute(condition, key, attributeName, $"'{attributeValue}' is not a valid string array"); diff --git a/src/ConfigCatClient/Extensions/SerializationExtensions.cs b/src/ConfigCatClient/Extensions/SerializationExtensions.cs index 9c483ccb..645a01e1 100644 --- a/src/ConfigCatClient/Extensions/SerializationExtensions.cs +++ b/src/ConfigCatClient/Extensions/SerializationExtensions.cs @@ -2,9 +2,10 @@ using System.IO; using Newtonsoft.Json; #else +using System.Globalization; using System.Text.Encodings.Web; using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.RegularExpressions; #endif namespace System; @@ -51,12 +52,40 @@ internal static class SerializationExtensions } } - public static string Serialize(this T objectToSerialize) + public static string Serialize(this T objectToSerialize, bool unescapeAstral = false) { #if USE_NEWTONSOFT_JSON return JsonConvert.SerializeObject(objectToSerialize); #else - return JsonSerializer.Serialize(objectToSerialize, TolerantSerializerOptions); + var json = JsonSerializer.Serialize(objectToSerialize, TolerantSerializerOptions); + if (unescapeAstral) + { + // NOTE: There's no easy way to configure System.Text.Json not to encode surrogate pairs (i.e. Unicode code points above U+FFFF). + // The only way of doing it during serialization (https://github.com/dotnet/runtime/issues/54193#issuecomment-861155179) needs unsafe code, + // which we want to avoid in this project. So, we resort to the following regex-based workaround: + json = Regex.Replace(json, @"\\u[dD][89abAB][0-9a-fA-F]{2}\\u[dD][c-fC-F][0-9a-fA-F]{2}", match => + { + // Ignore possible matches that aren't really escaped ('\\uD800\uDC00', '\\\\uD800\uDC00', etc.) + var isEscaped = true; + for (var i = match.Index - 1; i >= 0; i--) + { + if (json[i] != '\\') + { + break; + } + isEscaped = !isEscaped; + } + if (!isEscaped) + { + return match.Value; + } + + var highSurrogate = ushort.Parse(match.Value.AsSpan(2, 4).ToParsable(), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); + var lowSurrogate = ushort.Parse(match.Value.AsSpan(8, 4).ToParsable(), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); + return char.ConvertFromUtf32(char.ConvertToUtf32((char)highSurrogate, (char)lowSurrogate)); + }, RegexOptions.CultureInvariant, TimeSpan.FromSeconds(5)); + } + return json; #endif } } diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index 1db63f8e..3b6b0afc 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -78,6 +78,18 @@ public IDictionary Custom get => this.custom ??= new Dictionary(); set => this.custom = value; } + + internal object? GetAttribute(string name) + { + return name switch + { + nameof(Identifier) => Identifier, + nameof(Email) => Email, + nameof(Country) => Country, + _ => this.custom is not null && this.custom.TryGetValue(name, out var value) ? value : null + }; + } + /// /// Returns all attributes of the user. ///